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