package textbender.g.lang; // Copyright 2005, 2007 Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Textbender Software"), to deal in the Textbender Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Textbender Software, and to permit persons to whom the Textbender Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Textbender Software. THE TEXTBENDER SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE TEXTBENDER SOFTWARE OR THE USE OR OTHER DEALINGS IN THE TEXTBENDER SOFTWARE. import java.awt.*; import java.awt.event.*; import java.beans.*; import java.security.AccessControlException; import java.util.*; import java.util.prefs.Preferences; import javax.swing.*; import javax.swing.border.EmptyBorder; import textbender.o.awt.*; import textbender.g.hold.*; import textbender.g.util.prefs.PreferencesX; import static textbender.o.awt.FontX.EN; import static textbender.o.awt.FontX.EM; /** View of a throwable holder in an option pane. * The {@linkplain #dialog() main dialog} displays a brief summary of the throwable. * A supplementary {@linkplain #dialogST() stack trace dialog} may be requested by the user. */ @ThreadRestricted( "AWT event dispatch" ) public final class ThrowableHolderV { /** Partially creates a ThrowableHolderV, for {@linkplain #init2 init2}() to finish. * Prior to calling init2(), you can create other views in the same option pane. * * @param spool for internal holds. When unwound, this instance * will become disabled, and will release its internal holds. */ public ThrowableHolderV( JOptionPane optionPane, Spool spool ) { this.optionPane = optionPane; init_spool = spool; } /** Finishes and returns the new ThrowableHolderV. Call once only. * * @param m model per {@linkplain #getModel() getModel}(); or null to set it later * @param dialogParent Parent component per JOptionPane.getDialog(). * This parameter may be null. * @param preferences node for storage */ // * @param runDelayer configured per // * ComponentSizePreference.{@linkplain ComponentSizePreference#restore(int,int,RunDelayer) restore}() public ThrowableHolderV init2( final ThrowableHolderModel m, final Component dialogParent, final Preferences preferences //, final RunDelayer runDelayer ){ assert dialog == null: "call once only"; assert java.awt.EventQueue.isDispatchThread(); // Main dialog. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final JButton buttonST; final String titleST = "Trace"; { buttonST = new JButton( titleST + /*horizontal ellipsis*/'\u2026' ); buttonST.addActionListener( new ActionListener() { boolean wasPositioned; public void actionPerformed( ActionEvent e ) { if( !wasPositioned ) { dialogST.setLocationRelativeTo( buttonST ); wasPositioned = true; // once is enough, allow user to reposition } dialogST.setVisible( true ); } }); } final Object[] priorOptions = optionPane.getOptions(); // default options, or those previously defined by client final ArrayList optionList = new ArrayList(); optionList.add( buttonST ); if( priorOptions != null ) optionList.addAll( Arrays.asList( priorOptions )); optionPane.setOptions( optionList.toArray() ); dialog = optionPane.createDialog( dialogParent, /*title*/null ); init_spool.add( new Hold() { public void release() { dialog.dispose(); } }); try{ dialog.setAlwaysOnTop( true ); } catch( AccessControlException x ) {} new WindowX.LookAndFeelUpdater( dialog, init_spool, /*packed*/true ); // Stack trace dialog. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // dialogST = new JDialog( /*parent*/dialog, titleST, /*modal*/true ); dialogST = new JDialog( /*parent*/dialog, titleST ); init_spool.add( new Hold() { public void release() { dialogST.dispose(); } }); { Box panel = new Box( BoxLayout.Y_AXIS ); // panel.setAlignmentX( 0.5f ); panel.setBorder( new EmptyBorder ( /*top*/EN, /*left*/EN, /*bottom*/EN, /*right*/EN )); viewST = new ThrowableHolderVST( /*model*/null, panel, init_spool ); dialogST.getContentPane().add( panel, BorderLayout.CENTER ); } { Box panel = new Box( BoxLayout.X_AXIS ); panel.setBorder( new EmptyBorder ( /*top*/0, /*left*/EN/2, /*bottom*/EN/2, /*right*/EN/2 )); panel.add( Box.createHorizontalGlue() ); { JButton button = new JButton( "Close" ); button.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent eA ) { dialogST.dispatchEvent( new WindowEvent( dialogST, WindowEvent.WINDOW_CLOSING )); } }); // dialogST.addComponentListener( new ComponentAdapter() // to the dialog, not the button (for which this event never fires unless setVisible() called) // FIX to use a HierarchyListener // { // public void componentShown( ComponentEvent eC ) { button.requestFocus(); } // }); panel.add( button ); } dialogST.getContentPane().add( panel, BorderLayout.SOUTH ); } new ComponentSizePreference( dialogST, PreferencesX.childNodeForClass( preferences, ThrowableHolderVST.class )) .restore( /*default width*/60*EM, /*height*/30*EM ); // .restore( /*default width*/60*EM, /*height*/30*EM, runDelayer ); new WindowX.LookAndFeelUpdater( dialogST, init_spool ); // - - - setModel( m ); init_spool.add( new Hold() { public void release() { setModel( null ); } }); return ThrowableHolderV.this; } // ------------------------------------------------------------------------------------ /** Returns the main dialog of this view, as created from the option pane. * It is not visible until you make it visible. * Typically you will make it visible when the user requests it; * or automatically, whenever the throwable changes. *

