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