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 }