JAL-3676 More explicit tooltip about resetting log level on Console close. Also clari...
[jalview.git] / src / jalview / gui / Console.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ 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 java.awt.BorderLayout;
24 import java.awt.Color;
25 import java.awt.Dimension;
26 import java.awt.GraphicsEnvironment;
27 import java.awt.GridBagConstraints;
28 import java.awt.GridBagLayout;
29 import java.awt.Rectangle;
30 import java.awt.Toolkit;
31 import java.awt.datatransfer.Clipboard;
32 import java.awt.datatransfer.StringSelection;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.ActionListener;
35 import java.awt.event.MouseAdapter;
36 import java.awt.event.MouseEvent;
37 import java.awt.event.WindowAdapter;
38 import java.awt.event.WindowEvent;
39 import java.awt.event.WindowListener;
40 import java.io.IOException;
41 import java.io.PipedInputStream;
42 import java.io.PipedOutputStream;
43 import java.io.PrintStream;
44
45 import javax.swing.JButton;
46 import javax.swing.JComboBox;
47 import javax.swing.JFrame;
48 import javax.swing.JLabel;
49 import javax.swing.JPanel;
50 import javax.swing.JScrollPane;
51 import javax.swing.JTextArea;
52
53 import org.apache.log4j.Level;
54 import org.apache.log4j.SimpleLayout;
55
56 import jalview.bin.Cache;
57 import jalview.util.MessageManager;
58
59 /**
60  * Simple Jalview Java Console. Version 1 - allows viewing of console output
61  * after desktop is created. Acquired with thanks from RJHM's site
62  * http://www.comweb.nl/java/Console/Console.html A simple Java Console for your
63  * application (Swing version) Requires Java 1.1.5 or higher Disclaimer the use
64  * of this source is at your own risk. Permision to use and distribute into your
65  * own applications RJHM van den Bergh , rvdb@comweb.nl
66  */
67
68 public class Console extends WindowAdapter
69         implements WindowListener, ActionListener, Runnable
70 {
71   private JFrame frame;
72
73   private JTextArea textArea;
74
75   /*
76    * unused - tally and limit for lines in console window int lines = 0;
77    * 
78    * int lim = 1000;
79    */
80   int byteslim = 102400, bytescut = 76800; // 100k and 75k cut point.
81
82   private Thread reader, reader2, textAppender;
83
84   private boolean quit;
85
86   private final PrintStream stdout = System.out, stderr = System.err;
87
88   private PipedInputStream pin = new PipedInputStream();
89
90   private PipedInputStream pin2 = new PipedInputStream();
91
92   private StringBuffer displayPipe = new StringBuffer();
93
94   Thread errorThrower; // just for testing (Throws an Exception at this Console
95
96   // are we attached to some parent Desktop
97   Desktop parent = null;
98
99   private int MIN_WIDTH = 300;
100
101   private int MIN_HEIGHT = 250;
102
103   private JComboBox<Level> logLevelCombo = new JComboBox<Level>();
104
105   protected Level startingLogLevel = Level.INFO;
106
107   public Console()
108   {
109     // create all components and add them
110     Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
111     frame = initFrame("Java Console", screenSize.width / 2,
112             screenSize.height / 2, -1, -1);
113     initConsole(true);
114   }
115
116   private void initConsole(boolean visible)
117   {
118     initConsole(visible, true);
119   }
120
121   /**
122    * 
123    * @param visible
124    *          - open the window
125    * @param redirect
126    *          - redirect std*
127    */
128   private void initConsole(boolean visible, boolean redirect)
129   {
130     // CutAndPasteTransfer cpt = new CutAndPasteTransfer();
131     // textArea = cpt.getTextArea();
132     textArea = new JTextArea();
133     textArea.setEditable(false);
134     JButton clearButton = new JButton(
135             MessageManager.getString("action.clear"));
136     JButton copyToClipboardButton = new JButton(
137             MessageManager.getString("label.copy_to_clipboard"));
138     copyToClipboardButton.addActionListener(new ActionListener()
139     {
140       public void actionPerformed(ActionEvent e)
141       {
142         copyConsoleTextToClipboard();
143       }
144     });
145     copyToClipboardButton.addMouseListener(new MouseAdapter()
146     {
147       private Color bg = textArea.getBackground();
148
149       private Color fg = textArea.getForeground();
150
151       public void mousePressed(MouseEvent e)
152       {
153         textArea.setBackground(textArea.getSelectionColor());
154         textArea.setForeground(textArea.getSelectedTextColor());
155       }
156
157       public void mouseReleased(MouseEvent e)
158       {
159         textArea.setBackground(bg);
160         textArea.setForeground(fg);
161       }
162
163     });
164     copyToClipboardButton.setToolTipText(
165             MessageManager.getString("label.copy_to_clipboard_tooltip"));
166
167     JLabel logLevelLabel = new JLabel(
168             MessageManager.getString("label.log_level") + ":");
169
170     // logLevelCombo.addItem(Level.ALL);
171     logLevelCombo.addItem(Level.TRACE);
172     logLevelCombo.addItem(Level.DEBUG);
173     logLevelCombo.addItem(Level.INFO);
174     logLevelCombo.addItem(Level.WARN);
175     // logLevelCombo.addItem(Level.ERROR);
176     // logLevelCombo.addItem(Level.FATAL);
177     // logLevelCombo.addItem(Level.OFF);
178     // set startingLogLevel
179     startingLogLevel = Cache.log == null ? Level.INFO
180             : Cache.log.getLevel();
181     setChosenLogLevelCombo();
182     logLevelCombo.addActionListener(new ActionListener()
183     {
184       public void actionPerformed(ActionEvent e)
185       {
186         if (Cache.log != null)
187         {
188           Cache.log.setLevel((Level) logLevelCombo.getSelectedItem());
189         }
190       }
191
192     });
193
194     // frame = cpt;
195     frame.getContentPane().setLayout(new BorderLayout());
196     frame.getContentPane().add(new JScrollPane(textArea),
197             BorderLayout.CENTER);
198     JPanel southPanel = new JPanel();
199     southPanel.setLayout(new GridBagLayout());
200
201     JPanel logLevelPanel = new JPanel();
202     logLevelPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
203     logLevelPanel.add(logLevelLabel);
204     logLevelPanel.add(logLevelCombo);
205     String logLevelTooltip = MessageManager.formatMessage(
206             "label.log_level_tooltip", startingLogLevel.toString());
207     logLevelLabel.setToolTipText(logLevelTooltip);
208     logLevelCombo.setToolTipText(logLevelTooltip);
209
210     GridBagConstraints gbc = new GridBagConstraints();
211     gbc.gridx = 0;
212     gbc.gridy = 0;
213     gbc.gridwidth = 1;
214     gbc.gridheight = 1;
215     gbc.weightx = 0.1;
216     southPanel.add(logLevelPanel, gbc);
217
218     gbc.gridx++;
219     gbc.weightx = 0.8;
220     gbc.fill = GridBagConstraints.HORIZONTAL;
221     southPanel.add(clearButton, gbc);
222
223     gbc.gridx++;
224     gbc.weightx = 0.1;
225     gbc.fill = GridBagConstraints.NONE;
226     southPanel.add(copyToClipboardButton, gbc);
227
228     southPanel.setVisible(true);
229     frame.getContentPane().add(southPanel, BorderLayout.SOUTH);
230     frame.setVisible(visible);
231     updateConsole = visible;
232     frame.addWindowListener(this);
233     clearButton.addActionListener(this);
234
235     if (redirect)
236     {
237       redirectStreams();
238     }
239     else
240     {
241       unredirectStreams();
242     }
243     quit = false; // signals the Threads that they should exit
244
245     // Starting two seperate threads to read from the PipedInputStreams
246     //
247     reader = new Thread(this);
248     reader.setDaemon(true);
249     reader.start();
250     //
251     reader2 = new Thread(this);
252     reader2.setDaemon(true);
253     reader2.start();
254     // and a thread to append text to the textarea
255     textAppender = new Thread(this);
256     textAppender.setDaemon(true);
257     textAppender.start();
258   }
259
260   private void setChosenLogLevelCombo()
261   {
262     setChosenLogLevelCombo(startingLogLevel);
263   }
264
265   private void setChosenLogLevelCombo(Level setLogLevel)
266   {
267     logLevelCombo.setSelectedItem(setLogLevel);
268     if (!logLevelCombo.getSelectedItem().equals(setLogLevel))
269     {
270       // setLogLevel not (yet) in list
271       if (setLogLevel != null && setLogLevel instanceof Level)
272       {
273         // add new item to list (might be set via .jalview_properties)
274         boolean added = false;
275         for (int i = 0; i < logLevelCombo.getItemCount(); i++)
276         {
277           Level l = (Level) logLevelCombo.getItemAt(i);
278           if (l.isGreaterOrEqual(setLogLevel))
279           {
280             logLevelCombo.insertItemAt(setLogLevel, i);
281             added = true;
282             break;
283           }
284         }
285         if (!added) // lower priority than others or some confusion -- add to
286                     // end of list
287         {
288           logLevelCombo.addItem(setLogLevel);
289         }
290         logLevelCombo.setSelectedItem(setLogLevel);
291       }
292       else
293       {
294         logLevelCombo.setSelectedItem(Level.INFO);
295       }
296     }
297   }
298
299   private void copyConsoleTextToClipboard()
300   {
301     String consoleText = textArea.getText();
302     StringSelection consoleTextSelection = new StringSelection(consoleText);
303     Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
304     cb.setContents(consoleTextSelection, null);
305   }
306
307   PipedOutputStream pout = null, perr = null;
308
309   public void redirectStreams()
310   {
311     if (pout == null)
312     {
313       try
314       {
315         pout = new PipedOutputStream(this.pin);
316         System.setOut(new PrintStream(pout, true));
317       } catch (java.io.IOException io)
318       {
319         textArea.append("Couldn't redirect STDOUT to this console\n"
320                 + io.getMessage());
321         io.printStackTrace(stderr);
322       } catch (SecurityException se)
323       {
324         textArea.append("Couldn't redirect STDOUT to this console\n"
325                 + se.getMessage());
326         se.printStackTrace(stderr);
327       }
328
329       try
330       {
331         perr = new PipedOutputStream(this.pin2);
332         System.setErr(new PrintStream(perr, true));
333       } catch (java.io.IOException io)
334       {
335         textArea.append("Couldn't redirect STDERR to this console\n"
336                 + io.getMessage());
337         io.printStackTrace(stderr);
338       } catch (SecurityException se)
339       {
340         textArea.append("Couldn't redirect STDERR to this console\n"
341                 + se.getMessage());
342         se.printStackTrace(stderr);
343       }
344     }
345   }
346
347   public void unredirectStreams()
348   {
349     if (pout != null)
350     {
351       try
352       {
353         System.setOut(stdout);
354         pout.flush();
355         pout.close();
356         pin = new PipedInputStream();
357         pout = null;
358       } catch (java.io.IOException io)
359       {
360         textArea.append("Couldn't unredirect STDOUT to this console\n"
361                 + io.getMessage());
362         io.printStackTrace(stderr);
363       } catch (SecurityException se)
364       {
365         textArea.append("Couldn't unredirect STDOUT to this console\n"
366                 + se.getMessage());
367         se.printStackTrace(stderr);
368       }
369
370       try
371       {
372         System.setErr(stderr);
373         perr.flush();
374         perr.close();
375         pin2 = new PipedInputStream();
376         perr = null;
377       } catch (java.io.IOException io)
378       {
379         textArea.append("Couldn't unredirect STDERR to this console\n"
380                 + io.getMessage());
381         io.printStackTrace(stderr);
382       } catch (SecurityException se)
383       {
384         textArea.append("Couldn't unredirect STDERR to this console\n"
385                 + se.getMessage());
386         se.printStackTrace(stderr);
387       }
388     }
389   }
390
391   public void test()
392   {
393     // testing part
394     // you may omit this part for your application
395     //
396
397     System.out.println("Hello World 2");
398     System.out.println("All fonts available to Graphic2D:\n");
399     GraphicsEnvironment ge = GraphicsEnvironment
400             .getLocalGraphicsEnvironment();
401     String[] fontNames = ge.getAvailableFontFamilyNames();
402     for (int n = 0; n < fontNames.length; n++)
403     {
404       System.out.println(fontNames[n]);
405     }
406     // Testing part: simple an error thrown anywhere in this JVM will be printed
407     // on the Console
408     // We do it with a seperate Thread becasue we don't wan't to break a Thread
409     // used by the Console.
410     System.out.println("\nLets throw an error on this console");
411     errorThrower = new Thread(this);
412     errorThrower.setDaemon(true);
413     errorThrower.start();
414   }
415
416   private JFrame initFrame(String string, int i, int j, int x, int y)
417   {
418     JFrame frame = new JFrame(string);
419     frame.setName(string);
420     if (x == -1)
421     {
422       x = i / 2;
423     }
424     if (y == -1)
425     {
426       y = j / 2;
427     }
428     frame.setBounds(x, y, i, j);
429     return frame;
430   }
431
432   /**
433    * attach a console to the desktop - the desktop will open it if requested.
434    * 
435    * @param desktop
436    */
437   public Console(Desktop desktop)
438   {
439     this(desktop, true);
440   }
441
442   /**
443    * attach a console to the desktop - the desktop will open it if requested.
444    * 
445    * @param desktop
446    * @param showjconsole
447    *          - if true, then redirect stdout immediately
448    */
449   public Console(Desktop desktop, boolean showjconsole)
450   {
451     parent = desktop;
452     // window name - get x,y,width, height possibly scaled
453     Rectangle bounds = desktop.getLastKnownDimensions("JAVA_CONSOLE_");
454     if (bounds == null)
455     {
456       frame = initFrame("Jalview Java Console", desktop.getWidth() / 2,
457               desktop.getHeight() / 4, desktop.getX(), desktop.getY());
458     }
459     else
460     {
461       frame = initFrame("Jalview Java Console", bounds.width, bounds.height,
462               bounds.x, bounds.y);
463     }
464     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
465     // desktop.add(frame);
466     initConsole(false);
467     JalviewAppender jappender = new JalviewAppender();
468     jappender.setLayout(new SimpleLayout());
469     JalviewAppender.setTextArea(textArea);
470     org.apache.log4j.Logger.getRootLogger().addAppender(jappender);
471   }
472
473   public synchronized void stopConsole()
474   {
475     quit = true;
476     this.notifyAll();
477     /*
478      * reader.notify(); reader2.notify(); if (errorThrower!=null)
479      * errorThrower.notify(); // stop all threads if (textAppender!=null)
480      * textAppender.notify();
481      */
482     if (pout != null)
483     {
484       try
485       {
486         reader.join(10);
487         pin.close();
488       } catch (Exception e)
489       {
490       }
491       try
492       {
493         reader2.join(10);
494         pin2.close();
495       } catch (Exception e)
496       {
497       }
498       try
499       {
500         textAppender.join(10);
501       } catch (Exception e)
502       {
503       }
504     }
505     if (!frame.isVisible())
506     {
507       frame.dispose();
508     }
509     // System.exit(0);
510   }
511
512   @Override
513   public synchronized void windowClosed(WindowEvent evt)
514   {
515     frame.setVisible(false);
516     closeConsoleGui();
517   }
518
519   private void closeConsoleGui()
520   {
521     updateConsole = false;
522     if (parent == null)
523     {
524
525       stopConsole();
526     }
527     else
528     {
529       parent.showConsole(false);
530     }
531   }
532
533   @Override
534   public synchronized void windowClosing(WindowEvent evt)
535   {
536     frame.setVisible(false); // default behaviour of JFrame
537     closeConsoleGui();
538
539     // frame.dispose();
540   }
541
542   @Override
543   public synchronized void actionPerformed(ActionEvent evt)
544   {
545     trimBuffer(true);
546     // textArea.setText("");
547   }
548
549   @Override
550   public synchronized void run()
551   {
552     try
553     {
554       while (Thread.currentThread() == reader)
555       {
556         if (pin == null || pin.available() == 0)
557         {
558           try
559           {
560             this.wait(100);
561             if (pin.available() == 0)
562             {
563               trimBuffer(false);
564             }
565           } catch (InterruptedException ie)
566           {
567           }
568         }
569
570         while (pin.available() != 0)
571         {
572           String input = this.readLine(pin);
573           stdout.print(input);
574           long time = System.nanoTime();
575           appendToTextArea(input);
576           // stderr.println("Time taken to stdout append:\t"
577           // + (System.nanoTime() - time) + " ns");
578           // lines++;
579         }
580         if (quit)
581         {
582           return;
583         }
584       }
585
586       while (Thread.currentThread() == reader2)
587       {
588         if (pin2.available() == 0)
589         {
590           try
591           {
592             this.wait(100);
593             if (pin2.available() == 0)
594             {
595               trimBuffer(false);
596             }
597           } catch (InterruptedException ie)
598           {
599           }
600         }
601         while (pin2.available() != 0)
602         {
603           String input = this.readLine(pin2);
604           stderr.print(input);
605           long time = System.nanoTime();
606           appendToTextArea(input);
607           // stderr.println("Time taken to stderr append:\t"
608           // + (System.nanoTime() - time) + " ns");
609           // lines++;
610         }
611         if (quit)
612         {
613           return;
614         }
615       }
616       while (Thread.currentThread() == textAppender)
617       {
618         if (updateConsole)
619         {
620           // check string buffer - if greater than console, clear console and
621           // replace with last segment of content, otherwise, append all to
622           // content.
623           long count;
624           while (displayPipe.length() > 0)
625           {
626             count = 0;
627             StringBuffer tmp = new StringBuffer(), replace;
628             synchronized (displayPipe)
629             {
630               replace = displayPipe;
631               displayPipe = tmp;
632             }
633             // simply append whole buffer
634             textArea.append(replace.toString());
635             count += replace.length();
636             if (count > byteslim)
637             {
638               trimBuffer(false);
639             }
640           }
641           if (displayPipe.length() == 0)
642           {
643             try
644             {
645               this.wait(100);
646               if (displayPipe.length() == 0)
647               {
648                 trimBuffer(false);
649               }
650             } catch (InterruptedException e)
651             {
652             }
653           }
654         }
655         else
656         {
657           try
658           {
659             this.wait(100);
660           } catch (InterruptedException e)
661           {
662
663           }
664         }
665         if (quit)
666         {
667           return;
668         }
669
670       }
671     } catch (Exception e)
672     {
673       textArea.append("\nConsole reports an Internal error.");
674       textArea.append("The error is: " + e.getMessage());
675       // Need to uncomment this to ensure that line tally is synched.
676       // lines += 2;
677       stderr.println(
678               "Console reports an Internal error.\nThe error is: " + e);
679     }
680
681     // just for testing (Throw a Nullpointer after 1 second)
682     if (Thread.currentThread() == errorThrower)
683     {
684       try
685       {
686         this.wait(1000);
687       } catch (InterruptedException ie)
688       {
689       }
690       throw new NullPointerException(
691               MessageManager.getString("exception.application_test_npe"));
692     }
693   }
694
695   private void appendToTextArea(final String input)
696   {
697     if (updateConsole == false)
698     {
699       // do nothing;
700       return;
701     }
702     long time = System.nanoTime();
703     javax.swing.SwingUtilities.invokeLater(new Runnable()
704     {
705       @Override
706       public void run()
707       {
708         displayPipe.append(input); // change to stringBuffer
709         // displayPipe.flush();
710
711       }
712     });
713     // stderr.println("Time taken to Spawnappend:\t" + (System.nanoTime() -
714     // time)
715     // + " ns");
716   }
717
718   private String header = null;
719
720   private boolean updateConsole = false;
721
722   private synchronized void trimBuffer(boolean clear)
723   {
724     if (header == null && textArea.getLineCount() > 5)
725     {
726       try
727       {
728         header = textArea.getText(0, textArea.getLineStartOffset(5))
729                 + "\nTruncated...\n";
730       } catch (Exception e)
731       {
732         e.printStackTrace();
733       }
734     }
735     // trim the buffer
736     int tlength = textArea.getDocument().getLength();
737     if (header != null)
738     {
739       if (clear || (tlength > byteslim))
740       {
741         try
742         {
743           if (!clear)
744           {
745             long time = System.nanoTime();
746             textArea.replaceRange(header, 0, tlength - bytescut);
747             // stderr.println("Time taken to cut:\t"
748             // + (System.nanoTime() - time) + " ns");
749           }
750           else
751           {
752             textArea.setText(header);
753           }
754         } catch (Exception e)
755         {
756           e.printStackTrace();
757         }
758         // lines = textArea.getLineCount();
759       }
760     }
761
762   }
763
764   public synchronized String readLine(PipedInputStream in)
765           throws IOException
766   {
767     String input = "";
768     int lp = -1;
769     do
770     {
771       int available = in.available();
772       if (available == 0)
773       {
774         break;
775       }
776       byte b[] = new byte[available];
777       in.read(b);
778       input = input + new String(b, 0, b.length);
779       // counts lines - we don't do this for speed.
780       // while ((lp = input.indexOf("\n", lp + 1)) > -1)
781       // {
782       // lines++;
783       // }
784     } while (!input.endsWith("\n") && !input.endsWith("\r\n") && !quit);
785     return input;
786   }
787
788   /**
789    * @j2sIgnore
790    * @param arg
791    */
792   public static void main(String[] arg)
793   {
794     new Console().test(); // create console with not reference
795
796   }
797
798   public void setVisible(boolean selected)
799   {
800     frame.setVisible(selected);
801     if (selected == true)
802     {
803       setChosenLogLevelCombo();
804       redirectStreams();
805       updateConsole = true;
806       frame.toFront();
807     }
808     else
809     {
810       // reset log level to what it was before
811       if (Cache.log != null)
812       {
813         Cache.log.setLevel(startingLogLevel);
814       }
815
816       unredirectStreams();
817       updateConsole = false;
818     }
819   }
820
821   public Rectangle getBounds()
822   {
823     if (frame != null)
824     {
825       return frame.getBounds();
826     }
827     return null;
828   }
829
830   /**
831    * set the banner that appears at the top of the console output
832    * 
833    * @param string
834    */
835   public void setHeader(String string)
836   {
837     header = string;
838     if (header.charAt(header.length() - 1) != '\n')
839     {
840       header += "\n";
841     }
842     textArea.insert(header, 0);
843   }
844
845   /**
846    * get the banner
847    * 
848    * @return
849    */
850   public String getHeader()
851   {
852     return header;
853   }
854 }