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}