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