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