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