From 74bc7f138a473835fcda97fb508c7733116029d1 Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 26 May 2017 11:51:03 +0200 Subject: [PATCH 01/10] SwingConsolePane: extract ConsolePanel from SwingConsolePane SwingConsolePane is responsible for lazy initialization and showing the console window at correct time. ConsolePanel is responsible for holding a text area to show console output. ConsolePanel will become a starting point for implementing a LoggingPanel. --- .../ui/swing/console/ConsolePanel.java | 156 ++++++++++++++++++ .../ui/swing/console/SwingConsolePane.java | 125 +++----------- 2 files changed, 175 insertions(+), 106 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/console/ConsolePanel.java diff --git a/src/main/java/org/scijava/ui/swing/console/ConsolePanel.java b/src/main/java/org/scijava/ui/swing/console/ConsolePanel.java new file mode 100644 index 0000000..17282a0 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/ConsolePanel.java @@ -0,0 +1,156 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +import net.miginfocom.swing.MigLayout; + +import org.scijava.console.OutputEvent; +import org.scijava.console.OutputListener; +import org.scijava.log.IgnoreAsCallingClass; +import org.scijava.thread.ThreadService; +import org.scijava.ui.swing.StaticSwingUtils; + +/** + * {@link ConsolePanel} is a {@link JPanel} holding a {@link JTextArea}. It can + * be used to display text written to System.out and System.err. Therefor it can + * be added as {@link OutputListener} to + * {@link org.scijava.console.ConsoleService}. + * + * @author Matthias Arzt + */ +@IgnoreAsCallingClass +public class ConsolePanel extends JPanel implements OutputListener +{ + private JTextPane textPane; + private JScrollPane scrollPane; + + private StyledDocument doc; + private Style stdoutLocal; + private Style stderrLocal; + private Style stdoutGlobal; + private Style stderrGlobal; + + private final ThreadService threadService; + + public ConsolePanel(ThreadService threadService) { + this.threadService = threadService; + initGui(); + } + + public void clear() { + textPane.setText(""); + } + + @Override + public void outputOccurred(OutputEvent event) { + threadService.queue(new Runnable() { + + @Override + public void run() { + final boolean atBottom = + StaticSwingUtils.isScrolledToBottom(scrollPane); + try { + doc.insertString(doc.getLength(), event.getOutput(), getStyle(event)); + } + catch (final BadLocationException exc) { + throw new RuntimeException(exc); + } + if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane); + } + }); + } + + private synchronized void initGui() { + setLayout(new MigLayout("inset 0", "[grow,fill]", "[grow,fill,align top]")); + + textPane = new JTextPane(); + textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + textPane.setEditable(false); + + doc = textPane.getStyledDocument(); + + stdoutLocal = createStyle("stdoutLocal", null, Color.black, null, null); + stderrLocal = createStyle("stderrLocal", null, Color.red, null, null); + stdoutGlobal = createStyle("stdoutGlobal", stdoutLocal, null, null, true); + stderrGlobal = createStyle("stderrGlobal", stderrLocal, null, null, true); + + // NB: We wrap the JTextPane in a JPanel to disable + // the text pane's intelligent line wrapping behavior. + // I.e.: we want console lines _not_ to wrap, but instead + // for the scroll pane to show a horizontal scroll bar. + // Thanks to: https://tips4java.wordpress.com/2009/01/25/no-wrap-text-pane/ + final JPanel textPanel = new JPanel(); + textPanel.setLayout(new BorderLayout()); + textPanel.add(textPane); + + scrollPane = new JScrollPane(textPanel); + scrollPane.setPreferredSize(new Dimension(600, 600)); + + // Make the scroll bars move at a reasonable pace. + final FontMetrics fm = scrollPane.getFontMetrics(scrollPane.getFont()); + final int charWidth = fm.charWidth('a'); + final int lineHeight = fm.getHeight(); + scrollPane.getHorizontalScrollBar().setUnitIncrement(charWidth); + scrollPane.getVerticalScrollBar().setUnitIncrement(2 * lineHeight); + + add(scrollPane); + } + // -- Helper methods -- + + private Style createStyle(final String name, final Style parent, + final Color foreground, final Boolean bold, final Boolean italic) + { + final Style style = textPane.addStyle(name, parent); + if (foreground != null) StyleConstants.setForeground(style, foreground); + if (bold != null) StyleConstants.setBold(style, bold); + if (italic != null) StyleConstants.setItalic(style, italic); + return style; + } + + private Style getStyle(final OutputEvent event) { + final boolean stderr = event.getSource() == OutputEvent.Source.STDERR; + final boolean contextual = event.isContextual(); + if (stderr) return contextual ? stderrLocal : stderrGlobal; + return contextual ? stdoutLocal : stdoutGlobal; + } + + public JTextPane getTextPane() { + return textPane; + } +} diff --git a/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java b/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java index 5001f8b..abcd26d 100644 --- a/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java +++ b/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java @@ -30,31 +30,19 @@ package org.scijava.ui.swing.console; -import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.FontMetrics; import javax.swing.JPanel; -import javax.swing.JScrollPane; import javax.swing.JTextPane; -import javax.swing.text.BadLocationException; -import javax.swing.text.Style; -import javax.swing.text.StyleConstants; -import javax.swing.text.StyledDocument; import net.miginfocom.swing.MigLayout; import org.scijava.Context; import org.scijava.console.OutputEvent; -import org.scijava.console.OutputEvent.Source; import org.scijava.plugin.Parameter; import org.scijava.thread.ThreadService; import org.scijava.ui.console.AbstractConsolePane; import org.scijava.ui.console.ConsolePane; -import org.scijava.ui.swing.StaticSwingUtils; /** * Swing implementation of {@link ConsolePane}. @@ -66,15 +54,7 @@ public class SwingConsolePane extends AbstractConsolePane { @Parameter private ThreadService threadService; - private JPanel consolePanel; - private JTextPane textPane; - private JScrollPane scrollPane; - - private StyledDocument doc; - private Style stdoutLocal; - private Style stderrLocal; - private Style stdoutGlobal; - private Style stderrGlobal; + private ConsolePanel consolePanel; /** * The console pane's containing window; e.g., a {@link javax.swing.JFrame} or @@ -82,6 +62,8 @@ public class SwingConsolePane extends AbstractConsolePane { */ private Component window; + private JPanel component; + public SwingConsolePane(final Context context) { super(context); } @@ -93,41 +75,15 @@ public void setWindow(final Component window) { this.window = window; } - public JTextPane getTextPane() { - if (consolePanel == null) initConsolePanel(); - return textPane; - } - - public JScrollPane getScrollPane() { - if (consolePanel == null) initConsolePanel(); - return scrollPane; - } - public void clear() { - if (consolePanel == null) initConsolePanel(); - textPane.setText(""); + consolePanel().clear(); } // -- ConsolePane methods -- @Override public void append(final OutputEvent event) { - if (consolePanel == null) initConsolePanel(); - threadService.queue(new Runnable() { - - @Override - public void run() { - final boolean atBottom = - StaticSwingUtils.isScrolledToBottom(scrollPane); - try { - doc.insertString(doc.getLength(), event.getOutput(), getStyle(event)); - } - catch (final BadLocationException exc) { - throw new RuntimeException(exc); - } - if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane); - } - }); + consolePanel().outputOccurred(event); } @Override @@ -146,8 +102,8 @@ public void run() { @Override public JPanel getComponent() { - if (consolePanel == null) initConsolePanel(); - return consolePanel; + if (consolePanel == null) initLoggingPanel(); + return component; } @Override @@ -157,64 +113,21 @@ public Class getComponentType() { // -- Helper methods - lazy initialization -- - private synchronized void initConsolePanel() { - if (consolePanel != null) return; - - final JPanel panel = new JPanel(); - panel.setLayout(new MigLayout("", "[grow,fill]", "[grow,fill,align top]")); - - textPane = new JTextPane(); - textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - textPane.setEditable(false); - - doc = textPane.getStyledDocument(); - - stdoutLocal = createStyle("stdoutLocal", null, Color.black, null, null); - stderrLocal = createStyle("stderrLocal", null, Color.red, null, null); - stdoutGlobal = createStyle("stdoutGlobal", stdoutLocal, null, null, true); - stderrGlobal = createStyle("stderrGlobal", stderrLocal, null, null, true); - - // NB: We wrap the JTextPane in a JPanel to disable - // the text pane's intelligent line wrapping behavior. - // I.e.: we want console lines _not_ to wrap, but instead - // for the scroll pane to show a horizontal scroll bar. - // Thanks to: https://tips4java.wordpress.com/2009/01/25/no-wrap-text-pane/ - final JPanel textPanel = new JPanel(); - textPanel.setLayout(new BorderLayout()); - textPanel.add(textPane); - - scrollPane = new JScrollPane(textPanel); - scrollPane.setPreferredSize(new Dimension(600, 600)); - - // Make the scroll bars move at a reasonable pace. - final FontMetrics fm = scrollPane.getFontMetrics(scrollPane.getFont()); - final int charWidth = fm.charWidth('a'); - final int lineHeight = fm.getHeight(); - scrollPane.getHorizontalScrollBar().setUnitIncrement(charWidth); - scrollPane.getVerticalScrollBar().setUnitIncrement(2 * lineHeight); - - panel.add(scrollPane); - - consolePanel = panel; + private ConsolePanel consolePanel() { + if (consolePanel == null) initLoggingPanel(); + return consolePanel; } - // -- Helper methods -- - - private Style createStyle(final String name, final Style parent, - final Color foreground, final Boolean bold, final Boolean italic) - { - final Style style = textPane.addStyle(name, parent); - if (foreground != null) StyleConstants.setForeground(style, foreground); - if (bold != null) StyleConstants.setBold(style, bold); - if (italic != null) StyleConstants.setItalic(style, italic); - return style; + private synchronized void initLoggingPanel() { + if (consolePanel != null) return; + consolePanel = new ConsolePanel(threadService); + component = new JPanel(new MigLayout("", "[grow]", "[grow]")); + component.add(consolePanel, "grow"); } - private Style getStyle(final OutputEvent event) { - final boolean stderr = event.getSource() == Source.STDERR; - final boolean contextual = event.isContextual(); - if (stderr) return contextual ? stderrLocal : stderrGlobal; - return contextual ? stdoutLocal : stdoutGlobal; - } + // -- Helper methods - testing -- + JTextPane getTextPane() { + return consolePanel().getTextPane(); + } } From 26bb777c3ca2a7b062501c83b36e18855d43e05c Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 22 Sep 2017 17:37:26 +0200 Subject: [PATCH 02/10] Add LoggingPanel to show LogMessages separatly At this point LoggingPanel is basically a copy of ConsolPanel. But it will envole to something very different. Features like filtering messages by log source, level and text, and options for message formatting will be added. --- .../ui/swing/console/LogFormatter.java | 68 ++++++++ .../ui/swing/console/LoggingPanel.java | 161 ++++++++++++++++++ .../ui/swing/console/SwingConsolePane.java | 21 ++- 3 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/console/LogFormatter.java create mode 100644 src/main/java/org/scijava/ui/swing/console/LoggingPanel.java diff --git a/src/main/java/org/scijava/ui/swing/console/LogFormatter.java b/src/main/java/org/scijava/ui/swing/console/LogFormatter.java new file mode 100644 index 0000000..27e90fc --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/LogFormatter.java @@ -0,0 +1,68 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2017 Board of Regents of the University of + * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck + * Institute of Molecular Cell Biology and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.scijava.log.LogLevel; +import org.scijava.log.LogMessage; + +/** + * Used by {@link LoggingPanel} to simplify formatting log messages. + * + * @author Matthias Arzt + */ +public class LogFormatter { + + public String format(LogMessage message) { + + final StringWriter sw = new StringWriter(); + final PrintWriter printer = new PrintWriter(sw); + + printWithBrackets(printer, message.time().toString()); + printWithBrackets(printer, LogLevel.prefix(message.level())); + printWithBrackets(printer, message.source().toString()); + printer.println(message.text()); + if (message.throwable() != null) + message.throwable().printStackTrace(printer); + + return sw.toString(); + } + + private void printWithBrackets(PrintWriter printer, String prefix) { + printer.print('['); + printer.print(prefix); + printer.print("] "); + } + +} diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java new file mode 100644 index 0000000..03dfe0a --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -0,0 +1,161 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.text.BadLocationException; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyledDocument; + +import net.miginfocom.swing.MigLayout; + +import org.scijava.console.OutputListener; +import org.scijava.log.IgnoreAsCallingClass; +import org.scijava.log.LogListener; +import org.scijava.log.LogMessage; +import org.scijava.log.LogService; +import org.scijava.log.Logger; +import org.scijava.thread.ThreadService; +import org.scijava.ui.swing.StaticSwingUtils; + +/** + * LoggingPanel can display log message and console output as a list, and + * provides convenient ways for the user to filter this list. + * LoggingPanel implements {@link LogListener} and {@link OutputListener}, that + * way it can receive log message and console output from {@link LogService}, + * {@link Logger} and {@link org.scijava.console.ConsoleService} + * + * @see LogService + * @see Logger + * @see org.scijava.console.ConsoleService + * @author Matthias Arzt + */ +@IgnoreAsCallingClass +public class LoggingPanel extends JPanel implements LogListener +{ + private JTextPane textPane; + private JScrollPane scrollPane; + + private StyledDocument doc; + private Style defaultStyle; + + private final LogFormatter formatter = new LogFormatter(); + + private final ThreadService threadService; + + public LoggingPanel(ThreadService threadService) { + this.threadService = threadService; + initGui(); + } + + public void clear() { + textPane.setText(""); + } + + // -- LogListener methods -- + + @Override + public void messageLogged(LogMessage message) { + appendText(formatter.format(message), defaultStyle); + } + + // -- Helper methods -- + + private void appendText(final String text, final Style style) { + threadService.queue(new Runnable() { + + @Override + public void run() { + final boolean atBottom = + StaticSwingUtils.isScrolledToBottom(scrollPane); + try { + doc.insertString(doc.getLength(), text, style); + } + catch (final BadLocationException exc) { + throw new RuntimeException(exc); + } + if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane); + } + }); + } + + private synchronized void initGui() { + setLayout(new MigLayout("inset 0", "[grow,fill]", "[grow,fill,align top]")); + + textPane = new JTextPane(); + textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + textPane.setEditable(false); + + doc = textPane.getStyledDocument(); + + defaultStyle = createStyle("stdoutLocal", null, Color.black, null, null); + + // NB: We wrap the JTextPane in a JPanel to disable + // the text pane's intelligent line wrapping behavior. + // I.e.: we want console lines _not_ to wrap, but instead + // for the scroll pane to show a horizontal scroll bar. + // Thanks to: https://tips4java.wordpress.com/2009/01/25/no-wrap-text-pane/ + final JPanel textPanel = new JPanel(); + textPanel.setLayout(new BorderLayout()); + textPanel.add(textPane); + + scrollPane = new JScrollPane(textPanel); + scrollPane.setPreferredSize(new Dimension(600, 600)); + + // Make the scroll bars move at a reasonable pace. + final FontMetrics fm = scrollPane.getFontMetrics(scrollPane.getFont()); + final int charWidth = fm.charWidth('a'); + final int lineHeight = fm.getHeight(); + scrollPane.getHorizontalScrollBar().setUnitIncrement(charWidth); + scrollPane.getVerticalScrollBar().setUnitIncrement(2 * lineHeight); + + add(scrollPane); + } + + private Style createStyle(final String name, final Style parent, + final Color foreground, final Boolean bold, final Boolean italic) + { + final Style style = textPane.addStyle(name, parent); + if (foreground != null) StyleConstants.setForeground(style, foreground); + if (bold != null) StyleConstants.setBold(style, bold); + if (italic != null) StyleConstants.setItalic(style, italic); + return style; + } + + // -- Helper methods - testing -- + + JTextPane getTextPane() { + return textPane; + } +} diff --git a/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java b/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java index abcd26d..acbf24a 100644 --- a/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java +++ b/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java @@ -32,13 +32,13 @@ import java.awt.Component; -import javax.swing.JPanel; -import javax.swing.JTextPane; +import javax.swing.*; import net.miginfocom.swing.MigLayout; import org.scijava.Context; import org.scijava.console.OutputEvent; +import org.scijava.log.LogService; import org.scijava.plugin.Parameter; import org.scijava.thread.ThreadService; import org.scijava.ui.console.AbstractConsolePane; @@ -46,6 +46,11 @@ /** * Swing implementation of {@link ConsolePane}. + *

+ * This implementation consists of a console tab and a log + * tab, provided by a {@link ConsolePanel} and {@link LoggingPanel} + * respectively. + *

* * @author Curtis Rueden */ @@ -54,8 +59,13 @@ public class SwingConsolePane extends AbstractConsolePane { @Parameter private ThreadService threadService; + @Parameter + private LogService logService; + private ConsolePanel consolePanel; + private LoggingPanel loggingPanel; + /** * The console pane's containing window; e.g., a {@link javax.swing.JFrame} or * {@link javax.swing.JInternalFrame}. @@ -121,8 +131,13 @@ private ConsolePanel consolePanel() { private synchronized void initLoggingPanel() { if (consolePanel != null) return; consolePanel = new ConsolePanel(threadService); + loggingPanel = new LoggingPanel(threadService); + logService.addLogListener(loggingPanel); component = new JPanel(new MigLayout("", "[grow]", "[grow]")); - component.add(consolePanel, "grow"); + JTabbedPane tabs = new JTabbedPane(); + tabs.addTab("Console", consolePanel); + tabs.addTab("Log", loggingPanel); + component.add(tabs, "grow"); } // -- Helper methods - testing -- From 7f29f6349248e40731d8f990912c65bd092f89b4 Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 26 May 2017 15:17:42 +0200 Subject: [PATCH 03/10] LoggingPanel: use colors to visualize log levels --- .../ui/swing/console/LoggingPanel.java | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index 03dfe0a..fe06fd4 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -33,8 +33,10 @@ import java.awt.*; import javax.swing.*; +import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; -import javax.swing.text.Style; +import javax.swing.text.MutableAttributeSet; +import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; @@ -46,6 +48,7 @@ import org.scijava.log.LogMessage; import org.scijava.log.LogService; import org.scijava.log.Logger; +import org.scijava.log.LogLevel; import org.scijava.thread.ThreadService; import org.scijava.ui.swing.StaticSwingUtils; @@ -64,11 +67,18 @@ @IgnoreAsCallingClass public class LoggingPanel extends JPanel implements LogListener { + private static final AttributeSet DEFAULT_STYLE = new SimpleAttributeSet(); + private static final AttributeSet STYLE_ERROR = normal(new Color(200, 0, 0)); + private static final AttributeSet STYLE_WARN = normal(new Color(200, 140, 0)); + private static final AttributeSet STYLE_INFO = normal(Color.BLACK); + private static final AttributeSet STYLE_DEBUG = normal(new Color(0, 0, 200)); + private static final AttributeSet STYLE_TRACE = normal(Color.GRAY); + private static final AttributeSet STYLE_OTHERS = normal(Color.GRAY); + private JTextPane textPane; private JScrollPane scrollPane; private StyledDocument doc; - private Style defaultStyle; private final LogFormatter formatter = new LogFormatter(); @@ -87,12 +97,12 @@ public void clear() { @Override public void messageLogged(LogMessage message) { - appendText(formatter.format(message), defaultStyle); + appendText(formatter.format(message), getLevelStyle(message.level())); } // -- Helper methods -- - private void appendText(final String text, final Style style) { + private void appendText(final String text, final AttributeSet style) { threadService.queue(new Runnable() { @Override @@ -119,8 +129,6 @@ private synchronized void initGui() { doc = textPane.getStyledDocument(); - defaultStyle = createStyle("stdoutLocal", null, Color.black, null, null); - // NB: We wrap the JTextPane in a JPanel to disable // the text pane's intelligent line wrapping behavior. // I.e.: we want console lines _not_ to wrap, but instead @@ -143,13 +151,32 @@ private synchronized void initGui() { add(scrollPane); } - private Style createStyle(final String name, final Style parent, - final Color foreground, final Boolean bold, final Boolean italic) - { - final Style style = textPane.addStyle(name, parent); - if (foreground != null) StyleConstants.setForeground(style, foreground); - if (bold != null) StyleConstants.setBold(style, bold); - if (italic != null) StyleConstants.setItalic(style, italic); + private static AttributeSet getLevelStyle(int i) { + switch (i) { + case LogLevel.ERROR: + return STYLE_ERROR; + case LogLevel.WARN: + return STYLE_WARN; + case LogLevel.INFO: + return STYLE_INFO; + case LogLevel.DEBUG: + return STYLE_DEBUG; + case LogLevel.TRACE: + return STYLE_TRACE; + default: + return STYLE_OTHERS; + } + } + + private static MutableAttributeSet normal(Color color) { + MutableAttributeSet style = new SimpleAttributeSet(); + StyleConstants.setForeground(style, color); + return style; + } + + private static MutableAttributeSet italic(Color color) { + MutableAttributeSet style = normal(color); + StyleConstants.setItalic(style, true); return style; } From 9e67b2446e5c3ab47eae853f1a51e4081ca6542b Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 26 May 2017 16:03:56 +0200 Subject: [PATCH 04/10] LoggingPanel: add gui for text filtering --- .../ui/swing/console/LoggingPanel.java | 33 +++-- .../ui/swing/console/TextFilterField.java | 135 ++++++++++++++++++ 2 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/console/TextFilterField.java diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index fe06fd4..1d6fccc 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -31,6 +31,7 @@ package org.scijava.ui.swing.console; import java.awt.*; +import java.util.function.Predicate; import javax.swing.*; import javax.swing.text.AttributeSet; @@ -42,26 +43,22 @@ import net.miginfocom.swing.MigLayout; -import org.scijava.console.OutputListener; import org.scijava.log.IgnoreAsCallingClass; +import org.scijava.log.LogLevel; import org.scijava.log.LogListener; import org.scijava.log.LogMessage; import org.scijava.log.LogService; import org.scijava.log.Logger; -import org.scijava.log.LogLevel; import org.scijava.thread.ThreadService; import org.scijava.ui.swing.StaticSwingUtils; /** - * LoggingPanel can display log message and console output as a list, and - * provides convenient ways for the user to filter this list. - * LoggingPanel implements {@link LogListener} and {@link OutputListener}, that - * way it can receive log message and console output from {@link LogService}, - * {@link Logger} and {@link org.scijava.console.ConsoleService} + * {@link LoggingPanel} can display log messages, and provides convenient ways + * for the user to filter this list. LoggingPanel can receive log messages from + * {@link LogService} and {@link Logger}. * * @see LogService * @see Logger - * @see org.scijava.console.ConsoleService * @author Matthias Arzt */ @IgnoreAsCallingClass @@ -75,12 +72,14 @@ public class LoggingPanel extends JPanel implements LogListener private static final AttributeSet STYLE_TRACE = normal(Color.GRAY); private static final AttributeSet STYLE_OTHERS = normal(Color.GRAY); + private final TextFilterField textFilter = new TextFilterField(" Text Search (Alt-F)"); private JTextPane textPane; private JScrollPane scrollPane; private StyledDocument doc; private final LogFormatter formatter = new LogFormatter(); + private Predicate filter = text -> true; private final ThreadService threadService; @@ -103,6 +102,8 @@ public void messageLogged(LogMessage message) { // -- Helper methods -- private void appendText(final String text, final AttributeSet style) { + if(!filter.test(text)) return; + threadService.queue(new Runnable() { @Override @@ -120,8 +121,14 @@ public void run() { }); } - private synchronized void initGui() { - setLayout(new MigLayout("inset 0", "[grow,fill]", "[grow,fill,align top]")); + // -- Helper methods -- + + private void initGui() { + + setLayout(new MigLayout("insets 0", "[grow]", "[][grow]")); + + textFilter.setChangeListener(this::updateFilter); + add(textFilter.getComponent(), "grow, wrap"); textPane = new JTextPane(); textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); @@ -148,7 +155,11 @@ private synchronized void initGui() { scrollPane.getHorizontalScrollBar().setUnitIncrement(charWidth); scrollPane.getVerticalScrollBar().setUnitIncrement(2 * lineHeight); - add(scrollPane); + add(scrollPane, "grow"); + } + + private void updateFilter() { + filter = textFilter.getFilter(); } private static AttributeSet getLevelStyle(int i) { diff --git a/src/main/java/org/scijava/ui/swing/console/TextFilterField.java b/src/main/java/org/scijava/ui/swing/console/TextFilterField.java new file mode 100644 index 0000000..e03326d --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/TextFilterField.java @@ -0,0 +1,135 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.awt.*; +import java.util.function.Predicate; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * {@link TextFilterField} provides a {@link JTextField} and derives a text + * filter from the content. + * + * @author Matthias Arzt + */ +class TextFilterField { + + private final JTextField textField = new JTextField(); + + private JLabel prompt = new JLabel(); + + private Predicate filter = null; + + private Runnable changeListener = null; + + // -- constructor -- + + TextFilterField(String textForPrompt) { + initPrompt(textForPrompt); + textField.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void insertUpdate(DocumentEvent documentEvent) { + onUserInputChanged(); + } + + @Override + public void removeUpdate(DocumentEvent documentEvent) { + onUserInputChanged(); + } + + @Override + public void changedUpdate(DocumentEvent documentEvent) { + onUserInputChanged(); + } + }); + } + + // -- TextFilterField methods -- + + public JTextField getComponent() { + return textField; + } + + public void setChangeListener(Runnable changeListener) { + this.changeListener = changeListener; + } + + public Predicate getFilter() { + if (filter == null) filter = calculateFilter(); + return filter; + } + + // -- Helper methods -- + + private Predicate calculateFilter() { + String text = textField.getText(); + final String[] words = text.split(" "); + return s -> { + for (String word : words) + if (!s.contains(word)) return false; + return true; + }; + } + + private void onUserInputChanged() { + filter = null; + updatePromptVisibility(); + notifyChangeListener(); + } + + private void notifyChangeListener() { + if (changeListener != null) changeListener.run(); + } + + private void initPrompt(String text) { + prompt.setText(text); + prompt.setFont(textField.getFont().deriveFont(Font.ITALIC)); + prompt.setForeground(changeAlpha(textField.getForeground(), 128)); + prompt.setBorder(new EmptyBorder(textField.getInsets())); + prompt.setHorizontalAlignment(JLabel.LEADING); + textField.setLayout(new BorderLayout()); + textField.add(prompt); + updatePromptVisibility(); + } + + private static Color changeAlpha(Color color, int alpha) { + return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } + + private void updatePromptVisibility() { + prompt.setVisible(textField.getDocument().getLength() == 0); + } +} From 7e47d9dabd97c31c1cf4207985cf3556d4a4d634 Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 22 Sep 2017 17:16:29 +0200 Subject: [PATCH 05/10] LoggingPanel: apply text filtering to recorded log messages This requiers to actually store and log messages that are received by LoggingPanel, in correct order and with meta data. (LogRecorder is used for this purpose.) Whenever the user changes the text to filter for, it is neccessary to reprocess and redisplay the recorded log messages. This might require to much time to be processed in the swing thread, therefore the new class ItemTextPane is introduced. It can display an expanding list of items, and uses and worker thread when the list is replaced. --- .../ui/swing/console/ItemTextPane.java | 228 ++++++++++++++++++ .../scijava/ui/swing/console/LogRecorder.java | 205 ++++++++++++++++ .../ui/swing/console/LoggingPanel.java | 121 ++++------ .../ui/swing/console/ItemTextPaneTest.java | 69 ++++++ .../ui/swing/console/LogRecorderTest.java | 191 +++++++++++++++ 5 files changed, 740 insertions(+), 74 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/console/ItemTextPane.java create mode 100644 src/main/java/org/scijava/ui/swing/console/LogRecorder.java create mode 100644 src/test/java/org/scijava/ui/swing/console/ItemTextPaneTest.java create mode 100644 src/test/java/org/scijava/ui/swing/console/LogRecorderTest.java diff --git a/src/main/java/org/scijava/ui/swing/console/ItemTextPane.java b/src/main/java/org/scijava/ui/swing/console/ItemTextPane.java new file mode 100644 index 0000000..ca62b2a --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/ItemTextPane.java @@ -0,0 +1,228 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.awt.Font; +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; + +import javax.swing.*; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.StyledDocument; + +import org.scijava.thread.ThreadService; +import org.scijava.ui.swing.StaticSwingUtils; + +/** + * {@link ItemTextPane} provides a {@link JTextPane} in a {@link JScrollPane}. + * The content is provided as {@link Iterator} of {@link ItemTextPane.Item}. + * ItemTextPane is used to display a large list of items, which is often + * replaced and extended. + *

+ * Replacing the entire list requires an update to the {@link StyledDocument}. + * This is performed in a worker thread; as a result, the event dispatch thread + * will not be blocked. + *

+ * An {@link Item} can be tagged and incomplete. If this is the case the item + * will be removed, when the next item with the same tag is displayed. + *

+ * {@link ItemTextPane} is used in {@link LoggingPanel}. + * + * @author Matthias Arzt + */ +class ItemTextPane { + + private final ThreadService threadService; + + private DocumentCalculator initialCalculator = null; + + private JTextPane textPane = new JTextPane(); + + private JScrollPane scrollPane = new JScrollPane(textPane); + + private boolean waitingForProcessNewItems = false; + + private DocumentCalculator calculator = + new DocumentCalculator(Collections. emptyList().iterator()); + + // -- constructor -- + + ItemTextPane(ThreadService threadService) { + this.threadService = Objects.requireNonNull(threadService); + textPane.setEditable(false); + textPane.setFont(new Font("monospaced", Font.PLAIN, 12)); + } + + // -- ItemTextPane methods -- + + JComponent getJComponent() { + return scrollPane; + } + + public void setPopupMenu(JPopupMenu menu) { + textPane.setComponentPopupMenu(menu); + } + + /** + * Set the {@link ItemTextPane.Item}s to be displayed in the + * {@link JTextPane}. + * + * @param data The iterator will be used by a SwingWorker, and the + * SwingThread. NB: Each time {@link ItemTextPane#update()} is called + * {@link Iterator#hasNext()} will be called again (even if it + * returned false before) to check if maybe the iterator provides new + * items. + */ + public void setData(Iterator data) { + calculator.cancel(); + if (initialCalculator != null) initialCalculator.cancel(); + DocumentCalculator calculator = new DocumentCalculator(data); + initialCalculator = calculator; + threadService.run(() -> initCalculator(calculator)); + } + + /** + * This initiates to check {@link Iterator#hasNext()} of iterator previously + * set with {@link #setData(Iterator)} again. If {@link Iterator#hasNext()} + * returns true, the new items will be red from the Iterator, and displayed. + */ + public void update() { + if (waitingForProcessNewItems) return; + waitingForProcessNewItems = true; + threadService.queue(this::processNewItemsInSwingThread); + } + + /** Copy selected text to the clipboard. */ + public void copySelectionToClipboard() { + textPane.copy(); + } + + // -- Helper methods -- + + private void processNewItemsInSwingThread() { + if (calculator.isCanceled()) return; + + waitingForProcessNewItems = false; + boolean atBottom = StaticSwingUtils.isScrolledToBottom(scrollPane); + calculator.update(); + if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane); + } + + private void initCalculator(DocumentCalculator calculator) { + calculator.update(); + if (calculator.isCanceled()) return; + threadService.queue(() -> applyCalculator(calculator)); + } + + private void applyCalculator(DocumentCalculator calculator) { + if (initialCalculator != calculator) return; + this.calculator = calculator; + textPane.setDocument(calculator.document()); + processNewItemsInSwingThread(); + threadService.queue(() -> StaticSwingUtils.scrollToBottom(scrollPane)); + } + + // -- Helper methods - testing -- + + JTextPane getTextPane() { + return textPane; + } + + // -- public Helper classes -- + + public static class Item { + + private final String text; + private final AttributeSet style; + + Item(AttributeSet style, String text) { + this.style = style; + this.text = text; + } + + final String text() { + return text; + } + + final AttributeSet style() { + return style; + } + } + + // -- Helper classes -- + + /** + * {@link DocumentCalculator} is used to calculate a {@link StyledDocument} + * for a given {@link Iterator} of {@link Item}s. NB: Items can be incomplete + * and tagged. Such incomplete tagged Item will be replaced by the following + * item with the same tag. + */ + static class DocumentCalculator { + + private final Iterator data; + + private final StyledDocument document = new DefaultStyledDocument(); + + private boolean canceled = false; + + DocumentCalculator(Iterator data) { + this.data = data; + } + + public StyledDocument document() { + return document; + } + + public boolean isCanceled() { + return canceled; + } + + public void cancel() { + canceled = true; + } + + public synchronized void update() { + while (data.hasNext() && !canceled) + addText(data.next()); + } + + private void addText(Item item) { + try { + document.insertString(document.getLength(), item.text(), item.style()); + } catch (BadLocationException e) { + // ignore + } + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/console/LogRecorder.java b/src/main/java/org/scijava/ui/swing/console/LogRecorder.java new file mode 100644 index 0000000..702d3d2 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/LogRecorder.java @@ -0,0 +1,205 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2017 Board of Regents of the University of + * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck + * Institute of Molecular Cell Biology and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.scijava.log.CallingClassUtils; +import org.scijava.log.IgnoreAsCallingClass; +import org.scijava.log.LogListener; +import org.scijava.log.LogMessage; + +/** + * {@link LogRecorder} is used to decouple the GUI displaying log messages from + * the potentially highly concurrent code emitting log messages. + *

+ * The recorded {@link LogMessage}s are stored in a list. New messages can only + * be added to the end of the list. The iterators never fail and are always + * updated. + *

+ * + * @author Matthias Arzt + */ +@IgnoreAsCallingClass +public class LogRecorder implements LogListener, Iterable { + + private ConcurrentExpandableList recorded = + new ConcurrentExpandableList<>(); + + private List observers = new CopyOnWriteArrayList<>(); + + private boolean recordCallingClass = false; + + /** + * The {@link Runnable} observer will be executed, after every new log message + * or text recorded. The code executed by the {@link Runnable} must by highly + * thread safe and must not use any kind of locks. + */ + public void addObservers(Runnable observer) { + observers.add(observer); + } + + public void removeObserver(Runnable observer) { + observers.remove(observer); + } + + /** + * The returned Iterator never fails, and will always be updated. Even if an + * message is added after the iterator reached the end of the list, + * {@link Iterator#hasNext()} will return true again, and + * {@link Iterator#next()} will return the new log messages element. + */ + @Override + public Iterator iterator() { + return recorded.iterator(); + } + + public Stream stream() { + return recorded.stream(); + } + + /** + * Same as {@link #iterator()}, but the Iterator will only return log messages + * and text recorded after the iterator has been created. + */ + public Iterator iteratorAtEnd() { + return recorded.iteratorAtEnd(); + } + + public void clear() { + recorded.clear(); + } + + public boolean isRecordCallingClass() { + return recordCallingClass; + } + + public void setRecordCallingClass(boolean enable) { + this.recordCallingClass = enable; + } + + // -- LogListener methods -- + + @Override + public void messageLogged(LogMessage message) { + if (recordCallingClass) message.attach(CallingClassUtils.getCallingClass()); + recorded.add(message); + notifyListeners(); + } + + // -- Helper methods -- + + private void notifyListeners() { + for (Runnable listener : observers) + listener.run(); + } + + /** + * This Container manages a list of items. Items can only be added to end of + * the list. It's possible to add items, while iterating over the list. + * Iterators never fail, and they will always be updated. Even if an element + * is added after an iterator reached the end of the list, + * {@link Iterator#hasNext()} will return true again, and + * {@link Iterator#next()} will return the newly added element. This Container + * is fully thread safe. + * + * @author Matthias Arzt + */ + private class ConcurrentExpandableList implements Iterable { + + private final AtomicLong lastKey = new AtomicLong(0); + + private long firstKey = 0; + + private final Map map = new ConcurrentHashMap<>(); + + public Stream stream() { + Spliterator spliterator = Spliterators.spliteratorUnknownSize( + iterator(), Spliterator.ORDERED); + return StreamSupport.stream(spliterator, /* parallel */ false); + } + + @Override + public Iterator iterator() { + return new MyIterator(firstKey); + } + + public Iterator iteratorAtEnd() { + return new MyIterator(lastKey.get()); + } + + public long add(T value) { + long key = lastKey.getAndIncrement(); + map.put(key, value); + return key; + } + + public void clear() { + map.clear(); + firstKey = lastKey.get(); + } + + private class MyIterator implements Iterator { + + private long nextIndex; + + public MyIterator(long nextIndex) { + this.nextIndex = nextIndex; + } + + @Override + public boolean hasNext() { + return map.containsKey(nextIndex); + } + + @Override + public T next() { + T value = map.get(nextIndex); + if(value == null) + throw new NoSuchElementException(); + nextIndex++; + return value; + } + } + } + +} diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index 1d6fccc..add2202 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -32,17 +32,17 @@ import java.awt.*; import java.util.function.Predicate; +import java.util.stream.Stream; import javax.swing.*; import javax.swing.text.AttributeSet; -import javax.swing.text.BadLocationException; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; -import javax.swing.text.StyledDocument; import net.miginfocom.swing.MigLayout; +import org.scijava.Context; import org.scijava.log.IgnoreAsCallingClass; import org.scijava.log.LogLevel; import org.scijava.log.LogListener; @@ -50,7 +50,6 @@ import org.scijava.log.LogService; import org.scijava.log.Logger; import org.scijava.thread.ThreadService; -import org.scijava.ui.swing.StaticSwingUtils; /** * {@link LoggingPanel} can display log messages, and provides convenient ways @@ -59,12 +58,13 @@ * * @see LogService * @see Logger + * * @author Matthias Arzt */ @IgnoreAsCallingClass public class LoggingPanel extends JPanel implements LogListener { - private static final AttributeSet DEFAULT_STYLE = new SimpleAttributeSet(); + private static final AttributeSet STYLE_ERROR = normal(new Color(200, 0, 0)); private static final AttributeSet STYLE_WARN = normal(new Color(200, 140, 0)); private static final AttributeSet STYLE_INFO = normal(Color.BLACK); @@ -72,94 +72,73 @@ public class LoggingPanel extends JPanel implements LogListener private static final AttributeSet STYLE_TRACE = normal(Color.GRAY); private static final AttributeSet STYLE_OTHERS = normal(Color.GRAY); - private final TextFilterField textFilter = new TextFilterField(" Text Search (Alt-F)"); - private JTextPane textPane; - private JScrollPane scrollPane; + private final TextFilterField textFilter = + new TextFilterField(" Text Search (Alt-F)"); + private final ItemTextPane textArea; - private StyledDocument doc; + private final LogFormatter logFormatter = new LogFormatter(); - private final LogFormatter formatter = new LogFormatter(); - private Predicate filter = text -> true; + private LogRecorder recorder; - private final ThreadService threadService; + // -- constructor -- + + public LoggingPanel(Context context) { + this(context.getService(ThreadService.class)); + } public LoggingPanel(ThreadService threadService) { - this.threadService = threadService; + textArea = new ItemTextPane(threadService); initGui(); + setRecorder(new LogRecorder()); + } + + // --- LoggingPanel methods -- + + public void setRecorder(LogRecorder recorder) { + if (recorder != null) recorder.removeObserver(textArea::update); + this.recorder = recorder; + updateFilter(); + recorder.addObservers(textArea::update); } public void clear() { - textPane.setText(""); + recorder.clear(); + updateFilter(); } // -- LogListener methods -- @Override public void messageLogged(LogMessage message) { - appendText(formatter.format(message), getLevelStyle(message.level())); - } - - // -- Helper methods -- - - private void appendText(final String text, final AttributeSet style) { - if(!filter.test(text)) return; - - threadService.queue(new Runnable() { - - @Override - public void run() { - final boolean atBottom = - StaticSwingUtils.isScrolledToBottom(scrollPane); - try { - doc.insertString(doc.getLength(), text, style); - } - catch (final BadLocationException exc) { - throw new RuntimeException(exc); - } - if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane); - } - }); + recorder.messageLogged(message); } // -- Helper methods -- private void initGui() { + textFilter.setChangeListener(this::updateFilter); - setLayout(new MigLayout("insets 0", "[grow]", "[][grow]")); + textArea.getJComponent().setPreferredSize(new Dimension(200, 100)); - textFilter.setChangeListener(this::updateFilter); - add(textFilter.getComponent(), "grow, wrap"); - - textPane = new JTextPane(); - textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - textPane.setEditable(false); - - doc = textPane.getStyledDocument(); - - // NB: We wrap the JTextPane in a JPanel to disable - // the text pane's intelligent line wrapping behavior. - // I.e.: we want console lines _not_ to wrap, but instead - // for the scroll pane to show a horizontal scroll bar. - // Thanks to: https://tips4java.wordpress.com/2009/01/25/no-wrap-text-pane/ - final JPanel textPanel = new JPanel(); - textPanel.setLayout(new BorderLayout()); - textPanel.add(textPane); - - scrollPane = new JScrollPane(textPanel); - scrollPane.setPreferredSize(new Dimension(600, 600)); - - // Make the scroll bars move at a reasonable pace. - final FontMetrics fm = scrollPane.getFontMetrics(scrollPane.getFont()); - final int charWidth = fm.charWidth('a'); - final int lineHeight = fm.getHeight(); - scrollPane.getHorizontalScrollBar().setUnitIncrement(charWidth); - scrollPane.getVerticalScrollBar().setUnitIncrement(2 * lineHeight); - - add(scrollPane, "grow"); + this.setLayout(new MigLayout("insets 0", "[grow]", "[][grow]")); + this.add(textFilter.getComponent(), "grow, wrap"); + this.add(textArea.getJComponent(), "grow"); } private void updateFilter() { - filter = textFilter.getFilter(); + final Predicate quickSearchFilter = textFilter.getFilter(); + Stream stream = recorder.stream().map(this::wrapLogMessage) + .filter(item -> quickSearchFilter.test(item.text())); + textArea.setData(stream.iterator()); + } + + private ItemTextPane.Item wrapLogMessage(LogMessage message) { + return new ItemTextPane.Item(getLevelStyle(message.level()), + logFormatter.format(message)); + } + + private static String appendLn(String text) { + return text.endsWith("\n") ? text : text + "\n"; } private static AttributeSet getLevelStyle(int i) { @@ -185,15 +164,9 @@ private static MutableAttributeSet normal(Color color) { return style; } - private static MutableAttributeSet italic(Color color) { - MutableAttributeSet style = normal(color); - StyleConstants.setItalic(style, true); - return style; - } - // -- Helper methods - testing -- JTextPane getTextPane() { - return textPane; + return textArea.getTextPane(); } } diff --git a/src/test/java/org/scijava/ui/swing/console/ItemTextPaneTest.java b/src/test/java/org/scijava/ui/swing/console/ItemTextPaneTest.java new file mode 100644 index 0000000..80f15f0 --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/console/ItemTextPaneTest.java @@ -0,0 +1,69 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import org.junit.Test; + +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.SimpleAttributeSet; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Tests {@link ItemTextPane} + * + * @author Matthias Arzt + */ +public class ItemTextPaneTest { + + private AttributeSet style = new SimpleAttributeSet(); + + private List list = Arrays.asList( + new ItemTextPane.Item(style, "XYZ\n"), + new ItemTextPane.Item(style, "Foo "), + new ItemTextPane.Item(style, "Bar"), + new ItemTextPane.Item(style, "\n"), + new ItemTextPane.Item(style, "Hello ") + ); + + @Test + public void testCombiningItems() throws BadLocationException { + ItemTextPane.DocumentCalculator calculator = new ItemTextPane.DocumentCalculator(list.iterator()); + calculator.update(); + Document doc = calculator.document(); + assertEquals("XYZ\nFoo Bar\nHello ", doc.getText(0, doc.getLength())); + } +} diff --git a/src/test/java/org/scijava/ui/swing/console/LogRecorderTest.java b/src/test/java/org/scijava/ui/swing/console/LogRecorderTest.java new file mode 100644 index 0000000..bd896aa --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/console/LogRecorderTest.java @@ -0,0 +1,191 @@ +/* + * #%L + * SciJava Common shared library for SciJava software. + * %% + * Copyright (C) 2009 - 2017 Board of Regents of the University of + * Wisconsin-Madison, Broad Institute of MIT and Harvard, and Max Planck + * Institute of Molecular Cell Biology and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Before; +import org.junit.Test; +import org.scijava.log.LogLevel; +import org.scijava.log.LogMessage; +import org.scijava.log.LogSource; + +/** + * Tests {@link LogRecorderTest}. + * + * @author Matthias Arzt + */ +public class LogRecorderTest { + + private LogRecorder recorder; + private MyListener listener; + + @Before + public void setup() { + recorder = new LogRecorder(); + listener = new MyListener(recorder); + } + + @Test + public void testLogMessageDelivery() { + recorder.clear(); + LogMessage message = newLogMessage(); + recorder.messageLogged(message); + assertEquals(Arrays.asList(message), listener.messages()); + } + + private LogMessage newLogMessage() { + return new LogMessage(LogSource.newRoot(), LogLevel.INFO, "Hello World!"); + } + + @Test + public void testReadOldLog() { + LogRecorder recorder = new LogRecorder(); + LogMessage message = newLogMessage(); + recorder.messageLogged(message); + assertEquals(Arrays.asList(message), recorder.stream().collect(Collectors + .toList())); + } + + @Test + public void testUpdatedReading() { + // setup + LogRecorder recorder = new LogRecorder(); + LogMessage msgA = newLogMessage(); + LogMessage msgB = newLogMessage(); + Iterator iterator = recorder.iterator(); + // process & test + assertFalse(iterator.hasNext()); + recorder.messageLogged(msgA); + assertTrue(iterator.hasNext()); + assertSame(msgA, iterator.next()); + assertFalse(iterator.hasNext()); + recorder.messageLogged(msgB); + assertTrue(iterator.hasNext()); + assertSame(msgB, iterator.next()); + } + + @Test + public void testUpdatedStream() { + // setup + LogRecorder recorder = new LogRecorder(); + LogMessage msgA = newLogMessage(); + LogMessage msgB = newLogMessage(); + Iterator iterator = recorder.stream().iterator(); + // process & test + assertFalse(iterator.hasNext()); + recorder.messageLogged(msgA); + assertTrue(iterator.hasNext()); + assertEquals(msgA, iterator.next()); + assertFalse(iterator.hasNext()); + recorder.messageLogged(msgB); + assertTrue(iterator.hasNext()); + assertEquals(msgB, iterator.next()); + } + + @Test + public void testRecordCallingClass() { + listener.clear(); + recorder.setRecordCallingClass(true); + recorder.messageLogged(newLogMessage()); + recorder.setRecordCallingClass(false); + LogMessage message = listener.messages().get(0); + assertTrue(message.attachments().contains(this.getClass())); + } + + @Test + public void testIsRecordCallingClass() { + recorder.setRecordCallingClass(true); + assertTrue(recorder.isRecordCallingClass()); + recorder.setRecordCallingClass(false); + assertFalse(recorder.isRecordCallingClass()); + } + + @Test + public void testIteratorAtEnd() { + LogMessage messageA = newLogMessage(); + recorder.messageLogged(messageA); + Iterator iterator = recorder.iteratorAtEnd(); + LogMessage messageB = newLogMessage(); + recorder.messageLogged(messageB); + assertEquals(messageB, iterator.next()); + } + + @Test + public void testClear() { + LogRecorder recorder = new LogRecorder(); + LogMessage messageA = newLogMessage(); + recorder.messageLogged(messageA); + recorder.clear(); + Iterator iterator = recorder.iterator(); + LogMessage messageB = newLogMessage(); + recorder.messageLogged(messageB); + assertTrue(iterator.hasNext()); + assertEquals(messageB, iterator.next()); + } + + private static class MyListener implements Runnable { + + private final Iterator iterator; + + private List messages = new LinkedList<>(); + + private MyListener(LogRecorder recorder) { + this.iterator = recorder.iterator(); + recorder.addObservers(this); + } + + @Override + public void run() { + while (iterator.hasNext()) { + messages.add(iterator.next()); + } + } + + public void clear() { + messages.clear(); + } + + public List messages() { + return messages; + } + } +} From 4f568eb363b129c738301857fbd6f408a1ac1b2c Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 26 May 2017 15:37:51 +0200 Subject: [PATCH 06/10] LoggingPanel: add popup menu for copy and clear --- .../ui/swing/console/LoggingPanel.java | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index add2202..2448cb2 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -31,10 +31,12 @@ package org.scijava.ui.swing.console; import java.awt.*; +import java.awt.event.ActionEvent; import java.util.function.Predicate; import java.util.stream.Stream; import javax.swing.*; +import javax.swing.plaf.basic.BasicArrowButton; import javax.swing.text.AttributeSet; import javax.swing.text.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; @@ -76,6 +78,8 @@ public class LoggingPanel extends JPanel implements LogListener new TextFilterField(" Text Search (Alt-F)"); private final ItemTextPane textArea; + private final JPanel textFilterPanel = new JPanel(); + private final LogFormatter logFormatter = new LogFormatter(); private LogRecorder recorder; @@ -101,6 +105,18 @@ public void setRecorder(LogRecorder recorder) { recorder.addObservers(textArea::update); } + public void setTextFilterVisible(boolean visible) { + textFilterPanel.setVisible(visible); + } + + public void copySelectionToClipboard() { + textArea.copySelectionToClipboard(); + } + + public void focusTextFilter() { + textFilter.getComponent().requestFocus(); + } + public void clear() { recorder.clear(); updateFilter(); @@ -118,11 +134,63 @@ public void messageLogged(LogMessage message) { private void initGui() { textFilter.setChangeListener(this::updateFilter); + JPopupMenu menu = initMenu(); + + JButton menuButton = new BasicArrowButton(BasicArrowButton.SOUTH); + menuButton.addActionListener(a -> + menu.show(menuButton, 0, menuButton.getHeight())); + + textFilterPanel.setLayout(new MigLayout("insets 0", "[][grow]", "[]")); + textFilterPanel.add(menuButton); + textFilterPanel.add(textFilter.getComponent(), "grow"); + + textArea.setPopupMenu(menu); textArea.getJComponent().setPreferredSize(new Dimension(200, 100)); this.setLayout(new MigLayout("insets 0", "[grow]", "[][grow]")); - this.add(textFilter.getComponent(), "grow, wrap"); + this.add(textFilterPanel, "grow, wrap"); this.add(textArea.getJComponent(), "grow"); + + registerKeyStroke("alt F", "focusTextFilter", this::focusTextFilter); + } + + private void registerKeyStroke(String keyStroke, String id, final Runnable action) { + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke + .getKeyStroke(keyStroke), id); + getActionMap().put(id, new AbstractAction() { + + @Override + public void actionPerformed(ActionEvent actionEvent) { + action.run(); + } + }); + } + + private JPopupMenu initMenu() { + JPopupMenu menu = new JPopupMenu(); + menu.add(newMenuItem("Copy", "control C", + this::copySelectionToClipboard)); + registerKeyStroke("control C", "copyToClipBoard", + this::copySelectionToClipboard); + menu.add(newMenuItem("Clear", "alt C", + this::clear)); + registerKeyStroke("alt C", "clearLoggingPanel", + this::clear); + return menu; + } + + static private JMenuItem newMenuItem(String text, String keyStroke, + Runnable runnable) + { + JMenuItem item = newMenuItem(text, runnable); + item.setAccelerator(KeyStroke.getKeyStroke(keyStroke)); + return item; + } + + static private JMenuItem newMenuItem(String text, Runnable runnable) { + JMenuItem item = new JMenuItem(text); + item.addActionListener(actionEvent -> runnable.run()); + return item; } private void updateFilter() { From 2fdb44ae993dd8fb7a965186d461a62c5c801bcc Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Fri, 26 May 2017 15:43:29 +0200 Subject: [PATCH 07/10] LoggingPanel: add gui to filter log messages by source and level --- .../ui/swing/console/LogSourcesPanel.java | 347 ++++++++++++++++++ .../ui/swing/console/LoggingPanel.java | 74 +++- 2 files changed, 409 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/console/LogSourcesPanel.java diff --git a/src/main/java/org/scijava/ui/swing/console/LogSourcesPanel.java b/src/main/java/org/scijava/ui/swing/console/LogSourcesPanel.java new file mode 100644 index 0000000..1f755ae --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/console/LogSourcesPanel.java @@ -0,0 +1,347 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.awt.*; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; + +import org.scijava.log.LogLevel; +import org.scijava.log.LogMessage; +import org.scijava.log.LogSource; + +/** + * {@link LogSourcesPanel} is a {@link JPanel}, that contains a tree view, where + * {@link LogSource}s are listed. For each log source the user may select a set + * of visible log levels. + * + * @author Matthias Arzt + */ +class LogSourcesPanel extends JPanel { + + private static final EnumSet VALID_LEVELS = EnumSet.range(Level.ERROR, + Level.TRACE); + + private final JPopupMenu menu = new JPopupMenu(); + + private final Map sourceItems = new HashMap<>(); + private JTree tree; + private DefaultTreeModel treeModel; + private DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Log Sources:"); + + private Predicate filter = message -> true; + private Runnable changeListener = null; + private List selected; + + // -- constructor -- + + LogSourcesPanel(JButton reloadButton) { + initMenu(); + initTreeView(); + JButton visibilityButton = initVisibilityButton(); + setLayout(new MigLayout("inset 0", "[grow]", "[][grow][]")); + add(new JLabel("Log Sources:"), "grow, wrap"); + add(new JScrollPane(tree), "grow, wrap"); + add(new JLabel(""), "split 3, grow"); + add(reloadButton); + add(visibilityButton); + actionWhenFocusLost(() -> tree.clearSelection(), Arrays.asList(tree, visibilityButton)); + } + + // -- LogLevelPanel methods -- + + public void setChangeListener(Runnable changeListener) { + this.changeListener = changeListener; + } + + public Predicate getFilter() { + if (filter == null) updateFilter(); + return filter; + } + + public void updateSources(Set logSources) { + for (LogSource sources : logSources) + getItem(sources); + treeModel.reload(); + } + + // -- Helper methods -- + + private void initTreeView() { + treeModel = new DefaultTreeModel(rootNode); + tree = new JTree(treeModel); + tree.setRootVisible(false); + DefaultTreeCellRenderer cellRenderer = new DefaultTreeCellRenderer(); + cellRenderer.setIcon(null); + cellRenderer.setLeafIcon(null); + cellRenderer.setOpenIcon(null); + tree.setCellRenderer(cellRenderer); + tree.setComponentPopupMenu(menu); + tree.setEditable(false); + tree.setShowsRootHandles(true); + tree.addTreeSelectionListener(this::selectionChanged); + } + + private void selectionChanged(TreeSelectionEvent treeSelectionEvent) { + selected = getSelectedItems().map(item -> item.source).collect(Collectors.toList()); + settingsChanged(); + } + + private JButton initVisibilityButton() { + JButton button = new JButton("visibility"); + button.addActionListener(a -> menu.show(button, 0, button.getHeight())); + return button; + } + + private void initMenu() { + EnumSet levels = EnumSet.range(Level.ERROR, Level.TRACE); + addMenuItemPerLevel(EnumSet.of(Level.TRACE), level -> "show all", + this::onShowErrorUpToClicked); + addMenuItemPerLevel(EnumSet.of(Level.NONE), level -> "hide all", + this::onShowNoneClicked); + menu.addSeparator(); + addMenuItemPerLevel(levels, level -> "show " + level.toString(), + this::onShowLogLevelClicked); + menu.addSeparator(); + addMenuItemPerLevel(levels, level -> "hide " + level.toString(), + this::onHideLogLevelClicked); + menu.addSeparator(); + addMenuItemPerLevel(levels, LogSourcesPanel::listLevelsErrorTo, + this::onShowErrorUpToClicked); + } + + private void addMenuItemPerLevel(EnumSet levels, + Function title, Consumer consumer) + { + for (Level level : levels) { + JMenuItem menuItem = new JMenuItem(title.apply(level)); + menuItem.addActionListener(a -> consumer.accept(level)); + menu.add(menuItem); + } + } + + private void onShowLogLevelClicked(Level level) { + modifyFilterOnSelection(filter -> { + filter.add(level); + return filter; + }); + } + + private void onHideLogLevelClicked(Level level) { + modifyFilterOnSelection(filter -> { + filter.remove(level); + return filter; + }); + } + + private void onShowErrorUpToClicked(Level level) { + EnumSet enumSet = EnumSet.range(Level.ERROR, level); + modifyFilterOnSelection(ignore -> enumSet); + } + + private void onShowNoneClicked(Level level) { + EnumSet enumSet = EnumSet.noneOf(Level.class); + modifyFilterOnSelection(ignore -> enumSet); + } + + private void modifyFilterOnSelection( + Function, EnumSet> itemConsumer) + { + getSelectedItems().forEach(item -> { + item.setLevelSet(itemConsumer.apply(item.levels)); + treeModel.nodeChanged(item.node); + }); + settingsChanged(); + } + + private Stream getSelectedItems() { + TreePath[] selectionPaths = tree.getSelectionPaths(); + if (selectionPaths == null) return Collections. emptyList().stream(); + return Stream.of(selectionPaths).map(path -> getItem( + (DefaultMutableTreeNode) path.getLastPathComponent())); + } + + private void settingsChanged() { + filter = null; + if (changeListener != null) changeListener.run(); + } + + private Item getItem(DefaultMutableTreeNode node) { + return (Item) node.getUserObject(); + } + + private Item getItem(LogSource source) { + Item existing = sourceItems.get(source); + return existing == null ? initItem(source) : existing; + } + + private Item initItem(LogSource source) { + Item item = new Item(source); + sourceItems.put(item.source, item); + DefaultMutableTreeNode parent = source.isRoot() ? + rootNode : getItem(source.parent()).node; + parent.add(item.node); + return item; + } + + private void updateFilter() { + Map> filterData = new HashMap<>(); + EnumSet none = EnumSet.noneOf(Level.class); + sourceItems.forEach((name, item) -> filterData.put(name, item.visible + ? item.levels : none)); + Set selectedSources = new HashSet<>(selected); + filter = message -> { + if(!selectedSources.isEmpty() && !selectedSources.contains(message.source())) + return false; + EnumSet logLevels = filterData.get(message.source()); + if (logLevels == null) return true; + return logLevels.contains(Level.of(message.level())); + }; + } + + private static String listLevelsErrorTo(Level max) { + return enumSetToString(EnumSet.range(Level.ERROR, max)); + } + + private static String enumSetToString(EnumSet levels) { + StringJoiner s = new StringJoiner(", "); + for (Level level : levels) + s.add(level.toString()); + return s.toString(); + } + + private static void actionWhenFocusLost(Runnable action, List components) { + FocusListener l = new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + } + + @Override + public void focusLost(FocusEvent e) { + boolean keepSelection = components.contains(e.getOppositeComponent()) || e.isTemporary(); + if (!keepSelection) action.run(); + } + }; + components.forEach(c -> c.addFocusListener(l)); + } + + // -- Helper classes -- + + private static class Item { + + DefaultMutableTreeNode node; + LogSource source; + EnumSet levels; + boolean visible; + + public Item(LogSource source) { + this.levels = EnumSet.range(Level.ERROR, Level.TRACE); + this.source = source; + this.visible = true; + this.node = new DefaultMutableTreeNode(this); + } + + public String toString() { + String name = source.isRoot() ? "ROOT" : source.name(); + return name + " " + getLevelString(); + } + + private String getLevelString() { + if (levels.equals(VALID_LEVELS)) return ""; + if (levels.isEmpty()) return "[hidden]"; + return levels.toString(); + } + + public void setLevelSet(EnumSet value) { + levels = value.clone(); + } + } + + private enum Level { + NONE, ERROR, WARN, INFO, DEBUG, TRACE; + + static Level of(int x) { + switch (x) { + case LogLevel.NONE: + return NONE; + case LogLevel.ERROR: + return ERROR; + case LogLevel.WARN: + return WARN; + case LogLevel.INFO: + return INFO; + case LogLevel.DEBUG: + return DEBUG; + default: + if (x >= LogLevel.TRACE) return TRACE; + else throw new IllegalArgumentException(); + } + } + } + + public static void main(String... args) { + JFrame frame = new JFrame(); + LogSourcesPanel logLevelPanel = new LogSourcesPanel(new JButton("dummy")); + LogSource root = LogSource.newRoot(); + Set loggers = new HashSet<>(Arrays.asList( + root.subSource("Hello:World"), + root.subSource("Hello:Universe"), + root.subSource("Hello:foo:bar") + )); + logLevelPanel.updateSources(loggers); + frame.getContentPane().add(logLevelPanel, BorderLayout.CENTER); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index 2448cb2..ed6fa34 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -32,6 +32,11 @@ import java.awt.*; import java.awt.event.ActionEvent; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -50,6 +55,7 @@ import org.scijava.log.LogListener; import org.scijava.log.LogMessage; import org.scijava.log.LogService; +import org.scijava.log.LogSource; import org.scijava.log.Logger; import org.scijava.thread.ThreadService; @@ -76,10 +82,16 @@ public class LoggingPanel extends JPanel implements LogListener private final TextFilterField textFilter = new TextFilterField(" Text Search (Alt-F)"); + private final LogSourcesPanel sourcesPanel = initSourcesPanel(); + private final ItemTextPane textArea; private final JPanel textFilterPanel = new JPanel(); + private final JSplitPane splitPane = + new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + private final Set sources = Collections.newSetFromMap( + new ConcurrentHashMap<>()); private final LogFormatter logFormatter = new LogFormatter(); private LogRecorder recorder; @@ -105,6 +117,23 @@ public void setRecorder(LogRecorder recorder) { recorder.addObservers(textArea::update); } + public void toggleSourcesPanel() { + boolean isVisible = splitPane.getDividerLocation() > 10; + setSourcesPanelVisible(!isVisible); + } + + public void setSourcesPanelVisible(boolean visible) { + if (visible) { + reloadSources(); + splitPane.setResizeWeight(0.2); + splitPane.resetToPreferredSizes(); + } + else { + splitPane.setResizeWeight(0.0); + splitPane.setDividerLocation(0); + } + } + public void setTextFilterVisible(boolean visible) { textFilterPanel.setVisible(visible); } @@ -126,6 +155,7 @@ public void clear() { @Override public void messageLogged(LogMessage message) { + sources.add(message.source()); recorder.messageLogged(message); } @@ -144,16 +174,35 @@ private void initGui() { textFilterPanel.add(menuButton); textFilterPanel.add(textFilter.getComponent(), "grow"); + sourcesPanel.setChangeListener(this::updateFilter); + sourcesPanel.setMinimumSize(new Dimension()); + textArea.setPopupMenu(menu); textArea.getJComponent().setPreferredSize(new Dimension(200, 100)); + splitPane.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); + splitPane.setOneTouchExpandable(true); + setSourcesPanelVisible(false); + splitPane.add(sourcesPanel); + splitPane.add(textArea.getJComponent()); + this.setLayout(new MigLayout("insets 0", "[grow]", "[][grow]")); this.add(textFilterPanel, "grow, wrap"); - this.add(textArea.getJComponent(), "grow"); + this.add(splitPane, "grow"); registerKeyStroke("alt F", "focusTextFilter", this::focusTextFilter); } + private LogSourcesPanel initSourcesPanel() { + JButton reloadButton = new JButton("reload"); + reloadButton.addActionListener(actionEvent -> reloadSources()); + return new LogSourcesPanel(reloadButton); + } + + private void reloadSources() { + sourcesPanel.updateSources(sources); + } + private void registerKeyStroke(String keyStroke, String id, final Runnable action) { getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke .getKeyStroke(keyStroke), id); @@ -176,6 +225,8 @@ private JPopupMenu initMenu() { this::clear)); registerKeyStroke("alt C", "clearLoggingPanel", this::clear); + menu.add(newMenuItem("Log Sources", + this::toggleSourcesPanel)); return menu; } @@ -195,20 +246,19 @@ static private JMenuItem newMenuItem(String text, Runnable runnable) { private void updateFilter() { final Predicate quickSearchFilter = textFilter.getFilter(); - Stream stream = recorder.stream().map(this::wrapLogMessage) - .filter(item -> quickSearchFilter.test(item.text())); + final Predicate logLevelFilter = sourcesPanel.getFilter(); + Function transform = logMessage -> { + if (!logLevelFilter.test(logMessage)) return null; + ItemTextPane.Item item = new ItemTextPane.Item(getLevelStyle(logMessage.level()), + logFormatter.format(logMessage)); + if (!quickSearchFilter.test(item.text())) return null; + return item; + }; + Stream stream = + recorder.stream().map(transform).filter(Objects::nonNull); textArea.setData(stream.iterator()); } - private ItemTextPane.Item wrapLogMessage(LogMessage message) { - return new ItemTextPane.Item(getLevelStyle(message.level()), - logFormatter.format(message)); - } - - private static String appendLn(String text) { - return text.endsWith("\n") ? text : text + "\n"; - } - private static AttributeSet getLevelStyle(int i) { switch (i) { case LogLevel.ERROR: From cf6496fe2e8b4d29c018bc9857bb3967bd753809 Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Wed, 21 Jun 2017 17:45:11 +0200 Subject: [PATCH 08/10] LoggingPanel: add "Settings" to LoggingPanel's popup menu Now LoggingPanel can be used to record and show the calling class of the log messages. --- .../ui/swing/console/LogFormatter.java | 58 ++++++++++++++----- .../ui/swing/console/LoggingPanel.java | 39 ++++++++++++- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/console/LogFormatter.java b/src/main/java/org/scijava/ui/swing/console/LogFormatter.java index 27e90fc..e22ec5b 100644 --- a/src/main/java/org/scijava/ui/swing/console/LogFormatter.java +++ b/src/main/java/org/scijava/ui/swing/console/LogFormatter.java @@ -33,6 +33,7 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.EnumSet; import org.scijava.log.LogLevel; import org.scijava.log.LogMessage; @@ -44,25 +45,56 @@ */ public class LogFormatter { + public enum Field { + TIME, LEVEL, SOURCE, MESSAGE, THROWABLE, ATTACHMENT + } + + private EnumSet visibleFields = EnumSet.of(Field.TIME, + Field.LEVEL, Field.SOURCE, Field.MESSAGE, Field.THROWABLE); + + public boolean isVisible(Field field) { + return visibleFields.contains(field); + } + + public void setVisible(Field field, boolean visible) { + // copy on write to enable isVisible to be used concurrently + EnumSet copy = EnumSet.copyOf(visibleFields); + if (visible) copy.add(field); + else copy.remove(field); + visibleFields = copy; + } + public String format(LogMessage message) { + try { + final StringWriter sw = new StringWriter(); + final PrintWriter printer = new PrintWriter(sw); + + if (isVisible(Field.TIME)) + printWithBrackets(printer, message.time().toString()); + + if (isVisible(Field.LEVEL)) + printWithBrackets(printer, LogLevel.prefix(message.level())); - final StringWriter sw = new StringWriter(); - final PrintWriter printer = new PrintWriter(sw); + if (isVisible(Field.SOURCE)) + printWithBrackets(printer, message.source().toString()); - printWithBrackets(printer, message.time().toString()); - printWithBrackets(printer, LogLevel.prefix(message.level())); - printWithBrackets(printer, message.source().toString()); - printer.println(message.text()); - if (message.throwable() != null) - message.throwable().printStackTrace(printer); + if (isVisible(Field.ATTACHMENT)) { + printer.print(message.attachments()); + printer.print(" "); + } - return sw.toString(); + if (isVisible(Field.MESSAGE)) printer.println(message.text()); + + if (isVisible(Field.THROWABLE) && message.throwable() != null) + message.throwable().printStackTrace(printer); + return sw.toString(); + } + catch (Exception e) { + return "[Exception while formatting log message: " + e + "]\n"; + } } private void printWithBrackets(PrintWriter printer, String prefix) { - printer.print('['); - printer.print(prefix); - printer.print("] "); + printer.append('[').append(prefix).append("] "); } - } diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index ed6fa34..e3231d8 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -207,7 +207,6 @@ private void registerKeyStroke(String keyStroke, String id, final Runnable actio getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke .getKeyStroke(keyStroke), id); getActionMap().put(id, new AbstractAction() { - @Override public void actionPerformed(ActionEvent actionEvent) { action.run(); @@ -227,9 +226,47 @@ private JPopupMenu initMenu() { this::clear); menu.add(newMenuItem("Log Sources", this::toggleSourcesPanel)); + menu.add(initSettingsMenu()); + return menu; + } + + private JMenu initSettingsMenu() { + JMenu menu = new JMenu("Settings"); + menu.add(checkboxItem(LogFormatter.Field.TIME, "show time stamp")); + menu.add(checkboxItem(LogFormatter.Field.SOURCE, "show log source")); + menu.add(checkboxItem(LogFormatter.Field.LEVEL, "show log level")); + menu.add(checkboxItem(LogFormatter.Field.THROWABLE, "show exception")); + menu.add(checkboxItem(LogFormatter.Field.ATTACHMENT, "show attached data")); + menu.add(new JSeparator()); + menu.add(recordCallingClassMenuItem()); return menu; } + private JCheckBoxMenuItem recordCallingClassMenuItem() { + JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(); + menuItem.setState(false); + menuItem.setAction(new AbstractAction("record calling class") { + @Override + public void actionPerformed(ActionEvent e) { + recorder.setRecordCallingClass(menuItem.getState()); + updateFilter(); + } + }); + return menuItem; + } + + private JMenuItem checkboxItem(LogFormatter.Field field, String text) { + JCheckBoxMenuItem menuItem = new JCheckBoxMenuItem(text, logFormatter.isVisible(field)); + menuItem.setAction(new AbstractAction(text) { + @Override + public void actionPerformed(ActionEvent e) { + logFormatter.setVisible(field, menuItem.getState()); + updateFilter(); + } + }); + return menuItem; + } + static private JMenuItem newMenuItem(String text, String keyStroke, Runnable runnable) { From 6fbd8810b20f6a09dc6fa91220a43f8c7dd7ee29 Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Sun, 24 Sep 2017 01:58:43 +0200 Subject: [PATCH 09/10] SwingConsolePane: Store settings for log formatting in PrefService --- .../ui/swing/console/LogFormatter.java | 33 ++++++++++++ .../ui/swing/console/LoggingPanel.java | 6 ++- .../ui/swing/console/SwingConsolePane.java | 8 ++- .../ui/swing/console/LogFormatterTest.java | 51 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/scijava/ui/swing/console/LogFormatterTest.java diff --git a/src/main/java/org/scijava/ui/swing/console/LogFormatter.java b/src/main/java/org/scijava/ui/swing/console/LogFormatter.java index e22ec5b..46645a3 100644 --- a/src/main/java/org/scijava/ui/swing/console/LogFormatter.java +++ b/src/main/java/org/scijava/ui/swing/console/LogFormatter.java @@ -34,9 +34,11 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.EnumSet; +import java.util.Map; import org.scijava.log.LogLevel; import org.scijava.log.LogMessage; +import org.scijava.prefs.PrefService; /** * Used by {@link LoggingPanel} to simplify formatting log messages. @@ -45,6 +47,16 @@ */ public class LogFormatter { + private String prefKey = null; + + private PrefService prefService = null; + + public void setPrefService(PrefService prefService, String prefKey) { + this.prefService = (prefKey != null && !prefKey.isEmpty()) ? prefService : null; + this.prefKey = prefKey; + applySettings(); + } + public enum Field { TIME, LEVEL, SOURCE, MESSAGE, THROWABLE, ATTACHMENT } @@ -62,6 +74,7 @@ public void setVisible(Field field, boolean visible) { if (visible) copy.add(field); else copy.remove(field); visibleFields = copy; + changeSetting(field, visible); } public String format(LogMessage message) { @@ -97,4 +110,24 @@ public String format(LogMessage message) { private void printWithBrackets(PrintWriter printer, String prefix) { printer.append('[').append(prefix).append("] "); } + + // -- Helper methods -- + + public void applySettings() { + if(prefService == null) return; + Map settings = prefService.getMap(prefKey); + for(Field field : Field.values()) { + String defaultValue = Boolean.toString(isVisible(field)); + String value = settings.getOrDefault(field.toString(), defaultValue); + setVisible(field, Boolean.valueOf(value)); + } + visibleFields.toString(); + } + + public void changeSetting(Field field, boolean visible) { + if(prefService == null) return; + Map settings = prefService.getMap(prefKey); + settings.put(field.toString(), Boolean.toString(visible)); + prefService.putMap(prefKey, settings); + } } diff --git a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java index e3231d8..0b4b363 100644 --- a/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java +++ b/src/main/java/org/scijava/ui/swing/console/LoggingPanel.java @@ -57,6 +57,7 @@ import org.scijava.log.LogService; import org.scijava.log.LogSource; import org.scijava.log.Logger; +import org.scijava.prefs.PrefService; import org.scijava.thread.ThreadService; /** @@ -99,11 +100,12 @@ public class LoggingPanel extends JPanel implements LogListener // -- constructor -- public LoggingPanel(Context context) { - this(context.getService(ThreadService.class)); + this(context.getService(ThreadService.class), null, null); } - public LoggingPanel(ThreadService threadService) { + public LoggingPanel(ThreadService threadService, PrefService prefService, String prefKey) { textArea = new ItemTextPane(threadService); + logFormatter.setPrefService(prefService, prefKey); initGui(); setRecorder(new LogRecorder()); } diff --git a/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java b/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java index acbf24a..f8cdf6d 100644 --- a/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java +++ b/src/main/java/org/scijava/ui/swing/console/SwingConsolePane.java @@ -40,6 +40,7 @@ import org.scijava.console.OutputEvent; import org.scijava.log.LogService; import org.scijava.plugin.Parameter; +import org.scijava.prefs.PrefService; import org.scijava.thread.ThreadService; import org.scijava.ui.console.AbstractConsolePane; import org.scijava.ui.console.ConsolePane; @@ -56,12 +57,17 @@ */ public class SwingConsolePane extends AbstractConsolePane { + public static final String LOG_FORMATTING_SETTINGS_KEY = "/log-formatting"; + @Parameter private ThreadService threadService; @Parameter private LogService logService; + @Parameter + private PrefService prefService; + private ConsolePanel consolePanel; private LoggingPanel loggingPanel; @@ -131,7 +137,7 @@ private ConsolePanel consolePanel() { private synchronized void initLoggingPanel() { if (consolePanel != null) return; consolePanel = new ConsolePanel(threadService); - loggingPanel = new LoggingPanel(threadService); + loggingPanel = new LoggingPanel(threadService, prefService, LOG_FORMATTING_SETTINGS_KEY); logService.addLogListener(loggingPanel); component = new JPanel(new MigLayout("", "[grow]", "[grow]")); JTabbedPane tabs = new JTabbedPane(); diff --git a/src/test/java/org/scijava/ui/swing/console/LogFormatterTest.java b/src/test/java/org/scijava/ui/swing/console/LogFormatterTest.java new file mode 100644 index 0000000..3e2266c --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/console/LogFormatterTest.java @@ -0,0 +1,51 @@ +package org.scijava.ui.swing.console; + +import org.junit.Test; +import org.scijava.Context; +import org.scijava.prefs.PrefService; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests {@link LogFormatter}. + * + * @author Matthias Arzt + */ +public class LogFormatterTest { + + @Test + public void testPrefService() { + PrefService prefService = new Context(PrefService.class).service(PrefService.class); + String expected = "Hello World"; + prefService.put("foo", expected); + String actual = prefService.get("foo"); + assertEquals(expected, actual); + } + + @Test + public void testPrefServiceMap() { + PrefService prefService = new Context(PrefService.class).service(PrefService.class); + Map expected = Collections.singletonMap("Hello", "World"); + prefService.putMap("/foo", expected); + Map actual = prefService.getMap("/foo"); + assertEquals(expected, actual); + } + + @Test + public void testSettings() { + PrefService prefService = new Context(PrefService.class).service(PrefService.class); + LogFormatter formatter1 = new LogFormatter(); + formatter1.setPrefService(prefService, "/abc"); + formatter1.setVisible(LogFormatter.Field.ATTACHMENT, true); + formatter1.setVisible(LogFormatter.Field.LEVEL, false); + LogFormatter formatter2 = new LogFormatter(); + formatter2.setPrefService(prefService, "/abc"); + assertTrue(formatter2.isVisible(LogFormatter.Field.ATTACHMENT)); + assertFalse(formatter2.isVisible(LogFormatter.Field.LEVEL)); + } +} From 30f7e6afdc34d1fddfec7c8bab4d4bb949d5653b Mon Sep 17 00:00:00 2001 From: Matthias Arzt Date: Sat, 23 Sep 2017 00:24:10 +0200 Subject: [PATCH 10/10] Add LoggingDemo to demonstrate LoggingPanel and SwingConsolePane --- .../scijava/ui/swing/console/LoggingDemo.java | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/test/java/org/scijava/ui/swing/console/LoggingDemo.java diff --git a/src/test/java/org/scijava/ui/swing/console/LoggingDemo.java b/src/test/java/org/scijava/ui/swing/console/LoggingDemo.java new file mode 100644 index 0000000..c5d9a18 --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/console/LoggingDemo.java @@ -0,0 +1,131 @@ +/* + * #%L + * SciJava UI components for Java Swing. + * %% + * Copyright (C) 2010 - 2017 Board of Regents of the University of + * Wisconsin-Madison. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package org.scijava.ui.swing.console; + +import java.awt.*; + +import javax.swing.*; + +import net.miginfocom.swing.MigLayout; + +import org.scijava.Context; +import org.scijava.command.Command; +import org.scijava.command.CommandService; +import org.scijava.log.DefaultLogger; +import org.scijava.log.LogSource; +import org.scijava.log.Logger; +import org.scijava.plugin.Parameter; +import org.scijava.ui.UIService; + +/** + * {@link LoggingDemo} is an example to demonstrate the capabilitie of + * {@link SwingConsolePane} and {@link LoggingPanel}. It shows the scijava gui, + * and starts to example commands. The first command {@link LoggingLoop} print + * "Hello World" and logs a message every second. The second command + * {@link PluginThatLogs} opens a window with it's on {@link LoggingPanel} and + * provides some buttons to emit some example log messages. + * + * @author Matthias Arzt + */ +public class LoggingDemo { + + public static void main(String... args) { + Context context = new Context(); + UIService ui = context.service(UIService.class); + ui.showUI(); + CommandService commandService = context.service(CommandService.class); + commandService.run(PluginThatLogs.class, true); + commandService.run(LoggingLoop.class, true); + } + + public static class LoggingLoop implements Command { + + @Parameter + private Logger logger; + + @Override + public void run() { + while (true) { + logger.warn("Message Text"); + System.out.println("Hello World"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + logger.warn(e); + } + } + } + } + + public static class PluginThatLogs implements Command { + + @Parameter + private Context context; + + @Parameter + private Logger log; + + private Logger privateLogger = new DefaultLogger(ignore -> {}, LogSource.newRoot(), 100); + + @Override + public void run() { + LoggingPanel panel = new LoggingPanel(context); + Logger subLogger = log.subLogger(""); + subLogger.addLogListener(panel); + privateLogger.addLogListener(panel); + + JFrame frame = new JFrame("Plugin that logs"); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.setLayout(new MigLayout("","[grow]","[][grow]")); + frame.add(newButton("log to main window", () -> writeToLogger(log)), "split"); + frame.add(newButton("log to both window", () -> writeToLogger(subLogger))); + frame.add(newButton("log to this window", () -> writeToLogger(privateLogger)), "wrap"); + frame.add(panel, "grow"); + frame.pack(); + frame.setVisible(true); + } + + private void writeToLogger(Logger log) { + log.error("Error message test"); + log.warn("Text describing a warning"); + log.info("An Information"); + log.debug("Something help debugging"); + log.trace("Trace everything"); + log.log(42, "Whats the best log level"); + } + + private Component newButton(String title, Runnable action) { + JButton button = new JButton(title); + button.addActionListener(a -> action.run()); + return button; + } + } +}