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.BorderLayout;
033import java.awt.Color;
034import java.awt.Cursor;
035import java.awt.event.InputEvent;
036import java.awt.event.KeyAdapter;
037import java.awt.event.KeyEvent;
038import java.awt.event.WindowAdapter;
039import java.awt.event.WindowEvent;
040import java.awt.event.WindowListener;
041import java.util.Vector;
042
043import javax.swing.BorderFactory;
044import javax.swing.JFrame;
045import javax.swing.JPanel;
046import javax.swing.JScrollPane;
047import javax.swing.JSplitPane;
048import javax.swing.JTextArea;
049import javax.swing.SwingUtilities;
050
051import ptolemy.gui.MessageHandler;
052import ptolemy.gui.ShellInterpreter;
053
054//////////////////////////////////////////////////////////////////////////
055//// DoubleShellTextArea
056
057/**
058 * A read-only text area showing the result of the previous step and a text area
059 * supporting shell-style interactions.
060 * 
061 * @author Ilkay Altintas
062 * @version $Id: DoubleShellTextAreaPanel.java 11161 2005-11-01 20:39:16Z ruland
063 *          $
064 */
065public class DoubleShellTextAreaPanel extends JPanel {
066
067        /**
068         * Create a new instance with no initial message.
069         */
070        public DoubleShellTextAreaPanel() {
071                this(null);
072        }
073
074        /**
075         * Create a new instance with the specified initial message.
076         * 
077         * @param initialMessage
078         *            The initial message.
079         */
080        public DoubleShellTextAreaPanel(String initialMessage) {
081                // Graphics
082                super(new BorderLayout());
083                setBorder(BorderFactory.createTitledBorder(BorderFactory
084                                .createLineBorder(Color.black), ""));
085
086                // FIXME: Border and shell size needs to be configurable.
087                shell1 = new JTextArea("", 20, 80);
088                JScrollPane jSP_up = new JScrollPane(shell1);
089                // add(jSP1, BorderLayout.NORTH);
090                shell2 = new JTextArea("", 20, 80);
091                JScrollPane jSP_down = new JScrollPane(shell2);
092                // add(jSP2, BorderLayout.SOUTH);
093                JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, jSP_up,
094                                jSP_down);
095                pane.setDividerLocation(0.5);
096                add(pane, BorderLayout.CENTER);
097
098                shell2.addKeyListener(new ShellKeyListener());
099        }
100
101        public static void main(String[] args) {
102                JFrame jFrame = new JFrame("ShellTextArea Example");
103                WindowListener windowListener = new WindowAdapter() {
104                        public void windowClosing(WindowEvent e) {
105                                System.exit(0);
106                        }
107                };
108                jFrame.addWindowListener(windowListener);
109
110                final DoubleShellTextAreaPanel exec = new DoubleShellTextAreaPanel();
111                jFrame.getContentPane().add(exec);
112                jFrame.setSize(600, 400);
113                jFrame.pack();
114                jFrame.show();
115        }
116
117        /**
118         * Override the base class to output the first prompt. We need to do this
119         * here because we can't write to the TextArea until the peer has been
120         * created.
121         */
122        public void addNotify() {
123                super.addNotify();
124                initialize(_initialMessage);
125        }
126
127        /**
128         * Set the associated text area editable (with a true argument) or not
129         * editable (with a false argument). This should be called in the swing
130         * event thread.
131         * 
132         * @param editable
133         *            True to make the text area editable, false to make it
134         *            uneditable.
135         */
136        public void setEditable() {
137                shell2.setEditable(true);
138                shell1.setEditable(false);
139
140        }
141
142        /**
143         * Append the specified text to the JTextArea and update the prompt cursor.
144         * The text will actually be appended in the swing thread, not immediately.
145         * This method immediately returns.
146         * 
147         * @param text
148         *            The text to append to the text area.
149         */
150        public void appendShell2(final String text) {
151                Runnable doAppendSh2 = new Runnable() {
152                        public void run() {
153                                shell2.append(text);
154                                // Scroll down as we generate text.
155                                shell2.setCaretPosition(shell2.getText().length());
156                                // To prevent _promptCursor from being
157                                // updated before the JTextArea is actually updated,
158                                // this needs to be inside the Runnable.
159                                _promptCursor += text.length();
160                        }
161                };
162                SwingUtilities.invokeLater(doAppendSh2);
163        }
164
165        public void appendShell1(final String text) {
166                shell1.setEditable(true);
167                Runnable doAppendSh1 = new Runnable() {
168                        public void run() {
169                                shell1.append(text);
170                                // Scroll down as we generate text.
171                                shell1.setCaretPosition(shell1.getText().length());
172                        }
173                };
174                SwingUtilities.invokeLater(doAppendSh1);
175        }
176
177        /**
178         * Clear the JTextArea and reset the prompt cursor. The clearing is done in
179         * the swing thread, not immediately. This method immediately returns.
180         */
181        public void clearShell1() {
182                Runnable doClearShell1 = new Runnable() {
183                        public void run() {
184                                // shell1.setText(_greetSh1);
185                                shell1.setCaretPosition(0);
186                        }
187                };
188                SwingUtilities.invokeLater(doClearShell1);
189        }
190
191        /**
192         * Clear the JTextArea and reset the prompt cursor. The clearing is done in
193         * the swing thread, not immediately. This method immediately returns.
194         */
195        public void clearShell2() {
196                Runnable doClearShell2 = new Runnable() {
197                        public void run() {
198                                shell2.setText("");
199                                shell2.setCaretPosition(0);
200                                _promptCursor = 0;
201                        }
202                };
203                SwingUtilities.invokeLater(doClearShell2);
204        }
205
206        /**
207         * Initialize the text area with the given starting message, followed by a
208         * prompt. If the argument is null or the empty string, then only a prompt
209         * is shown.
210         * 
211         * @param initialMessage
212         *            The initial message.
213         */
214        public void initialize(String initialMessage) {
215                clearShell1();
216                appendShell1(initialMessage + "\n" + _greetSh1 + "\n");
217                clearShell2();
218                appendShell2(mainPrompt);
219                shell1.setEditable(false);
220        }
221
222        /**
223         * Replace a range in the JTextArea.
224         */
225        public void replaceRangeJTextArea(final String text, final int start,
226                        final int end) {
227                Runnable doReplaceRangeJTextArea = new Runnable() {
228                        public void run() {
229                                shell2.replaceRange(text, start, end);
230                        }
231                };
232                SwingUtilities.invokeLater(doReplaceRangeJTextArea);
233        }
234
235        /**
236         * Return the result of a command evaluation. This method is used when it is
237         * impractical to insist on the result being returned by evaluateCommand()
238         * of a ShellInterpreter. For example, computing the result may take a
239         * while.
240         * 
241         * @param result
242         *            The result to return.
243         */
244        public void returnResult(final String result) {
245                // Make the text area editable again.
246                Runnable doMakeEditable = new Runnable() {
247                        public void run() {
248                                setEditable();
249                                String toPrint = result + "\n" + _greetSh1;
250                                appendShell1(toPrint);
251                                appendShell2("\n" + mainPrompt);
252                        }
253                };
254                SwingUtilities.invokeLater(doMakeEditable);
255        }
256
257        /**
258         * Set the interpreter.
259         * 
260         * @param interpreter
261         *            The interpreter.
262         * @see #getInterpreter()
263         */
264        public void setInterpreter(ShellInterpreter interpreter) {
265                _interpreter = interpreter;
266        }
267
268        /**
269         * Get the interpreter that has been registered with setInterpreter().
270         * 
271         * @return The interpreter, or null if none has been set.
272         * @see #setInterpreter(ShellInterpreter)
273         */
274        public ShellInterpreter getInterpreter() {
275                return _interpreter;
276        }
277
278        // /////////////////////////////////////////////////////////////////
279        // // public variables ////
280
281        public JTextArea shell1;
282        public JTextArea shell2;
283
284        /** Main prompt. */
285        public String mainPrompt = ">> ";
286
287        /** Size of the history to keep. */
288        public int historyLength = 20;
289
290        // /////////////////////////////////////////////////////////////////
291        // // private methods ////
292
293        // Update the command history.
294        private void _updateHistory(String command) {
295                _historyCursor = 0;
296                if (_historyCommands.size() == historyLength) {
297                        _historyCommands.removeElementAt(0);
298                }
299                _historyCommands.addElement(command);
300        }
301
302        // Evaluate the command so far, if possible, printing
303        // a continuation prompt if not.
304        // NOTE: This must be called in the swing event thread.
305        private void _evalCommand() {
306                String newtext = shell2.getText().substring(_promptCursor);
307                _promptCursor += newtext.length();
308                if (_commandBuffer.length() > 0) {
309                        _commandBuffer.append("\n");
310                }
311                _commandBuffer.append(newtext);
312                String command = _commandBuffer.toString();
313
314                if (_interpreter == null) {
315                        appendShell2("\n" + mainPrompt);
316                } else {
317                        if (_interpreter.isCommandComplete(command)) {
318                                // Process it
319                                appendShell2("\n");
320                                Cursor oldCursor = shell2.getCursor();
321                                shell2.setCursor(new Cursor(Cursor.WAIT_CURSOR));
322                                String result;
323                                try {
324                                        result = _interpreter.evaluateCommand(command);
325                                } catch (RuntimeException e) {
326                                        // RuntimeException are due to bugs in the expression
327                                        // evaluation code, so we make the stack trace available.
328                                        MessageHandler.error("Failed to evaluate expression", e);
329                                        result = "Internal error evaluating expression.";
330                                        throw e;
331                                } catch (Exception e) {
332                                        result = e.getMessage();
333                                        // NOTE: Not ideal here to print the stack trace, but
334                                        // if we don't, it will be invisible, which makes
335                                        // debugging hard.
336                                        // e.printStackTrace();
337                                }
338                                if (result != null) {
339                                        if (result.trim().equals("")) {
340                                                appendShell2(mainPrompt);
341                                        } else {
342                                                appendShell2(result + "\n" + mainPrompt);
343                                        }
344                                } else {
345                                        // Result is incomplete.
346                                        // Make the text uneditable to prevent further input
347                                        // until returnResult() is called.
348                                        // NOTE: We are assuming this called in the swing thread.
349                                        setEditable();
350                                }
351                                _commandBuffer.setLength(0);
352                                shell2.setCursor(oldCursor);
353                                _updateHistory(command);
354                        } else {
355                                appendShell2("\n" + contPrompt);
356                        }
357                }
358        }
359
360        // /////////////////////////////////////////////////////////////////
361        // // private variables ////
362
363        // The initial message, if there is one.
364        private String _initialMessage = null;
365        private String _greetSh1 = "The outputs of the previous step.\nPlease double click on your selections to be sent to the next step\n";
366
367        private int _promptCursor = 0;
368
369        /** Prompt to use on continuation lines. */
370        public String contPrompt = "";
371
372        // The command input
373        private StringBuffer _commandBuffer = new StringBuffer();
374
375        // The interpreter.
376        private ShellInterpreter _interpreter;
377
378        // History
379        private int _historyCursor = 0;
380        private Vector _historyCommands = new Vector();
381
382        // /////////////////////////////////////////////////////////////////
383        // // inner classes ////
384        // The key listener
385        private class ShellKeyListener extends KeyAdapter {
386                public void keyTyped(KeyEvent keyEvent) {
387                        switch (keyEvent.getKeyCode()) {
388                        case KeyEvent.VK_UNDEFINED:
389                                if (keyEvent.getKeyChar() == '\b') {
390                                        if (shell2.getCaretPosition() == _promptCursor) {
391                                                keyEvent.consume(); // don't backspace over prompt!
392                                        }
393                                }
394                                break;
395
396                        case KeyEvent.VK_BACK_SPACE:
397                                if (shell2.getCaretPosition() == _promptCursor) {
398                                        keyEvent.consume(); // don't backspace over prompt!
399                                }
400                                break;
401                        default:
402                        }
403                }
404
405                public void keyReleased(KeyEvent keyEvent) {
406                        switch (keyEvent.getKeyCode()) {
407                        case KeyEvent.VK_BACK_SPACE:
408                                if (shell2.getCaretPosition() == _promptCursor) {
409                                        keyEvent.consume(); // don't backspace over prompt!
410                                }
411                                break;
412                        default:
413                        }
414                }
415
416                public void keyPressed(KeyEvent keyEvent) {
417                        // Process keys
418                        switch (keyEvent.getKeyCode()) {
419                        case KeyEvent.VK_ENTER:
420                                keyEvent.consume();
421                                _evalCommand();
422                                break;
423                        case KeyEvent.VK_BACK_SPACE:
424                                if (shell2.getCaretPosition() <= _promptCursor) {
425                                        // FIXME: Consuming the event is useless...
426                                        // The backspace still occurs. Why? Java bug?
427                                        keyEvent.consume(); // don't backspace over prompt!
428                                }
429                                break;
430                        case KeyEvent.VK_LEFT:
431                                if (shell2.getCaretPosition() == _promptCursor) {
432                                        keyEvent.consume();
433                                }
434                                break;
435                        case KeyEvent.VK_HOME:
436                                shell2.setCaretPosition(_promptCursor);
437                                keyEvent.consume();
438                                break;
439                        default:
440                                switch (keyEvent.getModifiers()) {
441                                case InputEvent.CTRL_MASK:
442                                        switch (keyEvent.getKeyCode()) {
443                                        case KeyEvent.VK_A:
444                                                shell2.setCaretPosition(_promptCursor);
445                                                keyEvent.consume();
446                                                break;
447                                        default:
448                                        }
449                                        break;
450                                default:
451                                        // Otherwise we got a regular character.
452                                        // Don't consume it, and TextArea will
453                                        // take care of displaying it.
454                                }
455                        }
456                }
457        }
458}