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}