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