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}