run halt hidden, help moved to menu item for stupid mac users
[jalview.git] / src / jalview / gui / ScriptWindow.java
1 /* $RCSfile$\r
2  * $Author$\r
3  * $Date$\r
4  * $Revision$\r
5  *\r
6  * Copyright (C) 2002-2005  The Jmol Development Team\r
7  *\r
8  * Contact: jmol-developers@lists.sf.net\r
9  *\r
10  *  This library is free software; you can redistribute it and/or\r
11  *  modify it under the terms of the GNU Lesser General Public\r
12  *  License as published by the Free Software Foundation; either\r
13  *  version 2.1 of the License, or (at your option) any later version.\r
14  *\r
15  *  This library is distributed in the hope that it will be useful,\r
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of\r
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
18  *  Lesser General Public License for more details.\r
19  *\r
20  *  You should have received a copy of the GNU Lesser General Public\r
21  *  License along with this library; if not, write to the Free Software\r
22  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.\r
23 \r
24  *\r
25  * Modified and added to Jalview by A Waterhouse to extend JInternalFrame\r
26  *\r
27 \r
28  */\r
29 package jalview.gui;\r
30 \r
31 import org.jmol.api.*;\r
32 \r
33 import java.awt.*;\r
34 import java.awt.event.*;\r
35 import javax.swing.*;\r
36 import javax.swing.text.*;\r
37 import java.util.Vector;\r
38 \r
39 import org.jmol.i18n.GT;\r
40 import org.jmol.util.Logger;\r
41 import org.jmol.util.CommandHistory;\r
42 \r
43 public final class ScriptWindow extends JPanel\r
44     implements ActionListener, EnterListener{\r
45 \r
46   private ConsoleTextPane console;\r
47   private JButton closeButton;\r
48   private JButton runButton;\r
49   private JButton haltButton;\r
50   private JButton clearButton;\r
51   private JButton historyButton;\r
52   private JButton stateButton;\r
53    JmolViewer viewer;\r
54   AppJMol appJmol;\r
55 \r
56   public ScriptWindow(AppJMol appJmol)\r
57   {\r
58     this.viewer = appJmol.viewer;\r
59     this.appJmol = appJmol;\r
60 \r
61 \r
62     setLayout(new BorderLayout());\r
63 \r
64     console = new ConsoleTextPane(this);\r
65 \r
66 \r
67     console.setPrompt();\r
68     add(new JScrollPane(console)\r
69                          , BorderLayout.CENTER);\r
70 \r
71     JPanel buttonPanel = new JPanel();\r
72     add(buttonPanel, BorderLayout.SOUTH);\r
73 \r
74     runButton = new JButton(GT._("Run"));\r
75     haltButton = new JButton(GT._("Halt"));\r
76     /*\r
77     runButton.addActionListener(this);\r
78     buttonPanel.add(runButton);\r
79     haltButton.addActionListener(this);\r
80     buttonPanel.add(haltButton);\r
81     haltButton.setEnabled(false);*/\r
82 \r
83     clearButton = new JButton(GT._("Clear"));\r
84     clearButton.addActionListener(this);\r
85     buttonPanel.add(clearButton);\r
86 \r
87     historyButton = new JButton(GT._("History"));\r
88     historyButton.addActionListener(this);\r
89     buttonPanel.add(historyButton);\r
90 \r
91     stateButton = new JButton(GT._("State"));\r
92     stateButton.addActionListener(this);\r
93     buttonPanel.add(stateButton);\r
94 \r
95     closeButton = new JButton(GT._("Close"));\r
96     closeButton.addActionListener(this);\r
97     buttonPanel.add(closeButton);\r
98 \r
99     for(int i=0; i<buttonPanel.getComponentCount(); i++)\r
100     {\r
101     //  ((JButton)buttonPanel.getComponent(i))\r
102     //      .setMargin(new Insets(0, 0, 0, 0));\r
103     }\r
104 \r
105   }\r
106 \r
107   public void sendConsoleEcho(String strEcho) {\r
108     if (strEcho != null && !isError) {\r
109 \r
110       console.outputEcho(strEcho);\r
111 \r
112     }\r
113     setError(false);\r
114   }\r
115 \r
116   boolean isError = false;\r
117   void setError(boolean TF) {\r
118     isError = TF;\r
119     //if (isError)\r
120       //console.recallCommand(true);\r
121   }\r
122 \r
123   public void sendConsoleMessage(String strStatus) {\r
124     if (strStatus == null) {\r
125       console.clearContent();\r
126       console.outputStatus("");\r
127     } else if (strStatus.indexOf("ERROR:") >= 0) {\r
128       console.outputError(strStatus);\r
129       isError = true;\r
130     } else if (!isError) {\r
131       console.outputStatus(strStatus);\r
132     }\r
133   }\r
134 \r
135   public void notifyScriptTermination(String strMsg, int msWalltime) {\r
136     if (strMsg != null && strMsg.indexOf("ERROR") >= 0) {\r
137       console.outputError(strMsg);\r
138     }\r
139     runButton.setEnabled(true);\r
140     haltButton.setEnabled(false);\r
141   }\r
142 \r
143   public void enterPressed() {\r
144     runButton.doClick(100);\r
145     //    executeCommand();\r
146   }\r
147 \r
148 \r
149   class ExecuteCommandThread extends Thread {\r
150 \r
151     String strCommand;\r
152     ExecuteCommandThread (String command) {\r
153       strCommand = command;\r
154     }\r
155 \r
156     public void run() {\r
157       try {\r
158         executeCommand(strCommand);\r
159       } catch (Exception ie) {\r
160         Logger.debug("execution command interrupted!"+ie);\r
161       }\r
162     }\r
163   }\r
164 \r
165   ExecuteCommandThread execThread;\r
166   void executeCommandAsThread(){\r
167     String strCommand = console.getCommandString().trim();\r
168     if (strCommand.length() > 0) {\r
169       execThread = new ExecuteCommandThread(strCommand);\r
170       execThread.start();\r
171     }\r
172   }\r
173 \r
174   void executeCommand(String strCommand) {\r
175     boolean doWait;\r
176     setError(false);\r
177     console.appendNewline();\r
178     console.setPrompt();\r
179     if (strCommand.length() > 0) {\r
180       String strErrorMessage = null;\r
181       doWait = (strCommand.indexOf("WAIT ") == 0);\r
182       if (doWait) { //for testing, mainly\r
183         // demonstrates using the statusManager system.\r
184         runButton.setEnabled(false);\r
185         haltButton.setEnabled(true);\r
186 \r
187         Vector info = (Vector) viewer\r
188             .scriptWaitStatus(strCommand.substring(5),\r
189                 "+fileLoaded,+scriptStarted,+scriptStatus,+scriptEcho,+scriptTerminated");\r
190         runButton.setEnabled(true);\r
191         haltButton.setEnabled(false);\r
192         /*\r
193          * info = [ statusRecortSet0, statusRecortSet1, statusRecortSet2, ...]\r
194          * statusRecordSet = [ statusRecord0, statusRecord1, statusRecord2, ...]\r
195          * statusRecord = [int msgPtr, String statusName, int intInfo, String msg]\r
196          */\r
197         for (int i = 0; i < info.size(); i++) {\r
198           Vector statusRecordSet = (Vector) info.get(i);\r
199           for (int j = 0; j < statusRecordSet.size(); j++) {\r
200             Vector statusRecord = (Vector) statusRecordSet.get(j);\r
201             Logger.info("msg#=" + statusRecord.get(0) + " "\r
202                 + statusRecord.get(1) + " intInfo=" + statusRecord.get(2)\r
203                 + " stringInfo=" + statusRecord.get(3));\r
204           }\r
205         }\r
206         console.appendNewline();\r
207       } else {\r
208         boolean isScriptExecuting = viewer.isScriptExecuting();\r
209         if (viewer.checkHalt(strCommand))\r
210           strErrorMessage = (isScriptExecuting ? "string execution halted with " + strCommand : "no script was executing");\r
211         else\r
212           strErrorMessage = "";//viewer.scriptCheck(strCommand);\r
213         //the problem is that scriptCheck is synchronized, so these might get backed up.\r
214         if (strErrorMessage != null && strErrorMessage.length() > 0) {\r
215           console.outputError(strErrorMessage);\r
216         } else {\r
217           //runButton.setEnabled(false);\r
218           haltButton.setEnabled(true);\r
219           viewer.script(strCommand);\r
220         }\r
221       }\r
222     }\r
223     console.grabFocus();\r
224   }\r
225 \r
226   public void actionPerformed(ActionEvent e) {\r
227     Object source = e.getSource();\r
228     if (source == closeButton) {\r
229       appJmol.showConsole(false);\r
230     } else if (source == runButton) {\r
231       executeCommandAsThread();\r
232     } else if (source == clearButton) {\r
233       console.clearContent();\r
234     } else if (source == historyButton) {\r
235       console.clearContent(viewer.getSetHistory(Integer.MAX_VALUE));\r
236     } else if (source == stateButton) {\r
237       console.clearContent(viewer.getStateInfo());\r
238     } else if (source == haltButton) {\r
239       viewer.haltScriptExecution();\r
240     }\r
241     console.grabFocus(); // always grab the focus (e.g., after clear)\r
242   }\r
243 }\r
244 \r
245 class ConsoleTextPane extends JTextPane {\r
246 \r
247   ConsoleDocument consoleDoc;\r
248   EnterListener enterListener;\r
249   JmolViewer viewer;\r
250 \r
251   ConsoleTextPane(ScriptWindow scriptWindow) {\r
252     super(new ConsoleDocument());\r
253     consoleDoc = (ConsoleDocument)getDocument();\r
254     consoleDoc.setConsoleTextPane(this);\r
255     this.enterListener = (EnterListener) scriptWindow;\r
256     this.viewer = scriptWindow.viewer;\r
257   }\r
258 \r
259   public String getCommandString() {\r
260     String cmd = consoleDoc.getCommandString();\r
261     return cmd;\r
262   }\r
263 \r
264   public void setPrompt() {\r
265     consoleDoc.setPrompt();\r
266   }\r
267 \r
268   public void appendNewline() {\r
269     consoleDoc.appendNewline();\r
270   }\r
271 \r
272   public void outputError(String strError) {\r
273     consoleDoc.outputError(strError);\r
274   }\r
275 \r
276   public void outputErrorForeground(String strError) {\r
277     consoleDoc.outputErrorForeground(strError);\r
278   }\r
279 \r
280   public void outputEcho(String strEcho) {\r
281     consoleDoc.outputEcho(strEcho);\r
282   }\r
283 \r
284   public void outputStatus(String strStatus) {\r
285     consoleDoc.outputStatus(strStatus);\r
286   }\r
287 \r
288   public void enterPressed() {\r
289     if (enterListener != null)\r
290       enterListener.enterPressed();\r
291   }\r
292 \r
293   public void clearContent() {\r
294     clearContent(null);\r
295   }\r
296   public void clearContent(String text) {\r
297     consoleDoc.clearContent();\r
298     if (text != null)\r
299       consoleDoc.outputEcho(text);\r
300     setPrompt();\r
301   }\r
302 \r
303    /* (non-Javadoc)\r
304     * @see java.awt.Component#processKeyEvent(java.awt.event.KeyEvent)\r
305     */\r
306 \r
307    /**\r
308     * Custom key event processing for command 0 implementation.\r
309     *\r
310     * Captures key up and key down strokes to call command history\r
311     * and redefines the same events with control down to allow\r
312     * caret vertical shift.\r
313     *\r
314     * @see java.awt.Component#processKeyEvent(java.awt.event.KeyEvent)\r
315     */\r
316    protected void processKeyEvent(KeyEvent ke)\r
317    {\r
318       // Id Control key is down, captures events does command\r
319       // history recall and inhibits caret vertical shift.\r
320       if (ke.getKeyCode() == KeyEvent.VK_UP\r
321          && ke.getID() == KeyEvent.KEY_PRESSED\r
322          && !ke.isControlDown())\r
323       {\r
324          recallCommand(true);\r
325       }\r
326       else if (\r
327          ke.getKeyCode() == KeyEvent.VK_DOWN\r
328             && ke.getID() == KeyEvent.KEY_PRESSED\r
329             && !ke.isControlDown())\r
330       {\r
331          recallCommand(false);\r
332       }\r
333       // If Control key is down, redefines the event as if it\r
334       // where a key up or key down stroke without modifiers.\r
335       // This allows to move the caret up and down\r
336       // with no command history recall.\r
337       else if (\r
338          (ke.getKeyCode() == KeyEvent.VK_DOWN\r
339             || ke.getKeyCode() == KeyEvent.VK_UP)\r
340             && ke.getID() == KeyEvent.KEY_PRESSED\r
341             && ke.isControlDown())\r
342       {\r
343          super\r
344             .processKeyEvent(new KeyEvent(\r
345                (Component) ke.getSource(),\r
346                ke.getID(),\r
347                ke.getWhen(),\r
348                0,         // No modifiers\r
349                ke.getKeyCode(),\r
350                ke.getKeyChar(),\r
351                ke.getKeyLocation()));\r
352       }\r
353       // Standard processing for other events.\r
354       else\r
355       {\r
356          super.processKeyEvent(ke);\r
357          //check command for compiler-identifyable syntax issues\r
358          //this may have to be taken out if people start complaining\r
359          //that only some of the commands are being checked\r
360          //that is -- that the script itself is not being fully checked\r
361 \r
362          //not perfect -- help here?\r
363          if (ke.getID() == KeyEvent.KEY_RELEASED\r
364              && (ke.getKeyCode() > KeyEvent.VK_DOWN) || ke.getKeyCode() == KeyEvent.VK_BACK_SPACE)\r
365            checkCommand();\r
366       }\r
367    }\r
368 \r
369    /**\r
370    * Recall command history.\r
371    *\r
372    * @param up - history up or down\r
373    */\r
374    void recallCommand(boolean up) {\r
375      String cmd = viewer.getSetHistory(up ? -1 : 1);\r
376     if (cmd == null) {\r
377       return;\r
378     }\r
379     try {\r
380       if (cmd.endsWith(CommandHistory.ERROR_FLAG)) {\r
381         cmd = cmd.substring(0, cmd.indexOf(CommandHistory.ERROR_FLAG));\r
382         consoleDoc.replaceCommand(cmd, true);\r
383       } else {\r
384         consoleDoc.replaceCommand(cmd, false);\r
385       }\r
386     } catch (BadLocationException e) {\r
387       e.printStackTrace();\r
388     }\r
389   }\r
390 \r
391    void checkCommand() {\r
392     String strCommand = consoleDoc.getCommandString();\r
393     if (strCommand.length() == 0)\r
394       return;\r
395     consoleDoc\r
396         .colorCommand(viewer.scriptCheck(strCommand) == null ? consoleDoc.attUserInput\r
397             : consoleDoc.attError);\r
398   }\r
399 \r
400 \r
401 }\r
402 \r
403 class ConsoleDocument extends DefaultStyledDocument {\r
404 \r
405   ConsoleTextPane consoleTextPane;\r
406 \r
407   SimpleAttributeSet attError;\r
408   SimpleAttributeSet attEcho;\r
409   SimpleAttributeSet attPrompt;\r
410   SimpleAttributeSet attUserInput;\r
411   SimpleAttributeSet attStatus;\r
412 \r
413   ConsoleDocument() {\r
414     super();\r
415 \r
416     attError = new SimpleAttributeSet();\r
417     StyleConstants.setForeground(attError, Color.red);\r
418 \r
419     attPrompt = new SimpleAttributeSet();\r
420     StyleConstants.setForeground(attPrompt, Color.magenta);\r
421 \r
422     attUserInput = new SimpleAttributeSet();\r
423     StyleConstants.setForeground(attUserInput, Color.black);\r
424 \r
425     attEcho = new SimpleAttributeSet();\r
426     StyleConstants.setForeground(attEcho, Color.blue);\r
427     StyleConstants.setBold(attEcho, true);\r
428 \r
429     attStatus = new SimpleAttributeSet();\r
430     StyleConstants.setForeground(attStatus, Color.black);\r
431     StyleConstants.setItalic(attStatus, true);\r
432   }\r
433 \r
434   void setConsoleTextPane(ConsoleTextPane consoleTextPane) {\r
435     this.consoleTextPane = consoleTextPane;\r
436   }\r
437 \r
438   Position positionBeforePrompt; // starts at 0, so first time isn't tracked (at least on Mac OS X)\r
439   Position positionAfterPrompt;  // immediately after $, so this will track\r
440   int offsetAfterPrompt;         // only still needed for the insertString override and replaceCommand\r
441 \r
442   /**\r
443    * Removes all content of the script window, and add a new prompt.\r
444    */\r
445   void clearContent() {\r
446       try {\r
447           super.remove(0, getLength());\r
448       } catch (BadLocationException exception) {\r
449           System.out.println("Could not clear script window content: " + exception.getMessage());\r
450       }\r
451   }\r
452 \r
453   void setPrompt() {\r
454     try {\r
455       super.insertString(getLength(), "$ ", attPrompt);\r
456       setOffsetPositions();\r
457       consoleTextPane.setCaretPosition(offsetAfterPrompt);\r
458     } catch (BadLocationException e) {\r
459       e.printStackTrace();\r
460     }\r
461   }\r
462 \r
463   void setOffsetPositions() {\r
464     try {\r
465       offsetAfterPrompt = getLength();\r
466       positionBeforePrompt = createPosition(offsetAfterPrompt - 2);\r
467       // after prompt should be immediately after $ otherwise tracks the end\r
468       // of the line (and no command will be found) at least on Mac OS X it did.\r
469       positionAfterPrompt = createPosition(offsetAfterPrompt - 1);\r
470     } catch (BadLocationException e) {\r
471       e.printStackTrace();\r
472     }\r
473   }\r
474 \r
475   void setNoPrompt() {\r
476     try {\r
477       offsetAfterPrompt = getLength();\r
478       positionAfterPrompt = positionBeforePrompt = createPosition(offsetAfterPrompt);\r
479       consoleTextPane.setCaretPosition(offsetAfterPrompt);\r
480     } catch (BadLocationException e) {\r
481       e.printStackTrace();\r
482     }\r
483   }\r
484 \r
485   // it looks like the positionBeforePrompt does not track when it started out as 0\r
486   // and a insertString at location 0 occurs. It may be better to track the\r
487   // position after the prompt in stead\r
488   void outputBeforePrompt(String str, SimpleAttributeSet attribute) {\r
489     try {\r
490       int pt = consoleTextPane.getCaretPosition();\r
491       Position caretPosition = createPosition(pt);\r
492       pt = positionBeforePrompt.getOffset();\r
493       super.insertString(pt, str+"\n", attribute);\r
494       setOffsetPositions();\r
495       pt = caretPosition.getOffset();\r
496       consoleTextPane.setCaretPosition(pt);\r
497     } catch (BadLocationException e) {\r
498       e.printStackTrace();\r
499     }\r
500   }\r
501 \r
502   void outputError(String strError) {\r
503     outputBeforePrompt(strError, attError);\r
504   }\r
505 \r
506   void outputErrorForeground(String strError) {\r
507     try {\r
508       super.insertString(getLength(), strError+"\n", attError);\r
509       consoleTextPane.setCaretPosition(getLength());\r
510     } catch (BadLocationException e) {\r
511       e.printStackTrace();\r
512 \r
513     }\r
514   }\r
515 \r
516   void outputEcho(String strEcho) {\r
517     outputBeforePrompt(strEcho, attEcho);\r
518   }\r
519 \r
520   void outputStatus(String strStatus) {\r
521     outputBeforePrompt(strStatus, attStatus);\r
522   }\r
523 \r
524   void appendNewline() {\r
525     try {\r
526       super.insertString(getLength(), "\n", attUserInput);\r
527       consoleTextPane.setCaretPosition(getLength());\r
528     } catch (BadLocationException e) {\r
529       e.printStackTrace();\r
530     }\r
531   }\r
532 \r
533   // override the insertString to make sure everything typed ends up at the end\r
534   // or in the 'command line' using the proper font, and the newline is processed.\r
535   public void insertString(int offs, String str, AttributeSet a)\r
536     throws BadLocationException {\r
537     int ichNewline = str.indexOf('\n');\r
538     if (ichNewline > 0)\r
539       str = str.substring(0, ichNewline);\r
540     if (ichNewline != 0) {\r
541       if (offs < offsetAfterPrompt) {\r
542         offs = getLength();\r
543       }\r
544       super.insertString(offs, str, a == attError ? a : attUserInput);\r
545       consoleTextPane.setCaretPosition(offs+str.length());\r
546     }\r
547     if (ichNewline >= 0) {\r
548       consoleTextPane.enterPressed();\r
549     }\r
550   }\r
551 \r
552   String getCommandString() {\r
553     String strCommand = "";\r
554     try {\r
555       int cmdStart = positionAfterPrompt.getOffset();\r
556       strCommand =  getText(cmdStart, getLength() - cmdStart);\r
557       while (strCommand.length() > 0 && strCommand.charAt(0) == ' ')\r
558         strCommand = strCommand.substring(1);\r
559     } catch (BadLocationException e) {\r
560       e.printStackTrace();\r
561     }\r
562     return strCommand;\r
563   }\r
564 \r
565   public void remove(int offs, int len)\r
566     throws BadLocationException {\r
567     if (offs < offsetAfterPrompt) {\r
568       len -= offsetAfterPrompt - offs;\r
569       if (len <= 0)\r
570         return;\r
571       offs = offsetAfterPrompt;\r
572     }\r
573     super.remove(offs, len);\r
574 //    consoleTextPane.setCaretPosition(offs);\r
575   }\r
576 \r
577   public void replace(int offs, int length, String str, AttributeSet attrs)\r
578     throws BadLocationException {\r
579     if (offs < offsetAfterPrompt) {\r
580       if (offs + length < offsetAfterPrompt) {\r
581         offs = getLength();\r
582         length = 0;\r
583       } else {\r
584         length -= offsetAfterPrompt - offs;\r
585         offs = offsetAfterPrompt;\r
586       }\r
587     }\r
588     super.replace(offs, length, str, attrs);\r
589 //    consoleTextPane.setCaretPosition(offs + str.length());\r
590   }\r
591 \r
592    /**\r
593    * Replaces current command on script.\r
594    *\r
595    * @param newCommand new command value\r
596    * @param isError    true to set error color  ends with #??\r
597    *\r
598    * @throws BadLocationException\r
599    */\r
600   void replaceCommand(String newCommand, boolean isError) throws BadLocationException {\r
601     if (positionAfterPrompt == positionBeforePrompt)\r
602       return;\r
603     replace(offsetAfterPrompt, getLength() - offsetAfterPrompt, newCommand,\r
604         isError ? attError : attUserInput);\r
605   }\r
606 \r
607   void colorCommand(SimpleAttributeSet att) {\r
608     if (positionAfterPrompt == positionBeforePrompt)\r
609       return;\r
610     setCharacterAttributes(offsetAfterPrompt, getLength() - offsetAfterPrompt, att, true);\r
611   }\r
612 }\r
613 \r
614 interface EnterListener {\r
615   public void enterPressed();\r
616 }\r
617 \r