/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.explorer.propertysheet; import java.awt.Component; import java.awt.Window; import java.awt.event.*; import java.beans.*; import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.JComponent; import org.openide.DialogDescriptor; import org.openide.nodes.Node; import org.openide.ErrorManager; import org.openide.util.NbBundle; import org.openide.util.HelpCtx; import org.openide.explorer.propertysheet.editors.EnhancedCustomPropertyEditor; /** * Helper dialog box manager for showing custom property editors. * * @author Jan Jancura, Dafe Simonek, David Strupl */ final class PropertyDialogManager implements VetoableChangeListener { /* JST: Made package private because PropertyPanel should be used instead. */ /** Listener to editor property changes. */ private PropertyChangeListener listener; /** Cache for reverting on cancel. */ private Object oldValue; /** Custom property editor. */ private PropertyEditor editor; /** model of the edited property */ private PropertyModel model; /** this is extracted from the model if possible, can be null */ private Node.Property prop; /** Set true when property is changed. */ private boolean changed = false; /** Given component stored for test on Enhance property ed. */ private Component component; /** Dialog instance. */ private Window dialog; /** Ok button can be enabled/disabled*/ private JButton okButton; /** */ private Runnable errorPerformer; private boolean okButtonState = true; private boolean defaultValue = false; private boolean isModal = true; private String title = null; private HelpCtx helpCtx = null; private Object defaultOption; private Object[] options; private Object envStateBeforeDisplay = null; /** Environment passed to the property editor. */ private PropertyEnv env; private static ThreadLocal caller = new ThreadLocal(); private ActionListener actionListener; private Object lastValueFromEditor; /** Name of the custom property that can be passed in PropertyEnv. */ private static final String PROPERTY_DESCRIPTION = "description"; // NOI18N // init ...................................................................... /** Create a dialog. * * @param title title of the dialog * @param component component to show * @param isModal <code>true</code> if the dialog should be modal * @param editor custom property editor. May be <code>null</code>. */ public PropertyDialogManager(final String title, final boolean isModal, final PropertyEditor editor, PropertyModel model, final PropertyEnv env) { this.editor = editor; if (env != null) { env.addVetoableChangeListener(this); } this.component = editor.getCustomEditor(); this.model = model; this.env = env; this.title = title; this.isModal = isModal; actionListener = new ActionListener() { public void actionPerformed(ActionEvent evt) { doButtonPressed(evt); } }; if (env != null) { Object helpID = env.getFeatureDescriptor().getValue(ExPropertyEditor.PROPERTY_HELP_ID); if ( helpID != null && helpID instanceof String && component != null && component instanceof JComponent ) { HelpCtx.setHelpIDString((JComponent)component, (String)helpID); helpCtx = new HelpCtx((String) helpID); } } // create dialog instance and initialize listeners createDialog(); initializeListeners(); } // public methods ............................................................ /** Get the created dialog instance. * @return the dialog instance managed by this class. */ public Window getDialog() { return dialog; } /** * PropertyDialogManager is attached to the PropertyEnv instance by * vetoableChangeListener. */ public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { if ((env != null) && (PropertyEnv.PROP_STATE.equals(evt.getPropertyName()))) { okButtonState = evt.getNewValue() != PropertyEnv.STATE_INVALID; if (okButton != null) { okButton.setEnabled(okButtonState); } } } // other methods ............................................................ /** Creates proper DialogDescriptor and obtain dialog instance * via DialogDisplayer.createDialog() call. */ private void createDialog() { if (component instanceof Window) { // custom component is already a window --> just return it // from getDialog dialog = (Window)component; dialog.pack(); return; } // prepare our options (buttons) boolean cannotWrite = false; if (model instanceof ExPropertyModel) { FeatureDescriptor fd = ((ExPropertyModel)model).getFeatureDescriptor(); if (fd instanceof Node.Property) { prop = (Node.Property)fd; cannotWrite = !prop.canWrite(); defaultValue = prop.supportsDefaultValue(); } } if ((editor == null) || (cannotWrite)) { JButton closeButton = new JButton(getString("CTL_Close")); closeButton.getAccessibleContext().setAccessibleDescription(getString("ACSD_CTL_Close")); options = new Object[] { closeButton }; defaultOption = closeButton; } else { okButton = new JButton(getString("CTL_OK")); okButton.getAccessibleContext().setAccessibleDescription(getString("ACSD_CTL_OK")); JButton cancelButton = new JButton(getString("CTL_Cancel")); cancelButton.setVerifyInputWhenFocusTarget(false); cancelButton.getAccessibleContext().setAccessibleDescription(getString("ACSD_CTL_Cancel")); cancelButton.setDefaultCapable(false); if (defaultValue) { JButton defaultButton = new JButton(getString("CTL_Default")); defaultButton.setMnemonic(getString("CTL_DefaultMnemonic").charAt(0)); defaultButton.getAccessibleContext().setAccessibleDescription(getString("ACSD_CTL_Default")); defaultButton.setDefaultCapable(false); defaultButton.setVerifyInputWhenFocusTarget(false); options = new Object[] { defaultButton, okButton, cancelButton }; } else { options = new Object[] { okButton, cancelButton }; } defaultOption = okButton; } if ((env != null) && (okButton != null)) { okButtonState = env.getState() != PropertyEnv.STATE_INVALID; if (okButton != null) { okButton.setEnabled(okButtonState); } } if (env != null) { envStateBeforeDisplay = env.getState(); } try { caller.set(this); Class c = Class.forName("org.openide.explorer.propertysheet.PropertyDialogManager$CreateDialogInvoker"); // NOI18N Runnable r = (Runnable) c.newInstance(); r.run(); return; } catch (Exception e) { // if something went wrong just // resort to swing (IDE probably not present) } catch (LinkageError e) { } if (dialog == null) { JOptionPane jop = new JOptionPane(component, JOptionPane.PLAIN_MESSAGE, JOptionPane.NO_OPTION, null, options, defaultOption); if (okButton != null) { okButton.addActionListener(actionListener); } dialog = jop.createDialog(null, title); } if (env != null) { Object obj = env.getFeatureDescriptor().getValue(PROPERTY_DESCRIPTION); if (obj instanceof String) { dialog.getAccessibleContext().setAccessibleDescription((String)obj); } } } /** Initializes dialog listeners. Must be called after * createDialog method call. (dialog variable must not be null) */ private void initializeListeners() { // dialog closing reactions dialog.addWindowListener(new WindowAdapter() { /** Ensure that values are reverted when user cancelles dialog * by clicking on x image */ public void windowClosing(WindowEvent e) { if ((editor != null) && ! (component instanceof Window)) { // if someone provides a window (s)he has to handle // the cancel him/herself cancelValue(); } // ensure that resources are released if (env != null) { env.removeVetoableChangeListener(PropertyDialogManager.this); } dialog.dispose(); } /** Remove property listener on window close */ public void windowClosed(WindowEvent e) { if (component instanceof Window) { // in this case we have to do similar thing as we do // directly after the Ok button is pressed. The difference // is in the fact that here we do not decide whether the // dialog will be closed or not - it is simply being closed // But we have to take care of propagating the value to // the model if (component instanceof EnhancedCustomPropertyEditor) { try { Object newValue = ((EnhancedCustomPropertyEditor) component).getPropertyValue(); model.setValue(newValue); } catch (java.lang.reflect.InvocationTargetException ite) { PropertyPanel.notifyUser (ite, prop==null?"":prop.getDisplayName ()); // NOI18N } catch (IllegalStateException ise) { notifyUser (ise); } } else if ((env!=null)&&(!env.isChangeImmediate())) { try { model.setValue(lastValueFromEditor); } catch (java.lang.reflect.InvocationTargetException ite) { PropertyPanel.notifyUser (ite, prop==null?"":prop.getDisplayName ()); // NOI18N } catch (IllegalStateException ise) { notifyUser (ise); } } } if (listener != null) editor.removePropertyChangeListener(listener); dialog.removeWindowListener(this); } }); // reactions to editor property changes if (editor != null) { try { oldValue = model.getValue(); } catch (Exception e) { // Ignored, there can be number of exceptions // when asking for old values... } lastValueFromEditor = editor.getValue(); editor.addPropertyChangeListener(listener = new PropertyChangeListener() { /** Notify displayer about property change in editor */ public void propertyChange(PropertyChangeEvent e) { changed = true; lastValueFromEditor = editor.getValue(); // enabling/disabling the okButton in response to // firing PROP_VALUE_VALID --- usage of this firing // has been deprecated in favor of directly calling // PropertyEnv.setState(...) if (ExPropertyEditor.PROP_VALUE_VALID.equals(e.getPropertyName())) { if (okButton != null) { if (e.getNewValue() instanceof Boolean) { Boolean newButtonState = (Boolean)e.getNewValue(); okButtonState = newButtonState.booleanValue(); if (env != null) { env.setState(okButtonState? PropertyEnv.STATE_VALID:PropertyEnv.STATE_INVALID); } else { okButton.setEnabled(okButtonState); } if (e.getOldValue() instanceof Runnable) { errorPerformer = (Runnable) e.getOldValue(); } else { errorPerformer = null; } } } } } } ); component.addPropertyChangeListener(listener = new PropertyChangeListener() { /** forward possible help context change in custom editor */ public void propertyChange(PropertyChangeEvent e) { if (DialogDescriptor.PROP_HELP_CTX.equals(e.getPropertyName())) { if ( dialog instanceof PropertyChangeListener ) ((PropertyChangeListener)dialog).propertyChange( e ); } } } ); } } /** * Reverts to old values. */ private void cancelValue() { if ( (!changed) || (component instanceof EnhancedCustomPropertyEditor) || ((env!=null)&&(!env.isChangeImmediate())) ) { if ((env != null) && (envStateBeforeDisplay != null)) { env.setState(envStateBeforeDisplay); } return; } try { model.setValue(oldValue); } catch (Exception e) { // Ignored, there can be number of exceptions // when asking for old values... } } /** Called when user presses a button on some option (button) in the * dialog. * @param evt The button press event. */ private void doButtonPressed(ActionEvent evt) { String label = evt.getActionCommand(); if (label.equals(getString("CTL_Cancel"))) { cancelValue(); } if (label.equals(getString("CTL_Default"))) { if (prop != null) { try { prop.restoreDefaultValue(); } catch (IllegalAccessException iae) { PropertyPanel.notifyUser (iae, prop.getDisplayName ()); } catch (java.lang.reflect.InvocationTargetException ite) { PropertyPanel.notifyUser (ite, prop.getDisplayName ()); } } } if ( label.equals(getString("CTL_OK"))) { if ((env!=null)&&(env.getState() == PropertyEnv.STATE_NEEDS_VALIDATION)) { env.setState(PropertyEnv.STATE_VALID); if (env.getState() != PropertyEnv.STATE_VALID) { // if the change was vetoed do nothing and return return; } } if (component instanceof EnhancedCustomPropertyEditor) { try { Object newValue = ((EnhancedCustomPropertyEditor) component).getPropertyValue(); model.setValue(newValue); } catch (java.lang.reflect.InvocationTargetException ite) { PropertyPanel.notifyUser (ite, prop==null?"":prop.getDisplayName ()); // NOI18N return; } catch (IllegalStateException ise) { notifyUser (ise); return; } } else if ((env!=null)&&(!env.isChangeImmediate())) { try { model.setValue(lastValueFromEditor); } catch (java.lang.reflect.InvocationTargetException ite) { PropertyPanel.notifyUser (ite, prop==null?"":prop.getDisplayName ()); // NOI18N return; } catch (IllegalStateException ise) { notifyUser (ise); return; } } // this is an old hack allowing to notify a cached value // obtained via propertyChangeEvent from the editor if (!okButtonState) { if (errorPerformer != null) { errorPerformer.run(); } return; } } // close the dialog changed = false; if (env != null) { env.removeVetoableChangeListener(this); } dialog.dispose(); } private static String getString(String key) { return NbBundle.getBundle(PropertyDialogManager.class).getString(key); } /** For testing purposes we need to _not_ notify some exceptions. * That is why here is a package private method to register an exception * that should not be fired. */ static void doNotNotify (Throwable ex) { doNotNotify = ex; } private static Throwable doNotNotify; /** Notifies an exception to error manager or prints its it to stderr. * @param ex exception to notify */ static void notify(Throwable ex) { Throwable d = doNotNotify; doNotNotify = null; if (d == ex) return; ErrorManager.getDefault ().notify (ex); } /** Notifies an exception to error manager or prints its it to stderr. * @param ex exception to notify */ static void notify(int severity, Throwable ex) { Throwable d = doNotNotify; doNotNotify = null; if (d == ex) return; ErrorManager.getDefault ().notify(severity, ex); } /** * Tries to find a localized message in the exception annotation * or directly in the exception. If the message is found it is * notified with user severity. If the message is not found the * exception is notified with informational severity. * @param ex exception to notify */ private static void notifyUser(Exception e) { ErrorManager em = ErrorManager.getDefault (); ErrorManager.Annotation[] an = em.findAnnotations(e); String userMessage = null; if (an != null) { for (int i = 0; i < an.length; i++) { String msg = an[i].getLocalizedMessage(); if (msg != null) { userMessage = msg; break; } } } if (userMessage == null) { userMessage = e.getLocalizedMessage(); } if (userMessage != null) { Throwable t = em.annotate(e, userMessage); em.notify(ErrorManager.USER, t); } else { em.notify(ErrorManager.INFORMATIONAL, e); } } static Throwable annotate(Throwable t, int severity, String message, String localizedMessage, Throwable stackTrace, java.util.Date date) { return ErrorManager.getDefault ().annotate(t, severity, message, localizedMessage, stackTrace, date); } static class CreateDialogInvoker implements Runnable { public void run() { final PropertyDialogManager pdm = (PropertyDialogManager)caller.get(); caller.set(null); if (pdm == null) { throw new IllegalStateException("Parameter caller not passed."); // NOI18N } // create dialog descriptor, create & return the dialog // bugfix #24998, set helpCtx obtain from PropertyEnv.getFeatureDescriptor() org.openide.DialogDescriptor descriptor = new org.openide.DialogDescriptor( pdm.component, pdm.title, pdm.isModal, pdm.options, pdm.defaultOption, org.openide.DialogDescriptor.DEFAULT_ALIGN, pdm.helpCtx, pdm.actionListener ); pdm.dialog = org.openide.DialogDisplayer.getDefault().createDialog(descriptor); } } }