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