* By default, the dialog is modal, and always on top. *

*/ public JDialog dialog() { return dialog; } private JDialog dialog; // final when initialized /** Returns the dialog containing the supplementary * {@linkplain textbender.g.lang.ThrowableHolderVST stack trace view}. * This dialog is normally made visible from the {@linkplain #dialog() main dialog}, * when requested by the user. */ public JDialog dialogST() { return dialogST; } private JDialog dialogST; // final when initialized /** Returns the model that is viewed. * * @return model; or null if there is none */ public ThrowableHolderModel getModel() { return model; } private ThrowableHolderModel model; /** Sets the model that is viewed, per {@linkplain #getModel() getModel}(). */ public void setModel( ThrowableHolderModel newModel ) { assert java.awt.EventQueue.isDispatchThread(); setModel_spool.unwind(); model = newModel; viewST.setModel( model ); if( model == null ) setModel_spool = Spool0.i(); else setModel_spool = new Spool1(); refresher = new Refresher( setModel_spool ); } private Spool setModel_spool = Spool0.i(); /** Returns the title of the view, if any. * This appears on the {@linkplain #dialog() main dialog} window. * If no title is specified, then the name of the current throwable is used. * * @return title of the view, or null if the title is taken from the throwable */ public final String getTitle() { return title; }; private String title = null; /** Sets the title, per {@linkplain #getTitle() getTitle}(). */ public final void setTitle( final String newTitle ) { if( ObjectX.nullEquals( newTitle, title )) return; title = newTitle; if( title == null ) refresher.refresh(); else dialog.setTitle( title ); }; //// P r i v a t e /////////////////////////////////////////////////////////////////////// private final Spool init_spool; private final JOptionPane optionPane; private ThrowableHolderVST viewST; // final when initialized // ==================================================================================== private Refresher refresher; /** Refreshes the view in sync with the model. */ private final class Refresher implements PropertyChangeListener { private Refresher( Spool spool ) { if( model != null ) { model.addPropertyChangeListener( property, Refresher.this ); spool.add( new Hold() { public void release() { model.removePropertyChangeListener( property, Refresher.this ); } }); } refresh(); // fire up // not dynamically bound when called from constructor/initializer } private void refresh() { assert java.awt.EventQueue.isDispatchThread(); final Throwable t; if( model == null ) t = null; else t = model.getThrowable(); if( title == null ) { final String dialogTitle; if( t == null ) dialogTitle = null; else dialogTitle = t.getClass().getSimpleName(); dialog.setTitle( dialogTitle ); } final StringBuilder b = AWT.emptyStringBuilder(); if( t != null ) { b.append( t.getClass().getName() ); b.append( '\n' ); String message = t.getMessage(); if( message != null ) { b.append( message ); // b.append( '\n' ); } // StackTraceElement[] traceArray = t.getStackTrace(); // if( traceArray.length > 0 ) // { // b.append( "at " + traceArray[0].toString() ); // } ///// sometimes confusing, e.g. in a doc parse error that has line numbers (of the doc) } optionPane.setMessage( b.toString() ); // final Dimension oldSize = AWT.dimension(); dialog.getSize( oldSize ); dialog.pack(); // final Point location = AWT.point(); dialog.getLocation( location ); // location.translate // keep centered at old location, despite size changes // ( // /*x*/(oldSize.width-dialog.getWidth())/2, /*y*/(oldSize.height-dialog.getHeight())/2 // ); // dialog.setLocation( location ); ///// it's OK to uncomment this again, just bug hunting } // - P r o p e r t y - C h a n g e - L i s t e n e r -------------------- private static final String property = "throwable"; public void propertyChange( PropertyChangeEvent e ) { EventQueueX.invokeNowOrLater( new Runnable() { public void run() { refresh(); } // in AWT event dispatch thread }); } } }