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