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}