001/* An interactive shell that reads and writes strings. 002 003 @Copyright (c) 1998-2018 The Regents of the University of California. 004 All rights reserved. 005 006 Permission is hereby granted, without written agreement and without 007 license or royalty fees, to use, copy, modify, and distribute this 008 software and its documentation for any purpose, provided that the 009 above copyright notice and the following two paragraphs appear in all 010 copies of this software. 011 012 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 013 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 014 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 015 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 016 SUCH DAMAGE. 017 018 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 019 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 020 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 021 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 022 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 023 ENHANCEMENTS, OR MODIFICATIONS. 024 025 PT_COPYRIGHT_VERSION 2 026 COPYRIGHTENDKEY 027 */ 028package ptolemy.actor.lib.gui; 029 030import java.awt.BorderLayout; 031import java.awt.Container; 032import java.io.IOException; 033import java.io.Writer; 034import java.util.HashSet; 035import java.util.LinkedList; 036import java.util.List; 037import java.util.Set; 038 039import javax.swing.BoxLayout; 040import javax.swing.JPanel; 041import javax.swing.SwingUtilities; 042 043import ptolemy.actor.TypedAtomicActor; 044import ptolemy.actor.TypedIOPort; 045import ptolemy.actor.gui.Configuration; 046import ptolemy.actor.gui.Effigy; 047import ptolemy.actor.gui.ExpressionShellEffigy; 048import ptolemy.actor.gui.Placeable; 049import ptolemy.actor.gui.Tableau; 050import ptolemy.actor.gui.TableauFrame; 051import ptolemy.actor.gui.WindowPropertiesAttribute; 052import ptolemy.data.BooleanToken; 053import ptolemy.data.StringToken; 054import ptolemy.data.Token; 055import ptolemy.data.expr.Parameter; 056import ptolemy.data.expr.SingletonParameter; 057import ptolemy.data.type.BaseType; 058import ptolemy.data.type.TypeConstant; 059import ptolemy.graph.Inequality; 060import ptolemy.gui.ShellInterpreter; 061import ptolemy.gui.UserDialog; 062import ptolemy.kernel.CompositeEntity; 063import ptolemy.kernel.util.Attribute; 064import ptolemy.kernel.util.IllegalActionException; 065import ptolemy.kernel.util.NameDuplicationException; 066import ptolemy.kernel.util.Nameable; 067import ptolemy.kernel.util.NamedObj; 068import ptolemy.kernel.util.Workspace; 069import ptolemy.util.MessageHandler; 070 071/////////////////////////////////////////////////////////////////// 072//// InteractiveDialog 073 074/** 075 <p> 076 This actor creates a window on the screen with a command entry box 077 and a results display box. When the user types a command and terminates 078 it with a return, this actor will emit an output with the value of that 079 command and also display the command in the results display box. 080 Any input that it receives it displays in the results display box. 081 082 @author Edward A. Lee 083 @version $Id$ 084 @since Ptolemy II 11.0 085 @Pt.ProposedRating Yellow (eal) 086 @Pt.AcceptedRating Red (cxh) 087 */ 088public class InteractiveDialog extends TypedAtomicActor 089 implements Placeable, ShellInterpreter, UsesInvokeAndWait { 090 091 /** Construct an actor with the given container and name. 092 * @param container The container. 093 * @param name The name of this actor. 094 * @exception IllegalActionException If the actor cannot be contained 095 * by the proposed container. 096 * @exception NameDuplicationException If the container already has an 097 * actor with this name. 098 */ 099 public InteractiveDialog(CompositeEntity container, String name) 100 throws IllegalActionException, NameDuplicationException { 101 super(container, name); 102 103 terminateWithNewline = new Parameter(this, "terminateWithNewline"); 104 terminateWithNewline.setTypeEquals(BaseType.BOOLEAN); 105 terminateWithNewline.setExpression("false"); 106 107 input = new TypedIOPort(this, "input", true, false); 108 // Parameter to get Vergil to label the fileOrURL port. 109 new SingletonParameter(input, "_showName").setToken(BooleanToken.TRUE); 110 111 output = new TypedIOPort(this, "output", false, true); 112 output.setTypeEquals(BaseType.STRING); 113 114 _windowProperties = new WindowPropertiesAttribute(this, 115 "_windowProperties"); 116 // Note that we have to force this to be persistent because 117 // there is no real mechanism for the value of the properties 118 // to be updated when the window is moved or resized. By 119 // making it persistent, when the model is saved, the 120 // attribute will determine the current size and position 121 // of the window and save it. 122 _windowProperties.setPersistent(true); 123 124 _attachText("_iconDescription", "<svg>\n" + "<rect x=\"-20\" y=\"-20\" " 125 + "width=\"40\" height=\"40\" " + "style=\"fill:lightGrey\"/>\n" 126 + "<rect x=\"-14\" y=\"-14\" " + "width=\"28\" height=\"28\" " 127 + "style=\"fill:white\"/>\n" 128 + "<polyline points=\"-10,-10, -5,-5, -10,0\" " 129 + "style=\"stroke:black\"/>\n" 130 + "<polyline points=\"-7,-10, -2,-5, -7,0\" " 131 + "style=\"stroke:black\"/>\n" + "</svg>\n"); 132 } 133 134 /////////////////////////////////////////////////////////////////// 135 //// ports and parameters //// 136 137 /** The input port. By default, this has undeclared type. 138 * If backward type inference is enabled, then it has type general. 139 * In either case, it can receive any data type. If it receives 140 * token of type string, it strips off the surrounding double 141 * quotes before displaying the value. 142 */ 143 public TypedIOPort input; 144 145 /** If true, append a newline to each output string. 146 * This is a boolean that defaults to false. 147 */ 148 public Parameter terminateWithNewline; 149 150 /** The output port. */ 151 public TypedIOPort output; 152 153 /** The dialog window object. */ 154 public UserDialog userDialog; 155 156 /////////////////////////////////////////////////////////////////// 157 //// public methods //// 158 159 /** Clone the actor into the specified workspace. 160 * @param workspace The workspace for the new object. 161 * @return A new actor. 162 * @exception CloneNotSupportedException If a derived class has an 163 * attribute that cannot be cloned. 164 */ 165 @Override 166 public Object clone(Workspace workspace) throws CloneNotSupportedException { 167 InteractiveDialog newObject = (InteractiveDialog) super.clone( 168 workspace); 169 newObject.userDialog = null; 170 newObject._container = null; 171 newObject._frame = null; 172 173 // Findbugs: 174 // [M M IS] Inconsistent synchronization [IS2_INCONSISTENT_SYNC] 175 // Actually this is not a problem since the object is 176 // being created and hence nobody else has access to it. 177 newObject._outputValues = new LinkedList<String>(); 178 179 try { 180 Attribute old = newObject.getAttribute("_windowProperties"); 181 if (old != null) { 182 old.setContainer(null); 183 } 184 newObject._windowProperties = new WindowPropertiesAttribute( 185 newObject, "_windowProperties"); 186 newObject._windowProperties.setPersistent(true); 187 } catch (Exception ex) { 188 // CloneNotSupportedException does not have a constructor 189 // that takes a cause argument, so we use initCause 190 CloneNotSupportedException throwable = new CloneNotSupportedException(); 191 throwable.initCause(ex); 192 throw throwable; 193 } 194 return newObject; 195 } 196 197 /** Record the specified command and request a firing to send it to the 198 * output. 199 * @param command The command. 200 * @return Null to indicate that the command evaluation is not complete. 201 * @exception Exception If something goes wrong processing the command. 202 */ 203 @Override 204 public String evaluateCommand(String command) throws Exception { 205 // NOTE: This method is typically called in the swing event thread. 206 // Be careful to avoid locking up the UI. 207 synchronized (this) { 208 _outputValues.add(command); 209 } 210 // Request a firing. 211 getDirector().fireAtCurrentTime(this); 212 213 // Return null to indicate that the command evaluation is not 214 // complete. 215 return null; 216 } 217 218 /** Read and display any input, then if a new command is available, 219 * display it and produce it on the output. 220 * @exception IllegalActionException If producing the output 221 * causes an exception. 222 */ 223 @Override 224 public void fire() throws IllegalActionException { 225 super.fire(); 226 // If window has been dismissed, there is nothing more to do. 227 if (userDialog == null) { 228 return; 229 } 230 231 String value = null; 232 while (input.numberOfSources() > 0 && input.hasToken(0)) { 233 Token inputToken = input.get(0); 234 if (inputToken instanceof StringToken) { 235 // To get the value without surrounding quotation marks. 236 value = ((StringToken) inputToken).stringValue(); 237 } else { 238 value = inputToken.toString(); 239 } 240 } 241 if (value != null) { 242 if (_firstTime) { 243 _firstTime = false; 244 userDialog.initialize(value); 245 } else { 246 userDialog.appendText(value); 247 } 248 } 249 250 synchronized (this) { 251 // For some reason, getExpression() returns an escaped string, "\\n", 252 // so I need to fix that here. 253 boolean terminate = ((BooleanToken) terminateWithNewline.getToken()) 254 .booleanValue(); 255 String format = "%s" + (terminate ? "\n" : ""); 256 for (String command : _outputValues) { 257 String formatted = String.format(format, command); 258 output.broadcast(new StringToken(formatted)); 259 } 260 _outputValues.clear(); 261 } 262 } 263 264 /** If the shell has not already been created, create it. 265 * Then wait for user input and produce it on the output. 266 * @exception IllegalActionException If the parent class throws it. 267 */ 268 @Override 269 public void initialize() throws IllegalActionException { 270 super.initialize(); 271 272 Runnable doInitialize = new Runnable() { 273 @Override 274 public void run() { 275 276 if (userDialog == null) { 277 // No container has been specified for the shell. 278 // Place the shell in its own frame. 279 // Need an effigy and a tableau so that menu ops work properly. 280 Effigy containerEffigy = Configuration 281 .findEffigy(toplevel()); 282 283 if (containerEffigy == null) { 284 MessageHandler 285 .error("Cannot find effigy for top level: " 286 + toplevel().getFullName()); 287 return; 288 } 289 290 try { 291 // Similar enough: use ExpressionShellEffigy. 292 ExpressionShellEffigy shellEffigy = new ExpressionShellEffigy( 293 containerEffigy, containerEffigy 294 .uniqueName("interactiveDialog")); 295 296 // The default identifier is "Unnamed", which is no good for 297 // two reasons: Wrong title bar label, and it causes a save-as 298 // to destroy the original window. 299 shellEffigy.identifier.setExpression(getFullName()); 300 301 _tableau = new DialogTableau(shellEffigy, "tableau"); 302 _frame = _tableau.frame; 303 userDialog = _tableau.dialog; 304 userDialog.setInterpreter(InteractiveDialog.this); 305 } catch (Exception ex) { 306 MessageHandler.error( 307 "Error creating effigy and tableau " 308 + InteractiveDialog.this.getFullName(), 309 ex); 310 return; 311 } 312 313 _windowProperties.setProperties(_frame); 314 _frame.pack(); 315 } else { 316 // Clear the display. 317 userDialog.initialize(""); 318 } 319 320 if (_frame != null) { 321 // show() used to override manual placement by calling pack. 322 // No more. 323 _frame.show(); 324 _frame.toFront(); 325 } 326 } 327 }; 328 try { 329 if (!SwingUtilities.isEventDispatchThread()) { 330 // Block initialization until the window is created. 331 SwingUtilities.invokeAndWait(doInitialize); 332 } else { 333 // Exporting HTML for 334 // ptolemy/actor/lib/hoc/demo/ThreadedComposite/ConcurrentChat.xml 335 // ends up running this in the Swing event dispatch 336 // thread. 337 doInitialize.run(); 338 } 339 } catch (Exception e) { 340 throw new IllegalActionException(this, e, "Failed to initialize."); 341 } 342 343 _firstTime = true; 344 } 345 346 /** Return true if the specified command is complete (ready 347 * to be interpreted). 348 * @param command The command. 349 * @return True. 350 */ 351 @Override 352 public boolean isCommandComplete(String command) { 353 return true; 354 } 355 356 /** Specify the container into which this shell should be placed. 357 * This method needs to be called before the first call to initialize(). 358 * Otherwise, the shell will be placed in its own frame. 359 * The background of the plot is set equal to that of the container 360 * (unless it is null). 361 * @param container The container into which to place the shell, or 362 * null to specify that a new shell should be created. 363 */ 364 @Override 365 public void place(Container container) { 366 _container = container; 367 368 if (_container == null) { 369 // Dissociate with any container. 370 // NOTE: _remove() doesn't work here. Why? 371 if (_frame != null) { 372 _frame.dispose(); 373 } 374 375 _frame = null; 376 userDialog = null; 377 return; 378 } 379 380 userDialog = new UserDialog(); 381 userDialog.setInterpreter(this); 382 383 _container.add(userDialog); 384 385 // java.awt.Component.setBackground(color) says that 386 // if the color "parameter is null then this component 387 // will inherit the background color of its parent." 388 userDialog.setBackground(null); 389 } 390 391 /** Override the base class to remove the shell from its graphical 392 * container if the argument is null. 393 * @param container The proposed container. 394 * @exception IllegalActionException If the base class throws it. 395 * @exception NameDuplicationException If the base class throws it. 396 */ 397 @Override 398 public void setContainer(CompositeEntity container) 399 throws IllegalActionException, NameDuplicationException { 400 Nameable previousContainer = getContainer(); 401 super.setContainer(container); 402 403 if (container != previousContainer && previousContainer != null) { 404 _remove(); 405 } 406 } 407 408 /** Set a name to present to the user. 409 * <p>If the Plot window has been rendered, then the title of the 410 * Plot window will be updated to the value of the name parameter.</p> 411 * @param name A name to present to the user. 412 * @see #getDisplayName() 413 */ 414 @Override 415 public void setDisplayName(String name) { 416 super.setDisplayName(name); 417 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4302 418 if (_tableau != null) { 419 _tableau.setTitle(name); 420 } 421 } 422 423 /** Set or change the name. If a null argument is given the 424 * name is set to an empty string. 425 * Increment the version of the workspace. 426 * This method is write-synchronized on the workspace. 427 * <p>If the Plot window has been rendered, then the title of the 428 * Plot window will be updated to the value of the name parameter.</p> 429 * @param name The new name. 430 * @exception IllegalActionException If the name contains a period 431 * or if the object is a derived object and the name argument does 432 * not match the current name. 433 * @exception NameDuplicationException Not thrown in this base class. 434 * May be thrown by derived classes if the container already contains 435 * an object with this name. 436 * @see #getName() 437 * @see #getName(NamedObj) 438 */ 439 @Override 440 public void setName(String name) 441 throws IllegalActionException, NameDuplicationException { 442 super.setName(name); 443 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4302 444 if (_tableau != null) { 445 _tableau.setTitle(name); 446 } 447 } 448 449 /** Override the base class to call notifyAll() to get out of 450 * any waiting. 451 */ 452 @Override 453 public void stop() { 454 synchronized (this) { 455 super.stop(); 456 notifyAll(); 457 } 458 } 459 460 /////////////////////////////////////////////////////////////////// 461 //// protected methods //// 462 463 /** Set the input port greater than or equal to 464 * <code>BaseType.GENERAL</code> in case backward type inference is 465 * enabled and the input port has no type declared. 466 * 467 * @return A set of inequalities. 468 */ 469 @Override 470 protected Set<Inequality> _customTypeConstraints() { 471 HashSet<Inequality> result = new HashSet<Inequality>(); 472 if (isBackwardTypeInferenceEnabled() 473 && input.getTypeTerm().isSettable()) { 474 result.add(new Inequality(new TypeConstant(BaseType.GENERAL), 475 input.getTypeTerm())); 476 } 477 return result; 478 } 479 480 /** Write a MoML description of the contents of this object. This 481 * overrides the base class to make sure that the current frame 482 * properties, if there is a frame, are recorded. 483 * @param output The output stream to write to. 484 * @param depth The depth in the hierarchy, to determine indenting. 485 * @exception IOException If an I/O error occurs. 486 */ 487 @Override 488 protected void _exportMoMLContents(Writer output, int depth) 489 throws IOException { 490 // Make sure that the current position of the frame, if any, 491 // is up to date. 492 if (_frame != null) { 493 _windowProperties.recordProperties(_frame); 494 } 495 496 super._exportMoMLContents(output, depth); 497 } 498 499 /////////////////////////////////////////////////////////////////// 500 //// private members //// 501 502 /** Container into which this plot should be placed. */ 503 private Container _container; 504 505 /** Indicator of the first time through. */ 506 private boolean _firstTime = true; 507 508 /** Frame into which plot is placed, if any. */ 509 private TableauFrame _frame; 510 511 /** The list of strings to send to the output. */ 512 private List<String> _outputValues = new LinkedList<String>(); 513 514 /** The version of ExpressionShellTableau that creates a Shell window. */ 515 private DialogTableau _tableau; 516 517 // A specification for the window properties of the frame. 518 private WindowPropertiesAttribute _windowProperties; 519 520 /////////////////////////////////////////////////////////////////// 521 //// private methods //// 522 523 /** Remove the shell from the current container, if there is one. 524 */ 525 private void _remove() { 526 SwingUtilities.invokeLater(new Runnable() { 527 @Override 528 public void run() { 529 if (userDialog != null) { 530 if (_container != null) { 531 _container.remove(userDialog); 532 _container.invalidate(); 533 _container.repaint(); 534 } else if (_frame != null) { 535 _frame.dispose(); 536 } 537 } 538 } 539 }); 540 } 541 542 /////////////////////////////////////////////////////////////////// 543 //// inner classes //// 544 545 /** Version of ExpressionShellTableau that records the size of 546 * the display when it is closed. 547 */ 548 public class DialogTableau extends Tableau { 549 /** Construct a new tableau for the model represented by the 550 * given effigy. 551 * @param container The container. 552 * @param name The name. 553 * @exception IllegalActionException If the container does not accept 554 * this entity (this should not occur). 555 * @exception NameDuplicationException If the name coincides with an 556 * attribute already in the container. 557 */ 558 public DialogTableau(ExpressionShellEffigy container, String name) 559 throws IllegalActionException, NameDuplicationException { 560 super(container, name); 561 dialog = new UserDialog(); 562 dialog.setInterpreter(InteractiveDialog.this); 563 frame = new DialogFrame(this); 564 frame.setTableau(this); 565 setFrame(frame); 566 } 567 568 /** The frame. */ 569 public DialogFrame frame; 570 571 /** The UserDialog. */ 572 public UserDialog dialog; 573 } 574 575 /** The frame that is created by an instance of ShellTableau. 576 */ 577 @SuppressWarnings("serial") 578 public class DialogFrame extends TableauFrame { 579 /** Construct a frame to display the ExpressionShell window. 580 * Override the base class to handle window closing. 581 * After constructing this, it is necessary 582 * to call setVisible(true) to make the frame appear. 583 * This is typically accomplished by calling show() on 584 * enclosing tableau. 585 * @param tableau The tableau responsible for this frame. 586 * @exception IllegalActionException If the model rejects the 587 * configuration attribute. 588 * @exception NameDuplicationException If a name collision occurs. 589 */ 590 public DialogFrame(DialogTableau tableau) 591 throws IllegalActionException, NameDuplicationException { 592 super(tableau); 593 594 JPanel component = new JPanel(); 595 component.setLayout(new BoxLayout(component, BoxLayout.Y_AXIS)); 596 component.add(tableau.dialog); 597 getContentPane().add(component, BorderLayout.CENTER); 598 } 599 600 /** Overrides the base class to record 601 * the size and location of the frame. 602 * @return False if the user cancels on a save query. 603 */ 604 @Override 605 protected boolean _close() { 606 if (_frame != null) { 607 _windowProperties.setProperties(_frame); 608 } 609 610 // Return value can be ignored since there is no issue of saving. 611 super._close(); 612 place(null); 613 return true; 614 } 615 } 616}