001/*
002 * Copyright (c) 1998-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: brooks $'
006 * '$Date: 2012-06-18 00:36:48 +0000 (Mon, 18 Jun 2012) $' 
007 * '$Revision: 29975 $'
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.gui;
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.Placeable;
045import ptolemy.actor.gui.TableauFrame;
046import ptolemy.actor.gui.WindowPropertiesAttribute;
047import ptolemy.data.StringToken;
048import ptolemy.data.type.BaseType;
049import ptolemy.gui.ShellInterpreter;
050import ptolemy.kernel.CompositeEntity;
051import ptolemy.kernel.util.IllegalActionException;
052import ptolemy.kernel.util.NameDuplicationException;
053import ptolemy.kernel.util.Nameable;
054import ptolemy.kernel.util.Workspace;
055
056//////////////////////////////////////////////////////////////////////////
057//// UserInteractiveShell
058/**
059 * This actor code is adopted from InteractiveShell.java by Edward A. Lee
060 * 
061 * @author Ilkay Altintas
062 * @version $Id: UserInteractiveShell.java 29975 2012-06-18 00:36:48Z brooks $
063 */
064public class UserInteractiveShell extends TypedAtomicActor implements
065                Placeable, ShellInterpreter {
066
067        /**
068         * Construct an actor with the given container and name.
069         * 
070         * @param container
071         *            The container.
072         * @param name
073         *            The name of this actor.
074         * @exception IllegalActionException
075         *                If the actor cannot be contained by the proposed
076         *                container.
077         * @exception NameDuplicationException
078         *                If the container already has an actor with this name.
079         */
080        public UserInteractiveShell(CompositeEntity container, String name)
081                        throws IllegalActionException, NameDuplicationException {
082                super(container, name);
083
084                input = new TypedIOPort(this, "input", true, false);
085                input.setTypeEquals(BaseType.STRING);
086
087                output = new TypedIOPort(this, "output", false, true);
088                output.setTypeEquals(BaseType.STRING);
089
090                _windowProperties = new WindowPropertiesAttribute(this,
091                                "_windowProperties");
092
093                _attachText("_iconDescription", "<svg>\n"
094                                + "<rect x=\"-20\" y=\"-20\" " + "width=\"40\" height=\"40\" "
095                                + "style=\"fill:lightGrey\"/>\n" + "<rect x=\"-14\" y=\"-14\" "
096                                + "width=\"28\" height=\"28\" " + "style=\"fill:white\"/>\n"
097                                + "<polyline points=\"-10,-10, -5,-5, -10,0\" "
098                                + "style=\"stroke:black\"/>\n"
099                                + "<polyline points=\"-7,-10, -2,-5, -7,0\" "
100                                + "style=\"stroke:black\"/>\n" + "</svg>\n");
101        }
102
103        // /////////////////////////////////////////////////////////////////
104        // // ports and parameters ////
105
106        /** The input port. */
107        public TypedIOPort input;
108
109        /** The output port. */
110        public TypedIOPort output;
111
112        /** The shell window object. */
113        public DoubleShellTextAreaPanel shellPanel;
114
115        // /////////////////////////////////////////////////////////////////
116        // // public methods ////
117
118        /**
119         * Clone the actor into the specified workspace.
120         * 
121         * @param workspace
122         *            The workspace for the new object.
123         * @return A new actor.
124         * @exception CloneNotSupportedException
125         *                If a derived class has an attribute that cannot be cloned.
126         */
127        public Object clone(Workspace workspace) throws CloneNotSupportedException {
128                UserInteractiveShell newObject = (UserInteractiveShell) super
129                                .clone(workspace);
130                newObject.shellPanel = null;
131                newObject._container = null;
132                newObject._frame = null;
133                return newObject;
134        }
135
136        /**
137         * Evaluate the specified command.
138         * 
139         * @param command
140         *            The command.
141         * @return The return value of the command, or null if there is none.
142         * @exception Exception
143         *                If something goes wrong processing the command.
144         */
145        public String evaluateCommand(String command) throws Exception {
146                // NOTE: This method is typically called in the swing event thread.
147                // Be careful to avoid locking up the UI.
148                setOutput(command);
149                // Return null to indicate that the command evaluation is not
150                // complete. This results in disabling editing on the text
151                // widget until returnResult() is called on it, which happens
152                // the next time fire() is called.
153                return null;
154        }
155
156        /**
157         * Read and display the input, then wait for user input and produce the user
158         * data on the output. If the user input is "quit" or "exit", then set a
159         * flag that causes postfire() to return false.
160         * 
161         * @exception IllegalActionException
162         *                If producing the output causes an exception.
163         */
164        public void fire() throws IllegalActionException {
165                super.fire();
166                if (input.numberOfSources() > 0 && input.hasToken(0)) {
167                        String value = ((StringToken) input.get(0)).stringValue();
168                        // If window has been dismissed, there is nothing more to do.
169                        if (shellPanel == null)
170                                return;
171                        if (_firstTime) {
172                                _firstTime = false;
173                                shellPanel.initialize(value);
174                        } else {
175                                shellPanel.returnResult(value);
176                        }
177                }
178                shellPanel.setEditable();
179                // FIXME: What to type if there is no input connected.
180                String userCommand = getOutput();
181                if (userCommand.trim().equalsIgnoreCase("quit")
182                                || userCommand.trim().equalsIgnoreCase("exit")) {
183                        _returnFalseInPostfire = true;
184                }
185                output.broadcast(new StringToken(userCommand));
186        }
187
188        /**
189         * Get the output string to be sent. This does not return until a value is
190         * entered on the shell by the user.
191         * 
192         * @return The output string to be sent.
193         */
194        public synchronized String getOutput() {
195                while (_outputValues.size() < 1 && !_stopRequested) {
196                        try {
197                                // NOTE: Do not call wait on this object directly!
198                                // If another thread tries to get write access to the
199                                // workspace, it will deadlock! This method releases
200                                // all read accesses on the workspace before doing the
201                                // wait.
202                                workspace().wait(this);
203                        } catch (InterruptedException ex) {
204                        }
205                }
206                if (_stopRequested) {
207                        return ("stop");
208                } else {
209                        return ((String) _outputValues.remove(0));
210                }
211        }
212
213        /**
214         * If the shell has not already been created, create it. Then wait for user
215         * input and produce it on the output.
216         * 
217         * @exception IllegalActionException
218         *                If the parent class throws it.
219         */
220        public void initialize() throws IllegalActionException {
221                super.initialize();
222                if (shellPanel == null) {
223                        // No container has been specified for the shell.
224                        // Place the shell in its own frame.
225                        // Need an effigy and a tableau so that menu ops work properly.
226                        Effigy containerEffigy = Configuration.findEffigy(toplevel());
227                        if (containerEffigy == null) {
228                                throw new IllegalActionException(this,
229                                                "Cannot find effigy for top level: "
230                                                                + toplevel().getFullName());
231                        }
232                        try {
233                                DoubleShellTAPEffigy shellEffigy = new DoubleShellTAPEffigy(
234                                                containerEffigy, containerEffigy.uniqueName("shell"));
235                                // The default identifier is "Unnamed", which is no good for
236                                // two reasons: Wrong title bar label, and it causes a save-as
237                                // to destroy the original window.
238                                shellEffigy.identifier.setExpression(getFullName());
239                                DoubleShellTAPTableau tableau = new DoubleShellTAPTableau(
240                                                shellEffigy, "tableau");
241                                _frame = tableau.frame;
242                                shellPanel = tableau.shellPanel;
243                                shellPanel.setInterpreter(this);
244                                // Prevent editing until the first firing.
245                                shellPanel.setEditable();
246                        } catch (Exception ex) {
247                                throw new IllegalActionException(this, null, ex,
248                                                "Error creating effigy and tableau");
249                        }
250                        _windowProperties.setProperties(_frame);
251                        _frame.show();
252                } else {
253                        shellPanel.clearShell2();
254                }
255                /*
256                 * NOTE: This causes a bug where manual resizes of the window get
257                 * overridden on re-run. if (_frame != null) { _frame.show(); }
258                 */
259                _firstTime = true;
260                _returnFalseInPostfire = false;
261        }
262
263        /**
264         * Return true if the specified command is complete (ready to be
265         * interpreted).
266         * 
267         * @param command
268         *            The command.
269         * @return True.
270         */
271        public boolean isCommandComplete(String command) {
272                return true;
273        }
274
275        /**
276         * Specify the container into which this shell should be placed. This method
277         * needs to be called before the first call to initialize(). Otherwise, the
278         * shell will be placed in its own frame. The background of the plot is set
279         * equal to that of the container (unless it is null).
280         * 
281         * @param container
282         *            The container into which to place the shell, or null to
283         *            specify that a new shell should be created.
284         */
285        public void place(Container container) {
286                _container = container;
287
288                if (_container == null) {
289                        // Dissociate with any container.
290                        // NOTE: _remove() doesn't work here. Why?
291                        if (_frame != null)
292                                _frame.dispose();
293                        _frame = null;
294                        shellPanel = null;
295                        return;
296                }
297                shellPanel = new DoubleShellTextAreaPanel();
298                shellPanel.setInterpreter(this);
299                shellPanel.shell1.setEditable(true);
300                // shellPanel.shell1.append(_greetSh1);
301                shellPanel.shell1.setEditable(false);
302                shellPanel.clearShell2();
303
304                _container.add(shellPanel);
305                // java.awt.Component.setBackground(color) says that
306                // if the color "parameter is null then this component
307                // will inherit the background color of its parent."
308                shellPanel.setBackground(null);
309        }
310
311        /**
312         * Override the base class to return false if the user has typed "quit" or
313         * "exit".
314         * 
315         * @return False if the user has typed "quit" or "exit".
316         * @exception IllegalActionException
317         *                If the superclass throws it.
318         */
319        public boolean postfire() throws IllegalActionException {
320                // if (_returnFalseInPostfire) {
321                _returnFalseInPostfire = true;
322                return false;
323
324                // }
325                // return super.postfire();
326        }
327
328        /**
329         * Override the base class to remove the shell from its graphical container
330         * if the argument is null.
331         * 
332         * @param container
333         *            The proposed container.
334         * @exception IllegalActionException
335         *                If the base class throws it.
336         * @exception NameDuplicationException
337         *                If the base class throws it.
338         */
339        public void setContainer(CompositeEntity container)
340                        throws IllegalActionException, NameDuplicationException {
341                Nameable previousContainer = getContainer();
342                super.setContainer(container);
343                if (container != previousContainer && previousContainer != null) {
344                        _remove();
345                }
346        }
347
348        /**
349         * Specify an output string to be sent. This method appends the specified
350         * string to a queue. Strings are retrieved from the queue by getOutput().
351         * 
352         * @see #getOutput()
353         * @param value
354         *            An output string to be sent.
355         */
356        public synchronized void setOutput(String value) {
357                _outputValues.add(value);
358                notifyAll();
359        }
360
361        /**
362         * Override the base class to call notifyAll() to get out of any waiting.
363         */
364        public void stop() {
365                super.stop();
366                synchronized (this) {
367                        notifyAll();
368                }
369        }
370
371        /**
372         * Override the base class to make the shell uneditable.
373         * 
374         * @exception IllegalActionException
375         *                If the parent class throws it.
376         */
377        public void wrapup() throws IllegalActionException {
378                super.wrapup();
379                if (_returnFalseInPostfire && _frame != null) {
380                        _frame.dispose();
381                } else if (shellPanel != null) {
382                        shellPanel.setEditable();
383                }
384        }
385
386        // /////////////////////////////////////////////////////////////////
387        // // protected methods ////
388
389        /**
390         * Write a MoML description of the contents of this object. This overrides
391         * the base class to make sure that the current frame properties, if there
392         * is a frame, are recorded.
393         * 
394         * @param output
395         *            The output stream to write to.
396         * @param depth
397         *            The depth in the hierarchy, to determine indenting.
398         * @exception IOException
399         *                If an I/O error occurs.
400         */
401        protected void _exportMoMLContents(Writer output, int depth)
402                        throws IOException {
403                // Make sure that the current position of the frame, if any,
404                // is up to date.
405                if (_frame != null) {
406                        _windowProperties.recordProperties(_frame);
407                }
408                super._exportMoMLContents(output, depth);
409        }
410
411        // /////////////////////////////////////////////////////////////////
412        // // private members ////
413
414        private String _greetSh1 = "The outputs of the previous step.\nPlease double click on your selections to be sent to the next step\n";
415
416        /** Container into which this plot should be placed. */
417        private Container _container;
418
419        /** Indicator of the first time through. */
420        private boolean _firstTime = true;
421
422        /** Frame into which plot is placed, if any. */
423        private TableauFrame _frame;
424
425        /** The list of strings to send to the output. */
426        private List _outputValues = new LinkedList();
427
428        /** Flag indicating that "exit" or "quit" has been entered. */
429        private boolean _returnFalseInPostfire = false;
430
431        // A specification for the window properties of the frame.
432        private WindowPropertiesAttribute _windowProperties;
433
434        // /////////////////////////////////////////////////////////////////
435        // // private methods ////
436
437        /**
438         * Remove the shell from the current container, if there is one.
439         */
440        private void _remove() {
441                SwingUtilities.invokeLater(new Runnable() {
442                        public void run() {
443                                if (shellPanel != null) {
444                                        if (_container != null) {
445                                                _container.remove(shellPanel);
446                                                _container.invalidate();
447                                                _container.repaint();
448                                        } else if (_frame != null) {
449                                                _frame.dispose();
450                                        }
451                                }
452                        }
453                });
454        }
455
456        // /////////////////////////////////////////////////////////////////
457        // // inner classes ////
458
459        /**
460         * Version of ExpressionShellTableau that records the size of the display
461         * when it is closed.
462         */
463        public class ShellTableau extends DoubleShellTAPTableau {
464
465                /**
466                 * Construct a new tableau for the model represented by the given
467                 * effigy.
468                 * 
469                 * @param container
470                 *            The container.
471                 * @param name
472                 *            The name.
473                 * @exception IllegalActionException
474                 *                If the container does not accept this entity (this
475                 *                should not occur).
476                 * @exception NameDuplicationException
477                 *                If the name coincides with an attribute already in the
478                 *                container.
479                 */
480                public ShellTableau(DoubleShellTAPEffigy container, String name)
481                                throws IllegalActionException, NameDuplicationException {
482                        super(container, name);
483                        frame = new ShellFrame(this);
484                        setFrame(frame);
485                        frame.setTableau(this);
486                }
487        }
488
489        /**
490         * The frame that is created by an instance of ShellTableau.
491         */
492        public class ShellFrame extends DoubleShellTAPTableauFrame {
493
494                /**
495                 * Construct a frame to display the ExpressionShell window. Override the
496                 * base class to handle window closing. After constructing this, it is
497                 * necessary to call setVisible(true) to make the frame appear. This is
498                 * typically accomplished by calling show() on enclosing tableau.
499                 * 
500                 * @param tableau
501                 *            The tableau responsible for this frame.
502                 * @exception IllegalActionException
503                 *                If the model rejects the configuration attribute.
504                 * @exception NameDuplicationException
505                 *                If a name collision occurs.
506                 */
507                public ShellFrame(DoubleShellTAPTableau tableau)
508                                throws IllegalActionException, NameDuplicationException {
509                        super(tableau);
510                }
511
512                /**
513                 * Overrides the base class to record the size and location of the
514                 * frame.
515                 * 
516                 * @return False if the user cancels on a save query.
517                 */
518                protected boolean _close() {
519                        if (_frame != null) {
520                                _windowProperties.setProperties(_frame);
521                        }
522                        // Return value can be ignored since there is no issue of saving.
523                        super._close();
524                        place(null);
525                        return true;
526                }
527        }
528}