001/* A subclass of Query supporting Ptolemy II attributes.
002
003 Copyright (c) 1997-2018 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
027
028 */
029package ptolemy.actor.gui;
030
031import java.awt.Color;
032import java.awt.Component;
033import java.awt.Window;
034import java.awt.event.ActionEvent;
035import java.awt.event.ActionListener;
036import java.awt.event.KeyEvent;
037import java.io.File;
038import java.net.URI;
039import java.util.HashMap;
040import java.util.Iterator;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Map;
044
045import javax.swing.AbstractAction;
046import javax.swing.Box;
047import javax.swing.BoxLayout;
048import javax.swing.JButton;
049import javax.swing.JComponent;
050import javax.swing.JLabel;
051import javax.swing.JOptionPane;
052import javax.swing.JTextArea;
053import javax.swing.JTextField;
054import javax.swing.KeyStroke;
055import javax.swing.SwingUtilities;
056import javax.swing.text.JTextComponent;
057
058import com.microstar.xml.XmlException;
059
060import ptolemy.actor.gui.style.ParameterEditorStyle;
061import ptolemy.actor.parameters.DoubleRangeParameter;
062import ptolemy.actor.parameters.FilePortParameter;
063import ptolemy.actor.parameters.IntRangeParameter;
064import ptolemy.data.BooleanToken;
065import ptolemy.data.DoubleToken;
066import ptolemy.data.IntToken;
067import ptolemy.data.Token;
068import ptolemy.data.expr.FileParameter;
069import ptolemy.data.expr.Parameter;
070import ptolemy.data.expr.Variable;
071import ptolemy.data.type.BaseType;
072import ptolemy.data.type.Type;
073import ptolemy.gui.CloseListener;
074import ptolemy.gui.ComponentDialog;
075import ptolemy.gui.Query;
076import ptolemy.gui.QueryListener;
077import ptolemy.gui.SettableQueryChooser;
078import ptolemy.kernel.attributes.Actionable;
079import ptolemy.kernel.attributes.URIAttribute;
080import ptolemy.kernel.util.Attribute;
081import ptolemy.kernel.util.ChangeListener;
082import ptolemy.kernel.util.ChangeRequest;
083import ptolemy.kernel.util.IllegalActionException;
084import ptolemy.kernel.util.InternalErrorException;
085import ptolemy.kernel.util.NamedObj;
086import ptolemy.kernel.util.Settable;
087import ptolemy.kernel.util.ValueListener;
088import ptolemy.moml.Documentation;
089import ptolemy.moml.ErrorHandler;
090import ptolemy.moml.MoMLChangeRequest;
091import ptolemy.moml.MoMLParser;
092import ptolemy.util.MessageHandler;
093import ptolemy.util.StringUtilities;
094
095///////////////////////////////////////////////////////////////////
096//// PtolemyQuery
097
098/**
099 This class is a query dialog box with various entries for setting
100 the values of Ptolemy II attributes that implement the Settable
101 interface and have visibility FULL.  One or more entries are
102 associated with an attribute so that if the entry is changed, the
103 attribute value is updated, and if the attribute value changes,
104 the entry is updated. To change an attribute, this class queues
105 a change request with a particular object called the <i>change
106 handler</i>.  The change handler is specified as a constructor
107 argument.
108 <p>
109 It is important to note that it may take
110 some time before the value of a attribute is actually changed, since it
111 is up to the change handler to decide when change requests are processed.
112 The change handler will typically delegate change requests to the
113 Manager, although this is not necessarily the case.
114 <p>
115 To use this class, add an entry to the query using addStyledEntry().
116
117 @author Brian K. Vogel and Edward A. Lee, Contributor: Christoph Daniel Schulze
118 @version $Id$
119 @since Ptolemy II 0.4
120 @Pt.ProposedRating Yellow (eal)
121 @Pt.AcceptedRating Yellow (neuendor)
122 */
123@SuppressWarnings("serial")
124public class PtolemyQuery extends Query
125        implements QueryListener, ValueListener, ChangeListener, CloseListener {
126    /** Construct a panel with no queries in it and with the specified
127     *  change handler. When an entry changes, a change request is
128     *  queued with the given change handler. The change handler should
129     *  normally be a composite actor that deeply contains all attributes
130     *  that are attached to query entries.  Otherwise, the change requests
131     *  might get queued with a handler that has nothing to do with
132     *  the attributes.  The handler is also used to report errors.
133     *  @param handler The change handler.
134     */
135    public PtolemyQuery(NamedObj handler) {
136        super();
137        addQueryListener(this);
138        _handler = handler;
139
140        if (_handler != null) {
141            // NOTE: Since we register as a listener to the handler,
142            // there is no need to also register as a listner with
143            // each change request.  EAL 9/15/02.
144            _handler.addChangeListener(this);
145        }
146
147        _varToListOfEntries = new HashMap<Settable, List<String>>();
148    }
149
150    ///////////////////////////////////////////////////////////////////
151    ////                         public methods                    ////
152
153    /** Create an entry box with a button for the specified action.
154     *  @param name The name used to identify the entry (when calling get).
155     *  @param label The label to attach to the entry.
156     *  @param defaultValue The default entry value.
157     *  @param actionable The specification for the action name and action.
158     *  @return The component.
159     */
160    public ActionableEntry addActionable(String name, String label,
161            String defaultValue, Actionable actionable) {
162        JLabel lbl = new JLabel(label + ": ");
163        lbl.setBackground(_background);
164
165        ActionableEntry actionButton = new ActionableEntry(this, name,
166                defaultValue, actionable);
167        _addPair(name, lbl, actionButton, actionButton);
168        return actionButton;
169    }
170
171    /** Add a new entry to this query that represents the given attribute.
172     *  The name of the entry will be set to the name of the attribute,
173     *  and the attribute will be attached to the entry, so that if the
174     *  attribute is updated, then the entry is updated. If the attribute
175     *  contains an instance of ParameterEditorStyle, then defer to
176     *  the style to create the entry, otherwise just create a default entry.
177     *  The style used in a default entry depends on the class of the
178     *  attribute and on its declared type, but defaults to a one-line
179     *  entry if there is no obviously better style.
180     *  Only the first style that is found is used to create an entry.
181     *  @param attribute The attribute to create an entry for.
182     */
183    public void addStyledEntry(Settable attribute) {
184        // Note: it would be nice to give
185        // multiple styles to specify to create more than one
186        // entry for a particular parameter.  However, the style configurer
187        // doesn't support it and we don't have a good way of representing
188        // it in this class.
189        // Look for a ParameterEditorStyle.
190        boolean foundStyle = false;
191
192        try {
193            _addingStyledEntryFor = attribute;
194
195            if (attribute instanceof NamedObj) {
196                Iterator<?> styles = ((NamedObj) attribute)
197                        .attributeList(ParameterEditorStyle.class).iterator();
198
199                while (styles.hasNext() && !foundStyle) {
200                    ParameterEditorStyle style = (ParameterEditorStyle) styles
201                            .next();
202
203                    try {
204                        style.addEntry(this);
205                        foundStyle = true;
206                    } catch (IllegalActionException ex) {
207                        // Ignore failures here, and just present
208                        // the default dialog.
209                    }
210                }
211            }
212
213            if (!foundStyle) {
214                // NOTE: Infer the style.
215                // This is a regrettable approach, but it keeps
216                // dependence on UI issues out of actor definitions.
217                // Also, the style code is duplicated here and in the
218                // style attributes. However, it won't work to create
219                // a style attribute here, because we don't necessarily
220                // have write access to the workspace.
221                String name = attribute.getName();
222                String displayName = attribute.getDisplayName();
223
224                try {
225                    JComponent component = null;
226                    if (attribute instanceof IntRangeParameter) {
227                        int current = ((IntRangeParameter) attribute)
228                                .getCurrentValue();
229                        int min = ((IntRangeParameter) attribute).getMinValue();
230                        int max = ((IntRangeParameter) attribute).getMaxValue();
231                        String minLabel = ((IntRangeParameter) attribute).minLabel
232                                .stringValue();
233                        String maxLabel = ((IntRangeParameter) attribute).maxLabel
234                                .stringValue();
235
236                        // minLabel and maxLabel can contain the special placeholders $min and
237                        // $max, which must be replaced by the actual limits of the range
238                        minLabel = minLabel.replace("$min",
239                                Double.toString(min));
240                        maxLabel = maxLabel.replace("$max",
241                                Double.toString(max));
242
243                        component = addSlider(name, displayName, current, min,
244                                max, minLabel, maxLabel);
245                        attachParameter(attribute, name);
246                        foundStyle = true;
247                        _addSubmitAction(component, attribute.getName(),
248                                attribute);
249                    } else if (attribute instanceof DoubleRangeParameter) {
250                        double current = ((DoubleToken) ((DoubleRangeParameter) attribute)
251                                .getToken()).doubleValue();
252                        double max = ((DoubleToken) ((DoubleRangeParameter) attribute).max
253                                .getToken()).doubleValue();
254                        double min = ((DoubleToken) ((DoubleRangeParameter) attribute).min
255                                .getToken()).doubleValue();
256                        int precision = ((IntToken) ((DoubleRangeParameter) attribute).precision
257                                .getToken()).intValue();
258                        String minLabel = ((DoubleRangeParameter) attribute).minLabel
259                                .stringValue();
260                        String maxLabel = ((DoubleRangeParameter) attribute).maxLabel
261                                .stringValue();
262
263                        // minLabel and maxLabel can contain the special placeholders $min and
264                        // $max, which must be replaced by the actual limits of the range
265                        minLabel = minLabel.replace("$min",
266                                Double.toString(min));
267                        maxLabel = maxLabel.replace("$max",
268                                Double.toString(max));
269
270                        // Get the quantized integer for the current value.
271                        int quantized = (int) Math.round(
272                                (current - min) * precision / (max - min));
273                        component = addSlider(name, displayName, quantized, 0,
274                                precision, minLabel, maxLabel);
275                        attachParameter(attribute, name);
276                        foundStyle = true;
277                        _addSubmitAction(component, attribute.getName(),
278                                attribute);
279                    } else if (attribute instanceof ColorAttribute) {
280                        component = addColorChooser(name, displayName,
281                                attribute.getExpression());
282                        attachParameter(attribute, name);
283                        foundStyle = true;
284                        _addSubmitAction(component, attribute.getName(),
285                                attribute);
286                    } else if (attribute instanceof Actionable) {
287                        component = addActionable(name, displayName,
288                                attribute.getExpression(),
289                                (Actionable) attribute);
290                        attachParameter(attribute, name);
291                        foundStyle = true;
292                        _addSubmitAction(component, attribute.getName(),
293                                attribute);
294                    } else if (attribute instanceof CustomQueryBoxParameter) {
295                        JLabel label = new JLabel(displayName + ": ");
296                        label.setBackground(_background);
297                        component = ((CustomQueryBoxParameter) attribute)
298                                .createQueryBox(this, attribute);
299                        _addPair(name, label, component, component);
300                        attachParameter(attribute, name);
301                        foundStyle = true;
302                        _addSubmitAction(component, attribute.getName(),
303                                attribute);
304                    } else if (attribute instanceof FileParameter
305                            || attribute instanceof FilePortParameter) {
306                        // Specify the directory in which to start browsing
307                        // to be the location where the model is defined,
308                        // if that is known.
309                        URI modelURI = URIAttribute
310                                .getModelURI((NamedObj) attribute);
311                        File directory = null;
312
313                        if (modelURI != null) {
314                            if (modelURI.getScheme().equals("file")) {
315                                try {
316                                    File modelFile = new File(modelURI);
317                                    directory = modelFile.getParentFile();
318                                } catch (Throwable ex) {
319                                    throw new RuntimeException(
320                                            "Failed to create a File for modelURI: "
321                                                    + ex);
322                                }
323                            }
324                        }
325
326                        URI base = null;
327
328                        if (directory != null) {
329                            base = directory.toURI();
330                        }
331
332                        // Check to see whether the attribute being configured
333                        // specifies whether files or directories should be listed.
334                        // By default, only files are selectable.
335                        boolean allowFiles = true;
336                        boolean allowDirectories = false;
337
338                        // attribute is always a NamedObj
339                        Parameter marker = (Parameter) ((NamedObj) attribute)
340                                .getAttribute("allowFiles", Parameter.class);
341
342                        if (marker != null) {
343                            Token value = marker.getToken();
344
345                            if (value instanceof BooleanToken) {
346                                allowFiles = ((BooleanToken) value)
347                                        .booleanValue();
348                            }
349                        }
350
351                        marker = (Parameter) ((NamedObj) attribute)
352                                .getAttribute("allowDirectories",
353                                        Parameter.class);
354
355                        if (marker != null) {
356                            Token value = marker.getToken();
357
358                            if (value instanceof BooleanToken) {
359                                allowDirectories = ((BooleanToken) value)
360                                        .booleanValue();
361                            }
362                        }
363
364                        // FIXME: What to do when neither files nor directories are allowed?
365                        if (!allowFiles && !allowDirectories) {
366                            // The given attribute will not have a query in the dialog.
367                            return;
368                        }
369
370                        boolean isOutput = false;
371                        if (attribute instanceof FileParameter
372                                && ((FileParameter) attribute).isOutput()) {
373                            isOutput = true;
374                        }
375
376                        // FIXME: Should remember previous browse location?
377                        // Next to last argument is the starting directory.
378                        component = addFileChooser(name, displayName,
379                                attribute.getExpression(), base, directory,
380                                allowFiles, allowDirectories, isOutput,
381                                preferredBackgroundColor(attribute),
382                                preferredForegroundColor(attribute));
383                        attachParameter(attribute, name);
384                        foundStyle = true;
385                        _addSubmitAction(component, attribute.getName(),
386                                attribute);
387                    } else if (attribute instanceof PasswordAttribute) {
388                        component = addPassword(name, displayName, "");
389                        attachParameter(attribute, name);
390                        foundStyle = true;
391                        _addSubmitAction(component, attribute.getName(),
392                                attribute);
393                    } else if (attribute instanceof Parameter
394                            && ((Parameter) attribute).getChoices() != null) {
395                        Parameter castAttribute = (Parameter) attribute;
396
397                        // NOTE: Make this always editable since Parameter
398                        // supports a form of expressions for value propagation.
399                        component = addChoice(name, displayName,
400                                castAttribute.getChoices(),
401                                castAttribute.getExpression(), true,
402                                preferredBackgroundColor(attribute),
403                                preferredForegroundColor(attribute));
404                        attachParameter(attribute, name);
405                        foundStyle = true;
406                        _addSubmitAction(component, attribute.getName(),
407                                attribute);
408                    } else if (attribute instanceof NamedObj
409                            && (((NamedObj) attribute)
410                                    .getAttribute("_textWidthHint") != null
411                                    || ((NamedObj) attribute).getAttribute(
412                                            "_textHeightHint") != null)) {
413                        // Support hints for text height and/or width so that actors
414                        // don't have to use a ParameterEditorStyle, which depends
415                        // on packages that depend on graphics.
416
417                        // Default values:
418                        int widthValue = 30;
419                        int heightValue = 10;
420
421                        Attribute widthAttribute = ((NamedObj) attribute)
422                                .getAttribute("_textWidthHint");
423                        if (widthAttribute instanceof Variable) {
424                            Token token = ((Variable) widthAttribute)
425                                    .getToken();
426                            if (token instanceof IntToken) {
427                                widthValue = ((IntToken) token).intValue();
428                            }
429                        }
430                        Attribute heightAttribute = ((NamedObj) attribute)
431                                .getAttribute("_textHeightHint");
432                        if (heightAttribute instanceof Variable) {
433                            Token token = ((Variable) heightAttribute)
434                                    .getToken();
435                            if (token instanceof IntToken) {
436                                heightValue = ((IntToken) token).intValue();
437                            }
438                        }
439
440                        component = addTextArea(name, displayName,
441                                attribute.getExpression(),
442                                preferredBackgroundColor(attribute),
443                                preferredForegroundColor(attribute),
444                                heightValue, widthValue);
445
446                        attachParameter(attribute, name);
447                        foundStyle = true;
448                        _addSubmitAction(component, attribute.getName(),
449                                attribute);
450                    } else if (attribute instanceof Variable) {
451                        Type declaredType = ((Variable) attribute)
452                                .getDeclaredType();
453                        Token current = ((Variable) attribute).getToken();
454
455                        if (declaredType == BaseType.BOOLEAN) {
456                            // NOTE: If the expression is something other than
457                            // "true" or "false", then this parameter is set
458                            // to an expression that evaluates to to a boolean,
459                            // and the default Line style should be used.
460                            if (attribute.getExpression().equals("true")
461                                    || attribute.getExpression()
462                                            .equals("false")) {
463                                component = addCheckBox(name, displayName,
464                                        ((BooleanToken) current)
465                                                .booleanValue());
466                                attachParameter(attribute, name);
467                                foundStyle = true;
468                                _addSubmitAction(component, attribute.getName(),
469                                        attribute);
470                            }
471                        }
472                    }
473
474                    // NOTE: Other attribute classes?
475
476                    if (attribute.getVisibility() == Settable.NOT_EDITABLE) {
477                        if (component == null) {
478                            String defaultValue = attribute.getExpression();
479                            component = addDisplay(name, displayName,
480                                    defaultValue);
481                            attachParameter(attribute, name);
482                            foundStyle = true;
483                            _addSubmitAction(component, attribute.getName(),
484                                    attribute);
485                        } else {
486                            adjustEditable(attribute, component);
487                        }
488                    }
489                } catch (IllegalActionException ex) {
490                    // Ignore and create a line entry.
491                }
492            }
493
494            String defaultValue = attribute.getExpression();
495
496            if (defaultValue == null) {
497                defaultValue = "";
498            }
499
500            if (!foundStyle) {
501
502                // Make the text scrollable.
503                final JTextArea area = addTextArea(attribute.getName(),
504                        attribute.getDisplayName(), defaultValue,
505                        preferredBackgroundColor(attribute),
506                        preferredForegroundColor(attribute), 1,
507                        DEFAULT_ENTRY_WIDTH);
508                area.setRows(Math.min(5, area.getLineCount()));
509
510                _addSubmitAction(area, attribute.getName(), attribute);
511
512                // The style itself does this, so we don't need to do it again.
513                attachParameter(attribute, attribute.getName());
514            }
515        } finally {
516            _addingStyledEntryFor = null;
517        }
518    }
519
520    /** Adjust the editability of the component depending on
521     *  whether the attribute has Settable.NOT_EDITABLE
522     *  visibility and if the _exportMode attribute is set
523     *  in the container.
524     *  @param settable The attribute to be tested
525     *  @param component The component to disabled if
526     *  the attribute has Settable.NOT_VISIBILITY and
527     *  _expertMode is not present in the container of the attribute.
528     *  @return true if the component should be editable,
529     *  false otherwise.
530     */
531    public boolean adjustEditable(Settable settable, Component component) {
532        if (settable.getVisibility() == Settable.NOT_EDITABLE) {
533            NamedObj container = settable.getContainer();
534            Attribute expertMode = container.getAttribute("_expertMode");
535            if (expertMode == null) {
536                // If the user has selected expert mode, then they can
537                // set the editor and edit the value.
538                if (component instanceof JTextComponent) {
539                    component.setBackground(_background);
540                    ((JTextComponent) component).setEditable(false);
541                } else {
542                    if (component != null) {
543                        component.setEnabled(false);
544                    }
545                }
546                return false;
547            }
548        }
549        return true;
550    }
551
552    /** Attach an attribute to an entry with name <i>entryName</i>,
553     *  of a Query. This will cause the attribute to be updated whenever
554     *  the specified entry changes.  In addition, a listener is registered
555     *  so that the entry will change whenever
556     *  the attribute changes. If the entry has previously been attached
557     *  to a attribute, then it is detached first from that attribute.
558     *  If the attribute argument is null, this has the effect of detaching
559     *  the entry from any attribute.
560     *  @param attribute The attribute to attach to an entry.
561     *  @param entryName The entry to attach the attribute to.
562     */
563    public void attachParameter(Settable attribute, String entryName) {
564        // Put the attribute in a Map from entryName -> attribute
565        _attributes.put(entryName, attribute);
566
567        // Make a record of the attribute value prior to the change,
568        // in case a change fails and the user chooses to revert.
569        // Use the translated expression in case the attribute
570        // is a DoubleRangeParameter.
571        _revertValue.put(entryName, _getTranslatedExpression(attribute));
572
573        // Attach the entry to the attribute by registering a listener.
574        attribute.addValueListener(this);
575
576        // If the attribute is a Variable, set a weak dependency to avoid
577        // warnings if the attribute changes containers.
578        // See https://projects.ecoinformatics.org/ecoinfo/issues/6681.
579        if (attribute instanceof Variable) {
580            ((Variable) attribute).setValueListenerAsWeakDependency(this);
581        }
582
583        // Put the attribute in a Map from attribute -> (list of entry names
584        // attached to attribute), but only if entryName is not already
585        // contained by the list.
586        if (_varToListOfEntries.get(attribute) == null) {
587            // No mapping for attribute exists.
588            List<String> entryNameList = new LinkedList<String>();
589            entryNameList.add(entryName);
590            _varToListOfEntries.put(attribute, entryNameList);
591        } else {
592            // attribute is mapped to a list of entry names, but need to
593            // check whether entryName is in the list. If not, add it.
594            List<String> entryNameList = _varToListOfEntries.get(attribute);
595            Iterator<String> entryNames = entryNameList.iterator();
596            boolean found = false;
597
598            while (entryNames.hasNext()) {
599                // Check whether entryName is in the list. If not, add it.
600                String name = entryNames.next();
601
602                if (name.equals(entryName)) {
603                    found = true;
604                }
605            }
606
607            if (found == false) {
608                // Add entryName to the list.
609                entryNameList.add(entryName);
610            }
611        }
612
613        // Handle tool tips.  This is almost certainly an instance
614        // of NamedObj, but check to be sure.
615        if (attribute instanceof NamedObj) {
616            Attribute tooltipAttribute = ((NamedObj) attribute)
617                    .getAttribute("tooltip");
618
619            if (tooltipAttribute != null
620                    && tooltipAttribute instanceof Documentation) {
621                setToolTip(entryName,
622                        ((Documentation) tooltipAttribute).getValueAsString());
623            } else {
624                String tip = Documentation.consolidate((NamedObj) attribute);
625
626                if (tip != null) {
627                    setToolTip(entryName, tip);
628                }
629            }
630        }
631    }
632
633    /** Notify this class that a change has been successfully executed
634     *  by the change handler.
635     *  @param change The change that has been executed.
636     */
637    @Override
638    public void changeExecuted(ChangeRequest change) {
639        // Ignore if this was not the originator.
640        if (change != null) {
641            if (change.getSource() != this) {
642                return;
643            }
644
645            // Restore the parser error handler.
646            if (_savedErrorHandler != null) {
647                MoMLParser.setErrorHandler(_savedErrorHandler);
648            }
649
650            String name = change.getDescription();
651
652            if (_attributes.containsKey(name)) {
653                final Settable attribute = (Settable) _attributes.get(name);
654
655                // Make a record of the successful attribute value change
656                // in case some future change fails and the user
657                // chooses to revert.
658                // Use the translated expression in case the attribute
659                // is a DoubleRangeParameter.
660                _revertValue.put(name, _getTranslatedExpression(attribute));
661            }
662        }
663    }
664
665    /** Notify the listener that a change attempted by the change handler
666     *  has resulted in an exception.  This method brings up a new dialog
667     *  to prompt the user for a corrected entry.  If the user hits the
668     *  cancel button, then the attribute is reverted to its original
669     *  value.
670     *  @param change The change that was attempted.
671     *  @param exception The exception that resulted.
672     */
673    @Override
674    public void changeFailed(final ChangeRequest change, Exception exception) {
675        // Ignore if this was not the originator, or if the error has already
676        // been reported, or if the change request is null.
677        if (change == null || change.getSource() != this) {
678            return;
679        }
680
681        // Restore the parser error handler.
682        if (_savedErrorHandler != null) {
683            MoMLParser.setErrorHandler(_savedErrorHandler);
684        }
685
686        // If this is already a dialog reporting an error, and is
687        // still visible, then just update the message.  Otherwise,
688        // create a new dialog to prompt the user for a corrected input.
689        if (_isOpenErrorWindow) {
690            setMessage(exception.getMessage()
691                    + "\n\nPlease enter a new value (or cancel to revert):");
692        } else {
693            if (change.isErrorReported()) {
694                // Error has already been reported.
695                return;
696            }
697
698            change.setErrorReported(true);
699
700            _query = new PtolemyQuery(_handler);
701            _query.setTextWidth(getTextWidth());
702            _query._isOpenErrorWindow = true;
703
704            String description = change.getDescription();
705            _query.setMessage(
706                    exception.getMessage() + "\n\nPlease enter a new value:");
707
708            /* NOTE: The error message used to be more verbose, as follows.
709             * But this is intimidating to users.
710             _query.setMessage("Change failed:\n"
711             + description
712             + "\n" + exception.getMessage()
713             + "\n\nPlease enter a new value:");
714             */
715
716            // Need to extract the name of the entry from the request.
717            // Default value is the description itself.
718            // NOTE: This is very fragile... depends on the particular
719            // form of the MoML change request.
720            String tmpEntryName = description;
721            int patternStart = description.lastIndexOf("<property name=\"");
722
723            if (patternStart >= 0) {
724                int nextQuote = description.indexOf("\"", patternStart + 16);
725
726                if (nextQuote > patternStart + 15) {
727                    tmpEntryName = description.substring(patternStart + 16,
728                            nextQuote);
729                }
730            }
731
732            final String entryName = tmpEntryName;
733            final Settable attribute = (Settable) _attributes.get(entryName);
734
735            // NOTE: Do this in the event thread, since this might be invoked
736            // in whatever thread is processing mutations.
737            SwingUtilities.invokeLater(new Runnable() {
738                @Override
739                public void run() {
740                    if (attribute != null) {
741                        _query.addStyledEntry(attribute);
742                    } else {
743                        throw new InternalErrorException(
744                                "Expected attribute attached to entry name: "
745                                        + entryName);
746                    }
747
748                    _dialog = new ComponentDialog(
749                            JOptionPane.getFrameForComponent(PtolemyQuery.this),
750                            "Error", _query, null);
751
752                    // The above returns only when the modal
753                    // dialog is closing.  The following will
754                    // force a new dialog to be created if the
755                    // value is not valid.
756                    _query._isOpenErrorWindow = false;
757
758                    if (_dialog.buttonPressed().equals("Cancel")) {
759                        if (_revertValue.containsKey(entryName)) {
760                            String revertValue = _revertValue.get(entryName);
761
762                            // NOTE: Do not use setAndNotify() here because
763                            // that checks whether the string entry has
764                            // changed, and we want to force revert even
765                            // if it appears to not have changed.
766                            set(((NamedObj) attribute).getName(), revertValue);
767                            changed(entryName);
768                        }
769                    } else {
770                        // Force evaluation to check validity of
771                        // the entry.  NOTE: Normally, we would
772                        // not need to force evaluation because if
773                        // the value has changed, then listeners
774                        // are automatically notified.  However,
775                        // if the value has not changed, then they
776                        // are not notified.  Since the original
777                        // value was invalid, it is not acceptable
778                        // to skip notification in this case.  So
779                        // we force it.
780                        try {
781                            attribute.validate();
782                        } catch (IllegalActionException ex) {
783                            change.setErrorReported(false);
784                            changeFailed(change, ex);
785                        }
786                    }
787                }
788            });
789        }
790    }
791
792    /** Queue a change request to alter the value of the attribute
793     *  attached to the specified entry, if there is one. This method is
794     *  called whenever an entry has been changed.
795     *  If no attribute is attached to the specified entry, then
796     *  do nothing.
797     *  @param name The name of the entry that has changed.
798     */
799    @Override
800    public void changed(final String name) {
801        // Check if the entry that changed is in the mapping.
802        if (_attributes.containsKey(name)) {
803            final Settable attribute = (Settable) _attributes.get(name);
804
805            if (attribute == null) {
806                // No associated attribute.
807                return;
808            }
809
810            ChangeRequest request;
811
812            if (attribute instanceof PasswordAttribute) {
813                // Passwords have to be handled specially because the password
814                // is not represented in a string.
815                request = new ChangeRequest(this, name) {
816                    @Override
817                    protected void _execute() throws IllegalActionException {
818                        char[] password = getCharArrayValue(name);
819                        ((PasswordAttribute) attribute).setPassword(password);
820                        attribute.validate();
821
822                        Iterator<?> derived = ((PasswordAttribute) attribute)
823                                .getDerivedList().iterator();
824
825                        while (derived.hasNext()) {
826                            PasswordAttribute derivedPassword = (PasswordAttribute) derived
827                                    .next();
828                            derivedPassword.setPassword(password);
829                        }
830                    }
831                };
832            } else if (attribute instanceof NamedObj) {
833                // NOTE: We must use a MoMLChangeRequest so that changes
834                // propagate to any objects that have been instantiating
835                // using this one as a class.  This is only an issue if
836                // attribute is a NamedObj.
837                NamedObj castAttribute = (NamedObj) attribute;
838
839                String stringValue = getStringValue(name);
840
841                // If the attribute is a DoubleRangeParameter, then we
842                // have to translate the integer value returned by the
843                // JSlider into a double.
844                if (attribute instanceof DoubleRangeParameter) {
845                    try {
846                        int newValue = Integer.parseInt(stringValue);
847                        int precision = ((IntToken) ((DoubleRangeParameter) attribute).precision
848                                .getToken()).intValue();
849                        double max = ((DoubleToken) ((DoubleRangeParameter) attribute).max
850                                .getToken()).doubleValue();
851                        double min = ((DoubleToken) ((DoubleRangeParameter) attribute).min
852                                .getToken()).doubleValue();
853                        double newValueAsDouble = min
854                                + (max - min) * newValue / precision;
855                        stringValue = "" + newValueAsDouble;
856                    } catch (IllegalActionException e) {
857                        throw new InternalErrorException(e);
858                    }
859                }
860
861                // The context for the MoML should be the first container
862                // above this attribute in the hierarchy that defers its
863                // MoML definition, or the immediate parent if there is none.
864                NamedObj parent = castAttribute.getContainer();
865                String moml = "<property name=\"" + castAttribute.getName()
866                        + "\" value=\""
867                        + StringUtilities.escapeForXML(stringValue) + "\"/>";
868                request = new MoMLChangeRequest(this, // originator
869                        parent, // context
870                        moml, // MoML code
871                        null) { // base
872                    @Override
873                    protected void _execute() throws Exception {
874                        synchronized (PtolemyQuery.this) {
875                            try {
876                                _ignoreChangeNotifications = true;
877                                super._execute();
878                            } catch (XmlException ex) {
879                                // Attempt to give a friendlier exception message.
880                                // In this case, the XML string is not really visible to the user,
881                                // so reporting this as an XML exception makes no sense.
882                                if (ex.getCause() instanceof Exception) {
883                                    throw (Exception) ex.getCause();
884                                } else {
885                                    throw ex;
886                                }
887                            } finally {
888                                _ignoreChangeNotifications = false;
889                            }
890                        }
891                    }
892                };
893            } else {
894                // If the attribute is not a NamedObj, then we
895                // set its value directly.
896                request = new ChangeRequest(this, name) {
897                    @Override
898                    protected void _execute() throws IllegalActionException {
899                        attribute.setExpression(getStringValue(name));
900
901                        attribute.validate();
902
903                        /* NOTE: Earlier version:
904                         // Here, we need to handle instances of Variable
905                         // specially.  This is too bad...
906                         if (attribute instanceof Variable) {
907
908                         // Will this ever happen?  A
909                         // Variable that is not a NamedObj???
910                         // Retrieve the token to force
911                         // evaluation, so as to check the
912                         // validity of the new value.
913
914                         ((Variable)attribute).getToken();
915                         }
916                         */
917                    }
918                };
919            }
920
921            // NOTE: This object is never removed as a listener from
922            // the change request.  This is OK because this query will
923            // be closed at some point, and all references to it will
924            // disappear, and thus both it and the change request should
925            // become accessible to the garbage collector.  However, I
926            // don't quite trust Java to do this right, since it's not
927            // completely clear that it releases resources when windows
928            // are closed.  It would be better if this listener were
929            // a weak reference.
930            // NOTE: This appears to be unnecessary, since we register
931            // as a change listener on the handler.  This results in
932            // two notifications.  EAL 9/15/02.
933            request.addChangeListener(this);
934
935            if (_handler == null) {
936                request.execute();
937            } else {
938                if (request instanceof MoMLChangeRequest) {
939                    ((MoMLChangeRequest) request).setUndoable(true);
940                }
941
942                // Remove the error handler so that this class handles
943                // the error through the notification.  Save the previous
944                // error handler to restore after this request has been
945                // processes.
946                _savedErrorHandler = MoMLParser.getErrorHandler();
947                MoMLParser.setErrorHandler(null);
948                _handler.requestChange(request);
949            }
950        }
951    }
952
953    /** Return the preferred background color for editing the specified
954     *  object.  The default is Color.white, but if the object is an
955     *  instance of Parameter and it is in string mode, then a light
956     *  blue is returned.
957     *  @param object The object to be edited.
958     *  @return the preferred background color.
959     */
960    public static Color preferredBackgroundColor(Object object) {
961        Color background = Color.white;
962
963        if (object instanceof Variable) {
964            if (((Variable) object).isStringMode()) {
965                background = _STRING_MODE_BACKGROUND_COLOR;
966                if (((Variable) object).getAttribute("_JSON") != null) {
967                    // String needs to be JSON. Use a different color.
968                    background = _JSON_MODE_BACKGROUND_COLOR;
969                }
970            }
971        }
972
973        return background;
974    }
975
976    /** Return the preferred foreground color for editing the specified
977     *  object.  This returns Color.black, but in the future this might
978     *  be changed to use color for some informative purpose.
979     *  @param object The object to be edited.
980     *  @return the preferred foreground color.
981     */
982    public static Color preferredForegroundColor(Object object) {
983        Color foreground = Color.black;
984
985        /* NOTE: This doesn't work very well because when you
986         * start typing on a red entry, it remains red rather
987         * than switching to black to indicate an override.
988         if (object instanceof NamedObj) {
989         if (!((NamedObj)object).isOverridden()) {
990         foreground = _NOT_OVERRIDDEN_FOREGROUND_COLOR;
991         }
992         }
993         */
994        return foreground;
995    }
996
997    /** Notify this query that the value of the specified attribute has
998     *  changed.  This is called by an attached attribute when its
999     *  value changes. This method updates the displayed value of
1000     *  all entries that are attached to the attribute.
1001     *  @param attribute The attribute whose value has changed.
1002     */
1003    @Override
1004    public void valueChanged(final Settable attribute) {
1005        // If our own change request is the cause of this notification,
1006        // then ignore it.
1007        if (_ignoreChangeNotifications) {
1008            return;
1009        }
1010
1011        // Do this in the event thread, since it depends on interacting
1012        // with the UI.  In particular, there is no assurance that
1013        // getStringValue() will return the correct value if it is called
1014        // from another thread.  And this method is called whenever an
1015        // attribute change has occurred, which can happen in any thread.
1016        SwingUtilities.invokeLater(new Runnable() {
1017            @Override
1018            public void run() {
1019                // Check that the attribute is attached
1020                // to at least one entry.
1021                if (_attributes.containsValue(attribute)) {
1022                    // Get the list of entry names that the attribute
1023                    // is attached to.
1024                    List<String> entryNameList = _varToListOfEntries
1025                            .get(attribute);
1026
1027                    // For each entry name, call set() to update its
1028                    // value with the value of attribute
1029                    Iterator<String> entryNames = entryNameList.iterator();
1030
1031                    String newValue = _getTranslatedExpression(attribute);
1032
1033                    while (entryNames.hasNext()) {
1034                        String name = entryNames.next();
1035
1036                        // Compare value against what is in
1037                        // already to avoid changing it again.
1038                        if (!getStringValue(name).equals(newValue)) {
1039                            set(name, newValue);
1040                        }
1041                    }
1042                }
1043            }
1044        });
1045    }
1046
1047    /** Unsubscribe as a listener to all objects that we have subscribed to.
1048     *  @param window The window that closed.
1049     *  @param button The name of the button that was used to close the window.
1050     */
1051    @Override
1052    public void windowClosed(Window window, String button) {
1053        // FIXME: It seems that we need to force notification of
1054        // all changes before doing the restore!  Otherwise, some
1055        // random time later, a line in the query might lose the focus,
1056        // causing it to override a restore.  However, this has the
1057        // unfortunate side effect of causing an erroneous entry to
1058        // trigger a dialog even if the cancel button is pressed!
1059        // No good workaround here.
1060        // notifyListeners();
1061        if (_handler != null) {
1062            _handler.removeChangeListener(PtolemyQuery.this);
1063        }
1064
1065        // It's a bit bizarre that we have to remove ourselves as a listener
1066        // to ourselves, since the window is closing.  But if we don't do
1067        // this, then somehow we continue to be notified of changes to
1068        // the attributes.
1069        removeQueryListener(this);
1070
1071        Iterator<Settable> attributes = _attributes.values().iterator();
1072
1073        while (attributes.hasNext()) {
1074            Settable attribute = attributes.next();
1075            attribute.removeValueListener(this);
1076        }
1077    }
1078
1079    ///////////////////////////////////////////////////////////////////
1080    ////                         protected methods                 ////
1081
1082    /** Override the base class to put a button on the right if
1083     *  the Settable object for which we are adding an entry itself
1084     *  contains Settable parameters.
1085     *  @param name The name of the entry.
1086     *  @param label The label.
1087     *  @param widget The interactive entry to the right of the label.
1088     *  @param entry The object that contains user data.
1089     */
1090    @Override
1091    protected void _addPair(String name, JLabel label, Component widget,
1092            Object entry) {
1093        if (_addingStyledEntryFor != null) {
1094            List<Settable> settables = ((NamedObj) _addingStyledEntryFor)
1095                    .attributeList(Settable.class);
1096            if (settables == null || settables.size() == 0) {
1097                super._addPair(name, label, widget, entry);
1098            } else {
1099                // Check to make sure at least one of the contained
1100                // parameters is visible.
1101                boolean foundOne = false;
1102                for (Settable settable : settables) {
1103                    if (Configurer.isVisible((NamedObj) _addingStyledEntryFor,
1104                            settable)) {
1105                        foundOne = true;
1106                        break;
1107                    }
1108                }
1109                if (foundOne) {
1110                    HierarchicalConfigurer configurer = new HierarchicalConfigurer(
1111                            PtolemyQuery.this, name, _addingStyledEntryFor,
1112                            widget);
1113                    super._addPair(name, label, configurer, entry);
1114                } else {
1115                    super._addPair(name, label, widget, entry);
1116                }
1117            }
1118        } else {
1119            super._addPair(name, label, widget, entry);
1120        }
1121    }
1122
1123    ///////////////////////////////////////////////////////////////////
1124    ////                         protected variables               ////
1125
1126    /** Maps an entry name to the attribute that is attached to it. */
1127    protected Map _attributes = new HashMap();
1128
1129    ///////////////////////////////////////////////////////////////////
1130    ////                         private methods                   ////
1131
1132    /** Add submit action to component in dialogue. If parameter could be
1133     *  validated close the dialogue after.
1134     *  @param component The component.
1135     *  @param attributeName The name of the attribute edited by the component.
1136     *  @param attribute The attribute edited by the component.
1137     */
1138    private void _addSubmitAction(final JComponent component,
1139            final String attributeName, final Settable attribute) {
1140        component.getInputMap()
1141                .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "submit");
1142        final PtolemyQuery query = this;
1143        component.getActionMap().put("submit", new AbstractAction() {
1144            @Override
1145            public void actionPerformed(ActionEvent e) {
1146                revalidate();
1147                try {
1148                    Component parent = component.getParent();
1149                    while (parent != null
1150                            && !(parent instanceof EditParametersDialog)) {
1151                        parent = parent.getParent();
1152                    }
1153                    if (parent != null) {
1154                        query.changed(attributeName);
1155                        attribute.validate();
1156                        EditParametersDialog dialog = (EditParametersDialog) parent;
1157                        ((Configurer) dialog.contents)._originalValues
1158                                .put(attribute, attribute.getValueAsString());
1159                        dialog._handleClosing();
1160                    }
1161                } catch (IllegalActionException e1) {
1162                    // Do not display errors here, just show error dialogue if attribute cannot be validated,
1163                    // do not update originalValues and do not close.
1164                }
1165            }
1166        });
1167    }
1168
1169    /** Return the expression for the specified Settable, unless it
1170     *  is an instance of DoubleRangeParameter, in which case, return
1171     *  the expression mapped into a integer suitable for use by
1172     *  JSlider.
1173     *  @param attribute The Settable whose expression we want.
1174     *  @return The expression.
1175     */
1176    private String _getTranslatedExpression(Settable attribute) {
1177        String newValue = attribute.getExpression();
1178
1179        // If the attribute is DoubleRangeParameter,
1180        // then we have to translate the value from a
1181        // double in the range to an int for the
1182        // JSlider.
1183        if (attribute instanceof DoubleRangeParameter) {
1184            try {
1185                double current = Double.parseDouble(newValue);
1186                double max = ((DoubleToken) ((DoubleRangeParameter) attribute).max
1187                        .getToken()).doubleValue();
1188                double min = ((DoubleToken) ((DoubleRangeParameter) attribute).min
1189                        .getToken()).doubleValue();
1190                int precision = ((IntToken) ((DoubleRangeParameter) attribute).precision
1191                        .getToken()).intValue();
1192
1193                // Get the quantized integer for the current value.
1194                int quantized = (int) Math
1195                        .round((current - min) * precision / (max - min));
1196
1197                newValue = "" + quantized;
1198            } catch (IllegalActionException e) {
1199                throw new InternalErrorException(e);
1200            }
1201        }
1202
1203        return newValue;
1204    }
1205
1206    ///////////////////////////////////////////////////////////////////
1207    ////                         private variables                 ////
1208
1209    // Settable for which we are adding a styled entry.
1210    private Settable _addingStyledEntryFor;
1211
1212    // Another dialog used to prompt for corrections to errors.
1213    private ComponentDialog _dialog;
1214
1215    // The handler that was specified in the constructors.
1216    private NamedObj _handler;
1217
1218    // Indicator that we are executing a change request, so we can safely
1219    // ignore change notifications.
1220    private boolean _ignoreChangeNotifications = false;
1221
1222    // Indicator that this is an open dialog reporting an erroneous entry.
1223    private boolean _isOpenErrorWindow = false;
1224
1225    // Background color for JSON string mode edit boxes.
1226    private static Color _JSON_MODE_BACKGROUND_COLOR = new Color(0xFFFFE0); // Light yellow
1227
1228    // Background color for string mode edit boxes.
1229    //private static Color _NOT_OVERRIDDEN_FOREGROUND_COLOR = new Color(200, 10,
1230    //        10, 255);
1231
1232    // A query box for dealing with an erroneous entry.
1233    private PtolemyQuery _query = null;
1234
1235    // Maps an entry name to the most recent error-free value.
1236    private Map<String, String> _revertValue = new HashMap<String, String>();
1237
1238    // Saved error handler to restore after change.
1239    private ErrorHandler _savedErrorHandler = null;
1240
1241    // Background color for string mode edit boxes.
1242    private static Color _STRING_MODE_BACKGROUND_COLOR = new Color(230, 255,
1243            255, 255);
1244
1245    // Maps an attribute name to a list of entry names that the
1246    // attribute is attached to.
1247    private Map<Settable, List<String>> _varToListOfEntries;
1248
1249    ///////////////////////////////////////////////////////////////////
1250    ////                         inner classes                     ////
1251
1252    /** Panel containing an entry box and button that performs the action specified
1253     *  by an Actionable.
1254     */
1255    public static class ActionableEntry extends Box
1256            implements ActionListener, SettableQueryChooser {
1257        /** Create a panel containing an entry box and a color chooser.
1258         *  @param owner The owner query
1259         *  @param name The name of the query
1260         *  @param defaultValue  The initial default color of the color chooser.
1261         *  @param actionable The specification for the action.
1262         */
1263        public ActionableEntry(Query owner, String name, String defaultValue,
1264                Actionable actionable) {
1265            super(BoxLayout.X_AXIS);
1266            _actionable = actionable;
1267            _owner = owner;
1268            _entryBox = new JTextField(defaultValue, _owner.getTextWidth());
1269
1270            _button = new JButton(actionable.actionName());
1271            _button.addActionListener(this);
1272            add(_entryBox);
1273            add(_button);
1274
1275            // Add the listener last so that there is no notification
1276            // of the first value.
1277            _entryBox.addActionListener(new QueryActionListener(_owner, name));
1278
1279            // Add a listener for loss of focus.  When the entry gains
1280            // and then loses focus, listeners are notified of an update,
1281            // but only if the value has changed since the last notification.
1282            // FIXME: Unfortunately, Java calls this listener some random
1283            // time after the window has been closed.  It is not even a
1284            // a queued event when the window is closed.  Thus, we have
1285            // a subtle bug where if you enter a value in a line, do not
1286            // hit return, and then click on the X to close the window,
1287            // the value is restored to the original, and then sometime
1288            // later, the focus is lost and the entered value becomes
1289            // the value of the parameter.  I don't know of any workaround.
1290            _entryBox.addFocusListener(new QueryFocusListener(_owner, name));
1291        }
1292
1293        /** Perform the specified action. */
1294        @Override
1295        public void actionPerformed(ActionEvent e) {
1296            try {
1297                _actionable.performAction();
1298            } catch (Exception e1) {
1299                MessageHandler.error("Action failed.", e1);
1300            }
1301        }
1302
1303        /** Return the contents of the entry box.
1304         *  @see #setQueryValue(String)
1305         */
1306        @Override
1307        public String getQueryValue() {
1308            return _entryBox.getText();
1309        }
1310
1311        /** Set the contents of the entry box.
1312         *  @see #getQueryValue()
1313         */
1314        @Override
1315        public void setQueryValue(String value) {
1316            _entryBox.setText(value);
1317        }
1318
1319        private Actionable _actionable;
1320        private JButton _button;
1321        private JTextField _entryBox;
1322        private Query _owner;
1323    }
1324
1325    /** Panel containing an entry box and button that opens another query
1326     *  to edit the parameters of a specified parameter.
1327     */
1328    public class HierarchicalConfigurer extends Box implements ActionListener {
1329        /** Create a panel containing an entry box and a button.
1330         *  @param owner The owner query.
1331         *  @param name The name of the query.
1332         *  @param parameter The parameter containing parameters.
1333         *  @param widget The widget to use to edit the parameter.
1334         */
1335        public HierarchicalConfigurer(Query owner, String name,
1336                Settable parameter, Component widget) {
1337            super(BoxLayout.X_AXIS);
1338            _owner = owner;
1339            _parameter = parameter;
1340            JButton button = new JButton("Configure");
1341            button.addActionListener(this);
1342            add(widget);
1343            add(button);
1344
1345            // Add the listener last so that there is no notification
1346            // of the first value.
1347            if (widget instanceof JTextField) {
1348                ((JTextField) widget).addActionListener(
1349                        new QueryActionListener(_owner, name));
1350
1351                // Add a listener for loss of focus.  When the entry gains
1352                // and then loses focus, listeners are notified of an update,
1353                // but only if the value has changed since the last notification.
1354                // FIXME: Unfortunately, Java calls this listener some random
1355                // time after the window has been closed.  It is not even a
1356                // a queued event when the window is closed.  Thus, we have
1357                // a subtle bug where if you enter a value in a line, do not
1358                // hit return, and then click on the X to close the window,
1359                // the value is restored to the original, and then sometime
1360                // later, the focus is lost and the entered value becomes
1361                // the value of the parameter.  I don't know of any workaround.
1362                ((JTextField) widget)
1363                        .addFocusListener(new QueryFocusListener(_owner, name));
1364            }
1365        }
1366
1367        @Override
1368        public void actionPerformed(ActionEvent e) {
1369            // Open a dialog to edit parameters contained by the parameter.
1370            new EditParametersDialog(
1371                    JOptionPane.getFrameForComponent(PtolemyQuery.this),
1372                    (NamedObj) _parameter);
1373        }
1374
1375        private Query _owner;
1376
1377        private Settable _parameter;
1378    }
1379}