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