001/* A top-level dialog window for editing parameters of a NamedObj.
002
003 Copyright (c) 1998-2014 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.actor.gui;
028
029import java.awt.Frame;
030import java.net.URL;
031import java.util.Iterator;
032import java.util.List;
033
034import javax.swing.SwingUtilities;
035
036import ptolemy.actor.gui.style.StyleConfigurer;
037import ptolemy.data.expr.StringParameter;
038import ptolemy.gui.ComponentDialog;
039import ptolemy.gui.Query;
040import ptolemy.kernel.util.Attribute;
041import ptolemy.kernel.util.ChangeListener;
042import ptolemy.kernel.util.ChangeRequest;
043import ptolemy.kernel.util.IllegalActionException;
044import ptolemy.kernel.util.NamedObj;
045import ptolemy.kernel.util.Settable;
046import ptolemy.moml.MoMLChangeRequest;
047import ptolemy.util.CancelException;
048import ptolemy.util.MessageHandler;
049import ptolemy.util.StringUtilities;
050
051///////////////////////////////////////////////////////////////////
052//// EditParametersDialog
053
054/**
055 This class is a modal dialog box for editing the parameters of a
056 target object, which is an instance of NamedObj. All attributes that
057 implement the Settable interface and have visibility FULL or
058 NOT_EDITABLE are included in the dialog. An instance of this class
059 contains an instance of Configurer, which examines the target for
060 attributes of type EditorPaneFactory.  Those attributes, if they are
061 present, define the panels that are used to edit the parameters of the
062 target.  If they are not present, then a default panel is created.
063
064 <p> If the panels returned by EditorPaneFactory implement the
065 CloseListener interface, then they are notified when this dialog
066 is closed, and are informed of which button (if any) was used to
067 close the dialog.
068
069 <p> The dialog is modal, so that (in lieu of a proper undo mechanism)
070 the Cancel button can properly undo any modifications that are made.
071 This means that the statement that creates the dialog will not return
072 until the user dismisses the dialog.  The method buttonPressed() can
073 then be called to find out whether the user clicked the Commit button
074 or the Cancel button (or any other button specified in the
075 constructor).  Then you can access the component to determine what
076 values were set by the user.
077
078 @author Edward A. Lee
079 @version $Id$
080 @since Ptolemy II 1.0
081 @Pt.ProposedRating Yellow (eal)
082 @Pt.AcceptedRating Yellow (neuendor)
083 */
084@SuppressWarnings("serial")
085public class EditParametersDialog extends ComponentDialog
086        implements ChangeListener {
087    /** Construct a dialog with the specified owner and target.
088     *  A "Commit" and a "Cancel" button are added to the dialog.
089     *  The dialog is placed relative to the owner.
090     *  @param owner The object that, per the user, appears to be
091     *   generating the dialog.
092     *  @param target The object whose parameters are being edited.
093     */
094    public EditParametersDialog(Frame owner, NamedObj target) {
095        this(owner, target, "Edit parameters for " + target.getName());
096    }
097
098    /** Construct a dialog with the specified owner and target.
099     *  A "Commit" and a "Cancel" button are added to the dialog.
100     *  The dialog is placed relative to the owner.
101     *  @param owner The object that, per the user, appears to be
102     *   generating the dialog.
103     *  @param target The object whose parameters are being edited.
104     *  @param label The label for the dialog box.
105     */
106    public EditParametersDialog(Frame owner, NamedObj target, String label) {
107        super(owner, label, new Configurer(target), _moreButtons);
108
109        // Once we get to here, the dialog has already been dismissed.
110        _owner = owner;
111        _target = target;
112
113        if (buttonPressed().equals("Add")) {
114            _openAddDialog(null, "", "", "ptolemy.data.expr.Parameter");
115            _target.removeChangeListener(this);
116        } else if (buttonPressed().equals("Remove")) {
117            // Create a new dialog to remove a parameter, then open a new
118            // EditParametersDialog.
119            // First, create a string array with the names of all the
120            // parameters.
121            List<Settable> attributeList = _target
122                    .attributeList(Settable.class);
123
124            // Count visible attributes
125            Iterator<Settable> parameters = attributeList.iterator();
126            int count = 0;
127
128            while (parameters.hasNext()) {
129                Settable parameter = parameters.next();
130
131                if (Configurer.isVisible(target, parameter)) {
132                    count++;
133                }
134            }
135
136            String[] attributeNames = new String[count];
137            parameters = attributeList.iterator();
138
139            int index = 0;
140
141            while (parameters.hasNext()) {
142                Settable parameter = parameters.next();
143
144                if (Configurer.isVisible(target, parameter)) {
145                    attributeNames[index++] = ((Attribute) parameter).getName();
146                }
147            }
148
149            Query query = new Query();
150            query.addChoice("delete", "Parameter to delete", attributeNames,
151                    null, false);
152
153            ComponentDialog dialog = new ComponentDialog(_owner,
154                    "Delete a parameter for " + _target.getFullName(), query,
155                    null);
156
157            // If the OK button was pressed, then queue a mutation
158            // to delete the parameter.
159            String deleteName = query.getStringValue("delete");
160
161            if (dialog.buttonPressed().equals("OK") && !deleteName.equals("")) {
162                String moml = "<deleteProperty name=\"" + deleteName + "\"/>";
163                _target.addChangeListener(this);
164
165                MoMLChangeRequest request = new MoMLChangeRequest(this, _target,
166                        moml);
167                request.setUndoable(true);
168                _target.requestChange(request);
169            }
170        } else if (buttonPressed().equals("Defaults")) {
171            ((Configurer) contents).restoreToDefaults();
172
173            // Open a new dialog (a modal dialog).
174            new EditParametersDialog(_owner, _target);
175        } else if (buttonPressed().equals("Preferences")) {
176            // Create a dialog for setting parameter styles.
177            try {
178                StyleConfigurer panel = new StyleConfigurer(target);
179                ComponentDialog dialog = new ComponentDialog(_owner,
180                        "Edit preferences for " + target.getName(), panel);
181
182                if (!dialog.buttonPressed().equals("OK")) {
183                    // Restore original parameter values.
184                    panel.restore();
185                }
186
187                new EditParametersDialog(_owner, _target);
188
189                // NOTE: Instead of te above line, this used
190                // to do the following. This isn't quite right because it violates
191                // the modal dialog premise, since this method will
192                // return and then a new dialog will open.
193                // In particular, the preferences manager relies on the
194                // modal behavior of the dialog. I'm sure other places do to.
195                // EAL 7/05.
196                // _reOpen();
197            } catch (IllegalActionException ex) {
198                MessageHandler.error("Edit Parameter Styles failed", ex);
199            }
200        } else if (buttonPressed().equals("Help")) {
201            String helpURL = _getHelpURL();
202
203            try {
204                URL doc = getClass().getClassLoader().getResource(helpURL);
205
206                // Try to use the configuration, if we can.
207                boolean success = false;
208
209                if (_owner instanceof TableauFrame) {
210                    // According to FindBugs the cast is an error:
211                    //  [M D BC] Unchecked/unconfirmed cast [BC_UNCONFIRMED_CAST]
212                    // However it is checked that _owner instanceof TableauFrame,
213                    // so FindBugs is wrong.
214
215                    Configuration configuration = ((TableauFrame) _owner)
216                            .getConfiguration();
217
218                    if (configuration != null) {
219                        configuration.openModel(null, doc,
220                                doc.toExternalForm());
221                        success = true;
222                    }
223                }
224
225                if (!success) {
226                    // Just open an HTML page.
227                    HTMLViewer viewer = new HTMLViewer();
228                    viewer.setPage(doc);
229                    viewer.pack();
230                    viewer.show();
231                }
232            } catch (Exception ex) {
233                try {
234                    MessageHandler.warning(
235                            "Cannot open help page \"" + helpURL + "\".", ex);
236                } catch (CancelException exception) {
237                    // Ignore the cancel.
238                }
239            }
240        }
241    }
242
243    ///////////////////////////////////////////////////////////////////
244    ////                         public methods                    ////
245
246    /** React to the fact that a change has been successfully executed.
247     *  This method opens a new parameter editor to replace the one that
248     *  was closed.
249     *  @param change The change that was executed.
250     */
251    @Override
252    public void changeExecuted(ChangeRequest change) {
253        // Ignore if this is not the originator.
254        if (change == null || change.getSource() != this) {
255            return;
256        }
257
258        // Open a new dialog.
259        // NOTE: this is ugly. It is necessary because the dialog
260        // has been dismissed.
261        // NOTE: Do this in the event thread, since this might be invoked
262        // in whatever thread is processing mutations.
263        Runnable changeExecutedRunnable = new ChangeExecutedRunnable();
264        SwingUtilities.invokeLater(changeExecutedRunnable);
265        _target.removeChangeListener(this);
266    }
267
268    /** Notify the listener that a change has resulted in an exception.
269     *  @param change The change that was attempted.
270     *  @param exception The exception that resulted.
271     */
272    @Override
273    public void changeFailed(ChangeRequest change, final Exception exception) {
274        // Ignore if this is not the originator.
275        if (change == null || change.getSource() != this) {
276            return;
277        }
278
279        _target.removeChangeListener(this);
280
281        if (change.isErrorReported()) {
282            // Error has already been reported.
283            return;
284        }
285
286        change.setErrorReported(true);
287
288        // NOTE: Do this in the event thread, since this might be invoked
289        // in whatever thread is processing mutations.
290        Runnable changeFailedRunnable = new ChangeFailedRunnable(exception);
291        SwingUtilities.invokeLater(changeFailedRunnable);
292    }
293
294    /** Do the layout and then pack.
295     */
296    public void doLayoutAndPack() {
297        // The doLayoutAndPack method is declared in the
298        // ptolemy.gui.EditableParametersDialog interface.
299        // That interface is necessary to avoid a dependency
300        // between ptolemy.gui.Query and this class.
301        doLayout();
302        pack();
303    }
304
305    ///////////////////////////////////////////////////////////////////
306    ////                         protected methods                 ////
307
308    /** If the contents of this dialog implements the CloseListener
309     *  interface, then notify it that the window has closed.
310     */
311    @Override
312    protected void _handleClosing() {
313        super._handleClosing();
314
315        if (!buttonPressed().equals("Commit") && !buttonPressed().equals("Add")
316                && !buttonPressed().equals("Preferences")
317                && !buttonPressed().equals("Help")
318                && !buttonPressed().equals("Remove")) {
319            // Restore original parameter values.
320            ((Configurer) contents).restore();
321        }
322    }
323
324    /** Open a dialog to add a new parameter.
325     *  @param message A message to place at the top, or null if none.
326     *  @param name The default name.
327     *  @param defValue The default value.
328     *  @param className The default class name.
329     *  @return The dialog that is created.
330     */
331    protected ComponentDialog _openAddDialog(String message, String name,
332            String defValue, String className) {
333        // Create a new dialog to add a parameter, then open a new
334        // EditParametersDialog.
335        _query = new Query();
336
337        if (message != null) {
338            _query.setMessage(message);
339        }
340
341        _query.addChoice("class", "Class",
342                new String[] { "ptolemy.data.expr.Parameter",
343                        "ptolemy.data.expr.FileParameter",
344                        "ptolemy.kernel.util.StringAttribute",
345                        "ptolemy.actor.gui.ColorAttribute" },
346                "ptolemy.data.expr.Parameter", true);
347
348        _query.addLine("name", "Name", name);
349        _query.addLine("default", "Default value", defValue);
350
351        ComponentDialog dialog = new ComponentDialog(_owner,
352                "Add a new parameter to " + _target.getFullName(), _query,
353                null);
354
355        String parameterClass = _query.getStringValue("class");
356
357        // If the OK button was pressed, then queue a mutation
358        // to create the parameter.
359        // A blank property name is interpreted as a cancel.
360        String newName = _query.getStringValue("name");
361
362        // Need to escape quotes in default value.
363        String newDefValue = StringUtilities
364                .escapeForXML(_query.getStringValue("default"));
365
366        if (dialog.buttonPressed().equals("OK") && !newName.equals("")) {
367            String moml = "<property name=\"" + newName + "\" value=\""
368                    + newDefValue + "\" class=\"" + parameterClass + "\"/>";
369            _target.addChangeListener(this);
370
371            MoMLChangeRequest request = new MoMLChangeRequest(this, _target,
372                    moml);
373            request.setUndoable(true);
374            _target.requestChange(request);
375        }
376        return dialog;
377
378    }
379
380    ///////////////////////////////////////////////////////////////////
381    ////                         inner classes                     ////
382
383    /** A runnable for the change executed event. */
384    class ChangeExecutedRunnable implements Runnable {
385        @Override
386        public void run() {
387            new EditParametersDialog(_owner, _target);
388        }
389    }
390
391    /** A runnable for the change failed event. */
392    class ChangeFailedRunnable implements Runnable {
393        public ChangeFailedRunnable(Exception exception) {
394            _exception = exception;
395        }
396
397        @Override
398        public void run() {
399            // When a parameter is removed, and something depends on
400            // it, this gets called when _query is null.
401            // FIXME: Is this the right thing to do?
402            if (_query == null) {
403                return;
404            }
405
406            String newName = _query.getStringValue("name");
407            ComponentDialog dialog = _openAddDialog(
408                    _exception.getMessage()
409                            + "\n\nPlease enter a new default value:",
410                    newName, _query.getStringValue("default"),
411                    _query.getStringValue("class"));
412            _target.removeChangeListener(EditParametersDialog.this);
413
414            if (!dialog.buttonPressed().equals("OK")) {
415                // Remove the parameter, since it seems to be erroneous
416                // and the user hit cancel or close.
417                String moml = "<deleteProperty name=\"" + newName + "\"/>";
418                MoMLChangeRequest request = new MoMLChangeRequest(this, _target,
419                        moml);
420                request.setUndoable(true);
421                _target.requestChange(request);
422            }
423        }
424
425        private Exception _exception;
426    }
427
428    ///////////////////////////////////////////////////////////////////
429    ////                         protected variable                ////
430
431    /** The owner window. */
432    protected Frame _owner;
433
434    /** The query window for adding parameters. */
435    protected Query _query;
436
437    /** The target object whose parameters are being edited. */
438    protected NamedObj _target;
439
440    ///////////////////////////////////////////////////////////////////
441    ////                         private methods                   ////
442
443    /** Returns the URL of the help file to be displayed when the user
444     *  clicks the Help button. The help file defaults to the expression
445     *  language documentation, but can be overridden by attaching a
446     *  parameter {@code _helpURL} to the target object.
447     *  @return URL of the help file to be displayed, parsable by the
448     *          Ptolemy class loader.
449     */
450    private String _getHelpURL() {
451        // Look for a _helpURL parameter attached to the target object
452        List<StringParameter> attributeList = _target
453                .attributeList(StringParameter.class);
454        for (StringParameter attribute : attributeList) {
455            if (attribute.getName().equals("_helpURL")) {
456                try {
457                    return attribute.stringValue();
458                } catch (IllegalActionException ex) {
459                    try {
460                        MessageHandler.warning(
461                                "Couldn't access help URL parameter.", ex);
462                    } catch (CancelException exception) {
463                        // Ignore the cancel.
464                    }
465                }
466            }
467        }
468
469        // We couldn't find one, so return the default path to the
470        // expression language help
471        return "doc/expressions.htm";
472    }
473
474    /** Open a new dialog in a change request that defers
475     *  to the Swing thread. This ensures no race conditions
476     *  when we are re-opening a dialog to display the result
477     *  of an edit change.
478     */
479    //    private void _reOpen() {
480    //        ChangeRequest reOpen = new ChangeRequest(this,
481    //                "Re-open configure dialog") {
482    //            protected void _execute() throws Exception {
483    //                SwingUtilities.invokeLater(new Runnable() {
484    //                    public void run() {
485    //                        new EditParametersDialog(_owner, _target);
486    //                    }
487    //                });
488    //            }
489    //        };
490    //
491    //        _target.requestChange(reOpen);
492    //    }
493
494    ///////////////////////////////////////////////////////////////////
495    ////                         private variables                 ////
496    // Button labels.
497    private static String[] _moreButtons = { "Commit", "Add", "Remove",
498            "Defaults", "Preferences", "Help", "Cancel" };
499}