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