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}