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