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>", "&lt;init&gt;") + "\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 &lt;, &gt; and &amp; characters
1113         */
1114        private static String escapeXml(String input) {
1115            String s = input == null ? "" : input.replace("&", "&amp;");
1116            s = s.replace("<", "&lt;");
1117            return s = s.replace(">", "&gt;");
1118        }
1119    }