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