001/* Singleton class for displaying exceptions, errors, warnings, and messages.
002
003 Copyright (c) 1999-2017 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.gui;
028
029import java.awt.Component;
030import java.awt.Dimension;
031import java.io.PrintWriter;
032import java.io.StringWriter;
033import java.lang.ref.WeakReference;
034
035import javax.swing.JOptionPane;
036import javax.swing.JScrollPane;
037import javax.swing.JTextArea;
038
039import ptolemy.util.CancelException;
040import ptolemy.util.MessageHandler;
041import ptolemy.util.StringUtilities;
042
043///////////////////////////////////////////////////////////////////
044//// UndeferredGraphicalMessageHandler
045
046/**
047 This is a message handler that reports errors in a graphical dialog box.
048 When an applet or application starts up, it should call setContext()
049 to specify a component with respect to which the display window
050 should be created.  This ensures that if the application is iconified
051 or deiconified, that the display window goes with it. If the context
052 is not specified, then the display window is centered on the screen,
053 but iconifying and deiconifying may not work as desired.
054 <p>
055
056 <p>Note that to display a window with an error message, this graphical
057 handler must be registered by calling
058 {@link ptolemy.util.MessageHandler#setMessageHandler(MessageHandler)}.
059 For example:
060 <pre>
061 GraphicalMessageHandler handler = new GraphicalMessageHandler();
062 GraphicalMessageHandler.setMessageHandler(handler);
063 GraphicalMessageHandler.error("My error", new Exception("My Exception"));
064 </pre>
065 If setMessageHandler() is not called, then the error() call will
066 use the default handler and possibly display the message on standard error.
067
068 <p>This class is based on (and contains code from) the diva GUIUtilities
069 class.
070
071 @author  Edward A. Lee, Steve Neuendorffer, John Reekie, and Elaine Cheong
072 @version $Id$
073 @since Ptolemy II 8.0
074 @Pt.ProposedRating Yellow (eal)
075 @Pt.AcceptedRating Red (reviewmoderator)
076 */
077public class UndeferredGraphicalMessageHandler
078        extends ptolemy.util.MessageHandler {
079
080    /** Get the component set by a call to setContext(), or null if none.
081     *  @see #setContext(Component)
082     *  @return The component with respect to which the display window
083     *   is iconified, or null if none has been specified.
084     */
085    public static Component getContext() {
086        if (_context == null) {
087            return null;
088        }
089
090        return (Component) _context.get();
091    }
092
093    /** Set the component with respect to which the display window
094     *  should be created.  This ensures that if the application is
095     *  iconified or deiconified, that the display window goes with it.
096     *  This is maintained in a weak reference so that the frame can be
097     *  garbage collected.
098     *  @see #getContext()
099     *  @param context The component context.
100     */
101    public static void setContext(Component context) {
102        // FIXME: This seems utterly incomplete...
103        // We will inevitably have multiple frames,
104        // so having one static context just doesn't
105        // work.
106        _context = new WeakReference(context);
107    }
108
109    ///////////////////////////////////////////////////////////////////
110    ////                         protected methods                 ////
111
112    /** Return an updated array of button names if the throwable meets
113     *  certain conditions.  In this base class, the options argument
114     *  is returned.  In derived classes, this method could check to
115     *  see if the throwable is a KernelException or
116     *  KernelRuntimeException, then add "Go To Actor" to the options
117     *  array.
118     *  @param options An array of Strings, suitable for passing to
119     *  JOptionPane.showOptionDialog().
120     *  @param throwable The throwable.
121     *  @return An array of Strings.  In this base class, return the value
122     *  of the options parameter.  Derived classes may add a new array
123     *  with an additional String that labels an additional button.
124     */
125    protected Object[] _checkThrowableNameable(Object[] options,
126            Throwable throwable) {
127        return options;
128    }
129
130    /** Show the specified error message.
131     *  This is deferred to execute in the swing event thread if it is
132     *  called outside that thread.
133     *  @param info The message.
134     */
135    @Override
136    protected void _error(String info) {
137        Object[] message = new Object[1];
138        String string = info;
139        message[0] = _messageComponent(StringUtilities.ellipsis(string,
140                StringUtilities.ELLIPSIS_LENGTH_SHORT));
141
142        Object[] options = { "Dismiss" };
143
144        // Show the MODAL dialog
145        JOptionPane.showOptionDialog(getContext(), message, "Error",
146                JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null,
147                options, options[0]);
148    }
149
150    /** Show the specified message and throwable information.
151     *  If the throwable is an instance of CancelException, then it
152     *  is not shown.  By default, only the message of the throwable
153     *  is thrown.  The stack trace information is only shown if the
154     *  user clicks on the "Display Stack Trace" button.
155     *  This is deferred to execute in the swing event thread if it is
156     *  called outside that thread.
157     *
158     *  @param info The message.
159     *  @param throwable The throwable.
160     *  @see ptolemy.util.CancelException
161     */
162    @Override
163    protected void _error(String info, Throwable throwable) {
164        if (throwable instanceof ptolemy.util.CancelException) {
165            return;
166        }
167
168        // Sometimes you find that errors are reported
169        // multiple times.  To find out who is calling
170        // this method, uncomment the following.
171        // System.out.println("------ reporting error:");
172        // (new Throwable()).printStackTrace();
173        Object[] message = new Object[1];
174        String string;
175
176        if (info != null) {
177            string = info + "\n" + throwable.getMessage();
178        } else {
179            string = throwable.getMessage();
180        }
181
182        message[0] = _messageComponent(StringUtilities.ellipsis(string,
183                StringUtilities.ELLIPSIS_LENGTH_SHORT));
184
185        Object[] options = { "Dismiss", "Display Stack Trace" };
186
187        // In this base class, merely return the options array.
188        // Derived classes: If the throwable is a KernelException or
189        // KernelRuntimeException, then add "Go To Actor" to the
190        // options array.
191        options = _checkThrowableNameable(options, throwable);
192
193        // Show the MODAL dialog
194        int selected = JOptionPane.showOptionDialog(getContext(), message,
195                MessageHandler.shortDescription(throwable),
196                JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null,
197                options, options[0]);
198
199        if (selected == 1) {
200            _showStackTrace(throwable, info);
201        } else if (selected == 2) {
202            // Derived classes
203            _showNameable(throwable);
204        }
205    }
206
207    /** Show the specified message in a modal dialog.
208     *  This is deferred to execute in the swing event thread if it is
209     *  called outside that thread.
210     *  @param info The message.
211     */
212    @Override
213    protected void _message(String info) {
214        Object[] message = new Object[1];
215        message[0] = _messageComponent(StringUtilities.ellipsis(info,
216                StringUtilities.ELLIPSIS_LENGTH_LONG));
217
218        Object[] options = { "OK" };
219
220        // Show the MODAL dialog
221        JOptionPane.showOptionDialog(getContext(), message, "Message",
222                JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE,
223                null, options, options[0]);
224    }
225
226    /** Open the level of hierarchy of the model that contains the
227     *  Nameable referred to by the KernelException or KernelRuntimeException.
228     *  In this base class, do nothing.
229     *  @param throwable The throwable that may be a KernelException
230     *  or KernelRuntimeException.
231     */
232    protected void _showNameable(Throwable throwable) {
233    }
234
235    /** Show the specified message in a modal dialog.  If the user
236     *  clicks on the "Cancel" button, then throw an exception.
237     *  This gives the user the option of not continuing the
238     *  execution, something that is particularly useful if continuing
239     *  execution will result in repeated warnings.
240     *  NOTE: If this is called outside the swing event thread, then
241     *  no cancel button is presented and no CancelException will be
242     *  thrown.  This is because the displaying of the message must
243     *  be deferred to the swing event thread, according to the swing
244     *  architecture, or we could get deadlock or rendering problems.
245     *  @param info The message.
246     *  @exception ptolemy.util.CancelException If the user clicks on the
247     * "Cancel" button.
248     */
249    @Override
250    protected void _warning(String info) throws CancelException {
251        Object[] options = { "OK", "Cancel" };
252        Object[] message = new Object[1];
253
254        // If the message lines are longer than 80 characters, we split it
255        // into shorter new line separated strings.
256        // Running vergil on a HSIF .xml file will create a line longer
257        // than 80 characters
258        message[0] = _messageComponent(StringUtilities.ellipsis(info,
259                StringUtilities.ELLIPSIS_LENGTH_LONG));
260
261        // Show the MODAL dialog
262        int selected = JOptionPane.showOptionDialog(getContext(), message,
263                "Warning", JOptionPane.YES_NO_OPTION,
264                JOptionPane.WARNING_MESSAGE, null, options, options[0]);
265
266        if (selected == 1) {
267            throw new ptolemy.util.CancelException();
268        }
269    }
270
271    /** Show the specified message and throwable information
272     *  in a modal dialog.  If the user
273     *  clicks on the "Cancel" button, then throw an exception.
274     *  This gives the user the option of not continuing the
275     *  execution, something that is particularly useful if continuing
276     *  execution will result in repeated warnings.
277     *  By default, only the message of the throwable
278     *  is shown.  The stack trace information is only shown if the
279     *  user clicks on the "Display Stack Trace" button.
280     *  NOTE: If this is called outside the swing event thread, then
281     *  no cancel button is presented and no CancelException will be
282     *  thrown.  This is because the displaying of the message must
283     *  be deferred to the swing event thread, according to the swing
284     *  architecture, or we could get deadlock or rendering problems.
285     *  @param info The message.
286     *  @param throwable The throwable.
287     *  @exception ptolemy.util.CancelException If the user clicks on the
288     *  "Cancel" button.
289     */
290    @Override
291    protected void _warning(String info, Throwable throwable)
292            throws CancelException {
293        Object[] message = new Object[1];
294        message[0] = _messageComponent(StringUtilities.ellipsis(info,
295                StringUtilities.ELLIPSIS_LENGTH_LONG));
296
297        Object[] options = { "OK", "Display Stack Trace", "Cancel" };
298
299        // In a derived class, if the throwable is a KernelException
300        // or KernelRuntimeException, then add "Go To Actor" to the
301        // options array.
302        options = _checkThrowableNameable(options, throwable);
303
304        // Show the MODAL dialog
305        int selected = JOptionPane.showOptionDialog(getContext(), message,
306                "Warning", JOptionPane.YES_NO_OPTION,
307                JOptionPane.WARNING_MESSAGE, null, options, options[0]);
308
309        if (selected == 1) {
310            _showStackTrace(throwable, info);
311        } else if (selected == 2) {
312            throw new ptolemy.util.CancelException();
313        } else if (selected == 3) {
314            _showNameable(throwable);
315        }
316    }
317
318    /** Ask the user a yes/no question, and return true if the answer
319     *  is yes.
320     *
321     *  If the length of the question is greater than
322     *  {@link ptolemy.util.StringUtilities#ELLIPSIS_LENGTH_LONG},
323     *  then the question is displayed in a JTextArea.
324     *
325     *  @param question The yes/no question.
326     *  @return True if the answer is yes.
327     */
328    @Override
329    protected boolean _yesNoQuestion(String question) {
330        Object[] message;
331
332        // If the question is long, then display a scrollable JTextArea.
333        if (question.length() <= StringUtilities.ELLIPSIS_LENGTH_LONG) {
334            message = new Object[1];
335        } else {
336            message = new Object[2];
337            JTextArea text = new JTextArea(question, 60, 80);
338            JScrollPane stext = new JScrollPane(text);
339            stext.setPreferredSize(new Dimension(600, 300));
340            text.setCaretPosition(0);
341            text.setEditable(false);
342            message[1] = stext;
343        }
344
345        message[0] = _messageComponent(StringUtilities.ellipsis(question,
346                StringUtilities.ELLIPSIS_LENGTH_LONG));
347
348        Object[] options = { "Yes", "No" };
349
350        // Show the MODAL dialog
351        int selected = JOptionPane.showOptionDialog(getContext(), message,
352                "Warning", JOptionPane.YES_NO_OPTION,
353                JOptionPane.WARNING_MESSAGE, null, options, options[0]);
354
355        if (selected == 0) {
356            return true;
357        } else {
358            return false;
359        }
360    }
361
362    /** Ask the user a question with three possible answers;
363     *  return true if the answer is the first one and false if
364     *  the answer is the second one; throw an exception if the
365     *  user selects the third one. The default (selected by return
366     *  and escape) is the third (the cancel option).
367     *  @param question The question.
368     *  @param trueOption The option for which to return true.
369     *  @param falseOption The option for which to return false.
370     *  @param exceptionOption The option for which to throw an exception.
371     *  @return True if the answer is the first option, false if it is the second.
372     *  @exception ptolemy.util.CancelException If the user selects the third option.
373     */
374    @Override
375    protected boolean _yesNoCancelQuestion(String question, String trueOption,
376            String falseOption, String exceptionOption)
377            throws ptolemy.util.CancelException {
378        Object[] message = new Object[1];
379        message[0] = _messageComponent(StringUtilities.ellipsis(question,
380                StringUtilities.ELLIPSIS_LENGTH_LONG));
381
382        Object[] options = { trueOption, falseOption, exceptionOption };
383
384        // Show the MODAL dialog
385        int selected = JOptionPane.showOptionDialog(getContext(), message,
386                "Warning", JOptionPane.YES_NO_CANCEL_OPTION,
387                JOptionPane.WARNING_MESSAGE, null, options, options[2]);
388
389        if (selected == 0) {
390            return true;
391        } else if (selected == 1) {
392            return false;
393        } else {
394            throw new ptolemy.util.CancelException();
395        }
396    }
397
398    ///////////////////////////////////////////////////////////////////
399    ////                         protected methods                 ////
400
401    /** Display a stack trace dialog. The "info" argument is a
402     *  string printed at the top of the dialog instead of the Throwable
403     *  message.
404     *  @param throwable The throwable.
405     *  @param info A message.
406     */
407    protected void _showStackTrace(Throwable throwable, String info) {
408        // FIXME: Eventually, the dialog should
409        // be able to email us a bug report.
410        // Show the stack trace in a scrollable text area.
411        StringWriter sw = new StringWriter();
412        PrintWriter pw = new PrintWriter(sw);
413        throwable.printStackTrace(pw);
414
415        JTextArea text = new JTextArea(sw.toString(), 60, 80);
416        JScrollPane stext = new JScrollPane(text);
417        stext.setPreferredSize(new Dimension(600, 300));
418        text.setCaretPosition(0);
419        text.setEditable(false);
420
421        // We want to stack the text area with another message
422        Object[] message = new Object[2];
423        String string;
424
425        if (info != null) {
426            string = info + "\n" + throwable.getMessage();
427        } else {
428            string = throwable.getMessage();
429        }
430
431        message[0] = _messageComponent(StringUtilities.ellipsis(string,
432                StringUtilities.ELLIPSIS_LENGTH_LONG));
433        message[1] = stext;
434
435        Object[] options = { "OK" };
436
437        // In this base class, merely return the options array.
438        // Derived classes: If the throwable is a KernelException or
439        // KernelRuntimeException, then add "Go To Actor" to the
440        // options array.
441        options = _checkThrowableNameable(options, throwable);
442
443        // Show the MODAL dialog
444        int selected = JOptionPane.showOptionDialog(getContext(), message,
445                "Stack trace", JOptionPane.YES_NO_OPTION,
446                JOptionPane.ERROR_MESSAGE, null, options, options[0]);
447
448        if (selected == 1) {
449            // Derived classes
450            _showNameable(throwable);
451        }
452    }
453
454    ///////////////////////////////////////////////////////////////////
455    ////                         protected variables               ////
456
457    /** The context. */
458    protected static WeakReference _context = null;
459
460    ///////////////////////////////////////////////////////////////////
461    ////                         private methods                   ////
462
463    // Return an Object suitable for JOptionPane so that the user
464    // can select it.
465    // We return a JTextField that looks like a JLabel, but is
466    // selectable.  Many thanks to:
467    // http://www.rgagnon.com/javadetails/java-0296.html
468    private Object _messageComponent(String message) {
469        // It is nice if error messages have selectable text.
470        // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4147
471        Object result = message;
472        // Unfortunately, this hack does not wrap text properly,
473        // so I'm commenting it out temporarily.
474        //         try {
475        //             JTextField textField = new JTextField(message);
476        //             textField.setEditable(false);
477        //             textField.setBorder(null);
478        //             textField.setForeground(UIManager.getColor("Label.foreground"));
479        //             textField.setBackground(UIManager.getColor("Label.background"));
480        //             textField.setFont(UIManager.getFont("Label.font"));
481        //             result = textField;
482        //         } catch (Exception ex) {
483        //             // Ignore, just return the string
484        //         }
485        return result;
486    }
487}