001/*
002 * Copyright (c) 2004-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: welker $'
006 * '$Date: 2010-05-06 05:21:26 +0000 (Thu, 06 May 2010) $' 
007 * '$Revision: 24234 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.sdm.spa;
031
032import java.awt.Container;
033import java.io.IOException;
034import java.io.Writer;
035import java.util.LinkedList;
036import java.util.List;
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.Tableau;
049import ptolemy.actor.gui.TableauFactory;
050import ptolemy.actor.gui.TableauFrame;
051import ptolemy.actor.gui.WindowPropertiesAttribute;
052import ptolemy.data.StringToken;
053import ptolemy.data.type.BaseType;
054import ptolemy.gui.ShellInterpreter;
055import ptolemy.gui.ShellTextArea;
056import ptolemy.kernel.CompositeEntity;
057import ptolemy.kernel.util.IllegalActionException;
058import ptolemy.kernel.util.NameDuplicationException;
059import ptolemy.kernel.util.Nameable;
060import ptolemy.kernel.util.Workspace;
061
062//////////////////////////////////////////////////////////////////////////
063//// TextualInteractionShell
064/**
065 * This actor creates a command shell on the screen, sending commands that are
066 * typed by the user to its output port, and reporting strings received at its
067 * input by displaying them. Each time it fires, it reads the input, displays
068 * it, then displays a command prompt (which by default is ">&gt"), and waits
069 * for a command to be typed. The command is terminated by an enter or return
070 * character, which then results in the command being produced on the output. In
071 * a typical use of this actor, it will be preceded by a SampleDelay actor which
072 * will provide an initial welcome message or instructions. The output will then
073 * be routed to some subsystem for processing, and the result will be fed back
074 * to the input.
075 * <p>
076 * Note that because of complexities in Swing, if you resize the display window,
077 * then, unlike the plotters, the new size will not be persistent. That is, if
078 * you save the model and then re-open it, the new size is forgotten. The
079 * position, however, is persistent.
080 * 
081 * @author Ilkay Altintas
082 * @version $Id: TextualInteractionShell.java 11161 2005-11-01 20:39:16Z ruland
083 *          $
084 * @category.name user interaction
085 * @category.name flow control
086 */
087public class TextualInteractionShell extends TypedAtomicActor implements
088                Placeable, ShellInterpreter {
089
090        /**
091         * Construct an actor with the given container and name.
092         * 
093         * @param container
094         *            The container.
095         * @param name
096         *            The name of this actor.
097         * @exception IllegalActionException
098         *                If the actor cannot be contained by the proposed
099         *                container.
100         * @exception NameDuplicationException
101         *                If the container already has an actor with this name.
102         */
103        public TextualInteractionShell(CompositeEntity container, String name)
104                        throws IllegalActionException, NameDuplicationException {
105                super(container, name);
106
107                input = new TypedIOPort(this, "input", true, false);
108                input.setTypeEquals(BaseType.STRING);
109
110                output = new TypedIOPort(this, "output", false, true);
111                output.setTypeEquals(BaseType.STRING);
112
113                _windowProperties = new WindowPropertiesAttribute(this,
114                                "_windowProperties");
115
116                _attachText("_iconDescription", "<svg>\n"
117                                + "<rect x=\"-20\" y=\"-20\" " + "width=\"40\" height=\"40\" "
118                                + "style=\"fill:lightGrey\"/>\n" + "<rect x=\"-14\" y=\"-14\" "
119                                + "width=\"28\" height=\"28\" " + "style=\"fill:white\"/>\n"
120                                + "<polyline points=\"-10,-10, -5,-5, -10,0\" "
121                                + "style=\"stroke:black\"/>\n"
122                                + "<polyline points=\"-7,-10, -2,-5, -7,0\" "
123                                + "style=\"stroke:black\"/>\n" + "</svg>\n");
124        }
125
126        // /////////////////////////////////////////////////////////////////
127        // // ports and parameters ////
128
129        /**
130         * @entity.description The input is a string. The actor will first display
131         *                     this in an interavice shell, than accept the
132         *                     expression and evaluate it.
133         */
134        public TypedIOPort input;
135
136        /**
137         * @entity.description The output is a string that represents the output of
138         *                     the command that was entered in the upper shell.
139         */
140        public TypedIOPort output;
141
142        /** The shell window object. */
143        public ShellTextArea shell;
144        public ShellTextArea shell1;
145
146        public ShellTableau tableau;
147        public Effigy containerEffigy;
148        public Effigy midEffigy;
149
150        // /////////////////////////////////////////////////////////////////
151        // // public methods ////
152
153        /**
154         * Clone the actor into the specified workspace.
155         * 
156         * @param workspace
157         *            The workspace for the new object.
158         * @return A new actor.
159         * @exception CloneNotSupportedException
160         *                If a derived class has an attribute that cannot be cloned.
161         */
162        public Object clone(Workspace workspace) throws CloneNotSupportedException {
163                TextualInteractionShell newObject = (TextualInteractionShell) super
164                                .clone(workspace);
165                newObject.shell = null;
166                newObject.shell1 = null;
167                newObject._container = null;
168                newObject._frame = null;
169                return newObject;
170        }
171
172        /**
173         * Evaluate the specified command.
174         * 
175         * @param command
176         *            The command.
177         * @return The return value of the command, or null if there is none.
178         * @exception Exception
179         *                If something goes wrong processing the command.
180         */
181        public String evaluateCommand(String command) throws Exception {
182                // NOTE: This method is typically called in the swing event thread.
183                // Be careful to avoid locking up the UI.
184                setOutput(command);
185                // Return null to indicate that the command evaluation is not
186                // complete. This results in disabling editing on the text
187                // widget until returnResult() is called on it, which happens
188                // the next time fire() is called.
189                return null;
190        }
191
192        /**
193         * Read and display the input, then wait for user input and produce the user
194         * data on the output. If the user input is "quit" or "exit", then set a
195         * flag that causes postfire() to return false.
196         * 
197         * @exception IllegalActionException
198         *                If producing the output causes an exception.
199         */
200        public void fire() throws IllegalActionException {
201                super.fire();
202                if (input.numberOfSources() > 0 && input.hasToken(0)) {
203                        String value = ((StringToken) input.get(0)).stringValue();
204                        // If window has been dismissed, there is nothing more to do.
205                        if ((shell == null) || (shell1 == null))
206                                return;
207                        if (_firstTime) {
208                                _firstTime = false;
209                                shell.initialize(value);
210                                shell1
211                                                .initialize("Please enter the selected input from last step\'s output");
212                        } else {
213                                shell.returnResult(value);
214                                // shell1.returnResult(value);
215                        }
216                }
217                shell.setEditable(false);
218                shell1.setEditable(true);
219                // FIXME: What to type if there is no input connected.
220                String userCommand = getOutput();
221                if (userCommand.trim().equalsIgnoreCase("quit")
222                                || userCommand.trim().equalsIgnoreCase("exit")) {
223                        _returnFalseInPostfire = true;
224                }
225                output.broadcast(new StringToken(userCommand));
226        }
227
228        /**
229         * Get the output string to be sent. This does not return until a value is
230         * entered on the shell by the user.
231         * 
232         * @return The output string to be sent.
233         */
234        public synchronized String getOutput() {
235                while (_outputValues.size() < 1 && !_stopRequested) {
236                        try {
237                                // NOTE: Do not call wait on this object directly!
238                                // If another thread tries to get write access to the
239                                // workspace, it will deadlock! This method releases
240                                // all read accesses on the workspace before doing the
241                                // wait.
242                                workspace().wait(this);
243                        } catch (InterruptedException ex) {
244                        }
245                }
246                if (_stopRequested) {
247                        return ("stop");
248                } else {
249                        return ((String) _outputValues.remove(0));
250                }
251        }
252
253        /**
254         * If the shell has not already been created, create it. Then wait for user
255         * input and produce it on the output.
256         * 
257         * @exception IllegalActionException
258         *                If the parent class throws it.
259         */
260        public void initialize() throws IllegalActionException {
261                super.initialize();
262                containerEffigy = Configuration.findEffigy(toplevel());
263                try {
264                        midEffigy = new Effigy(containerEffigy, containerEffigy
265                                        .uniqueName("shell"));
266                } catch (NameDuplicationException ex) {
267                }
268                if (shell == null) {
269                        // No container has been specified for the shell.
270                        // Place the shell in its own frame.
271                        // Need an effigy and a tableau so that menu ops work properly.
272                        if (containerEffigy == null) {
273                                throw new IllegalActionException(this,
274                                                "Cannot find effigy for top level: "
275                                                                + toplevel().getFullName());
276                        }
277                        try {
278                                // ExpressionShellEffigy shellEffigy = new
279                                // ExpressionShellEffigy(
280                                ExpressionShellEffigy shellEffigy = new ExpressionShellEffigy(
281                                // containerEffigy, containerEffigy.uniqueName("shell"));
282                                                midEffigy, midEffigy.uniqueName("shell"));
283                                // The default identifier is "Unnamed", which is no good for
284                                // two reasons: Wrong title bar label, and it causes a save-as
285                                // to destroy the original window.
286                                shellEffigy.identifier.setExpression(getFullName());
287                                // ShellTableau tableau = new ShellTableau(
288                                tableau = new ShellTableau(shellEffigy, "tableau");
289                                _frame = tableau.frame;
290                                shell = tableau.shell;
291                                shell.setInterpreter(this);
292                                // Prevent editing until the first firing.
293                                shell.setEditable(false);
294                        } catch (Exception ex) {
295                                throw new IllegalActionException(this, null, ex,
296                                                "Error creating effigy and tableau");
297                        }
298                        // _windowProperties.setProperties(_frame);
299                        _frame.show();
300                } else {
301                        shell.clearJTextArea();
302                }
303                if (shell1 == null) {
304                        // No container has been specified for the shell.
305                        // Place the shell in its own frame.
306                        // Need an effigy and a tableau so that menu ops work properly.
307                        // Effigy containerEffigy1 = Configuration.findEffigy(toplevel());
308                        // if (containerEffigy1 == null) {
309                        if (containerEffigy == null) {
310                                throw new IllegalActionException(this,
311                                                "Cannot find effigy for top level: "
312                                                                + toplevel().getFullName());
313                        }
314                        try {
315                                ExpressionShellEffigy shellEffigy1 = new ExpressionShellEffigy(
316                                // containerEffigy, containerEffigy.uniqueName("shell1"));
317                                                midEffigy, midEffigy.uniqueName("shell"));
318                                // The default identifier is "Unnamed", which is no good for
319                                // two reasons: Wrong title bar label, and it causes a save-as
320                                // to destroy the original window.
321                                shellEffigy1.identifier.setExpression(getFullName());
322                                ShellTableau tableau1 = new ShellTableau(shellEffigy1,
323                                                "tableau1");
324                                // tableau1.setContainer(shellEffigy);
325
326                                _frame1 = tableau1.frame;
327                                shell1 = tableau1.shell;
328                                shell1.setInterpreter(this);
329                                // Prevent editing until the first firing.
330                                shell1.setEditable(false);
331                        } catch (Exception ex) {
332                                throw new IllegalActionException(this, null, ex,
333                                                "Error creating effigy and tableau");
334                        }
335                        // _windowProperties.setProperties(_frame1);
336                        _frame1.show();
337                } else {
338                        shell1.clearJTextArea();
339                }
340
341                try {
342                        TableauFactory factory = (TableauFactory) getAttribute("tableauFactory");
343                        Tableau midtableau = factory.createTableau(midEffigy);
344                        midtableau.show();
345                } catch (Exception ex) {
346                }
347
348                /*
349                 * NOTE: This causes a bug where manual resizes of the window get
350                 * overridden on re-run. if (_frame != null) { _frame.show(); }
351                 */
352                _firstTime = true;
353                _returnFalseInPostfire = false;
354        }
355
356        /**
357         * Return true if the specified command is complete (ready to be
358         * interpreted).
359         * 
360         * @param command
361         *            The command.
362         * @return True.
363         */
364        public boolean isCommandComplete(String command) {
365                return true;
366        }
367
368        /**
369         * Specify the container into which this shell should be placed. This method
370         * needs to be called before the first call to initialize(). Otherwise, the
371         * shell will be placed in its own frame. The background of the plot is set
372         * equal to that of the container (unless it is null).
373         * 
374         * @param container
375         *            The container into which to place the shell, or null to
376         *            specify that a new shell should be created.
377         */
378        public void place(Container container) {
379                _container = container;
380
381                if (_container == null) {
382                        // Dissociate with any container.
383                        // NOTE: _remove() doesn't work here. Why?
384                        if (_frame != null)
385                                _frame.dispose();
386                        _frame = null;
387                        _frame1 = null;
388                        shell = null;
389                        shell1 = null;
390                        return;
391                }
392                shell = new ShellTextArea();
393                shell.setInterpreter(this);
394                shell.clearJTextArea();
395                shell.setEditable(false);
396
397                shell1 = new ShellTextArea();
398                shell1.setInterpreter(this);
399                shell1.clearJTextArea();
400                shell1.setEditable(false);
401
402                _container.add(shell);
403                _container.add(shell1);
404                // java.awt.Component.setBackground(color) says that
405                // if the color "parameter is null then this component
406                // will inherit the background color of its parent."
407                shell.setBackground(null);
408                shell1.setBackground(null);
409        }
410
411        /**
412         * Override the base class to return false if the user has typed "quit" or
413         * "exit".
414         * 
415         * @return False if the user has typed "quit" or "exit".
416         * @exception IllegalActionException
417         *                If the superclass throws it.
418         */
419        public boolean postfire() throws IllegalActionException {
420                if (_returnFalseInPostfire) {
421                        return false;
422                }
423                return super.postfire();
424        }
425
426        /**
427         * Override the base class to remove the shell from its graphical container
428         * if the argument is null.
429         * 
430         * @param container
431         *            The proposed container.
432         * @exception IllegalActionException
433         *                If the base class throws it.
434         * @exception NameDuplicationException
435         *                If the base class throws it.
436         */
437        public void setContainer(CompositeEntity container)
438                        throws IllegalActionException, NameDuplicationException {
439                Nameable previousContainer = getContainer();
440                super.setContainer(container);
441                if (container != previousContainer && previousContainer != null) {
442                        _remove();
443                }
444        }
445
446        /**
447         * Specify an output string to be sent. This method appends the specified
448         * string to a queue. Strings are retrieved from the queue by getOutput().
449         * 
450         * @see #getOutput()
451         * @param value
452         *            An output string to be sent.
453         */
454        public synchronized void setOutput(String value) {
455                _outputValues.add(value);
456                notifyAll();
457        }
458
459        /**
460         * Override the base class to call notifyAll() to get out of any waiting.
461         */
462        public void stop() {
463                super.stop();
464                synchronized (this) {
465                        notifyAll();
466                }
467        }
468
469        /**
470         * Override the base class to make the shell uneditable.
471         * 
472         * @exception IllegalActionException
473         *                If the parent class throws it.
474         */
475        public void wrapup() throws IllegalActionException {
476                super.wrapup();
477                if (_returnFalseInPostfire && _frame != null) {
478                        _frame.dispose();
479                } else if (shell != null) {
480                        shell.setEditable(false);
481                }
482                if (_returnFalseInPostfire && _frame1 != null) {
483                        _frame1.dispose();
484                } else if (shell1 != null) {
485                        shell1.setEditable(false);
486                }
487        }
488
489        // /////////////////////////////////////////////////////////////////
490        // // protected methods ////
491
492        /**
493         * Write a MoML description of the contents of this object. This overrides
494         * the base class to make sure that the current frame properties, if there
495         * is a frame, are recorded.
496         * 
497         * @param output
498         *            The output stream to write to.
499         * @param depth
500         *            The depth in the hierarchy, to determine indenting.
501         * @exception IOException
502         *                If an I/O error occurs.
503         */
504        protected void _exportMoMLContents(Writer output, int depth)
505                        throws IOException {
506                // Make sure that the current position of the frame, if any,
507                // is up to date.
508                if (_frame != null) {
509                        _windowProperties.recordProperties(_frame);
510                }
511                super._exportMoMLContents(output, depth);
512        }
513
514        // /////////////////////////////////////////////////////////////////
515        // // private members ////
516
517        /** Container into which this plot should be placed. */
518        private Container _container;
519
520        /** Indicator of the first time through. */
521        private boolean _firstTime = true;
522
523        /** Frame into which plot is placed, if any. */
524        private TableauFrame _frame;
525        private TableauFrame _frame1;
526        private TableauFrame _frame2;
527
528        /** The list of strings to send to the output. */
529        private List _outputValues = new LinkedList();
530
531        /** Flag indicating that "exit" or "quit" has been entered. */
532        private boolean _returnFalseInPostfire = false;
533
534        // A specification for the window properties of the frame.
535        private WindowPropertiesAttribute _windowProperties;
536
537        // /////////////////////////////////////////////////////////////////
538        // // private methods ////
539
540        /**
541         * Remove the shell from the current container, if there is one.
542         */
543        private void _remove() {
544                SwingUtilities.invokeLater(new Runnable() {
545                        public void run() {
546                                if (shell != null) {
547                                        if (_container != null) {
548                                                _container.remove(shell);
549                                                _container.invalidate();
550                                                _container.repaint();
551                                        } else if (_frame != null) {
552                                                _frame.dispose();
553                                        }
554                                }
555                        }
556                });
557        }
558
559        // /////////////////////////////////////////////////////////////////
560        // // inner classes ////
561
562        /**
563         * Version of ExpressionShellTableau that records the size of the display
564         * when it is closed.
565         */
566        public class ShellTableau extends ExpressionShellTableau {
567
568                /**
569                 * Construct a new tableau for the model represented by the given
570                 * effigy.
571                 * 
572                 * @param container
573                 *            The container.
574                 * @param name
575                 *            The name.
576                 * @exception IllegalActionException
577                 *                If the container does not accept this entity (this
578                 *                should not occur).
579                 * @exception NameDuplicationException
580                 *                If the name coincides with an attribute already in the
581                 *                container.
582                 */
583                public ShellTableau(ExpressionShellEffigy container, String name)
584                                throws IllegalActionException, NameDuplicationException {
585                        super(container, name);
586                        frame = new ShellFrame(this);
587                        setFrame(frame);
588                        frame.setTableau(this);
589                }
590        }
591
592        /**
593         * The frame that is created by an instance of ShellTableau.
594         */
595        public class ShellFrame extends ExpressionShellFrame {
596
597                /**
598                 * Construct a frame to display the ExpressionShell window. Override the
599                 * base class to handle window closing. After constructing this, it is
600                 * necessary to call setVisible(true) to make the frame appear. This is
601                 * typically accomplished by calling show() on enclosing tableau.
602                 * 
603                 * @param tableau
604                 *            The tableau responsible for this frame.
605                 * @exception IllegalActionException
606                 *                If the model rejects the configuration attribute.
607                 * @exception NameDuplicationException
608                 *                If a name collision occurs.
609                 */
610                public ShellFrame(ExpressionShellTableau tableau)
611                                throws IllegalActionException, NameDuplicationException {
612                        super(tableau);
613                }
614
615                /**
616                 * Overrides the base class to record the size and location of the
617                 * frame.
618                 * 
619                 * @return False if the user cancels on a save query.
620                 */
621                protected boolean _close() {
622                        if (_frame != null) {
623                                _windowProperties.setProperties(_frame);
624                        }
625                        // Return value can be ignored since there is no issue of saving.
626                        super._close();
627                        place(null);
628                        return true;
629                }
630        }
631}