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