001/* An interactive shell that reads and writes strings. 002 003 @Copyright (c) 1998-2016 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.Container; 031import java.io.IOException; 032import java.io.Writer; 033import java.util.HashSet; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Set; 037 038import javax.swing.SwingUtilities; 039 040import ptolemy.actor.TypedAtomicActor; 041import ptolemy.actor.TypedIOPort; 042import ptolemy.actor.gui.Configuration; 043import ptolemy.actor.gui.Effigy; 044import ptolemy.actor.gui.ExpressionShellEffigy; 045import ptolemy.actor.gui.ExpressionShellFrame; 046import ptolemy.actor.gui.ExpressionShellTableau; 047import ptolemy.actor.gui.Placeable; 048import ptolemy.actor.gui.TableauFrame; 049import ptolemy.actor.gui.WindowPropertiesAttribute; 050import ptolemy.actor.parameters.PortParameter; 051import ptolemy.data.BooleanToken; 052import ptolemy.data.StringToken; 053import ptolemy.data.Token; 054import ptolemy.data.expr.SingletonParameter; 055import ptolemy.data.type.BaseType; 056import ptolemy.data.type.TypeConstant; 057import ptolemy.graph.Inequality; 058import ptolemy.gui.ShellInterpreter; 059import ptolemy.gui.ShellTextArea; 060import ptolemy.kernel.CompositeEntity; 061import ptolemy.kernel.util.Attribute; 062import ptolemy.kernel.util.IllegalActionException; 063import ptolemy.kernel.util.NameDuplicationException; 064import ptolemy.kernel.util.Nameable; 065import ptolemy.kernel.util.NamedObj; 066import ptolemy.kernel.util.Workspace; 067import ptolemy.util.MessageHandler; 068 069/////////////////////////////////////////////////////////////////// 070//// InteractiveShell 071 072/** 073 <p>This actor creates a command shell on the screen, sending commands 074 that are typed by the user to its output port, and reporting values 075 received at its input by displaying them. Each time it fires, it 076 reads the input, displays it, then displays a command prompt 077 (which by default is ">>"), and waits for a command to be 078 typed. The command is terminated by an enter or return character, 079 which then results in the command being produced on the output. 080 In a typical use of this actor, it will be preceded by a SampleDelay 081 actor which will provide an initial welcome message or instructions. 082 The output will then be routed to some subsystem for processing, 083 and the result will be fed back to the input. 084 </p><p> 085 If the user types "quit" or "exit" (without the quotation marks) 086 on the prompt, then this actor's postfire() method will return false. 087 Depending on the domain, this can result in the model execution stopping 088 (in SDF, for example) or in subsequent firings of this actor being 089 skipped (in DE, for example). 090 </p><p> 091 Note that because of complexities in Swing, if you resize the display 092 window, then, unlike the plotters, the new size will not be persistent. 093 That is, if you save the model and then re-open it, the new size is 094 forgotten. The position, however, is persistent.</p> 095 096 @author Edward A. Lee 097 @version $Id$ 098 @since Ptolemy II 1.0 099 @Pt.ProposedRating Yellow (eal) 100 @Pt.AcceptedRating Red (cxh) 101 */ 102public class InteractiveShell extends TypedAtomicActor 103 implements Placeable, ShellInterpreter, UsesInvokeAndWait { 104 /** Construct an actor with the given container and name. 105 * @param container The container. 106 * @param name The name of this actor. 107 * @exception IllegalActionException If the actor cannot be contained 108 * by the proposed container. 109 * @exception NameDuplicationException If the container already has an 110 * actor with this name. 111 */ 112 public InteractiveShell(CompositeEntity container, String name) 113 throws IllegalActionException, NameDuplicationException { 114 super(container, name); 115 116 input = new TypedIOPort(this, "input", true, false); 117 // Parameter to get Vergil to label the fileOrURL port. 118 new SingletonParameter(input, "_showName").setToken(BooleanToken.TRUE); 119 120 output = new TypedIOPort(this, "output", false, true); 121 output.setTypeEquals(BaseType.STRING); 122 123 prompt = new PortParameter(this, "prompt"); 124 // Parameter to get Vergil to label the fileOrURL port. 125 new SingletonParameter(prompt.getPort(), "_showName") 126 .setToken(BooleanToken.TRUE); 127 128 // Make command be a StringParameter (no surrounding double quotes). 129 prompt.setTypeEquals(BaseType.STRING); 130 prompt.setStringMode(true); 131 prompt.setExpression(">> "); 132 133 _windowProperties = new WindowPropertiesAttribute(this, 134 "_windowProperties"); 135 // Note that we have to force this to be persistent because 136 // there is no real mechanism for the value of the properties 137 // to be updated when the window is moved or resized. By 138 // making it persistent, when the model is saved, the 139 // attribute will determine the current size and position 140 // of the window and save it. 141 _windowProperties.setPersistent(true); 142 143 _attachText("_iconDescription", "<svg>\n" + "<rect x=\"-20\" y=\"-20\" " 144 + "width=\"40\" height=\"40\" " + "style=\"fill:lightGrey\"/>\n" 145 + "<rect x=\"-14\" y=\"-14\" " + "width=\"28\" height=\"28\" " 146 + "style=\"fill:white\"/>\n" 147 + "<polyline points=\"-10,-10, -5,-5, -10,0\" " 148 + "style=\"stroke:black\"/>\n" 149 + "<polyline points=\"-7,-10, -2,-5, -7,0\" " 150 + "style=\"stroke:black\"/>\n" + "</svg>\n"); 151 } 152 153 /////////////////////////////////////////////////////////////////// 154 //// ports and parameters //// 155 156 /** The input port. By default, this has undeclared type. 157 * If backward type inference is enabled, then it has type general. 158 * In either case, it can receive any data type. If it receives 159 * token of type string, it strips off the surrounding double 160 * quotes before displaying the value. 161 */ 162 public TypedIOPort input; 163 164 /** The output port. */ 165 public TypedIOPort output; 166 167 /** The prompt. The initial default is the string ">> ". Double 168 * quotes are not necessary. If you would like to have no prompt 169 * (aka, the empty string), create a Parameter that has the value 170 * "" (for example <code>foo</code>) and then set the value of the 171 * prompt parameter to <code>$foo</code>. 172 */ 173 public PortParameter prompt; 174 175 /** The shell window object. */ 176 public ShellTextArea shell; 177 178 /////////////////////////////////////////////////////////////////// 179 //// public methods //// 180 181 /** Clone the actor into the specified workspace. 182 * @param workspace The workspace for the new object. 183 * @return A new actor. 184 * @exception CloneNotSupportedException If a derived class has an 185 * attribute that cannot be cloned. 186 */ 187 @Override 188 public Object clone(Workspace workspace) throws CloneNotSupportedException { 189 InteractiveShell newObject = (InteractiveShell) super.clone(workspace); 190 newObject.shell = null; 191 newObject._container = null; 192 newObject._frame = null; 193 194 // Findbugs: 195 // [M M IS] Inconsistent synchronization [IS2_INCONSISTENT_SYNC] 196 // Actually this is not a problem since the object is 197 // being created and hence nobody else has access to it. 198 newObject._outputValues = new LinkedList<String>(); 199 200 try { 201 Attribute old = newObject.getAttribute("_windowProperties"); 202 if (old != null) { 203 old.setContainer(null); 204 } 205 newObject._windowProperties = new WindowPropertiesAttribute( 206 newObject, "_windowProperties"); 207 newObject._windowProperties.setPersistent(true); 208 } catch (Exception ex) { 209 // CloneNotSupportedException does not have a constructor 210 // that takes a cause argument, so we use initCause 211 CloneNotSupportedException throwable = new CloneNotSupportedException(); 212 throwable.initCause(ex); 213 throw throwable; 214 } 215 return newObject; 216 } 217 218 /** Evaluate the specified command. 219 * @param command The command. 220 * @return The return value of the command, or null if there is none. 221 * @exception Exception If something goes wrong processing the command. 222 */ 223 @Override 224 public String evaluateCommand(String command) throws Exception { 225 // NOTE: This method is typically called in the swing event thread. 226 // Be careful to avoid locking up the UI. 227 setOutput(command); 228 229 // Return null to indicate that the command evaluation is not 230 // complete. This results in disabling editing on the text 231 // widget until returnResult() is called on it, which happens 232 // the next time fire() is called. 233 return null; 234 } 235 236 /** Read and display the input, then 237 * wait for user input and produce the user data on the output. 238 * If the user input is "quit" or "exit", then set a flag that causes 239 * postfire() to return false. 240 * @exception IllegalActionException If producing the output 241 * causes an exception. 242 */ 243 @Override 244 public void fire() throws IllegalActionException { 245 super.fire(); 246 // If window has been dismissed, there is nothing more to do. 247 if (shell == null) { 248 return; 249 } 250 251 prompt.update(); 252 shell.mainPrompt = ((StringToken) prompt.getToken()).stringValue(); 253 254 String value = ""; 255 if (input.numberOfSources() > 0 && input.hasToken(0)) { 256 Token inputToken = input.get(0); 257 if (inputToken instanceof StringToken) { 258 // To get the value without surrounding quotation marks. 259 value = ((StringToken) inputToken).stringValue(); 260 } else { 261 value = inputToken.toString(); 262 } 263 } 264 if (_firstTime) { 265 _firstTime = false; 266 shell.initialize(value); 267 } else { 268 shell.returnResult(value); 269 } 270 // Enable user input, now that we have a response from the previous command. 271 Runnable doSetEditable = new Runnable() { 272 @Override 273 public void run() { 274 shell.setEditable(true); 275 } 276 }; 277 SwingUtilities.invokeLater(doSetEditable); 278 279 String userCommand = getOutput(); 280 281 if (userCommand.trim().equalsIgnoreCase("quit") 282 || userCommand.trim().equalsIgnoreCase("exit")) { 283 _returnFalseInPostfire = true; 284 } 285 286 output.broadcast(new StringToken(userCommand)); 287 } 288 289 /** Get the output string to be sent. This does not 290 * return until a value is entered on the shell by the user. 291 * @return The output string to be sent. 292 * @see #setOutput(String) 293 */ 294 public synchronized String getOutput() { 295 // Added synchronized again to not miss 296 // notifications. Wait will release the lock and 297 // retake it after it is notified. 298 while (_outputValues.size() < 1 && !_stopRequested) { 299 try { 300 // NOTE: Do not call wait on this object directly! 301 // If another thread tries to get write access to the 302 // workspace, it will deadlock! This method releases 303 // all read accesses on the workspace before doing the 304 // wait. 305 workspace().wait(this); 306 } catch (InterruptedException ex) { 307 } 308 } 309 if (_stopRequested) { 310 return ""; 311 } else { 312 return _outputValues.remove(0); 313 } 314 } 315 316 /** If the shell has not already been created, create it. 317 * Then wait for user input and produce it on the output. 318 * @exception IllegalActionException If the parent class throws it. 319 */ 320 @Override 321 public void initialize() throws IllegalActionException { 322 super.initialize(); 323 324 Runnable doInitialize = new Runnable() { 325 @Override 326 public void run() { 327 328 if (shell == null) { 329 // No container has been specified for the shell. 330 // Place the shell in its own frame. 331 // Need an effigy and a tableau so that menu ops work properly. 332 Effigy containerEffigy = Configuration 333 .findEffigy(toplevel()); 334 335 if (containerEffigy == null) { 336 MessageHandler 337 .error("Cannot find effigy for top level: " 338 + toplevel().getFullName()); 339 return; 340 } 341 342 try { 343 ExpressionShellEffigy shellEffigy = new ExpressionShellEffigy( 344 containerEffigy, 345 containerEffigy.uniqueName("shell")); 346 347 // The default identifier is "Unnamed", which is no good for 348 // two reasons: Wrong title bar label, and it causes a save-as 349 // to destroy the original window. 350 shellEffigy.identifier.setExpression(getFullName()); 351 352 _tableau = new ShellTableau(shellEffigy, "tableau"); 353 _frame = _tableau.frame; 354 shell = _tableau.shell; 355 shell.setInterpreter(InteractiveShell.this); 356 357 // Prevent editing until the first firing. 358 shell.setEditable(false); 359 } catch (Exception ex) { 360 MessageHandler.error( 361 "Error creating effigy and tableau " 362 + InteractiveShell.this.getFullName(), 363 ex); 364 return; 365 } 366 367 _windowProperties.setProperties(_frame); 368 _frame.pack(); 369 } else { 370 shell.clearJTextArea(); 371 } 372 373 if (_frame != null) { 374 // show() used to override manual placement by calling pack. 375 // No more. 376 _frame.show(); 377 _frame.toFront(); 378 } 379 } 380 }; 381 try { 382 if (!SwingUtilities.isEventDispatchThread()) { 383 SwingUtilities.invokeAndWait(doInitialize); 384 } else { 385 // Exporting HTML for 386 // ptolemy/actor/lib/hoc/demo/ThreadedComposite/ConcurrentChat.xml 387 // ends up running this in the Swing event dispatch 388 // thread. 389 doInitialize.run(); 390 } 391 } catch (Exception e) { 392 throw new IllegalActionException(this, e, "Failed to initialize."); 393 } 394 395 _firstTime = true; 396 _returnFalseInPostfire = false; 397 } 398 399 /** Return true if the specified command is complete (ready 400 * to be interpreted). 401 * @param command The command. 402 * @return True. 403 */ 404 @Override 405 public boolean isCommandComplete(String command) { 406 return true; 407 } 408 409 /** Specify the container into which this shell should be placed. 410 * This method needs to be called before the first call to initialize(). 411 * Otherwise, the shell will be placed in its own frame. 412 * The background of the plot is set equal to that of the container 413 * (unless it is null). 414 * @param container The container into which to place the shell, or 415 * null to specify that a new shell should be created. 416 */ 417 @Override 418 public void place(Container container) { 419 _container = container; 420 421 if (_container == null) { 422 // Dissociate with any container. 423 // NOTE: _remove() doesn't work here. Why? 424 if (_frame != null) { 425 _frame.dispose(); 426 } 427 428 _frame = null; 429 shell = null; 430 return; 431 } 432 433 shell = new ShellTextArea(); 434 shell.setInterpreter(this); 435 shell.clearJTextArea(); 436 shell.setEditable(false); 437 438 _container.add(shell); 439 440 // java.awt.Component.setBackground(color) says that 441 // if the color "parameter is null then this component 442 // will inherit the background color of its parent." 443 shell.setBackground(null); 444 } 445 446 /** Override the base class to return false if the user has typed 447 * "quit" or "exit". 448 * @return False if the user has typed "quit" or "exit". 449 * @exception IllegalActionException If the superclass throws it. 450 */ 451 @Override 452 public boolean postfire() throws IllegalActionException { 453 if (_returnFalseInPostfire) { 454 return false; 455 } 456 457 return super.postfire(); 458 } 459 460 /** Override the base class to remove the shell from its graphical 461 * container if the argument is null. 462 * @param container The proposed container. 463 * @exception IllegalActionException If the base class throws it. 464 * @exception NameDuplicationException If the base class throws it. 465 */ 466 @Override 467 public void setContainer(CompositeEntity container) 468 throws IllegalActionException, NameDuplicationException { 469 Nameable previousContainer = getContainer(); 470 super.setContainer(container); 471 472 if (container != previousContainer && previousContainer != null) { 473 _remove(); 474 } 475 } 476 477 /** Set a name to present to the user. 478 * <p>If the Plot window has been rendered, then the title of the 479 * Plot window will be updated to the value of the name parameter.</p> 480 * @param name A name to present to the user. 481 * @see #getDisplayName() 482 */ 483 @Override 484 public void setDisplayName(String name) { 485 super.setDisplayName(name); 486 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4302 487 if (_tableau != null) { 488 _tableau.setTitle(name); 489 } 490 } 491 492 /** Set or change the name. If a null argument is given the 493 * name is set to an empty string. 494 * Increment the version of the workspace. 495 * This method is write-synchronized on the workspace. 496 * <p>If the Plot window has been rendered, then the title of the 497 * Plot window will be updated to the value of the name parameter.</p> 498 * @param name The new name. 499 * @exception IllegalActionException If the name contains a period 500 * or if the object is a derived object and the name argument does 501 * not match the current name. 502 * @exception NameDuplicationException Not thrown in this base class. 503 * May be thrown by derived classes if the container already contains 504 * an object with this name. 505 * @see #getName() 506 * @see #getName(NamedObj) 507 */ 508 @Override 509 public void setName(String name) 510 throws IllegalActionException, NameDuplicationException { 511 super.setName(name); 512 // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4302 513 if (_tableau != null) { 514 _tableau.setTitle(name); 515 } 516 } 517 518 /** Specify an output string to be sent. This method 519 * appends the specified string to a queue. Strings 520 * are retrieved from the queue by getOutput(). 521 * @param value An output string to be sent. 522 * @see #getOutput() 523 */ 524 public synchronized void setOutput(String value) { 525 _outputValues.add(value); 526 notifyAll(); 527 } 528 529 /** Override the base class to call notifyAll() to get out of 530 * any waiting. 531 */ 532 @Override 533 public void stop() { 534 synchronized (this) { 535 super.stop(); 536 notifyAll(); 537 } 538 } 539 540 /** Override the base class to make the shell uneditable. 541 * @exception IllegalActionException If the parent class throws it. 542 */ 543 @Override 544 public void wrapup() throws IllegalActionException { 545 super.wrapup(); 546 547 if (_returnFalseInPostfire && _frame != null) { 548 _frame.dispose(); 549 _frame = null; 550 shell = null; 551 } else if (shell != null) { 552 shell.setEditable(false); 553 } 554 } 555 556 /////////////////////////////////////////////////////////////////// 557 //// protected methods //// 558 559 /** Set the input port greater than or equal to 560 * <code>BaseType.GENERAL</code> in case backward type inference is 561 * enabled and the input port has no type declared. 562 * 563 * @return A set of inequalities. 564 */ 565 @Override 566 protected Set<Inequality> _customTypeConstraints() { 567 HashSet<Inequality> result = new HashSet<Inequality>(); 568 if (isBackwardTypeInferenceEnabled() 569 && input.getTypeTerm().isSettable()) { 570 result.add(new Inequality(new TypeConstant(BaseType.GENERAL), 571 input.getTypeTerm())); 572 } 573 return result; 574 } 575 576 /** Write a MoML description of the contents of this object. This 577 * overrides the base class to make sure that the current frame 578 * properties, if there is a frame, are recorded. 579 * @param output The output stream to write to. 580 * @param depth The depth in the hierarchy, to determine indenting. 581 * @exception IOException If an I/O error occurs. 582 */ 583 @Override 584 protected void _exportMoMLContents(Writer output, int depth) 585 throws IOException { 586 // Make sure that the current position of the frame, if any, 587 // is up to date. 588 if (_frame != null) { 589 _windowProperties.recordProperties(_frame); 590 } 591 592 super._exportMoMLContents(output, depth); 593 } 594 595 /////////////////////////////////////////////////////////////////// 596 //// private members //// 597 598 /** Container into which this plot should be placed. */ 599 private Container _container; 600 601 /** Indicator of the first time through. */ 602 private boolean _firstTime = true; 603 604 /** Frame into which plot is placed, if any. */ 605 private TableauFrame _frame; 606 607 /** The list of strings to send to the output. */ 608 private List<String> _outputValues = new LinkedList<String>(); 609 610 /** Flag indicating that "exit" or "quit" has been entered. */ 611 private boolean _returnFalseInPostfire = false; 612 613 /** The version of ExpressionShellTableau that creates a Shell window. */ 614 private ShellTableau _tableau; 615 616 // A specification for the window properties of the frame. 617 private WindowPropertiesAttribute _windowProperties; 618 619 /////////////////////////////////////////////////////////////////// 620 //// private methods //// 621 622 /** Remove the shell from the current container, if there is one. 623 */ 624 private void _remove() { 625 SwingUtilities.invokeLater(new Runnable() { 626 @Override 627 public void run() { 628 if (shell != null) { 629 if (_container != null) { 630 _container.remove(shell); 631 _container.invalidate(); 632 _container.repaint(); 633 } else if (_frame != null) { 634 _frame.dispose(); 635 } 636 } 637 } 638 }); 639 } 640 641 /////////////////////////////////////////////////////////////////// 642 //// inner classes //// 643 644 /** Version of ExpressionShellTableau that records the size of 645 * the display when it is closed. 646 */ 647 public class ShellTableau extends ExpressionShellTableau { 648 /** Construct a new tableau for the model represented by the 649 * given effigy. 650 * @param container The container. 651 * @param name The name. 652 * @exception IllegalActionException If the container does not accept 653 * this entity (this should not occur). 654 * @exception NameDuplicationException If the name coincides with an 655 * attribute already in the container. 656 */ 657 public ShellTableau(ExpressionShellEffigy container, String name) 658 throws IllegalActionException, NameDuplicationException { 659 super(container, name); 660 frame = new ShellFrame(this); 661 setFrame(frame); 662 frame.setTableau(this); 663 } 664 } 665 666 /** The frame that is created by an instance of ShellTableau. 667 */ 668 @SuppressWarnings("serial") 669 public class ShellFrame extends ExpressionShellFrame { 670 /** Construct a frame to display the ExpressionShell window. 671 * Override the base class to handle window closing. 672 * After constructing this, it is necessary 673 * to call setVisible(true) to make the frame appear. 674 * This is typically accomplished by calling show() on 675 * enclosing tableau. 676 * @param tableau The tableau responsible for this frame. 677 * @exception IllegalActionException If the model rejects the 678 * configuration attribute. 679 * @exception NameDuplicationException If a name collision occurs. 680 */ 681 public ShellFrame(ExpressionShellTableau tableau) 682 throws IllegalActionException, NameDuplicationException { 683 super(tableau); 684 } 685 686 /** Overrides the base class to record 687 * the size and location of the frame. 688 * @return False if the user cancels on a save query. 689 */ 690 @Override 691 protected boolean _close() { 692 if (_frame != null) { 693 _windowProperties.setProperties(_frame); 694 } 695 696 // Return value can be ignored since there is no issue of saving. 697 super._close(); 698 place(null); 699 return true; 700 } 701 } 702}