001/* A text area for shell-style interactions.
002
003 Copyright (c) 1998-2014 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.gui;
029
030import java.awt.BorderLayout;
031import java.awt.Color;
032import java.awt.Cursor;
033import java.awt.Font;
034import java.awt.Toolkit;
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.JTextArea;
048import javax.swing.SwingUtilities;
049
050import ptolemy.util.MessageHandler;
051import ptolemy.util.StringUtilities;
052
053///////////////////////////////////////////////////////////////////
054//// ShellTextArea
055
056/**
057 A text area supporting shell-style interactions.
058
059 @author John Reekie, Christopher Hylands, Edward A. Lee
060 @version $Id$
061 @since Ptolemy II 3.0
062 @Pt.ProposedRating Red (cxh)
063 @Pt.AcceptedRating Red (cxh)
064 */
065@SuppressWarnings("serial")
066public class ShellTextArea extends JPanel {
067    /** Create a new instance with no initial message.
068     */
069    public ShellTextArea() {
070        this(null);
071    }
072
073    /** Create a new instance with the specified initial message.
074     *  @param initialMessage The initial message.
075     */
076    public ShellTextArea(String initialMessage) {
077        // Graphics
078        super(new BorderLayout());
079        _initialMessage = initialMessage;
080
081        // FIXME: Size needs to be configurable.
082        _jTextArea = new JTextArea("", 20, 80);
083
084        // FIXME: Large font for demo. Font needs to be configurable.
085        // _jTextArea.setFont(new Font("DialogInput", 0, 24));
086        _jTextArea.setFont(new Font("Monospaced", 0, 14));
087        JScrollPane jScrollPane = new JScrollPane(_jTextArea);
088        add(jScrollPane);
089
090        setBorder(BorderFactory.createTitledBorder(
091                BorderFactory.createLineBorder(Color.black), ""));
092
093        // Event handling
094        _jTextArea.addKeyListener(new ShellKeyListener());
095    }
096
097    ///////////////////////////////////////////////////////////////////
098    ////                         public methods                    ////
099
100    /** Override the base class to output the first prompt.
101     *  We need to do this here because we can't write to
102     *  the TextArea until the peer has been created.
103     */
104    @Override
105    public void addNotify() {
106        super.addNotify();
107        initialize(_initialMessage);
108    }
109
110    /** Append the specified text to the JTextArea and
111     *  update the prompt cursor.  The text will actually be appended
112     *  in the swing thread, not immediately.  This method immediately
113     *  returns.
114     *  @param text The text to append to the text area.
115     */
116    public void appendJTextArea(final String text) {
117        Runnable doAppendJTextArea = new Runnable() {
118            @Override
119            public void run() {
120                _jTextArea.append(text);
121
122                // Scroll down as we generate text.
123                _jTextArea.setCaretPosition(_jTextArea.getText().length());
124
125                // To prevent _promptCursor from being
126                // updated before the JTextArea is actually updated,
127                // this needs to be inside the Runnable.
128                _promptCursor += text.length();
129            }
130        };
131
132        SwingUtilities.invokeLater(doAppendJTextArea);
133    }
134
135    /** Clear the JTextArea and reset the prompt cursor.
136     *  The clearing is done in the swing thread, not immediately.
137     *  This method immediately returns.
138     */
139    public void clearJTextArea() {
140        Runnable doClearJTextArea = new Runnable() {
141            @Override
142            public void run() {
143                _jTextArea.setText("");
144                _jTextArea.setCaretPosition(0);
145                _promptCursor = 0;
146            }
147        };
148
149        SwingUtilities.invokeLater(doClearJTextArea);
150    }
151
152    /** Get the interpreter that has been registered with setInterpreter().
153     *  @return The interpreter, or null if none has been set.
154     *  @see #setInterpreter(ShellInterpreter)
155     */
156    public ShellInterpreter getInterpreter() {
157        return _interpreter;
158    }
159
160    /** Initialize the text area with the given starting message,
161     *  followed by a prompt. If the argument is null or the empty
162     *  string, then only a prompt is shown.
163     *  @param initialMessage The initial message.
164     */
165    public void initialize(final String initialMessage) {
166        if (_jTextArea == null) {
167            _initialMessage = initialMessage;
168        } else {
169            _initialMessage = null;
170            Runnable doInitialize = new Runnable() {
171                @Override
172                public void run() {
173                    clearJTextArea();
174
175                    if (initialMessage != null && !initialMessage.equals("")) {
176                        appendJTextArea(initialMessage + "\n" + mainPrompt);
177                    } else {
178                        appendJTextArea(mainPrompt);
179                    }
180                }
181            };
182            SwingUtilities.invokeLater(doInitialize);
183        }
184    }
185
186    /** Main method used for testing. To run a simple test, use:
187     *  <pre>
188     *        java -classpath $PTII ptolemy.gui.ShellTextArea
189     *  </pre>
190     *  @param args Currently ignored.
191     */
192    public static void main(String[] args) {
193        try {
194            // Run this in the Swing Event Thread.
195            Runnable doActions = new Runnable() {
196                @Override
197                public void run() {
198                    try {
199                        JFrame jFrame = new JFrame("ShellTextArea Example");
200                        WindowListener windowListener = new WindowAdapter() {
201                            @Override
202                            public void windowClosing(WindowEvent e) {
203                                StringUtilities.exit(0);
204                            }
205                        };
206
207                        jFrame.addWindowListener(windowListener);
208
209                        final ShellTextArea exec = new ShellTextArea();
210                        jFrame.getContentPane().add(exec);
211                        jFrame.pack();
212                        jFrame.setVisible(true);
213                    } catch (Exception ex) {
214                        System.err.println(ex.toString());
215                        ex.printStackTrace();
216                    }
217                }
218            };
219            SwingUtilities.invokeAndWait(doActions);
220        } catch (Exception ex) {
221            System.err.println(ex.toString());
222            ex.printStackTrace();
223        }
224    }
225
226    /** Replace a range in the JTextArea.
227     *  @param text The text with which the JTextArea is updated.
228     *  @param start The start index.
229     *  @param end The end index.
230     */
231    public void replaceRangeJTextArea(final String text, final int start,
232            final int end) {
233        Runnable doReplaceRangeJTextArea = new Runnable() {
234            @Override
235            public void run() {
236                _jTextArea.replaceRange(text, start, end);
237            }
238        };
239
240        SwingUtilities.invokeLater(doReplaceRangeJTextArea);
241    }
242
243    /** Return the result of a command evaluation.  This method is used
244     *  when it is impractical to insist on the result being returned by
245     *  evaluateCommand() of a ShellInterpreter.  For example, computing
246     *  the result may take a while.
247     *  @param result The result to return.
248     */
249    public void returnResult(final String result) {
250        // Make the text area editable again.
251        Runnable doMakeEditable = new Runnable() {
252            @Override
253            public void run() {
254                setEditable(true);
255
256                StringBuffer toPrint = new StringBuffer(result);
257                if (!result.equals("")) {
258                    toPrint.append("\n");
259                }
260                toPrint.append(mainPrompt);
261                appendJTextArea(toPrint.toString());
262            }
263        };
264
265        SwingUtilities.invokeLater(doMakeEditable);
266    }
267
268    /** Set the associated text area editable (with a true argument)
269     *  or not editable (with a false argument).  This should be called
270     *  in the swing event thread.
271     *  @param editable True to make the text area editable, false to
272     *   make it uneditable.
273     */
274    public void setEditable(boolean editable) {
275        _jTextArea.setEditable(editable);
276    }
277
278    /** Set the interpreter.
279     *  @param interpreter The interpreter.
280     *  @see #getInterpreter()
281     */
282    public void setInterpreter(ShellInterpreter interpreter) {
283        _interpreter = interpreter;
284    }
285
286    ///////////////////////////////////////////////////////////////////
287    ////                         public variables                  ////
288
289    /** Main prompt. */
290    public String mainPrompt = ">> ";
291
292    /** Prompt to use on continuation lines. */
293    public String contPrompt = "";
294
295    /** Size of the history to keep. */
296    public int historyLength = 20;
297
298    ///////////////////////////////////////////////////////////////////
299    ////                         private methods                   ////
300    // Evaluate the command so far, if possible, printing
301    // a continuation prompt if not.
302    // NOTE: This must be called in the swing event thread.
303    private void _evalCommand() {
304        String newtext = _jTextArea.getText().substring(_promptCursor);
305        _promptCursor += newtext.length();
306
307        if (_commandBuffer.length() > 0) {
308            _commandBuffer.append("\n");
309        }
310
311        _commandBuffer.append(newtext);
312
313        String command = _commandBuffer.toString();
314
315        if (_interpreter == null) {
316            appendJTextArea("\n" + mainPrompt);
317        } else {
318            if (_interpreter.isCommandComplete(command)) {
319                // Process it
320                appendJTextArea("\n");
321
322                Cursor oldCursor = _jTextArea.getCursor();
323                _jTextArea.setCursor(new Cursor(Cursor.WAIT_CURSOR));
324
325                String result;
326
327                try {
328                    result = _interpreter.evaluateCommand(command);
329                } catch (RuntimeException e) {
330                    // RuntimeException are due to bugs in the expression
331                    // evaluation code, so we make the stack trace available.
332                    MessageHandler.error("Failed to evaluate expression", e);
333                    result = "Internal error evaluating expression.";
334                    throw e;
335                } catch (Exception e) {
336                    result = e.getMessage();
337
338                    // NOTE: Not ideal here to print the stack trace, but
339                    // if we don't, it will be invisible, which makes
340                    // debugging hard.
341                    // e.printStackTrace();
342                }
343
344                if (result != null) {
345                    if (result.trim().equals("")) {
346                        appendJTextArea(mainPrompt);
347                    } else {
348                        appendJTextArea(result + "\n" + mainPrompt);
349                    }
350                } else {
351                    // Result is incomplete.
352                    // Make the text uneditable to prevent further input
353                    // until returnResult() is called.
354                    // NOTE: We are assuming this called in the swing thread.
355                    setEditable(false);
356                }
357
358                _commandBuffer.setLength(0);
359                _jTextArea.setCursor(oldCursor);
360                _updateHistory(command);
361            } else {
362                appendJTextArea("\n" + contPrompt);
363            }
364        }
365    }
366
367    // Replace the command with an entry from the history.
368    private void _nextCommand() {
369        String text;
370
371        if (_historyCursor == 0) {
372            text = "";
373        } else {
374            _historyCursor--;
375            text = (String) _historyCommands
376                    .elementAt(_historyCommands.size() - _historyCursor - 1);
377        }
378
379        replaceRangeJTextArea(text, _promptCursor,
380                _jTextArea.getText().length());
381    }
382
383    // Replace the command with an entry from the history.
384    private void _previousCommand() {
385        String text;
386
387        if (_historyCursor == _historyCommands.size()) {
388            return;
389        } else {
390            _historyCursor++;
391            text = (String) _historyCommands
392                    .elementAt(_historyCommands.size() - _historyCursor);
393        }
394
395        replaceRangeJTextArea(text, _promptCursor,
396                _jTextArea.getText().length());
397    }
398
399    // Update the command history.
400    private void _updateHistory(String command) {
401        _historyCursor = 0;
402
403        if (_historyCommands.size() == historyLength) {
404            _historyCommands.removeElementAt(0);
405        }
406
407        _historyCommands.addElement(command);
408    }
409
410    ///////////////////////////////////////////////////////////////////
411    ////                         private variables                 ////
412    // The command input
413    private StringBuffer _commandBuffer = new StringBuffer();
414
415    // The TextArea widget for displaying commands and results
416    private JTextArea _jTextArea;
417
418    // Cursor, showing where last prompt or result ended.
419    private int _promptCursor = 0;
420
421    // History
422    private int _historyCursor = 0;
423
424    private Vector _historyCommands = new Vector();
425
426    // The initial message, if there is one.
427    private String _initialMessage = null;
428
429    // The interpreter.
430    private ShellInterpreter _interpreter;
431
432    ///////////////////////////////////////////////////////////////////
433    ////                         inner classes                     ////
434    // The key listener
435    private class ShellKeyListener extends KeyAdapter {
436        @Override
437        public void keyTyped(KeyEvent keyEvent) {
438            switch (keyEvent.getKeyCode()) {
439            case KeyEvent.VK_UNDEFINED:
440
441                if (keyEvent.getKeyChar() == '\b') {
442                    if (_jTextArea.getCaretPosition() == _promptCursor) {
443                        keyEvent.consume(); // don't backspace over prompt!
444                    }
445                }
446
447                break;
448
449            case KeyEvent.VK_BACK_SPACE:
450
451                if (_jTextArea.getCaretPosition() == _promptCursor) {
452                    keyEvent.consume(); // don't backspace over prompt!
453                }
454
455                break;
456
457            default:
458            }
459        }
460
461        @Override
462        public void keyReleased(KeyEvent keyEvent) {
463            switch (keyEvent.getKeyCode()) {
464            case KeyEvent.VK_BACK_SPACE:
465
466                if (_jTextArea.getCaretPosition() == _promptCursor) {
467                    keyEvent.consume(); // don't backspace over prompt!
468                }
469
470                break;
471
472            default:
473            }
474        }
475
476        @Override
477        public void keyPressed(KeyEvent keyEvent) {
478            if (!_jTextArea.isEditable()) {
479                // NOTE: This doesn't seem to always work.
480                Toolkit.getDefaultToolkit().beep();
481                return;
482            }
483
484            // Process keys
485            switch (keyEvent.getKeyCode()) {
486            case KeyEvent.VK_ENTER:
487                keyEvent.consume();
488                _evalCommand();
489                break;
490
491            case KeyEvent.VK_BACK_SPACE:
492
493                if (_jTextArea.getCaretPosition() <= _promptCursor) {
494                    // FIXME: Consuming the event is useless...
495                    // The backspace still occurs.  Why?  Java bug?
496                    keyEvent.consume(); // don't backspace over prompt!
497                }
498
499                break;
500
501            case KeyEvent.VK_LEFT:
502
503                if (_jTextArea.getCaretPosition() == _promptCursor) {
504                    keyEvent.consume();
505                }
506
507                break;
508
509            case KeyEvent.VK_UP:
510                _previousCommand();
511                keyEvent.consume();
512                break;
513
514            case KeyEvent.VK_DOWN:
515                _nextCommand();
516                keyEvent.consume();
517                break;
518
519            case KeyEvent.VK_HOME:
520                _jTextArea.setCaretPosition(_promptCursor);
521                keyEvent.consume();
522                break;
523
524            default:
525
526                switch (keyEvent.getModifiers()) {
527                case InputEvent.CTRL_MASK:
528
529                    switch (keyEvent.getKeyCode()) {
530                    case KeyEvent.VK_A:
531                        _jTextArea.setCaretPosition(_promptCursor);
532                        keyEvent.consume();
533                        break;
534
535                    case KeyEvent.VK_N:
536                        _nextCommand();
537                        keyEvent.consume();
538                        break;
539
540                    case KeyEvent.VK_P:
541                        _previousCommand();
542                        keyEvent.consume();
543                        break;
544
545                    default:
546                    }
547
548                    break;
549
550                default:
551                    // Otherwise we got a regular character.
552                    // Don't consume it, and TextArea will
553                    // take care of displaying it.
554                }
555            }
556        }
557    }
558}