update author list in license for (JAL-826)
[jalview.git] / src / jalview / gui / ScriptWindow.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
3  * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, 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 // TODO: this class is copied in from jmol 11.0.2 - upgrade to 12.0.2 ?
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.jmb.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, true))
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
503   // (at least on Mac OS X)
504
505   Position positionAfterPrompt; // immediately after $, so this will track
506
507   int offsetAfterPrompt; // only still needed for the insertString override and
508
509   // replaceCommand
510
511   /**
512    * Removes all content of the script window, and add a new prompt.
513    */
514   void clearContent()
515   {
516     try
517     {
518       super.remove(0, getLength());
519     } catch (BadLocationException exception)
520     {
521       System.out.println("Could not clear script window content: "
522               + exception.getMessage());
523     }
524   }
525
526   void setPrompt()
527   {
528     try
529     {
530       super.insertString(getLength(), "$ ", attPrompt);
531       setOffsetPositions();
532       consoleTextPane.setCaretPosition(offsetAfterPrompt);
533     } catch (BadLocationException e)
534     {
535       e.printStackTrace();
536     }
537   }
538
539   void setOffsetPositions()
540   {
541     try
542     {
543       offsetAfterPrompt = getLength();
544       positionBeforePrompt = createPosition(offsetAfterPrompt - 2);
545       // after prompt should be immediately after $ otherwise tracks the end
546       // of the line (and no command will be found) at least on Mac OS X it did.
547       positionAfterPrompt = createPosition(offsetAfterPrompt - 1);
548     } catch (BadLocationException e)
549     {
550       e.printStackTrace();
551     }
552   }
553
554   void setNoPrompt()
555   {
556     try
557     {
558       offsetAfterPrompt = getLength();
559       positionAfterPrompt = positionBeforePrompt = createPosition(offsetAfterPrompt);
560       consoleTextPane.setCaretPosition(offsetAfterPrompt);
561     } catch (BadLocationException e)
562     {
563       e.printStackTrace();
564     }
565   }
566
567   // it looks like the positionBeforePrompt does not track when it started out
568   // as 0
569   // and a insertString at location 0 occurs. It may be better to track the
570   // position after the prompt in stead
571   void outputBeforePrompt(String str, SimpleAttributeSet attribute)
572   {
573     try
574     {
575       int pt = consoleTextPane.getCaretPosition();
576       Position caretPosition = createPosition(pt);
577       pt = positionBeforePrompt.getOffset();
578       super.insertString(pt, str + "\n", attribute);
579       setOffsetPositions();
580       pt = caretPosition.getOffset();
581       consoleTextPane.setCaretPosition(pt);
582     } catch (BadLocationException e)
583     {
584       e.printStackTrace();
585     }
586   }
587
588   void outputError(String strError)
589   {
590     outputBeforePrompt(strError, attError);
591   }
592
593   void outputErrorForeground(String strError)
594   {
595     try
596     {
597       super.insertString(getLength(), strError + "\n", attError);
598       consoleTextPane.setCaretPosition(getLength());
599     } catch (BadLocationException e)
600     {
601       e.printStackTrace();
602
603     }
604   }
605
606   void outputEcho(String strEcho)
607   {
608     outputBeforePrompt(strEcho, attEcho);
609   }
610
611   void outputStatus(String strStatus)
612   {
613     outputBeforePrompt(strStatus, attStatus);
614   }
615
616   void appendNewline()
617   {
618     try
619     {
620       super.insertString(getLength(), "\n", attUserInput);
621       consoleTextPane.setCaretPosition(getLength());
622     } catch (BadLocationException e)
623     {
624       e.printStackTrace();
625     }
626   }
627
628   // override the insertString to make sure everything typed ends up at the end
629   // or in the 'command line' using the proper font, and the newline is
630   // processed.
631   public void insertString(int offs, String str, AttributeSet a)
632           throws BadLocationException
633   {
634     int ichNewline = str.indexOf('\n');
635     if (ichNewline > 0)
636       str = str.substring(0, ichNewline);
637     if (ichNewline != 0)
638     {
639       if (offs < offsetAfterPrompt)
640       {
641         offs = getLength();
642       }
643       super.insertString(offs, str, a == attError ? a : attUserInput);
644       consoleTextPane.setCaretPosition(offs + str.length());
645     }
646     if (ichNewline >= 0)
647     {
648       consoleTextPane.enterPressed();
649     }
650   }
651
652   String getCommandString()
653   {
654     String strCommand = "";
655     try
656     {
657       int cmdStart = positionAfterPrompt.getOffset();
658       strCommand = getText(cmdStart, getLength() - cmdStart);
659       while (strCommand.length() > 0 && strCommand.charAt(0) == ' ')
660         strCommand = strCommand.substring(1);
661     } catch (BadLocationException e)
662     {
663       e.printStackTrace();
664     }
665     return strCommand;
666   }
667
668   public void remove(int offs, int len) throws BadLocationException
669   {
670     if (offs < offsetAfterPrompt)
671     {
672       len -= offsetAfterPrompt - offs;
673       if (len <= 0)
674         return;
675       offs = offsetAfterPrompt;
676     }
677     super.remove(offs, len);
678     // consoleTextPane.setCaretPosition(offs);
679   }
680
681   public void replace(int offs, int length, String str, AttributeSet attrs)
682           throws BadLocationException
683   {
684     if (offs < offsetAfterPrompt)
685     {
686       if (offs + length < offsetAfterPrompt)
687       {
688         offs = getLength();
689         length = 0;
690       }
691       else
692       {
693         length -= offsetAfterPrompt - offs;
694         offs = offsetAfterPrompt;
695       }
696     }
697     super.replace(offs, length, str, attrs);
698     // consoleTextPane.setCaretPosition(offs + str.length());
699   }
700
701   /**
702    * Replaces current command on script.
703    * 
704    * @param newCommand
705    *          new command value
706    * @param isError
707    *          true to set error color ends with #??
708    * 
709    * @throws BadLocationException
710    */
711   void replaceCommand(String newCommand, boolean isError)
712           throws BadLocationException
713   {
714     if (positionAfterPrompt == positionBeforePrompt)
715       return;
716     replace(offsetAfterPrompt, getLength() - offsetAfterPrompt, newCommand,
717             isError ? attError : attUserInput);
718   }
719
720   void colorCommand(SimpleAttributeSet att)
721   {
722     if (positionAfterPrompt == positionBeforePrompt)
723       return;
724     setCharacterAttributes(offsetAfterPrompt, getLength()
725             - offsetAfterPrompt, att, true);
726   }
727 }
728
729 interface EnterListener
730 {
731   public void enterPressed();
732 }