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