001 /* 002 * $Id: BasicErrorPaneUI.java 3354 2009-05-29 19:34:56Z kschaefe $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 package org.jdesktop.swingx.plaf.basic; 022 023 import java.awt.BorderLayout; 024 import java.awt.Component; 025 import java.awt.Container; 026 import java.awt.Dialog; 027 import java.awt.Dimension; 028 import java.awt.Frame; 029 import java.awt.GridBagConstraints; 030 import java.awt.GridBagLayout; 031 import java.awt.Insets; 032 import java.awt.LayoutManager; 033 import java.awt.Point; 034 import java.awt.Window; 035 import java.awt.datatransfer.StringSelection; 036 import java.awt.datatransfer.Transferable; 037 import java.awt.event.ActionEvent; 038 import java.awt.event.ActionListener; 039 import java.awt.event.ComponentAdapter; 040 import java.awt.event.ComponentEvent; 041 import java.awt.event.KeyEvent; 042 import java.awt.event.WindowAdapter; 043 import java.awt.event.WindowEvent; 044 import java.beans.PropertyChangeEvent; 045 import java.beans.PropertyChangeListener; 046 import java.util.logging.Level; 047 048 import javax.swing.AbstractAction; 049 import javax.swing.AbstractButton; 050 import javax.swing.Action; 051 import javax.swing.BorderFactory; 052 import javax.swing.Icon; 053 import javax.swing.JButton; 054 import javax.swing.JComponent; 055 import javax.swing.JDialog; 056 import javax.swing.JEditorPane; 057 import javax.swing.JFrame; 058 import javax.swing.JInternalFrame; 059 import javax.swing.JLabel; 060 import javax.swing.JOptionPane; 061 import javax.swing.JPanel; 062 import javax.swing.JScrollPane; 063 import javax.swing.KeyStroke; 064 import javax.swing.LookAndFeel; 065 import javax.swing.SwingUtilities; 066 import javax.swing.TransferHandler; 067 import javax.swing.UIManager; 068 import javax.swing.border.EmptyBorder; 069 import javax.swing.plaf.ComponentUI; 070 import javax.swing.plaf.UIResource; 071 import javax.swing.plaf.basic.BasicHTML; 072 import javax.swing.text.JTextComponent; 073 import javax.swing.text.StyledEditorKit; 074 import javax.swing.text.html.HTMLEditorKit; 075 076 import org.jdesktop.swingx.JXEditorPane; 077 import org.jdesktop.swingx.JXErrorPane; 078 import org.jdesktop.swingx.action.AbstractActionExt; 079 import org.jdesktop.swingx.error.ErrorInfo; 080 import org.jdesktop.swingx.error.ErrorLevel; 081 import org.jdesktop.swingx.error.ErrorReporter; 082 import org.jdesktop.swingx.plaf.ErrorPaneUI; 083 import org.jdesktop.swingx.plaf.UIManagerExt; 084 import org.jdesktop.swingx.util.WindowUtils; 085 086 /** 087 * Base implementation of the <code>JXErrorPane</code> UI. 088 * 089 * @author rbair 090 * @author rah003 091 */ 092 public class BasicErrorPaneUI extends ErrorPaneUI { 093 /** 094 * Used as a prefix when pulling data out of UIManager for i18n 095 */ 096 protected static final String CLASS_NAME = "JXErrorPane"; 097 098 /** 099 * The error pane this UI is for 100 */ 101 protected JXErrorPane pane; 102 /** 103 * Error message text area 104 */ 105 protected JEditorPane errorMessage; 106 107 /** 108 * Error message text scroll pane wrapper. 109 */ 110 protected JScrollPane errorScrollPane; 111 /** 112 * details text area 113 */ 114 protected JXEditorPane details; 115 /** 116 * detail button 117 */ 118 protected AbstractButton detailButton; 119 /** 120 * ok/close button 121 */ 122 protected JButton closeButton; 123 /** 124 * label used to display the warning/error icon 125 */ 126 protected JLabel iconLabel; 127 /** 128 * report an error button 129 */ 130 protected AbstractButton reportButton; 131 /** 132 * details panel 133 */ 134 protected JPanel detailsPanel; 135 protected JScrollPane detailsScrollPane; 136 protected JButton copyToClipboardButton; 137 138 /** 139 * Property change listener for the error pane ensures that the pane's UI 140 * is reinitialized. 141 */ 142 protected PropertyChangeListener errorPaneListener; 143 144 /** 145 * Action listener for the detail button. 146 */ 147 protected ActionListener detailListener; 148 149 /** 150 * Action listener for the copy to clipboard button. 151 */ 152 protected ActionListener copyToClipboardListener; 153 154 //------------------------------------------------------ private helpers 155 /** 156 * The height of the window when collapsed. This value is stashed when the 157 * dialog is expanded 158 */ 159 private int collapsedHeight = 0; 160 /** 161 * The height of the window when last expanded. This value is stashed when 162 * the dialog is collapsed 163 */ 164 private int expandedHeight = 0; 165 166 //---------------------------------------------------------- constructor 167 168 /** 169 * @inheritDoc 170 */ 171 public static ComponentUI createUI(JComponent c) { 172 return new BasicErrorPaneUI(); 173 } 174 175 /** 176 * {@inheritDoc} 177 */ 178 @Override 179 public void installUI(JComponent c) { 180 super.installUI(c); 181 182 this.pane = (JXErrorPane)c; 183 184 installDefaults(); 185 installComponents(); 186 installListeners(); 187 188 //if the report action needs to be defined, do so 189 Action a = c.getActionMap().get(JXErrorPane.REPORT_ACTION_KEY); 190 if (a == null) { 191 final JXErrorPane pane = (JXErrorPane)c; 192 AbstractActionExt reportAction = new AbstractActionExt() { 193 public void actionPerformed(ActionEvent e) { 194 ErrorReporter reporter = pane.getErrorReporter(); 195 if (reporter != null) { 196 reporter.reportError(pane.getErrorInfo()); 197 } 198 } 199 }; 200 configureReportAction(reportAction); 201 c.getActionMap().put(JXErrorPane.REPORT_ACTION_KEY, reportAction); 202 } 203 } 204 205 /** 206 * {@inheritDoc} 207 */ 208 @Override 209 public void uninstallUI(JComponent c) { 210 super.uninstallUI(c); 211 212 uninstallListeners(); 213 uninstallComponents(); 214 uninstallDefaults(); 215 } 216 217 /** 218 * Installs the default colors, and default font into the Error Pane 219 */ 220 protected void installDefaults() { 221 } 222 223 224 /** 225 * Uninstalls the default colors, and default font into the Error Pane. 226 */ 227 protected void uninstallDefaults() { 228 LookAndFeel.uninstallBorder(pane); 229 } 230 231 232 /** 233 * Create and install the listeners for the Error Pane. 234 * This method is called when the UI is installed. 235 */ 236 protected void installListeners() { 237 //add a listener to the pane so I can reinit() whenever the 238 //bean properties change (particularly error info) 239 errorPaneListener = new ErrorPaneListener(); 240 pane.addPropertyChangeListener(errorPaneListener); 241 } 242 243 244 /** 245 * Remove the installed listeners from the Error Pane. 246 * The number and types of listeners removed and in this method should be 247 * the same that was added in <code>installListeners</code> 248 */ 249 protected void uninstallListeners() { 250 //remove the property change listener from the pane 251 pane.removePropertyChangeListener(errorPaneListener); 252 } 253 254 255 // =============================== 256 // begin Sub-Component Management 257 // 258 259 /** 260 * Creates and initializes the components which make up the 261 * aggregate combo box. This method is called as part of the UI 262 * installation process. 263 */ 264 protected void installComponents() { 265 iconLabel = new JLabel(pane.getIcon()); 266 267 errorMessage = new JEditorPane(); 268 errorMessage.setEditable(false); 269 errorMessage.setContentType("text/html"); 270 errorMessage.setEditorKitForContentType("text/plain", new StyledEditorKit()); 271 errorMessage.setEditorKitForContentType("text/html", new HTMLEditorKit()); 272 273 errorMessage.setOpaque(false); 274 errorMessage.putClientProperty(JXEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); 275 276 closeButton = new JButton(UIManagerExt.getString( 277 CLASS_NAME + ".ok_button_text", errorMessage.getLocale())); 278 279 reportButton = new EqualSizeJButton(pane.getActionMap().get(JXErrorPane.REPORT_ACTION_KEY)); 280 281 detailButton = new EqualSizeJButton(UIManagerExt.getString( 282 CLASS_NAME + ".details_expand_text", errorMessage.getLocale())); 283 284 details = new JXEditorPane(); 285 details.setContentType("text/html"); 286 details.putClientProperty(JXEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); 287 details.setTransferHandler(createDetailsTransferHandler(details)); 288 detailsScrollPane = new JScrollPane(details); 289 detailsScrollPane.setPreferredSize(new Dimension(10, 250)); 290 details.setEditable(false); 291 detailsPanel = new JPanel(); 292 detailsPanel.setVisible(false); 293 copyToClipboardButton = new JButton(UIManagerExt.getString( 294 CLASS_NAME + ".copy_to_clipboard_button_text", errorMessage.getLocale())); 295 copyToClipboardListener = new ActionListener() { 296 public void actionPerformed(ActionEvent ae) { 297 details.copy(); 298 } 299 }; 300 copyToClipboardButton.addActionListener(copyToClipboardListener); 301 302 detailsPanel.setLayout(createDetailPanelLayout()); 303 detailsPanel.add(detailsScrollPane); 304 detailsPanel.add(copyToClipboardButton); 305 306 // Create error scroll pane. Make sure this happens before call to createErrorPaneLayout() in case any extending 307 // class wants to manipulate the component there. 308 errorScrollPane = new JScrollPane(errorMessage); 309 errorScrollPane.setBorder(new EmptyBorder(0,0,5,0)); 310 errorScrollPane.setOpaque(false); 311 errorScrollPane.getViewport().setOpaque(false); 312 313 //initialize the gui. Most of this code is similar between Mac and PC, but 314 //where they differ protected methods have been written allowing the 315 //mac implementation to alter the layout of the dialog. 316 pane.setLayout(createErrorPaneLayout()); 317 318 //An empty border which constitutes the padding from the edge of the 319 //dialog to the content. All content that butts against this border should 320 //not be padded. 321 Insets borderInsets = new Insets(16, 24, 16, 17); 322 pane.setBorder(BorderFactory.createEmptyBorder(borderInsets.top, borderInsets.left, borderInsets.bottom, borderInsets.right)); 323 324 //add the JLabel responsible for displaying the icon. 325 //TODO: in the future, replace this usage of a JLabel with a JXImagePane, 326 //which may add additional "coolness" such as allowing the user to drag 327 //the image off the dialog onto the desktop. This kind of coolness is common 328 //in the mac world. 329 pane.add(iconLabel); 330 pane.add(errorScrollPane); 331 pane.add(closeButton); 332 pane.add(reportButton); 333 reportButton.setVisible(false); // not visible by default 334 pane.add(detailButton); 335 pane.add(detailsPanel); 336 337 //make the buttons the same size 338 EqualSizeJButton[] buttons = new EqualSizeJButton[] { 339 (EqualSizeJButton)detailButton, (EqualSizeJButton)reportButton }; 340 ((EqualSizeJButton)reportButton).setGroup(buttons); 341 ((EqualSizeJButton)detailButton).setGroup(buttons); 342 343 reportButton.setMinimumSize(reportButton.getPreferredSize()); 344 detailButton.setMinimumSize(detailButton.getPreferredSize()); 345 346 //set the event handling 347 detailListener = new DetailsClickEvent(); 348 detailButton.addActionListener(detailListener); 349 } 350 351 /** 352 * The aggregate components which compise the combo box are 353 * unregistered and uninitialized. This method is called as part of the 354 * UI uninstallation process. 355 */ 356 protected void uninstallComponents() { 357 iconLabel = null; 358 errorMessage = null; 359 closeButton = null; 360 reportButton = null; 361 362 detailButton.removeActionListener(detailListener); 363 detailButton = null; 364 365 details.setTransferHandler(null); 366 details = null; 367 368 detailsScrollPane.removeAll(); 369 detailsScrollPane = null; 370 371 detailsPanel.setLayout(null); 372 detailsPanel.removeAll(); 373 detailsPanel = null; 374 375 copyToClipboardButton.removeActionListener(copyToClipboardListener); 376 copyToClipboardButton = null; 377 378 pane.removeAll(); 379 pane.setLayout(null); 380 pane.setBorder(null); 381 } 382 383 // 384 // end Sub-Component Management 385 // =============================== 386 387 /** 388 * @inheritDoc 389 */ 390 @Override 391 public JFrame getErrorFrame(Component owner) { 392 reinit(); 393 expandedHeight = 0; 394 collapsedHeight = 0; 395 JXErrorFrame frame = new JXErrorFrame(pane); 396 centerWindow(frame, owner); 397 return frame; 398 } 399 400 /** 401 * @inheritDoc 402 */ 403 @Override 404 public JDialog getErrorDialog(Component owner) { 405 reinit(); 406 expandedHeight = 0; 407 collapsedHeight = 0; 408 Window w = WindowUtils.findWindow(owner); 409 JXErrorDialog dlg = null; 410 if (w instanceof Dialog) { 411 dlg = new JXErrorDialog((Dialog)w, pane); 412 } else if (w instanceof Frame) { 413 dlg = new JXErrorDialog((Frame)w, pane); 414 } else { 415 // default fallback to null 416 dlg = new JXErrorDialog(JOptionPane.getRootFrame(), pane); 417 } 418 centerWindow(dlg, owner); 419 return dlg; 420 } 421 422 /** 423 * @inheritDoc 424 */ 425 @Override 426 public JInternalFrame getErrorInternalFrame(Component owner) { 427 reinit(); 428 expandedHeight = 0; 429 collapsedHeight = 0; 430 JXInternalErrorFrame frame = new JXInternalErrorFrame(pane); 431 centerWindow(frame, owner); 432 return frame; 433 } 434 435 /** 436 * Create and return the LayoutManager to use with the error pane. 437 */ 438 protected LayoutManager createErrorPaneLayout() { 439 return new ErrorPaneLayout(); 440 } 441 442 protected LayoutManager createDetailPanelLayout() { 443 GridBagLayout layout = new GridBagLayout(); 444 layout.addLayoutComponent(detailsScrollPane, new GridBagConstraints(0,0,1,1,1.0,1.0,GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(6,0,0,0),0,0)); 445 GridBagConstraints gbc = new GridBagConstraints(); 446 gbc.anchor = GridBagConstraints.LINE_END; 447 gbc.fill = GridBagConstraints.NONE; 448 gbc.gridwidth = 1; 449 gbc.gridx = 0; 450 gbc.gridy = 1; 451 gbc.weighty = 0.0; 452 gbc.weightx = 1.0; 453 gbc.insets = new Insets(6, 0, 6, 0); 454 layout.addLayoutComponent(copyToClipboardButton, gbc); 455 return layout; 456 } 457 458 public Dimension calculatePreferredSize() { 459 //TODO returns a Dimension that is either X wide, or as wide as necessary 460 //to show the title. It is Y high. 461 return new Dimension(iconLabel.getPreferredSize().width + errorMessage.getPreferredSize().width, 206); 462 } 463 464 protected int getDetailsHeight() { 465 return 300; 466 } 467 468 protected void configureReportAction(AbstractActionExt reportAction) { 469 reportAction.setName(UIManagerExt.getString(CLASS_NAME + ".report_button_text", pane.getLocale())); 470 } 471 472 //----------------------------------------------- private helper methods 473 474 /** 475 * Creates and returns a TransferHandler which can be used to copy the details 476 * from the details component. It also disallows pasting into the component, or 477 * cutting from the component. 478 * 479 * @return a TransferHandler for the details area 480 */ 481 private TransferHandler createDetailsTransferHandler(JTextComponent detailComponent) { 482 return new DetailsTransferHandler(detailComponent); 483 } 484 485 /** 486 * @return the default error icon 487 */ 488 protected Icon getDefaultErrorIcon() { 489 try { 490 Icon icon = UIManager.getIcon(CLASS_NAME + ".errorIcon"); 491 return icon == null ? UIManager.getIcon("OptionPane.errorIcon") : icon; 492 } catch (Exception e) { 493 return null; 494 } 495 } 496 497 /** 498 * @return the default warning icon 499 */ 500 protected Icon getDefaultWarningIcon() { 501 try { 502 Icon icon = UIManager.getIcon(CLASS_NAME + ".warningIcon"); 503 return icon == null ? UIManager.getIcon("OptionPane.warningIcon") : icon; 504 } catch (Exception e) { 505 return null; 506 } 507 } 508 509 /** 510 * Set the details section of the error dialog. If the details are either 511 * null or an empty string, then hide the details button and hide the detail 512 * scroll pane. Otherwise, just set the details section. 513 * 514 * @param details Details to be shown in the detail section of the dialog. 515 * This can be null if you do not want to display the details section of the 516 * dialog. 517 */ 518 private void setDetails(String details) { 519 if (details == null || details.equals("")) { 520 detailButton.setVisible(false); 521 } else { 522 this.details.setText(details); 523 detailButton.setVisible(true); 524 } 525 } 526 527 protected void configureDetailsButton(boolean expanded) { 528 if (expanded) { 529 detailButton.setText(UIManagerExt.getString( 530 CLASS_NAME + ".details_contract_text", detailButton.getLocale())); 531 } else { 532 detailButton.setText(UIManagerExt.getString( 533 CLASS_NAME + ".details_expand_text", detailButton.getLocale())); 534 } 535 } 536 537 /** 538 * Set the details section to be either visible or invisible. Set the 539 * text of the Details button accordingly. 540 * @param b if true details section will be visible 541 */ 542 private void setDetailsVisible(boolean b) { 543 if (b) { 544 collapsedHeight = pane.getHeight(); 545 pane.setSize(pane.getWidth(), expandedHeight == 0 ? collapsedHeight + getDetailsHeight() : expandedHeight); 546 detailsPanel.setVisible(true); 547 configureDetailsButton(true); 548 detailsPanel.applyComponentOrientation(detailButton.getComponentOrientation()); 549 550 // workaround for bidi bug, if the text is not set "again" and the component orientation has changed 551 // then the text won't be aligned correctly. To reproduce this (in JDK 1.5) show two dialogs in one 552 // use LTOR orientation and in the second use RTOL orientation and press "details" in both. 553 // Text in the text box should be aligned to right/left respectively, without this line this doesn't 554 // occure I assume because bidi properties are tested when the text is set and are not updated later 555 // on when setComponentOrientation is invoked. 556 details.setText(details.getText()); 557 details.setCaretPosition(0); 558 } else if (collapsedHeight != 0) { //only collapse if the dialog has been expanded 559 expandedHeight = pane.getHeight(); 560 detailsPanel.setVisible(false); 561 configureDetailsButton(false); 562 // Trick to force errorMessage JTextArea to resize according 563 // to its columns property. 564 errorMessage.setSize(0, 0); 565 errorMessage.setSize(errorMessage.getPreferredSize()); 566 pane.setSize(pane.getWidth(), collapsedHeight); 567 } else { 568 detailsPanel.setVisible(false); 569 } 570 571 pane.doLayout(); 572 } 573 574 /** 575 * Set the error message for the dialog box 576 * @param errorMessage Message for the error dialog 577 */ 578 private void setErrorMessage(String errorMessage) { 579 if(BasicHTML.isHTMLString(errorMessage)) { 580 this.errorMessage.setContentType("text/html"); 581 } else { 582 this.errorMessage.setContentType("text/plain"); 583 } 584 this.errorMessage.setText(errorMessage); 585 this.errorMessage.setCaretPosition(0); 586 } 587 588 /** 589 * Reconfigures the dialog if settings have changed, such as the 590 * errorInfo, errorIcon, warningIcon, etc 591 */ 592 protected void reinit() { 593 setDetailsVisible(false); 594 Action reportAction = pane.getActionMap().get(JXErrorPane.REPORT_ACTION_KEY); 595 reportButton.setAction(reportAction); 596 reportButton.setVisible(reportAction != null && reportAction.isEnabled() && pane.getErrorReporter() != null); 597 reportButton.setEnabled(reportButton.isVisible()); 598 ErrorInfo errorInfo = pane.getErrorInfo(); 599 if (errorInfo == null) { 600 iconLabel.setIcon(pane.getIcon()); 601 setErrorMessage(""); 602 closeButton.setText(UIManagerExt.getString( 603 CLASS_NAME + ".ok_button_text", closeButton.getLocale())); 604 setDetails(""); 605 //TODO Does this ever happen? It seems like if errorInfo is null and 606 //this is called, it would be an IllegalStateException. 607 } else { 608 //change the "closeButton"'s text to either the default "ok"/"close" text 609 //or to the "fatal" text depending on the error level of the incident info 610 if (errorInfo.getErrorLevel() == ErrorLevel.FATAL) { 611 closeButton.setText(UIManagerExt.getString( 612 CLASS_NAME + ".fatal_button_text", closeButton.getLocale())); 613 } else { 614 closeButton.setText(UIManagerExt.getString( 615 CLASS_NAME + ".ok_button_text", closeButton.getLocale())); 616 } 617 618 //if the icon for the pane has not been specified by the developer, 619 //then set it to the default icon based on the error level 620 Icon icon = pane.getIcon(); 621 if (icon == null || icon instanceof UIResource) { 622 if (errorInfo.getErrorLevel().intValue() <= Level.WARNING.intValue()) { 623 icon = getDefaultWarningIcon(); 624 } else { 625 icon = getDefaultErrorIcon(); 626 } 627 } 628 iconLabel.setIcon(icon); 629 setErrorMessage(errorInfo.getBasicErrorMessage()); 630 String details = errorInfo.getDetailedErrorMessage(); 631 if(details == null) { 632 details = getDetailsAsHTML(errorInfo); 633 } 634 setDetails(details); 635 } 636 } 637 638 /** 639 * Creates and returns HTML representing the details of this incident info. This 640 * method is only called if the details needs to be generated: ie: the detailed 641 * error message property of the incident info is null. 642 */ 643 protected String getDetailsAsHTML(ErrorInfo errorInfo) { 644 if(errorInfo.getErrorException() != null) { 645 //convert the stacktrace into a more pleasent bit of HTML 646 StringBuffer html = new StringBuffer("<html>"); 647 html.append("<h2>" + escapeXml(errorInfo.getTitle()) + "</h2>"); 648 html.append("<HR size='1' noshade>"); 649 html.append("<div></div>"); 650 html.append("<b>Message:</b>"); 651 html.append("<pre>"); 652 html.append(" " + escapeXml(errorInfo.getErrorException().toString())); 653 html.append("</pre>"); 654 html.append("<b>Level:</b>"); 655 html.append("<pre>"); 656 html.append(" " + errorInfo.getErrorLevel()); 657 html.append("</pre>"); 658 html.append("<b>Stack Trace:</b>"); 659 Throwable ex = errorInfo.getErrorException(); 660 while(ex != null) { 661 html.append("<h4>"+ex.getMessage()+"</h4>"); 662 html.append("<pre>"); 663 for (StackTraceElement el : ex.getStackTrace()) { 664 html.append(" " + el.toString().replace("<init>", "<init>") + "\n"); 665 } 666 html.append("</pre>"); 667 ex = ex.getCause(); 668 } 669 html.append("</html>"); 670 return html.toString(); 671 } else { 672 return null; 673 } 674 } 675 676 //------------------------------------------------ actions/inner classes 677 678 /** 679 * Default action for closing the JXErrorPane's enclosing window 680 * (JDialog, JFrame, or JInternalFrame) 681 */ 682 private static final class CloseAction extends AbstractAction { 683 private Window w; 684 685 /** 686 * @param w cannot be null 687 */ 688 private CloseAction(Window w) { 689 if (w == null) { 690 throw new NullPointerException("Window cannot be null"); 691 } 692 this.w = w; 693 } 694 695 /** 696 * @inheritDoc 697 */ 698 public void actionPerformed(ActionEvent e) { 699 w.setVisible(false); 700 w.dispose(); 701 } 702 } 703 704 705 /** 706 * Listener for Details click events. Alternates whether the details section 707 * is visible or not. 708 * 709 * @author rbair 710 */ 711 private final class DetailsClickEvent implements ActionListener { 712 713 /* (non-Javadoc) 714 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) 715 */ 716 public void actionPerformed(ActionEvent e) { 717 setDetailsVisible(!detailsPanel.isVisible()); 718 } 719 } 720 721 private final class ResizeWindow implements ActionListener { 722 private Window w; 723 private ResizeWindow(Window w) { 724 if (w == null) { 725 throw new NullPointerException(); 726 } 727 this.w = w; 728 } 729 730 public void actionPerformed(ActionEvent ae) { 731 Dimension contentSize = null; 732 if (w instanceof JDialog) { 733 contentSize = ((JDialog)w).getContentPane().getSize(); 734 } else { 735 contentSize = ((JFrame)w).getContentPane().getSize(); 736 } 737 738 Dimension dialogSize = w.getSize(); 739 int ydiff = dialogSize.height - contentSize.height; 740 Dimension paneSize = pane.getSize(); 741 w.setSize(new Dimension(dialogSize.width, paneSize.height + ydiff)); 742 w.validate(); 743 w.repaint(); 744 } 745 } 746 747 /** 748 * This is a button that maintains the size of the largest button in the button 749 * group by returning the largest size from the getPreferredSize method. 750 * This is better than using setPreferredSize since this will work regardless 751 * of changes to the text of the button and its language. 752 */ 753 private static final class EqualSizeJButton extends JButton { 754 public EqualSizeJButton() { 755 } 756 757 public EqualSizeJButton(String text) { 758 super(text); 759 } 760 761 public EqualSizeJButton(Action a) { 762 super(a); 763 } 764 765 /** 766 * Buttons whose size should be taken into consideration 767 */ 768 private EqualSizeJButton[] group; 769 770 public void setGroup(EqualSizeJButton[] group) { 771 this.group = group; 772 } 773 774 /** 775 * Returns the actual preferred size on a different instance of this button 776 */ 777 private Dimension getRealPreferredSize() { 778 return super.getPreferredSize(); 779 } 780 781 /** 782 * If the <code>preferredSize</code> has been set to a 783 * non-<code>null</code> value just returns it. 784 * If the UI delegate's <code>getPreferredSize</code> 785 * method returns a non <code>null</code> value then return that; 786 * otherwise defer to the component's layout manager. 787 * 788 * @return the value of the <code>preferredSize</code> property 789 * @see #setPreferredSize 790 * @see ComponentUI 791 */ 792 public Dimension getPreferredSize() { 793 int width = 0; 794 int height = 0; 795 for(int iter = 0 ; iter < group.length ; iter++) { 796 Dimension size = group[iter].getRealPreferredSize(); 797 width = Math.max(size.width, width); 798 height = Math.max(size.height, height); 799 } 800 801 return new Dimension(width, height); 802 } 803 804 } 805 806 /** 807 * Returns the text as non-HTML in a COPY operation, and disabled CUT/PASTE 808 * operations for the Details pane. 809 */ 810 private static final class DetailsTransferHandler extends TransferHandler { 811 private JTextComponent details; 812 private DetailsTransferHandler(JTextComponent detailComponent) { 813 if (detailComponent == null) { 814 throw new NullPointerException("detail component cannot be null"); 815 } 816 this.details = detailComponent; 817 } 818 819 protected Transferable createTransferable(JComponent c) { 820 String text = details.getSelectedText(); 821 if (text == null || text.equals("")) { 822 details.selectAll(); 823 text = details.getSelectedText(); 824 details.select(-1, -1); 825 } 826 return new StringSelection(text); 827 } 828 829 public int getSourceActions(JComponent c) { 830 return TransferHandler.COPY; 831 } 832 833 } 834 835 private final class JXErrorDialog extends JDialog { 836 public JXErrorDialog(Frame parent, JXErrorPane p) { 837 super(parent, true); 838 init(p); 839 } 840 841 public JXErrorDialog(Dialog parent, JXErrorPane p) { 842 super(parent, true); 843 init(p); 844 } 845 846 protected void init(JXErrorPane p) { 847 // FYI: info can be null 848 setTitle(p.getErrorInfo() == null ? null : p.getErrorInfo().getTitle()); 849 initWindow(this, p); 850 } 851 } 852 853 private final class JXErrorFrame extends JFrame { 854 public JXErrorFrame(JXErrorPane p) { 855 setTitle(p.getErrorInfo().getTitle()); 856 initWindow(this, p); 857 } 858 } 859 860 private final class JXInternalErrorFrame extends JInternalFrame { 861 public JXInternalErrorFrame(JXErrorPane p) { 862 setTitle(p.getErrorInfo().getTitle()); 863 864 setLayout(new BorderLayout()); 865 add(p, BorderLayout.CENTER); 866 final Action closeAction = new AbstractAction() { 867 public void actionPerformed(ActionEvent evt) { 868 setVisible(false); 869 dispose(); 870 } 871 }; 872 closeButton.addActionListener(closeAction); 873 addComponentListener(new ComponentAdapter() { 874 public void componentHidden(ComponentEvent e) { 875 //remove the action listener 876 closeButton.removeActionListener(closeAction); 877 exitIfFatal(); 878 } 879 }); 880 881 getRootPane().setDefaultButton(closeButton); 882 setResizable(false); 883 setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE); 884 KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); 885 getRootPane().registerKeyboardAction(closeAction, ks, JComponent.WHEN_IN_FOCUSED_WINDOW); 886 //setPreferredSize(calculatePreferredDialogSize()); 887 } 888 } 889 890 /** 891 * Utility method for initializing a Window for displaying a JXErrorPane. 892 * This is particularly useful because the differences between JFrame and 893 * JDialog are so minor. 894 * removed. 895 */ 896 private void initWindow(final Window w, final JXErrorPane pane) { 897 w.setLayout(new BorderLayout()); 898 w.add(pane, BorderLayout.CENTER); 899 final Action closeAction = new CloseAction(w); 900 closeButton.addActionListener(closeAction); 901 final ResizeWindow resizeListener = new ResizeWindow(w); 902 //make sure this action listener is last (or, oddly, the first in the list) 903 ActionListener[] list = detailButton.getActionListeners(); 904 for (ActionListener a : list) { 905 detailButton.removeActionListener(a); 906 } 907 detailButton.addActionListener(resizeListener); 908 for (ActionListener a : list) { 909 detailButton.addActionListener(a); 910 } 911 912 if (w instanceof JFrame) { 913 final JFrame f = (JFrame)w; 914 f.getRootPane().setDefaultButton(closeButton); 915 f.setResizable(true); 916 f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 917 KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); 918 f.getRootPane().registerKeyboardAction(closeAction, ks, JComponent.WHEN_IN_FOCUSED_WINDOW); 919 } else if (w instanceof JDialog) { 920 final JDialog d = (JDialog)w; 921 d.getRootPane().setDefaultButton(closeButton); 922 d.setResizable(true); 923 d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 924 KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); 925 d.getRootPane().registerKeyboardAction(closeAction, ks, JComponent.WHEN_IN_FOCUSED_WINDOW); 926 } 927 928 w.addWindowListener(new WindowAdapter() { 929 public void windowClosed(WindowEvent e) { 930 //remove the action listener 931 closeButton.removeActionListener(closeAction); 932 detailButton.removeActionListener(resizeListener); 933 exitIfFatal(); 934 } 935 }); 936 w.pack(); 937 } 938 939 private void exitIfFatal() { 940 ErrorInfo info = pane.getErrorInfo(); 941 // FYI: info can be null 942 if (info != null && info.getErrorLevel() == ErrorLevel.FATAL) { 943 Action fatalAction = pane.getActionMap().get(JXErrorPane.FATAL_ACTION_KEY); 944 if (fatalAction == null) { 945 System.exit(1); 946 } else { 947 ActionEvent ae = new ActionEvent(closeButton, -1, "fatal"); 948 fatalAction.actionPerformed(ae); 949 } 950 } 951 } 952 953 private final class ErrorPaneListener implements PropertyChangeListener { 954 public void propertyChange(PropertyChangeEvent evt) { 955 reinit(); 956 } 957 } 958 959 /** 960 * Lays out the BasicErrorPaneUI components. 961 */ 962 private final class ErrorPaneLayout implements LayoutManager { 963 private JEditorPane dummy = new JEditorPane(); 964 965 public void addLayoutComponent(String name, Component comp) {} 966 public void removeLayoutComponent(Component comp) {} 967 968 /** 969 * The preferred size is: 970 * The width of the parent container 971 * The height necessary to show the entire message text 972 * (as long as said height does not go off the screen) 973 * plus the buttons 974 * 975 * The preferred height changes depending on whether the details 976 * are visible, or not. 977 */ 978 public Dimension preferredLayoutSize(Container parent) { 979 int prefWidth = parent.getWidth(); 980 int prefHeight = parent.getHeight(); 981 final Insets insets = parent.getInsets(); 982 int pw = detailButton.isVisible() ? detailButton.getPreferredSize().width : 0; 983 pw += detailButton.isVisible() ? detailButton.getPreferredSize().width : 0; 984 pw += reportButton.isVisible() ? (5 + reportButton.getPreferredSize().width) : 0; 985 pw += closeButton.isVisible() ? (5 + closeButton.getPreferredSize().width) : 0; 986 prefWidth = Math.max(prefWidth, pw) + insets.left + insets.right; 987 if (errorMessage != null) { 988 //set a temp editor to a certain size, just to determine what its 989 //pref height is 990 dummy.setContentType(errorMessage.getContentType()); 991 dummy.setEditorKit(errorMessage.getEditorKit()); 992 dummy.setText(errorMessage.getText()); 993 dummy.setSize(prefWidth, 20); 994 int errorMessagePrefHeight = dummy.getPreferredSize().height; 995 996 prefHeight = 997 //the greater of the error message height or the icon height 998 Math.max(errorMessagePrefHeight, iconLabel.getPreferredSize().height) + 999 //the space between the error message and the button 1000 10 + 1001 //the button preferred height 1002 closeButton.getPreferredSize().height; 1003 1004 if (detailsPanel.isVisible()) { 1005 prefHeight += getDetailsHeight(); 1006 } 1007 1008 } 1009 1010 if (iconLabel != null && iconLabel.getIcon() != null) { 1011 prefWidth += iconLabel.getIcon().getIconWidth(); 1012 prefHeight += 10; // top of icon is positioned 10px above the text 1013 } 1014 1015 return new Dimension( 1016 prefWidth + insets.left + insets.right, 1017 prefHeight + insets.top + insets.bottom); 1018 } 1019 1020 public Dimension minimumLayoutSize(Container parent) { 1021 return preferredLayoutSize(parent); 1022 } 1023 1024 public void layoutContainer(Container parent) { 1025 final Insets insets = parent.getInsets(); 1026 int x = insets.left; 1027 int y = insets.top; 1028 1029 //place the icon 1030 if (iconLabel != null) { 1031 Dimension dim = iconLabel.getPreferredSize(); 1032 iconLabel.setBounds(x, y, dim.width, dim.height); 1033 x += dim.width + 17; 1034 int leftEdge = x; 1035 1036 //place the error message 1037 dummy.setContentType(errorMessage.getContentType()); 1038 dummy.setText(errorMessage.getText()); 1039 dummy.setSize(parent.getWidth() - leftEdge - insets.right, 20); 1040 dim = dummy.getPreferredSize(); 1041 int spx = x; 1042 int spy = y; 1043 Dimension spDim = new Dimension (parent.getWidth() - leftEdge - insets.right, dim.height); 1044 y += dim.height + 10; 1045 int rightEdge = parent.getWidth() - insets.right; 1046 x = rightEdge; 1047 dim = detailButton.getPreferredSize(); //all buttons should be the same height! 1048 int buttonY = y + 5; 1049 if (detailButton.isVisible()) { 1050 dim = detailButton.getPreferredSize(); 1051 x -= dim.width; 1052 detailButton.setBounds(x, buttonY, dim.width, dim.height); 1053 } 1054 if (detailButton.isVisible()) { 1055 detailButton.setBounds(x, buttonY, dim.width, dim.height); 1056 } 1057 errorScrollPane.setBounds(spx, spy, spDim.width, buttonY - spy); 1058 if (reportButton.isVisible()) { 1059 dim = reportButton.getPreferredSize(); 1060 x -= dim.width; 1061 x -= 5; 1062 reportButton.setBounds(x, buttonY, dim.width, dim.height); 1063 } 1064 1065 dim = closeButton.getPreferredSize(); 1066 x -= dim.width; 1067 x -= 5; 1068 closeButton.setBounds(x, buttonY, dim.width, dim.height); 1069 1070 //if the dialog is expanded... 1071 if (detailsPanel.isVisible()) { 1072 //layout the details 1073 y = buttonY + dim.height + 6; 1074 x = leftEdge; 1075 int width = rightEdge - x; 1076 detailsPanel.setBounds(x, y, width, parent.getHeight() - (y + insets.bottom) ); 1077 } 1078 } 1079 } 1080 } 1081 1082 private static void centerWindow(Window w, Component owner) { 1083 //center based on the owner component, if it is not null 1084 //otherwise, center based on the center of the screen 1085 if (owner != null) { 1086 Point p = owner.getLocation(); 1087 p.x += owner.getWidth()/2; 1088 p.y += owner.getHeight()/2; 1089 SwingUtilities.convertPointToScreen(p, owner); 1090 w.setLocation(p); 1091 } else { 1092 w.setLocation(WindowUtils.getPointForCentering(w)); 1093 } 1094 } 1095 1096 private static void centerWindow(JInternalFrame w, Component owner) { 1097 //center based on the owner component, if it is not null 1098 //otherwise, center based on the center of the screen 1099 if (owner != null) { 1100 Point p = owner.getLocation(); 1101 p.x += owner.getWidth()/2; 1102 p.y += owner.getHeight()/2; 1103 SwingUtilities.convertPointToScreen(p, owner); 1104 w.setLocation(p); 1105 } else { 1106 w.setLocation(WindowUtils.getPointForCentering(w)); 1107 } 1108 } 1109 1110 /** 1111 * Converts the incoming string to an escaped output string. This method 1112 * is far from perfect, only escaping <, > and & characters 1113 */ 1114 private static String escapeXml(String input) { 1115 String s = input == null ? "" : input.replace("&", "&"); 1116 s = s.replace("<", "<"); 1117 return s = s.replace(">", ">"); 1118 } 1119 }