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