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