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 "&gt;&gt;"), 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 "&gt;&gt; ".  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}