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    }