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