001 /* 002 * $Id: Console.java,v 1.32 2012/02/19 17:35:46 davep Exp $ 003 * 004 * This file is part of McIDAS-V 005 * 006 * Copyright 2007-2012 007 * Space Science and Engineering Center (SSEC) 008 * University of Wisconsin - Madison 009 * 1225 W. Dayton Street, Madison, WI 53706, USA 010 * https://www.ssec.wisc.edu/mcidas 011 * 012 * All Rights Reserved 013 * 014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 015 * some McIDAS-V source code is based on IDV and VisAD source code. 016 * 017 * McIDAS-V is free software; you can redistribute it and/or modify 018 * it under the terms of the GNU Lesser Public License as published by 019 * the Free Software Foundation; either version 3 of the License, or 020 * (at your option) any later version. 021 * 022 * McIDAS-V is distributed in the hope that it will be useful, 023 * but WITHOUT ANY WARRANTY; without even the implied warranty of 024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 025 * GNU Lesser Public License for more details. 026 * 027 * You should have received a copy of the GNU Lesser Public License 028 * along with this program. If not, see http://www.gnu.org/licenses. 029 */ 030 031 package edu.wisc.ssec.mcidasv.jython; 032 033 import static edu.wisc.ssec.mcidasv.util.Contract.notNull; 034 035 import java.awt.BorderLayout; 036 import java.awt.Color; 037 import java.awt.Dimension; 038 import java.awt.EventQueue; 039 import java.awt.Font; 040 import java.awt.event.KeyEvent; 041 import java.awt.event.KeyListener; 042 import java.awt.event.MouseAdapter; 043 import java.awt.event.MouseEvent; 044 import java.awt.Toolkit; 045 import java.util.ArrayList; 046 import java.util.Collections; 047 import java.util.HashMap; 048 import java.util.List; 049 import java.util.Map; 050 import java.util.Properties; 051 import java.util.Set; 052 import java.util.TreeSet; 053 054 import javax.swing.JFrame; 055 import javax.swing.JPanel; 056 import javax.swing.JPopupMenu; 057 import javax.swing.JScrollPane; 058 import javax.swing.JTextPane; 059 import javax.swing.KeyStroke; 060 import javax.swing.text.BadLocationException; 061 import javax.swing.text.Document; 062 import javax.swing.text.JTextComponent; 063 import javax.swing.text.SimpleAttributeSet; 064 import javax.swing.text.StyleConstants; 065 066 import org.python.core.PyJavaType; 067 import org.python.core.PyList; 068 import org.python.core.PyObject; 069 import org.python.core.PyObjectDerived; 070 import org.python.core.PyString; 071 import org.python.core.PyStringMap; 072 import org.python.core.PyTuple; 073 import org.python.util.InteractiveConsole; 074 import org.slf4j.Logger; 075 import org.slf4j.LoggerFactory; 076 077 import ucar.unidata.util.StringUtil; 078 079 // TODO(jon): Console should become an interface. there is no reason people 080 // should have to deal with the UI stuff when they only want to use 081 // an interpreter. 082 public class Console implements Runnable, KeyListener { 083 084 public enum HistoryType { INPUT, SYSTEM }; 085 086 /** Color of the Jython text as it is being entered. */ 087 protected static final Color TXT_NORMAL = Color.BLACK; 088 089 /** Color of text coming from {@literal "stdout"}. */ 090 protected static final Color TXT_GOOD = Color.BLUE; 091 092 /** Not used just yet... */ 093 protected static final Color TXT_WARN = Color.ORANGE; 094 095 /** Color of text coming from {@literal "stderr"}. */ 096 protected static final Color TXT_ERROR = Color.RED; 097 098 /** {@link Logger} object for Jython consoles. */ 099 private static final Logger logger = LoggerFactory.getLogger(Console.class); 100 101 /** Offset array used when actual offsets cannot be determined. */ 102 private static final int[] BAD_OFFSETS = { -1, -1 }; 103 104 /** Normal jython prompt. */ 105 private static final String PS1 = ">>> "; 106 107 /** Prompt that indicates more input is needed. */ 108 private static final String PS2 = "... "; 109 110 /** Actual {@link String} of whitespace to insert for blocks and whatnot. */ 111 private static final String WHITESPACE = " "; 112 113 /** Not used yet. */ 114 private static final String BANNER = InteractiveConsole.getDefaultBanner(); 115 116 /** All text will appear in this font. */ 117 private static final Font FONT = new Font("Monospaced", Font.PLAIN, 14); 118 119 /** Jython statements entered by the user. */ 120 // TODO(jon): consider implementing a limit to the number of lines stored? 121 private final List<String> jythonHistory; 122 123 /** Thread that handles Jython command execution. */ 124 private Runner jythonRunner; 125 126 /** A hook that allows external classes to respond to events. */ 127 private ConsoleCallback callback; 128 129 /** Where the user interacts with the Jython interpreter. */ 130 private JTextPane textPane; 131 132 /** {@link #textPane}'s internal representation. */ 133 private Document document; 134 135 /** Panel that holds {@link #textPane}. */ 136 private JPanel panel; 137 138 /** Title of the console window. */ 139 private String windowTitle = "Super Happy Jython Fun Console "+hashCode(); 140 141 private MenuWrangler menuWrangler; 142 143 /** 144 * Build a console with no initial commands. 145 */ 146 public Console() { 147 this(Collections.<String>emptyList()); 148 } 149 150 /** 151 * Builds a console and executes a list of Jython statements. It's been 152 * useful for dirty tricks needed during setup. 153 * 154 * @param initialCommands Jython statements to execute. 155 */ 156 public Console(final List<String> initialCommands) { 157 notNull(initialCommands, "List of initial commands cannot be null"); 158 jythonHistory = new ArrayList<String>(); 159 jythonRunner = new Runner(this, initialCommands); 160 jythonRunner.start(); 161 // err, shouldn't the gui stuff be done explicitly in the EDT!? 162 menuWrangler = new DefaultMenuWrangler(this); 163 panel = new JPanel(new BorderLayout()); 164 textPane = new JTextPane() { 165 @Override public void paste() { 166 167 super.paste(); 168 } 169 }; 170 document = textPane.getDocument(); 171 panel.add(BorderLayout.CENTER, new JScrollPane(textPane)); 172 setCallbackHandler(new DummyCallbackHandler()); 173 try { 174 showBanner(); 175 document.createPosition(document.getLength() - 1); 176 } catch (BadLocationException e) { 177 logger.error("had difficulties setting up the console msg", e); 178 } 179 180 new EndAction(this, Actions.END); 181 new EnterAction(this, Actions.ENTER); 182 new DeleteAction(this, Actions.DELETE); 183 new HomeAction(this, Actions.HOME); 184 new TabAction(this, Actions.TAB); 185 new PasteAction(this, Actions.PASTE); 186 // new UpAction(this, Actions.UP); 187 // new DownAction(this, Actions.DOWN); 188 189 JTextComponent.addKeymap("jython", textPane.getKeymap()); 190 191 textPane.setFont(FONT); 192 textPane.addKeyListener(this); 193 textPane.addMouseListener(new PopupListener()); 194 } 195 196 /** 197 * Returns the panel containing the various UI components. 198 */ 199 public JPanel getPanel() { 200 return panel; 201 } 202 203 /** 204 * Returns the {@link JTextPane} used by the console. 205 */ 206 protected JTextPane getTextPane() { 207 return textPane; 208 } 209 210 /** 211 * Inserts the specified object into Jython's local namespace using the 212 * specified name. 213 * 214 * <p><b>Example:</b><br/> 215 * {@code console.injectObject("test", new PyJavaInstance("a test"))}<br/> 216 * Allows the interpreter to refer to the {@link String} {@code "a test"} 217 * as {@code test}. 218 * 219 * @param name Object name as it will appear within the interpreter. 220 * @param object Object to place in the interpreter's local namespace. 221 */ 222 // public void injectObject(final String name, final PyObject pyObject) { 223 // jythonRunner.queueObject(name, pyObject); 224 // } 225 public void injectObject(final String name, final Object object) { 226 jythonRunner.queueObject(name, object); 227 } 228 229 public void ejectObjectByName(final String name) { 230 jythonRunner.queueRemoval(name); 231 } 232 233 // TODO(jon): may not need this one. 234 public void ejectObject(final PyObject pyObject) { 235 Map<String, PyObject> locals = getLocalNamespace(); 236 for (Map.Entry<String, PyObject> entry : locals.entrySet()) { 237 if (pyObject == entry.getValue()) { 238 jythonRunner.queueRemoval(entry.getKey()); 239 } 240 } 241 } 242 243 /** 244 * Runs the file specified by {@code path} in the {@link Interpreter}. 245 * 246 * @param name {@code __name__} attribute to use for loading {@code path}. 247 * @param path The path to the Jython file. 248 */ 249 public void runFile(final String name, final String path) { 250 jythonRunner.queueFile(name, path); 251 } 252 253 /** 254 * Displays non-error output. 255 * 256 * @param text The message to display. 257 */ 258 public void result(final String text) { 259 insert(TXT_GOOD, '\n'+text); 260 } 261 262 /** 263 * Displays an error. 264 * 265 * @param text The error message. 266 */ 267 public void error(final String text) { 268 if (getLineText(getLineCount()-1).trim().length() > 0) { 269 endln(TXT_ERROR); 270 } 271 insert(TXT_ERROR, '\n'+text); 272 } 273 274 /** 275 * Shows the normal Jython prompt. 276 */ 277 public void prompt() { 278 if (getLineText(getLineCount()-1).trim().length() > 0) { 279 endln(TXT_NORMAL); 280 } 281 insert(TXT_NORMAL, PS1); 282 } 283 284 /** 285 * Displays non-error output that was not the result of an 286 * {@literal "associated"} {@link Command}. 287 * 288 * @param text The text to display. 289 * @see #generatedError(String) 290 */ 291 public void generatedOutput(final String text) { 292 if (getPromptLength(getLineText(getLineCount()-1)) > 0) { 293 endln(TXT_GOOD); 294 } 295 insert(TXT_GOOD, text); 296 } 297 298 /** 299 * Displays error output. Differs from {@link #error(String)} in that this 300 * is intended for output not {@literal "associated"} with a {@link Command}. 301 * 302 * <p>Example: say you fire off a background thread. If it generates an 303 * error somehow, this is the method you want. 304 * 305 * @param text The error message. 306 */ 307 public void generatedError(final String text) { 308 if (getPromptLength(getLineText(getLineCount()-1)) > 0) { 309 insert(TXT_ERROR, '\n'+text); 310 } else { 311 insert(TXT_ERROR, text); 312 } 313 } 314 315 /** 316 * Shows the prompt that indicates more input is needed. 317 */ 318 public void moreInput() { 319 insert(TXT_NORMAL, '\n'+PS2); 320 } 321 322 public void moreInput(final int blockLevel) { 323 324 } 325 326 /** 327 * Will eventually display an initial greeting to the user. 328 * 329 * @throws BadLocationException Upon attempting to clear out an invalid 330 * portion of the document. 331 */ 332 private void showBanner() throws BadLocationException { 333 document.remove(0, document.getLength()); 334 prompt(); 335 textPane.requestFocus(); 336 } 337 338 /** 339 * Inserts a newline character at the end of the input. 340 * 341 * @param color Perhaps this should go!? 342 */ 343 protected void endln(final Color color) { 344 insert(color, "\n"); 345 } 346 347 /** 348 * Does the actual work of displaying color-coded messages in 349 * {@link #textPane}. 350 * 351 * @param color The color of the message. 352 * @param text The actual message. 353 */ 354 protected void insert(final Color color, final String text) { 355 SimpleAttributeSet style = new SimpleAttributeSet(); 356 style.addAttribute(StyleConstants.Foreground, color); 357 try { 358 document.insertString(document.getLength(), text, style); 359 textPane.setCaretPosition(document.getLength()); 360 } catch (BadLocationException e) { 361 logger.error("bad location", e); 362 } 363 } 364 365 protected void insertAtCaret(final Color color, final String text) { 366 assert color != null : color; 367 assert text != null : text; 368 369 int position = textPane.getCaretPosition(); 370 if (!canInsertAt(position)) { 371 return; 372 } 373 374 SimpleAttributeSet style = new SimpleAttributeSet(); 375 style.addAttribute(StyleConstants.Foreground, color); 376 377 try { 378 document.insertString(position, text, style); 379 } catch (BadLocationException e) { 380 logger.trace("position={}", position); 381 logger.error("couldn't insert text", e); 382 } 383 } 384 385 /** 386 * Determines whether or not {@code position} is an acceptable place to 387 * insert text. Currently the criteria for {@literal "acceptable"} means 388 * that {@code position} is located within the last (or active) line, and 389 * not within either {@link #PS1} or {@link #PS2}. 390 * 391 * @param position Position to test. Values less than zero are not allowed. 392 * 393 * @return Whether or not text can be inserted at {@code position}. 394 */ 395 private boolean canInsertAt(final int position) { 396 assert position >= 0; 397 398 if (!onLastLine()) { 399 return false; 400 } 401 402 int lineNumber = getCaretLine(); 403 String currentLine = getLineText(lineNumber); 404 int[] offsets = getLineOffsets(lineNumber); 405 logger.debug("position={} offsets[0]={} promptLen={}", new Object[] { position, offsets[0], getPromptLength(currentLine)}); 406 return ((position - offsets[0]) >= getPromptLength(currentLine)); 407 } 408 409 /** 410 * @return Number of lines in the document. 411 */ 412 public int getLineCount() { 413 return document.getRootElements()[0].getElementCount(); 414 } 415 416 // TODO(jon): Rethink some of these methods names, especially getLineOffsets and getOffsetLine!! 417 418 public int getLineOffsetStart(final int lineNumber) { 419 return document.getRootElements()[0].getElement(lineNumber).getStartOffset(); 420 } 421 422 public int getLineOffsetEnd(final int lineNumber) { 423 return document.getRootElements()[0].getElement(lineNumber).getEndOffset(); 424 } 425 426 public int[] getLineOffsets(final int lineNumber) { 427 if (lineNumber >= getLineCount()) { 428 return BAD_OFFSETS; 429 } 430 // TODO(jon): possible inline these calls? 431 int start = getLineOffsetStart(lineNumber); 432 int end = getLineOffsetEnd(lineNumber); 433 return new int[] { start, end }; 434 } 435 436 /** 437 * Returns the line number that contains the specified offset. 438 * 439 * @param offset Offset whose line number you want. 440 * 441 * @return Line number. 442 */ 443 public int getOffsetLine(final int offset) { 444 return document.getRootElements()[0].getElementIndex(offset); 445 } 446 447 /** 448 * Returns the offsets of the beginning and end of the last line. 449 */ 450 private int[] locateLastLine() { 451 return getLineOffsets(getLineCount() - 1); 452 } 453 454 /** 455 * Determines whether or not the caret is on the last line. 456 */ 457 private boolean onLastLine() { 458 int[] offsets = locateLastLine(); 459 int position = textPane.getCaretPosition(); 460 return (position >= offsets[0] && position <= offsets[1]); 461 } 462 463 /** 464 * @return The line number of the caret's offset within the text. 465 */ 466 public int getCaretLine() { 467 return getOffsetLine(textPane.getCaretPosition()); 468 } 469 470 /** 471 * Returns the line of text that occupies the specified line number. 472 * 473 * @param lineNumber Line number whose text is to be returned. 474 * 475 * @return Either the line of text or null if there was an error. 476 */ 477 public String getLineText(final int lineNumber) { 478 int start = getLineOffsetStart(lineNumber); 479 int stop = getLineOffsetEnd(lineNumber); 480 String line = null; 481 try { 482 line = document.getText(start, stop - start); 483 } catch (BadLocationException e) { 484 e.printStackTrace(); 485 } 486 return line; 487 } 488 489 /** 490 * Returns the line of Jython that occupies a specified line number. 491 * This is different than {@link #getLineText(int)} in that both 492 * {@link #PS1} and {@link #PS2} are removed from the returned line. 493 * 494 * @param lineNumber Line number whose text is to be returned. 495 * 496 * @return Either the line of Jython or null if there was an error. 497 */ 498 public String getLineJython(final int lineNumber) { 499 String text = getLineText(lineNumber); 500 if (text == null) { 501 return null; 502 } 503 int start = getPromptLength(text); 504 return text.substring(start, text.length() - 1); 505 } 506 507 /** 508 * Returns the length of {@link #PS1} or {@link #PS2} depending on the 509 * contents of the specified line. 510 * 511 * @param line The line in question. Cannot be {@code null}. 512 * 513 * @return Either the prompt length or zero if there was none. 514 * 515 * @throws NullPointerException if {@code line} is {@code null}. 516 */ 517 public static int getPromptLength(final String line) { 518 notNull(line, "Null lines do not have prompt lengths"); 519 if (line.startsWith(PS1)) { 520 return PS1.length(); 521 } else if (line.startsWith(PS2)) { 522 return PS2.length(); 523 } else { 524 return 0; 525 } 526 } 527 528 /** 529 * Returns the {@literal "block depth"} of a given line of Jython. 530 * 531 * <p>Examples:<pre> 532 * "print 'x'" -> 0 533 * " print 'x'" -> 1 534 * " die()" -> 3 535 * </pre> 536 * 537 * @param line Line to test. Can't be {@code null}. 538 * @param whitespace The indent {@link String} used with {@code line}. Can't be {@code null}. 539 * 540 * @return Either the block depth ({@code >= 0}) or {@code -1} if there was an error. 541 */ 542 // TODO(jon): maybe need to explicitly use getLineJython? 543 public static int getBlockDepth(final String line, final String whitespace) { 544 int indent = whitespace.length(); 545 int blockDepth = 0; 546 int tmpIndex = 0; 547 while ((tmpIndex+indent) < line.length()) { 548 int stop = tmpIndex + indent; 549 if (line.substring(tmpIndex, stop).trim().length() != 0) { 550 break; 551 } 552 tmpIndex += indent; 553 blockDepth++; 554 } 555 return blockDepth; 556 } 557 558 /** 559 * Registers a new callback handler with the console. Note that to maximize 560 * utility, this method also registers the same handler with 561 * {@link #jythonRunner}. 562 * 563 * @param newHandler The new callback handler. 564 * 565 * @throws NullPointerException if the new handler is null. 566 */ 567 public void setCallbackHandler(final ConsoleCallback newHandler) { 568 notNull(newHandler, "Callback handler cannot be null"); 569 jythonRunner.setCallbackHandler(newHandler); 570 } 571 572 public Set<String> getJythonReferencesTo(final Object obj) { 573 notNull(obj, "Cannot find references to a null object"); 574 Set<String> refs = new TreeSet<String>(); 575 // TODO(jon): possibly inline getJavaInstances()? 576 for (Map.Entry<String, Object> entry : getJavaInstances().entrySet()) { 577 if (obj == entry.getValue()) { 578 refs.add(entry.getKey()); 579 } 580 } 581 return refs; 582 } 583 584 /** 585 * Returns a subset of Jython's local namespace containing only variables 586 * that are {@literal "pure"} Java objects. 587 * 588 * @return Jython variable names mapped to their Java instantiation. 589 */ 590 public Map<String, Object> getJavaInstances() { 591 Map<String, Object> javaMap = new HashMap<String, Object>(); 592 Map<String, PyObject> locals = getLocalNamespace(); 593 for (Map.Entry<String, PyObject> entry : locals.entrySet()) { 594 PyObject val = entry.getValue(); 595 if (val instanceof PyObjectDerived) { 596 PyObjectDerived derived = (PyObjectDerived)val; 597 if (derived.getType() instanceof PyJavaType) { 598 javaMap.put(entry.getKey(), val.__tojava__(Object.class)); 599 } 600 } 601 } 602 return javaMap; 603 } 604 605 /** 606 * Retrieves the specified Jython variable from the interpreters local 607 * namespace. 608 * 609 * @param var Variable name to retrieve. 610 * @return Either the variable or null. Note that null will also be 611 * returned if {@link Runner#copyLocals()} returned null. 612 */ 613 public PyObject getJythonObject(final String var) { 614 PyStringMap locals = jythonRunner.copyLocals(); 615 if (locals == null) { 616 return null; 617 } 618 return locals.__finditem__(var); 619 } 620 621 /** 622 * Returns a copy of Jython's local namespace. 623 * 624 * @return Jython variable names mapped to {@link PyObject}s. 625 */ 626 public Map<String, PyObject> getLocalNamespace() { 627 Map<String, PyObject> localsMap = new HashMap<String, PyObject>(); 628 PyStringMap jythonLocals = jythonRunner.copyLocals(); 629 if (jythonLocals != null) { 630 PyList items = jythonLocals.items(); 631 for (int i = 0; i < items.__len__(); i++) { 632 PyTuple tuple = (PyTuple)items.__finditem__(i); 633 String key = ((PyString)tuple.__finditem__(0)).toString(); 634 PyObject val = tuple.__finditem__(1); 635 localsMap.put(key, val); 636 } 637 } 638 return localsMap; 639 } 640 641 public void handlePaste() { 642 logger.trace("not terribly sure..."); 643 getTextPane().paste(); 644 logger.trace("after forcing paste!"); 645 } 646 647 /** 648 * Handles the user hitting the {@code Home} key. If the caret is on a line 649 * that begins with either {@link #PS1} or {@link #PS2}, the caret will be 650 * moved to just after the prompt. This is done mostly to emulate CPython's 651 * IDLE. 652 */ 653 public void handleHome() { 654 int caretPosition = getCaretLine(); 655 int[] offsets = getLineOffsets(caretPosition); 656 int linePosition = getPromptLength(getLineText(caretPosition)); 657 textPane.setCaretPosition(offsets[0] + linePosition); 658 } 659 660 /** 661 * Moves the caret to the end of the line it is currently on, rather than 662 * the end of the document. 663 */ 664 public void handleEnd() { 665 int[] offsets = getLineOffsets(getCaretLine()); 666 textPane.setCaretPosition(offsets[1] - 1); 667 } 668 669 public void handleUp() { 670 logger.trace("handleUp"); 671 } 672 673 public void handleDown() { 674 logger.trace("handleDown"); 675 } 676 677 /** 678 * Inserts the contents of {@link #WHITESPACE} wherever the cursor is 679 * located. 680 */ 681 // TODO(jon): completion! 682 public void handleTab() { 683 logger.trace("handling tab!"); 684 insertAtCaret(TXT_NORMAL, WHITESPACE); 685 } 686 687 // TODO(jon): what about selected regions? 688 // TODO(jon): what about multi lines? 689 public void handleDelete() { 690 if (!onLastLine()) { 691 return; 692 } 693 694 String line = getLineText(getCaretLine()); 695 if (line == null) { 696 return; 697 } 698 699 int position = textPane.getCaretPosition(); 700 int start = getPromptLength(line); 701 702 // don't let the user delete parts of PS1 or PS2 703 int lineStart = getLineOffsetStart(getCaretLine()); 704 if (((position-1)-lineStart) < start) { 705 return; 706 } 707 708 try { 709 document.remove(position - 1, 1); 710 } catch (BadLocationException e) { 711 logger.error("failed to backspace at position={}", (position-1)); 712 } 713 } 714 715 /** 716 * Handles the user pressing enter by basically grabbing the line of jython 717 * under the caret. If the caret is on the last line, the line is queued 718 * for execution. Otherwise the line is reinserted at the end of the 719 * document--this lets the user preview a previous command before they 720 * rerun it. 721 */ 722 // TODO(jon): if you hit enter at the start of a block, maybe it should 723 // replicate the enter block at the end of the document? 724 public void handleEnter() { 725 String line = getLineJython(getCaretLine()); 726 if (line == null) { 727 line = ""; 728 } 729 730 if (onLastLine()) { 731 queueLine(line); 732 } else { 733 insert(TXT_NORMAL, line); 734 } 735 } 736 737 /** 738 * Returns the Jython statements as entered by the user, ordered from first 739 * to last. 740 * 741 * @return User's history. 742 */ 743 public List<String> getHistory() { 744 return new ArrayList<String>(jythonHistory); 745 } 746 747 /** 748 * Sends a line of Jython to the interpreter via {@link #jythonRunner} and 749 * saves it to the history. 750 * 751 * @param line Jython to queue for execution. 752 */ 753 public void queueLine(final String line) { 754 jythonRunner.queueLine(line); 755 jythonHistory.add(line); 756 } 757 758 /** 759 * Sends a batch of Jython commands to the interpreter. <i>This is 760 * different than simply calling {@link #queueLine(String)} for each 761 * command;</i> the interpreter will attempt to execute each batched 762 * command before returning {@literal "control"} to the console. 763 * 764 * <p>This method is mostly useful for restoring Console sessions. Each 765 * command in {@code commands} will appear in the console as though the 766 * user typed it. The batch of commands will also be saved to the history. 767 * 768 * @param name Identifier for the batch. Doesn't need to be unique, merely 769 * non-null. 770 * @param commands The commands to execute. 771 */ 772 public void queueBatch(final String name, final List<String> commands) { 773 // jythonRunner.queueBatch(this, name, commands); 774 jythonRunner.queueBatch(name, commands); 775 jythonHistory.addAll(commands); 776 } 777 778 public void addPretendHistory(final String line) { 779 jythonHistory.add(line); 780 } 781 782 /** 783 * Puts together the GUI once EventQueue has processed all other pending 784 * events. 785 */ 786 public void run() { 787 JFrame frame = new JFrame(windowTitle); 788 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 789 frame.getContentPane().add(getPanel()); 790 frame.getContentPane().setPreferredSize(new Dimension(600, 200)); 791 frame.pack(); 792 frame.setVisible(true); 793 } 794 795 /** 796 * Noop. 797 */ 798 public void keyPressed(final KeyEvent e) { } 799 800 /** 801 * Noop. 802 */ 803 public void keyReleased(final KeyEvent e) { } 804 805 // this is weird: hasAction is always false 806 // seems to work so long as the ConsoleActions fire first... 807 // might want to look at default actions again 808 public void keyTyped(final KeyEvent e) { 809 logger.trace("hasAction={} key={}", hasAction(textPane, e), e.getKeyChar()); 810 int caretPosition = textPane.getCaretPosition(); 811 if (!hasAction(textPane, e) && !canInsertAt(caretPosition)) { 812 logger.trace("hasAction={} lastLine={}", hasAction(textPane, e), onLastLine()); 813 e.consume(); 814 } 815 } 816 817 private static boolean hasAction(final JTextPane jtp, final KeyEvent e) { 818 assert jtp != null; 819 assert e != null; 820 KeyStroke stroke = 821 KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()); 822 return (jtp.getKeymap().getAction(stroke) != null); 823 } 824 825 /** 826 * Maps a {@literal "jython action"} to a keystroke. 827 */ 828 public enum Actions { 829 TAB("jython.tab", KeyEvent.VK_TAB, 0), 830 DELETE("jython.delete", KeyEvent.VK_BACK_SPACE, 0), 831 END("jython.end", KeyEvent.VK_END, 0), 832 ENTER("jython.enter", KeyEvent.VK_ENTER, 0), 833 HOME("jython.home", KeyEvent.VK_HOME, 0), 834 PASTE("jython.paste", KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), 835 // PASTE("jython.paste", KeyEvent.VK_V, KeyEvent.CTRL_MASK); 836 // UP("jython.up", KeyEvent.VK_UP, 0), 837 // DOWN("jython.down", KeyEvent.VK_DOWN, 0); 838 ; 839 840 private final String id; 841 private final int keyCode; 842 private final int modifier; 843 844 Actions(final String id, final int keyCode, final int modifier) { 845 this.id = id; 846 this.keyCode = keyCode; 847 this.modifier = modifier; 848 } 849 850 public String getId() { 851 return id; 852 } 853 854 public KeyStroke getKeyStroke() { 855 return KeyStroke.getKeyStroke(keyCode, modifier); 856 } 857 } 858 859 public static class HistoryEntry { 860 private HistoryType type; 861 private String entry; 862 863 public HistoryEntry() {} 864 865 public HistoryEntry(final HistoryType type, final String entry) { 866 this.type = notNull(type, "type cannot be null"); 867 this.entry = notNull(entry, "entry cannot be null"); 868 } 869 870 public void setEntry(final String entry) { 871 this.entry = notNull(entry, "entry cannot be null"); 872 } 873 874 public void setType(final HistoryType type) { 875 this.type = notNull(type, "type cannot be null"); 876 } 877 878 public String getEntry() { 879 return entry; 880 } 881 882 public HistoryType getType() { 883 return type; 884 } 885 886 @Override public String toString() { 887 return String.format("[HistoryEntry@%x: type=%s, entry=\"%s\"]", 888 hashCode(), type, entry); 889 } 890 } 891 892 private class PopupListener extends MouseAdapter { 893 public void mouseClicked(final MouseEvent e) { 894 checkPopup(e); 895 } 896 897 public void mousePressed(final MouseEvent e) { 898 checkPopup(e); 899 } 900 901 public void mouseReleased(final MouseEvent e) { 902 checkPopup(e); 903 } 904 905 private void checkPopup(final MouseEvent e) { 906 if (!e.isPopupTrigger()) { 907 return; 908 } 909 JPopupMenu popup = menuWrangler.buildMenu(); 910 popup.show(textPane, e.getX(), e.getY()); 911 } 912 } 913 914 public static String getUserPath(String[] args) { 915 for (int i = 0; i < args.length; i++) { 916 if ("-userpath".equals(args[i]) && (i+1) < args.length) { 917 return args[i+1]; 918 } 919 } 920 return System.getProperty("user.home"); 921 } 922 923 public static void main(String[] args) { 924 String os = System.getProperty("os.name"); 925 String sep = "/"; 926 if (os.startsWith("Windows")) { 927 sep = "\\"; 928 } 929 String pythonHome = getUserPath(args); 930 931 Properties systemProperties = System.getProperties(); 932 Properties jythonProperties = new Properties(); 933 jythonProperties.setProperty("python.home", pythonHome+sep+"jython"); 934 Interpreter.initialize(systemProperties, jythonProperties, new String[]{""}); 935 EventQueue.invokeLater(new Console()); 936 EventQueue.invokeLater(new Console()); 937 } 938 }