001/** 002 * AbstractPlotWindow -- Partial implementation of a plot window. 003 * 004 * Copyright (C) 2011-2024, by Joseph A. Huwaldt. All rights reserved. 005 * 006 * This library is free software; you can redistribute it and/or modify it under the terms 007 * of the GNU Lesser General Public License as published by the Free Software Foundation; 008 * either version 2.1 of the License, or (at your option) any later version. 009 * 010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 012 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public License along with 015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - 016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html 017 */ 018package geomss.ui; 019 020import geomss.app.GeomSSGUI; 021import jahuwaldt.io.ExtFilenameFilter; 022import jahuwaldt.swing.*; 023import java.awt.Dimension; 024import java.awt.Graphics2D; 025import java.awt.Image; 026import java.awt.Toolkit; 027import java.awt.event.ActionEvent; 028import java.awt.event.ActionListener; 029import java.awt.event.WindowEvent; 030import java.awt.geom.Rectangle2D; 031import java.awt.print.PageFormat; 032import java.awt.print.PrinterException; 033import java.awt.print.PrinterJob; 034import java.io.*; 035import java.lang.reflect.Constructor; 036import java.lang.reflect.InvocationTargetException; 037import java.lang.reflect.Method; 038import java.text.MessageFormat; 039import static java.util.Objects.isNull; 040import static java.util.Objects.nonNull; 041import java.util.ResourceBundle; 042import java.util.logging.Level; 043import java.util.logging.Logger; 044import javax.swing.*; 045import org.jfree.chart.ChartPanel; 046import org.jfree.chart.ChartUtilities; 047import org.jfree.chart.JFreeChart; 048 049/** 050 * A partial implementation of a window that displays a plot using JFreeChart. 051 * 052 * <p> Modified by: Joseph A. Huwaldt </p> 053 * 054 * @author Joseph A. Huwaldt Date: September 3, 2011 055 * @version January 1, 2024 056 */ 057public abstract class AbstractPlotWindow extends JFrame { 058 059 private static final long serialVersionUID = 1L; 060 061 // Application frame width & height. 062 private static final short FRAME_WIDTH = 800; 063 private static final short FRAME_HEIGHT = 600; 064 065 // Flag indicating if we have SVG output capability. 066 private static final boolean HAS_SVG = nonNull(createSVGGraphics2D(10, 10)); 067 068 /** 069 * The resource bundle for this application containing the application strings. 070 */ 071 protected final ResourceBundle RESOURCES; 072 073 // The name of the plot. 074 private final String _dataName; 075 076 // The page format used for printing. 077 private PageFormat _pf = null; 078 079 // The panel used to display the chart. 080 private ChartPanel _chartPanel; 081 082 /** 083 * Construct a new AbstractPlotWindow with the given parameters. 084 * 085 * @param title The title for the window. 086 * @param dataName Name of the data being plotted (possibly the name of the file it 087 * came from). 088 */ 089 @SuppressWarnings({"LeakingThisInConstructor", "OverridableMethodCallInConstructor"}) 090 public AbstractPlotWindow(String title, String dataName) { 091 super(title); 092 093 MDIApplication app = MDIApplication.getInstance(); 094 RESOURCES = app.getResourceBundle(); 095 MDIApplication.addWindow(this); 096 097 _dataName = dataName; 098 099 setResizable(true); 100 setSize(FRAME_WIDTH, FRAME_HEIGHT); 101 102 // Position the window so that each new window can be seen. 103 AppUtilities.positionWindow(this, FRAME_WIDTH, FRAME_HEIGHT); 104 105 // Have the window dispose of itself when it closes. 106 setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 107 108 try { 109 110 // Create a menu bar for this window. 111 JMenuBar menuBar = createMenuBar(); 112 this.setJMenuBar(menuBar); 113 114 } catch (NoSuchMethodException ex) { 115 // Shouldn't happen if the menu bar methods all exist. 116 Logger.getLogger(AbstractPlotWindow.class.getName()).log(Level.WARNING, "", ex); 117 } 118 119 } 120 121 /** 122 * Sets the title for this window to the specified string. Also notifies the main 123 * application class so that the "Windows" menu gets updated too. 124 * 125 * @param title The title to be displayed in the frame's border. A null value is 126 * treated as an empty string, "". 127 */ 128 @Override 129 public void setTitle(String title) { 130 super.setTitle(title); 131 MDIApplication.windowTitleChanged(this); 132 } 133 134 /** 135 * Return the name to use for the data in this window. 136 * 137 * @return The name to use for the data in this window. 138 */ 139 public String getDataName() { 140 return _dataName; 141 } 142 143 /** 144 * Initializes the menus associated with this window. 145 */ 146 private JMenuBar createMenuBar() throws NoSuchMethodException { 147 JMenuBar menuBar = new JMenuBar(); 148 149 // Set up the file menu. 150 int row = 0; 151 String[][] menuStrings = new String[11][3]; 152 menuStrings[row][0] = RESOURCES.getString("newItemText"); 153 menuStrings[row][1] = RESOURCES.getString("newItemKey"); 154 menuStrings[row][2] = null; 155 ++row; 156 menuStrings[row][0] = RESOURCES.getString("openItemText"); 157 menuStrings[row][1] = RESOURCES.getString("openItemKey"); 158 menuStrings[row][2] = null; 159 ++row; 160 ++row; // Blank line 161 menuStrings[row][0] = RESOURCES.getString("closeItemText"); 162 menuStrings[row][1] = RESOURCES.getString("closeItemKey"); 163 menuStrings[row][2] = "handleClose"; 164 ++row; 165 menuStrings[row][0] = RESOURCES.getString("saveItemText"); 166 menuStrings[row][1] = RESOURCES.getString("saveItemKey"); 167 menuStrings[row][2] = null; 168 ++row; 169 menuStrings[row][0] = RESOURCES.getString("saveAsPNGItemText"); 170 menuStrings[row][1] = null; 171 menuStrings[row][2] = "handleSaveAsPNG"; 172 ++row; 173 if (HAS_SVG) { 174 menuStrings[row][0] = RESOURCES.getString("saveAsSVGItemText"); 175 menuStrings[row][1] = null; 176 menuStrings[row][2] = "handleSaveAsSVG"; 177 ++row; 178 } 179 int exportAsMItem = row; 180 menuStrings[row][0] = RESOURCES.getString("exportAsItemText"); 181 menuStrings[row][1] = null; 182 menuStrings[row][2] = null; 183 ++row; 184 ++row; // Blank line 185 menuStrings[row][0] = RESOURCES.getString("pageSetupItemText"); 186 menuStrings[row][1] = null; 187 menuStrings[row][2] = "handlePageSetup"; 188 ++row; 189 menuStrings[row][0] = RESOURCES.getString("printItemText"); 190 menuStrings[row][1] = RESOURCES.getString("printItemKey"); 191 menuStrings[row][2] = "handlePrint"; 192 JMenu menu = AppUtilities.buildMenu(this, RESOURCES.getString("fileMenuText"), menuStrings); 193 menuBar.add(menu); 194 195 // Add the "Export As" sub-menu. 196 JMenu exportMenu = buildExportMenu(menu.getItem(exportAsMItem).getText()); 197 if (nonNull(exportMenu)) { 198 menu.remove(exportAsMItem); 199 menu.add(exportMenu, exportAsMItem); 200 } 201 202 // Add a Quit menu item (if it isn't automatically present). 203 MDIApplication guiApp = MDIApplication.getInstance(); 204 if (!QuitJMenuItem.isAutomaticallyPresent()) { 205 menu.addSeparator(); 206 menu.add(guiApp.getQuitJMenuItem()); 207 } 208 209 210 // Set up the edit menu. 211 row = 0; 212 menuStrings = new String[6][3]; 213 menuStrings[row][0] = RESOURCES.getString("undoItemText"); 214 menuStrings[row][1] = RESOURCES.getString("undoItemKey"); 215 menuStrings[row][2] = null; 216 ++row; 217 menuStrings[row][0] = RESOURCES.getString("redoItemText"); 218 menuStrings[row][1] = null; 219 menuStrings[row][2] = null; 220 ++row; 221 ++row; // Blank line. 222 menuStrings[row][0] = RESOURCES.getString("cutItemText"); 223 menuStrings[row][1] = RESOURCES.getString("cutItemKey"); 224 menuStrings[row][2] = null; 225 ++row; 226 menuStrings[row][0] = RESOURCES.getString("copyItemText"); 227 menuStrings[row][1] = RESOURCES.getString("copyItemKey"); 228 menuStrings[row][2] = "handleCopy"; 229 ++row; 230 menuStrings[row][0] = RESOURCES.getString("pasteItemText"); 231 menuStrings[row][1] = RESOURCES.getString("pasteItemKey"); 232 menuStrings[row][2] = null; 233 menu = AppUtilities.buildMenu(this, RESOURCES.getString("editMenuText"), menuStrings); 234 menuBar.add(menu); 235 236 // Create a Window's menu. 237 menu = MDIApplication.newWindowsMenu(RESOURCES.getString("windowsMenuText")); 238 menuBar.add(menu); 239 240 // Create an about menu item (if not automatically present). 241 if (!AboutJMenuItem.isAutomaticallyPresent()) { 242 // Create Help menu. 243 menu = new JMenu(RESOURCES.getString("helpMenuText")); 244 menuBar.add(menu); 245 246 // Add the "About" item to the Help menu. 247 menu.add(guiApp.getAboutJMenuItem()); 248 } 249 250 return menuBar; 251 } 252 253 /** 254 * Method that will build an "Export" menu containing a list of all the objects that 255 * are capable of writing to files. 256 * 257 * @param title The title for the menu that is created. 258 * @return A JMenu instance containing a list of all the objects that can write to 259 * files in various formats. If there are no appropriate readers available, 260 * null is returned. 261 * @throws java.lang.NoSuchMethodException 262 */ 263 protected abstract JMenu buildExportMenu(String title) throws NoSuchMethodException; 264 265 /** 266 * Return the panel used to display the chart (may be <code>null</code> if the chart 267 * has not been created yet). 268 * 269 * @return The panel used to display the chart. 270 */ 271 public ChartPanel getChartPanel() { 272 return _chartPanel; 273 } 274 275 /** 276 * Creates a panel that contains the chart of the data and modifies the chart pop-up 277 * menu to provide custom behavior. 278 * 279 * @param chart The chart to add to the chart panel. 280 * @param target The object that receives the action events. Must have the following 281 * methods defined: "handlePrint(ActionEvent e)", 282 * "handlePageSetup(ActionEvent e)", "handleSaveAsPNG(ActionEvent e)", 283 * and "handleSaveAsSVG(ActionEvent e)". 284 * @throws java.lang.NoSuchMethodException 285 * @see getChartPanel 286 */ 287 protected void createChartPanel(JFreeChart chart, Object target) throws NoSuchMethodException { 288 289 _chartPanel = new ChartPanel(chart); 290 _chartPanel.setMouseZoomable(true); 291 _chartPanel.setFillZoomRectangle(false); // Use an outline (performance issues on Windows with filled rectangle). 292 _chartPanel.setHorizontalAxisTrace(false); 293 _chartPanel.setVerticalAxisTrace(false); 294 _chartPanel.setPreferredSize(new Dimension(FRAME_WIDTH-20, FRAME_HEIGHT-50)); 295 296 // Modify the chart panel's pop-up menu. 297 JPopupMenu chartMenu = _chartPanel.getPopupMenu(); 298 299 // Replace the default "Print..." menu item with our own. 300 chartMenu.remove(5); 301 302 // Add our own "Print..." item to chart menu. 303 JMenuItem item = new JMenuItem(RESOURCES.getString("printItemText")); 304 ActionListener listener = AppUtilities.getActionListenerForMethod(target, "handlePrint"); 305 item.addActionListener(listener); 306 item.setActionCommand(RESOURCES.getString("printItemText")); 307 item.setEnabled(true); 308 chartMenu.add(item, 5); 309 310 // Add "Page Setup" to the chart menu. 311 item = new JMenuItem(RESOURCES.getString("pageSetupItemText")); 312 listener = AppUtilities.getActionListenerForMethod(target, "handlePageSetup"); 313 item.addActionListener(listener); 314 item.setActionCommand(RESOURCES.getString("pageSetupItemText")); 315 item.setEnabled(true); 316 chartMenu.add(item, 5); 317 318 // Replace the default "Save As..." item with our own implementation. 319 chartMenu.remove(3); 320 321 // Add our own "Save As PNG..." item to chart menu. 322 item = new JMenuItem(RESOURCES.getString("saveAsPNGItemText")); 323 listener = AppUtilities.getActionListenerForMethod(this, "handleSaveAsPNG"); 324 item.addActionListener(listener); 325 item.setActionCommand(RESOURCES.getString("saveAsPNGItemText")); 326 item.setEnabled(true); 327 chartMenu.add(item, 3); 328 329 if (HAS_SVG) { 330 // Add our own "Save As SVG..." item to chart menu. 331 item = new JMenuItem(RESOURCES.getString("saveAsSVGItemText")); 332 listener = AppUtilities.getActionListenerForMethod(this, "handleSaveAsSVG"); 333 item.addActionListener(listener); 334 item.setActionCommand(RESOURCES.getString("saveAsSVGItemText")); 335 item.setEnabled(true); 336 chartMenu.add(item, 4); 337 } 338 } 339 340 /** 341 * Handle the user choosing "Close" from the File menu. This implementation dispatches 342 * a "Window Closing" event to this window. 343 * 344 * @param event The event that caused this method to be called. 345 */ 346 public void handleClose(ActionEvent event) { 347 this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); 348 } 349 350 /** 351 * Handle the user choosing "Save As PNG..." from the File menu. Saves the currently 352 * displayed plot as a PNG format image file. 353 * 354 * @param event The event that caused this method to be called. 355 */ 356 public void handleSaveAsPNG(ActionEvent event) { 357 try { 358 // Go off and actually do the Save As PNG.... Catch exceptions here. 359 doSaveAsPNG(); 360 361 } catch (IOException e) { 362 String msg = e.getMessage(); 363 if (isNull(msg)) { 364 Throwable cause = e.getCause(); 365 if (nonNull(cause)) 366 msg = cause.getMessage(); 367 } 368 JOptionPane.showMessageDialog(this, msg, RESOURCES.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE); 369 370 } catch (Exception ex) { 371 AppUtilities.showException(this, RESOURCES.getString("unexpectedTitle"), RESOURCES.getString("unexpectedMsg"), ex); 372 Logger.getLogger(AbstractPlotWindow.class.getName()).log(Level.WARNING, "", ex); 373 } 374 375 } 376 377 /** 378 * Handle the user choosing "Save As SVG..." from the File menu. Saves the currently 379 * displayed plot as an SVG format file. 380 * 381 * @param event The event that caused this method to be called. 382 */ 383 public void handleSaveAsSVG(ActionEvent event) { 384 try { 385 // Go off and actually do the Save As SVG.... Catch exceptions here. 386 doSaveAsSVG(); 387 388 } catch (IOException e) { 389 String msg = e.getMessage(); 390 if (isNull(msg)) { 391 Throwable cause = e.getCause(); 392 if (nonNull(cause)) 393 msg = cause.getMessage(); 394 } 395 JOptionPane.showMessageDialog(this, msg, RESOURCES.getString("ioErrorTitle"), JOptionPane.ERROR_MESSAGE); 396 397 } catch (Exception ex) { 398 AppUtilities.showException(this, RESOURCES.getString("unexpectedTitle"), RESOURCES.getString("unexpectedMsg"), ex); 399 Logger.getLogger(AbstractPlotWindow.class.getName()).log(Level.WARNING, "", ex); 400 } 401 402 } 403 404 /** 405 * Handle the user choosing "Page Setup..." from the File menu. Displays a Page Setup 406 * dialog allowing the user to change the page settings. 407 * 408 * @param event The event that caused this method to be called. 409 */ 410 public void handlePageSetup(ActionEvent event) { 411 PrinterJob job = PrinterJob.getPrinterJob(); 412 if (isNull(_pf)) 413 _pf = job.defaultPage(); 414 _pf = job.pageDialog(_pf); 415 } 416 417 /** 418 * Handle the user choosing "Print" from the File menu. Prints the currently displayed 419 * plot. 420 * 421 * @param event The event that caused this method to be called. 422 */ 423 public void handlePrint(ActionEvent event) { 424 PrinterJob job = PrinterJob.getPrinterJob(); 425 426 if (isNull(_pf)) 427 _pf = job.defaultPage(); 428 job.setPrintable(getChartPanel(), _pf); 429 430 if (job.printDialog()) { 431 try { 432 433 job.print(); 434 435 } catch (PrinterException e) { 436 JOptionPane.showMessageDialog(this, e); 437 } 438 } 439 440 } 441 442 /** 443 * Handle the user choosing "Save As PNG..." from the File menu. This asks the user 444 * for input and then saves the file. This method throws all exceptions. 445 * 446 * @return true if the user cancels, false if the file was saved. 447 */ 448 private boolean doSaveAsPNG() throws IOException { 449 450 // Build the filename to prompt the user with. 451 String extension = "png"; 452 String dir = GeomSSGUI.getInstance().getPreferences().getLastPath(); 453 String fileName = _dataName; 454 fileName += "." + extension; 455 456 ExtFilenameFilter fnFilter = new ExtFilenameFilter(); 457 fnFilter.addExtension(extension); 458 459 // Ask the user to select a file for saving. 460 File theFile = AppUtilities.selectFile4Save(this, 461 MessageFormat.format(RESOURCES.getString("fileSaveDialog"), extension.toUpperCase()), 462 dir, fileName, fnFilter, extension, 463 RESOURCES.getString("fileExists"), RESOURCES.getString("warningTitle")); 464 465 if (nonNull(theFile)) { 466 if (theFile.exists() && !theFile.canWrite()) 467 throw new IOException(RESOURCES.getString("canNotWrite2File")); 468 469 // Write out the PNG file. 470 ChartPanel plotPanel = getChartPanel(); 471 ChartUtilities.saveChartAsPNG(theFile, plotPanel.getChart(), 472 plotPanel.getWidth(), plotPanel.getHeight()); 473 474 return false; 475 } 476 477 return true; 478 } 479 480 /** 481 * Handle the user choosing "Save As SVG..." from the File menu. This asks the user 482 * for input and then saves the file. This method throws all exceptions. 483 * 484 * @return true if the user cancels, false if the file was saved. 485 */ 486 private boolean doSaveAsSVG() throws IOException { 487 488 // Build the filename to prompt the user with. 489 String extension = "svg"; 490 String dir = GeomSSGUI.getInstance().getPreferences().getLastPath(); 491 String fileName = _dataName; 492 fileName += "." + extension; 493 494 ExtFilenameFilter fnFilter = new ExtFilenameFilter(); 495 fnFilter.addExtension(extension); 496 497 // Ask the user to select a file for saving. 498 File theFile = AppUtilities.selectFile4Save(this, 499 MessageFormat.format(RESOURCES.getString("fileSaveDialog"), extension.toUpperCase()), 500 dir, fileName, fnFilter, extension, 501 RESOURCES.getString("fileExists"), RESOURCES.getString("warningTitle")); 502 503 if (nonNull(theFile)) { 504 if (theFile.exists() && !theFile.canWrite()) 505 throw new IOException(RESOURCES.getString("canNotWrite2File")); 506 507 // Write out the SVG file. 508 try (FileOutputStream outputStream = new FileOutputStream(theFile)) { 509 ChartPanel plotPanel = getChartPanel(); 510 saveChartAsSVG(plotPanel.getChart(), outputStream, plotPanel.getWidth(), plotPanel.getHeight()); 511 } 512 513 return false; 514 } 515 516 return true; 517 } 518 519 /** 520 * Write .svg file based on provided JFreeChart. 521 * 522 * @param aChart Chart to be written out as SVG. 523 * @param outputStream An OutputStream to a file that will hold SVG XML. 524 * @param aWidth Width of image. 525 * @param aHeight Height of image. 526 * @throws java.io.IOException If there is any problem saving the SVG file. 527 */ 528 private void saveChartAsSVG(JFreeChart aChart, OutputStream outputStream, 529 int aWidth, int aHeight) throws IOException { 530 531 // use reflection to get the SVG string 532 String svg = generateSVG(aChart, aWidth, aHeight); 533 if (isNull(svg)) 534 throw new IOException(RESOURCES.getString("svgErrMsg")); 535 536 PrintWriter output = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8")); 537 output.println("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">"); 538 output.println(svg); 539 output.flush(); 540 541 } 542 543 /** 544 * Generates a string containing a rendering of the chart in SVG format. This feature 545 * is only supported if the JFreeSVG library is included on the classpath. 546 * 547 * @return A string containing an SVG element for the current chart, or 548 * <code>null</code> if there is a problem with the method invocation by 549 * reflection. 550 */ 551 private static String generateSVG(JFreeChart chart, int width, int height) { 552 Graphics2D g2 = createSVGGraphics2D(width, height); 553 if (isNull(g2)) { 554 throw new IllegalStateException("The JFreeSVG library could not be found."); 555 } 556 // we suppress shadow generation, because SVG is a vector format and 557 // the shadow effect is applied via bitmap effects... 558 g2.setRenderingHint(JFreeChart.KEY_SUPPRESS_SHADOW_GENERATION, true); 559 String svg = null; 560 Rectangle2D drawArea = new Rectangle2D.Double(0, 0, width, height); 561 chart.draw(g2, drawArea); 562 try { 563 Method m = g2.getClass().getMethod("getSVGElement"); 564 svg = (String)m.invoke(g2); 565 } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) { 566 // null will be returned 567 } 568 569 return svg; 570 } 571 572 /** 573 * Create and return a Graphics2D that points to an SVG writing library. If the SVG 574 * library can not be found, null is returned. 575 * 576 * @param w The width of the SVG Graphics2D to create in pixels. 577 * @param h The height of the SVG Graphics2D to create in pixels. 578 * @return A Graphics2D that points to an SVG writing library or null if the SVG 579 * library could not be found. 580 */ 581 private static Graphics2D createSVGGraphics2D(int w, int h) { 582 try { 583 Class svgGraphics2d = Class.forName("org.jfree.graphics2d.svg.SVGGraphics2D"); 584 Constructor ctor = svgGraphics2d.getConstructor(int.class, int.class); 585 return (Graphics2D)ctor.newInstance(w, h); 586 } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { 587 return null; 588 } 589 } 590 591 /** 592 * Handle the user choosing "Copy" from the Edit menu. Copies the currently displayed 593 * plot as an image to the clipboard. 594 * 595 * @param event The event that caused this method to be called. 596 */ 597 public void handleCopy(ActionEvent event) { 598 599 // Get a copy of the current plot as an image. 600 ChartPanel plotPanel = getChartPanel(); 601 int width = plotPanel.getWidth(); 602 int height = plotPanel.getHeight(); 603 JFreeChart chart = plotPanel.getChart(); 604 Image chartImage = chart.createBufferedImage(width * 2, height * 2, width, height, null); 605 606 // Place the image in the clipboard. 607 setClipboard(chartImage); 608 } 609 610 /** 611 * Method that places an image on the clipboard. 612 */ 613 private static void setClipboard(Image image) { 614 ImageSelection imgSel = new ImageSelection(image); 615 Toolkit.getDefaultToolkit().getSystemClipboard().setContents(imgSel, null); 616 } 617}