001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.system;
020
021import icy.file.FileUtil;
022import icy.gui.component.ExternalizablePanel;
023import icy.gui.component.button.IcyButton;
024import icy.gui.component.button.IcyToggleButton;
025import icy.gui.frame.progress.ProgressFrame;
026import icy.gui.util.GuiUtil;
027import icy.preferences.GeneralPreferences;
028import icy.resource.ResourceUtil;
029import icy.resource.icon.IcyIcon;
030import icy.system.IcyExceptionHandler;
031import icy.util.EventUtil;
032
033import java.awt.BorderLayout;
034import java.awt.Color;
035import java.awt.Dimension;
036import java.awt.datatransfer.Clipboard;
037import java.awt.datatransfer.ClipboardOwner;
038import java.awt.datatransfer.StringSelection;
039import java.awt.datatransfer.Transferable;
040import java.awt.event.ActionEvent;
041import java.awt.event.ActionListener;
042import java.awt.event.KeyAdapter;
043import java.awt.event.KeyEvent;
044import java.awt.event.MouseAdapter;
045import java.awt.event.MouseEvent;
046import java.io.FileWriter;
047import java.io.IOException;
048import java.io.PrintStream;
049import java.io.Writer;
050import java.util.EventListener;
051
052import javax.swing.Box;
053import javax.swing.JLabel;
054import javax.swing.JPanel;
055import javax.swing.JScrollPane;
056import javax.swing.JSpinner;
057import javax.swing.JTextField;
058import javax.swing.JTextPane;
059import javax.swing.SpinnerNumberModel;
060import javax.swing.event.ChangeEvent;
061import javax.swing.event.ChangeListener;
062import javax.swing.text.BadLocationException;
063import javax.swing.text.Element;
064import javax.swing.text.SimpleAttributeSet;
065import javax.swing.text.StyleConstants;
066import javax.swing.text.StyledDocument;
067
068/**
069 * @author Stephane
070 */
071public class OutputConsolePanel extends ExternalizablePanel implements ClipboardOwner
072{
073    public static interface OutputConsoleChangeListener extends EventListener
074    {
075        public void outputConsoleChanged(OutputConsolePanel source, boolean isError);
076    }
077
078    private class WindowsOutPrintStream extends PrintStream
079    {
080        boolean isStdErr;
081
082        public WindowsOutPrintStream(PrintStream out, boolean isStdErr)
083        {
084            super(out);
085
086            this.isStdErr = isStdErr;
087        }
088
089        @Override
090        public void write(byte[] buf, int off, int len)
091        {
092            try
093            {
094                super.write(buf, off, len);
095
096                final String text = new String(buf, off, len);
097                addText(text, isStdErr);
098                // want file log as well ?
099                if (fileLogButton.isSelected() && (logWriter != null))
100                {
101                    // write and save to file immediately
102                    logWriter.write(text);
103                    logWriter.flush();
104                }
105            }
106            catch (Throwable t)
107            {
108                addText(t.getMessage(), isStdErr);
109            }
110        }
111    }
112
113    /**
114     * 
115     */
116    private static final long serialVersionUID = 7142067146669860938L;
117
118    final protected JTextPane textPane;
119    final protected StyledDocument doc;
120    final SimpleAttributeSet normalAttributes;
121    final SimpleAttributeSet errorAttributes;
122
123    final protected JSpinner logMaxLineField;
124    final protected JTextField logMaxLineTextField;
125    final public IcyButton clearLogButton;
126    final public IcyButton copyLogButton;
127    final public IcyButton reportLogButton;
128    final public IcyToggleButton scrollLockButton;
129    final public IcyToggleButton fileLogButton;
130    final public JPanel bottomPanel;
131
132    int nbUpdate;
133    Writer logWriter;
134
135    public OutputConsolePanel()
136    {
137        super("Output", "outputConsole");
138
139        textPane = new JTextPane();
140        doc = textPane.getStyledDocument();
141        nbUpdate = 0;
142
143        errorAttributes = new SimpleAttributeSet();
144        normalAttributes = new SimpleAttributeSet();
145
146        StyleConstants.setFontFamily(errorAttributes, "arial");
147        StyleConstants.setFontSize(errorAttributes, 11);
148        StyleConstants.setForeground(errorAttributes, Color.red);
149
150        StyleConstants.setFontFamily(normalAttributes, "arial");
151        StyleConstants.setFontSize(normalAttributes, 11);
152        StyleConstants.setForeground(normalAttributes, Color.black);
153
154        logMaxLineField = new JSpinner(
155                new SpinnerNumberModel(GeneralPreferences.getOutputLogSize(), 100, 1000000, 100));
156        logMaxLineTextField = ((JSpinner.DefaultEditor) logMaxLineField.getEditor()).getTextField();
157        clearLogButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_DELETE));
158        copyLogButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_DOC_COPY));
159        reportLogButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_DOC_EXPORT));
160        scrollLockButton = new IcyToggleButton(new IcyIcon(ResourceUtil.ICON_LOCK_OPEN));
161        fileLogButton = new IcyToggleButton(new IcyIcon(ResourceUtil.ICON_SAVE));
162        fileLogButton.setSelected(GeneralPreferences.getOutputLogToFile());
163
164        // ComponentUtil.setFontSize(textPane, 10);
165        textPane.setEditable(false);
166
167        clearLogButton.setFlat(true);
168        copyLogButton.setFlat(true);
169        reportLogButton.setFlat(true);
170        scrollLockButton.setFlat(true);
171        fileLogButton.setFlat(true);
172
173        logMaxLineField.setPreferredSize(new Dimension(80, 24));
174        // no focusable
175        logMaxLineField.setFocusable(false);
176        logMaxLineTextField.setFocusable(false);
177        logMaxLineTextField.addMouseListener(new MouseAdapter()
178        {
179            @Override
180            public void mouseClicked(MouseEvent e)
181            {
182                // get focus on double click to enable manual edition
183                if (EventUtil.isLeftMouseButton(e))
184                {
185                    logMaxLineField.setFocusable(true);
186                    logMaxLineTextField.setFocusable(true);
187                    logMaxLineTextField.requestFocus();
188                }
189            }
190        });
191        logMaxLineTextField.addKeyListener(new KeyAdapter()
192        {
193            @Override
194            public void keyPressed(KeyEvent e)
195            {
196                if (e.getKeyCode() == KeyEvent.VK_ESCAPE)
197                {
198                    // cancel manual edition ? --> remove focus
199                    if (logMaxLineTextField.isFocusable())
200                    {
201                        logMaxLineTextField.setFocusable(false);
202                        logMaxLineField.setFocusable(false);
203                    }
204                }
205            }
206        });
207        logMaxLineField.addChangeListener(new ChangeListener()
208        {
209            @Override
210            public void stateChanged(ChangeEvent e)
211            {
212                GeneralPreferences.setOutputLogSize(getLogMaxLine());
213
214                // manual edition ? --> remove focus
215                if (logMaxLineTextField.isFocusable())
216                {
217                    logMaxLineTextField.setFocusable(false);
218                    logMaxLineField.setFocusable(false);
219                }
220
221                try
222                {
223                    limitLog();
224                }
225                catch (Exception ex)
226                {
227                    // ignore
228
229                }
230            }
231        });
232        logMaxLineField.setToolTipText("Double-click to edit the maximum number of line (max = 1000000)");
233        clearLogButton.setToolTipText("Clear all");
234        copyLogButton.setToolTipText("Copy to clipboard");
235        reportLogButton.setToolTipText("Report content to dev team");
236        scrollLockButton.setToolTipText("Scroll Lock");
237        fileLogButton.setToolTipText("Enable/Disable log file saving (log.txt)");
238
239        clearLogButton.addActionListener(new ActionListener()
240        {
241            @Override
242            public void actionPerformed(ActionEvent e)
243            {
244                textPane.setText("");
245            }
246        });
247        copyLogButton.addActionListener(new ActionListener()
248        {
249            @Override
250            public void actionPerformed(ActionEvent e)
251            {
252                final Clipboard clipboard = getToolkit().getSystemClipboard();
253
254                clipboard.setContents(new StringSelection(getText()), OutputConsolePanel.this);
255            }
256        });
257        reportLogButton.addActionListener(new ActionListener()
258        {
259            @Override
260            public void actionPerformed(ActionEvent e)
261            {
262                final ProgressFrame progressFrame = new ProgressFrame("Sending report...");
263
264                try
265                {
266                    // send report
267                    IcyExceptionHandler.report(getText());
268                }
269                finally
270                {
271                    progressFrame.close();
272                }
273            }
274        });
275        scrollLockButton.addActionListener(new ActionListener()
276        {
277            @Override
278            public void actionPerformed(ActionEvent e)
279            {
280                if (scrollLockButton.isSelected())
281                    scrollLockButton.setIconImage(ResourceUtil.ICON_LOCK_CLOSE);
282                else
283                    scrollLockButton.setIconImage(ResourceUtil.ICON_LOCK_OPEN);
284            }
285        });
286        fileLogButton.addActionListener(new ActionListener()
287        {
288            @Override
289            public void actionPerformed(ActionEvent e)
290            {
291                GeneralPreferences.setOutputLogFile(fileLogButton.isSelected());
292            }
293        });
294
295        bottomPanel = GuiUtil.createPageBoxPanel(Box.createVerticalStrut(4),
296                GuiUtil.createLineBoxPanel(clearLogButton, Box.createHorizontalStrut(4), copyLogButton,
297                        Box.createHorizontalStrut(4), reportLogButton, Box.createHorizontalGlue(),
298                        Box.createHorizontalStrut(4), new JLabel("Limit"), Box.createHorizontalStrut(4),
299                        logMaxLineField, Box.createHorizontalStrut(4), scrollLockButton, Box.createHorizontalStrut(4),
300                        fileLogButton));
301
302        final JPanel panel = new JPanel();
303        panel.setLayout(new BorderLayout());
304        panel.add(textPane, BorderLayout.CENTER);
305
306        final JScrollPane scrollPane = new JScrollPane(panel);
307
308        setLayout(new BorderLayout());
309
310        add(scrollPane, BorderLayout.CENTER);
311        add(bottomPanel, BorderLayout.SOUTH);
312
313        validate();
314
315        // redirect standard output
316        System.setOut(new WindowsOutPrintStream(System.out, false));
317        System.setErr(new WindowsOutPrintStream(System.err, true));
318
319        try
320        {
321            // define log file writer (always clear log.txt file if present)
322            logWriter = new FileWriter(FileUtil.getApplicationDirectory() + "/icy.log", false);
323        }
324        catch (IOException e1)
325        {
326            logWriter = null;
327        }
328    }
329
330    public void addText(String text, boolean isError)
331    {
332        try
333        {
334            nbUpdate++;
335
336            // insert text
337            synchronized (doc)
338            {
339                if (isError)
340                    doc.insertString(doc.getLength(), text, errorAttributes);
341                else
342                    doc.insertString(doc.getLength(), text, normalAttributes);
343
344                // do clean sometime..
345                if ((nbUpdate & 0x7F) == 0)
346                    limitLog();
347
348                // scroll lock feature
349                if (!scrollLockButton.isSelected())
350                    textPane.setCaretPosition(doc.getLength());
351            }
352        }
353        catch (Exception e)
354        {
355            // ignore
356        }
357
358        changed(isError);
359    }
360
361    /**
362     * Get console content.
363     */
364    public String getText()
365    {
366        try
367        {
368            synchronized (doc)
369            {
370                return doc.getText(0, doc.getLength());
371            }
372        }
373        catch (BadLocationException e)
374        {
375            return "";
376        }
377    }
378
379    /**
380     * Apply maximum line limitation to the log output
381     * 
382     * @throws BadLocationException
383     */
384    public void limitLog() throws BadLocationException
385    {
386        final Element root = doc.getDefaultRootElement();
387        final int numLine = root.getElementCount();
388        final int logMaxLine = getLogMaxLine();
389
390        // limit to maximum wanted lines
391        if (numLine > logMaxLine)
392        {
393            final Element line = root.getElement(numLine - (logMaxLine + 1));
394            // remove "old" lines
395            doc.remove(0, line.getEndOffset());
396        }
397    }
398
399    /**
400     * Returns maximum log line number
401     */
402    public int getLogMaxLine()
403    {
404        return ((Integer) logMaxLineField.getValue()).intValue();
405    }
406
407    /**
408     * Sets maximum log line number
409     */
410    public void setLogMaxLine(int value)
411    {
412        logMaxLineField.setValue(Integer.valueOf(value));
413    }
414
415    private void changed(boolean isError)
416    {
417        fireChangedEvent(isError);
418    }
419
420    public void fireChangedEvent(boolean isError)
421    {
422        for (OutputConsoleChangeListener listener : listenerList.getListeners(OutputConsoleChangeListener.class))
423            listener.outputConsoleChanged(this, isError);
424    }
425
426    public void addOutputConsoleChangeListener(OutputConsoleChangeListener listener)
427    {
428        listenerList.add(OutputConsoleChangeListener.class, listener);
429    }
430
431    public void removeOutputConsoleChangeListener(OutputConsoleChangeListener listener)
432    {
433        listenerList.remove(OutputConsoleChangeListener.class, listener);
434    }
435
436    @Override
437    public void lostOwnership(Clipboard clipboard, Transferable contents)
438    {
439        // ignore
440    }
441}