JAL-3594 JAL-3728 Added taskbar icons to desktop and Java console. Changed "Jalview...
[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.ChannelProperties;
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           Cache.log.setLevel((Level) logLevelCombo.getSelectedItem());
190         }
191       }
192
193     });
194
195     // frame = cpt;
196     frame.getContentPane().setLayout(new BorderLayout());
197     frame.getContentPane().add(new JScrollPane(textArea),
198             BorderLayout.CENTER);
199     JPanel southPanel = new JPanel();
200     southPanel.setLayout(new GridBagLayout());
201
202     JPanel logLevelPanel = new JPanel();
203     logLevelPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
204     logLevelPanel.add(logLevelLabel);
205     logLevelPanel.add(logLevelCombo);
206     String logLevelTooltip = MessageManager.formatMessage(
207             "label.log_level_tooltip", startingLogLevel.toString());
208     logLevelLabel.setToolTipText(logLevelTooltip);
209     logLevelCombo.setToolTipText(logLevelTooltip);
210
211     GridBagConstraints gbc = new GridBagConstraints();
212     gbc.gridx = 0;
213     gbc.gridy = 0;
214     gbc.gridwidth = 1;
215     gbc.gridheight = 1;
216     gbc.weightx = 0.1;
217     southPanel.add(logLevelPanel, gbc);
218
219     gbc.gridx++;
220     gbc.weightx = 0.8;
221     gbc.fill = GridBagConstraints.HORIZONTAL;
222     southPanel.add(clearButton, gbc);
223
224     gbc.gridx++;
225     gbc.weightx = 0.1;
226     gbc.fill = GridBagConstraints.NONE;
227     southPanel.add(copyToClipboardButton, gbc);
228
229     southPanel.setVisible(true);
230     frame.getContentPane().add(southPanel, BorderLayout.SOUTH);
231     frame.setVisible(visible);
232     updateConsole = visible;
233     frame.addWindowListener(this);
234     clearButton.addActionListener(this);
235
236     if (redirect)
237     {
238       redirectStreams();
239     }
240     else
241     {
242       unredirectStreams();
243     }
244     quit = false; // signals the Threads that they should exit
245
246     // Starting two seperate threads to read from the PipedInputStreams
247     //
248     reader = new Thread(this);
249     reader.setDaemon(true);
250     reader.start();
251     //
252     reader2 = new Thread(this);
253     reader2.setDaemon(true);
254     reader2.start();
255     // and a thread to append text to the textarea
256     textAppender = new Thread(this);
257     textAppender.setDaemon(true);
258     textAppender.start();
259
260     // set icons
261     frame.setIconImages(ChannelProperties.getIconList());
262   }
263
264   private void setChosenLogLevelCombo()
265   {
266     setChosenLogLevelCombo(startingLogLevel);
267   }
268
269   private void setChosenLogLevelCombo(Level setLogLevel)
270   {
271     logLevelCombo.setSelectedItem(setLogLevel);
272     if (!logLevelCombo.getSelectedItem().equals(setLogLevel))
273     {
274       // setLogLevel not (yet) in list
275       if (setLogLevel != null && setLogLevel instanceof Level)
276       {
277         // add new item to list (might be set via .jalview_properties)
278         boolean added = false;
279         for (int i = 0; i < logLevelCombo.getItemCount(); i++)
280         {
281           Level l = (Level) logLevelCombo.getItemAt(i);
282           if (l.isGreaterOrEqual(setLogLevel))
283           {
284             logLevelCombo.insertItemAt(setLogLevel, i);
285             added = true;
286             break;
287           }
288         }
289         if (!added) // lower priority than others or some confusion -- add to
290                     // end of list
291         {
292           logLevelCombo.addItem(setLogLevel);
293         }
294         logLevelCombo.setSelectedItem(setLogLevel);
295       }
296       else
297       {
298         logLevelCombo.setSelectedItem(Level.INFO);
299       }
300     }
301   }
302
303   private void copyConsoleTextToClipboard()
304   {
305     String consoleText = textArea.getText();
306     StringSelection consoleTextSelection = new StringSelection(consoleText);
307     Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
308     cb.setContents(consoleTextSelection, null);
309   }
310
311   PipedOutputStream pout = null, perr = null;
312
313   public void redirectStreams()
314   {
315     if (pout == null)
316     {
317       try
318       {
319         pout = new PipedOutputStream(this.pin);
320         System.setOut(new PrintStream(pout, true));
321       } catch (java.io.IOException io)
322       {
323         textArea.append("Couldn't redirect STDOUT to this console\n"
324                 + io.getMessage());
325         io.printStackTrace(stderr);
326       } catch (SecurityException se)
327       {
328         textArea.append("Couldn't redirect STDOUT to this console\n"
329                 + se.getMessage());
330         se.printStackTrace(stderr);
331       }
332
333       try
334       {
335         perr = new PipedOutputStream(this.pin2);
336         System.setErr(new PrintStream(perr, true));
337       } catch (java.io.IOException io)
338       {
339         textArea.append("Couldn't redirect STDERR to this console\n"
340                 + io.getMessage());
341         io.printStackTrace(stderr);
342       } catch (SecurityException se)
343       {
344         textArea.append("Couldn't redirect STDERR to this console\n"
345                 + se.getMessage());
346         se.printStackTrace(stderr);
347       }
348     }
349   }
350
351   public void unredirectStreams()
352   {
353     if (pout != null)
354     {
355       try
356       {
357         System.setOut(stdout);
358         pout.flush();
359         pout.close();
360         pin = new PipedInputStream();
361         pout = null;
362       } catch (java.io.IOException io)
363       {
364         textArea.append("Couldn't unredirect STDOUT to this console\n"
365                 + io.getMessage());
366         io.printStackTrace(stderr);
367       } catch (SecurityException se)
368       {
369         textArea.append("Couldn't unredirect STDOUT to this console\n"
370                 + se.getMessage());
371         se.printStackTrace(stderr);
372       }
373
374       try
375       {
376         System.setErr(stderr);
377         perr.flush();
378         perr.close();
379         pin2 = new PipedInputStream();
380         perr = null;
381       } catch (java.io.IOException io)
382       {
383         textArea.append("Couldn't unredirect STDERR to this console\n"
384                 + io.getMessage());
385         io.printStackTrace(stderr);
386       } catch (SecurityException se)
387       {
388         textArea.append("Couldn't unredirect STDERR to this console\n"
389                 + se.getMessage());
390         se.printStackTrace(stderr);
391       }
392     }
393   }
394
395   public void test()
396   {
397     // testing part
398     // you may omit this part for your application
399     //
400
401     System.out.println("Hello World 2");
402     System.out.println("All fonts available to Graphic2D:\n");
403     GraphicsEnvironment ge = GraphicsEnvironment
404             .getLocalGraphicsEnvironment();
405     String[] fontNames = ge.getAvailableFontFamilyNames();
406     for (int n = 0; n < fontNames.length; n++)
407     {
408       System.out.println(fontNames[n]);
409     }
410     // Testing part: simple an error thrown anywhere in this JVM will be printed
411     // on the Console
412     // We do it with a seperate Thread becasue we don't wan't to break a Thread
413     // used by the Console.
414     System.out.println("\nLets throw an error on this console");
415     errorThrower = new Thread(this);
416     errorThrower.setDaemon(true);
417     errorThrower.start();
418   }
419
420   private JFrame initFrame(String string, int i, int j, int x, int y)
421   {
422     JFrame frame = new JFrame(string);
423     frame.setName(string);
424     if (x == -1)
425     {
426       x = i / 2;
427     }
428     if (y == -1)
429     {
430       y = j / 2;
431     }
432     frame.setBounds(x, y, i, j);
433     return frame;
434   }
435
436   /**
437    * attach a console to the desktop - the desktop will open it if requested.
438    * 
439    * @param desktop
440    */
441   public Console(Desktop desktop)
442   {
443     this(desktop, true);
444   }
445
446   /**
447    * attach a console to the desktop - the desktop will open it if requested.
448    * 
449    * @param desktop
450    * @param showjconsole
451    *          - if true, then redirect stdout immediately
452    */
453   public Console(Desktop desktop, boolean showjconsole)
454   {
455     parent = desktop;
456     // window name - get x,y,width, height possibly scaled
457     Rectangle bounds = desktop.getLastKnownDimensions("JAVA_CONSOLE_");
458     if (bounds == null)
459     {
460       frame = initFrame(
461               ChannelProperties.getProperty("app_name") + " Java Console",
462               desktop.getWidth() / 2, desktop.getHeight() / 4,
463               desktop.getX(), desktop.getY());
464     }
465     else
466     {
467       frame = initFrame(
468               ChannelProperties.getProperty("app_name") + " Java Console",
469               bounds.width, bounds.height, bounds.x, bounds.y);
470     }
471     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
472     // desktop.add(frame);
473     initConsole(false);
474     JalviewAppender jappender = new JalviewAppender();
475     jappender.setLayout(new SimpleLayout());
476     JalviewAppender.setTextArea(textArea);
477     org.apache.log4j.Logger.getRootLogger().addAppender(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         Cache.log.setLevel(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 }