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 }