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