Skip to content

Commit 221d3c6

Browse files
authored
Merge pull request #25 from maarzt/log-revamp
Enhance console window with regard to log messages
2 parents 993b164 + 30f7e6a commit 221d3c6

12 files changed

+2017
-108
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* #%L
3+
* SciJava UI components for Java Swing.
4+
* %%
5+
* Copyright (C) 2010 - 2017 Board of Regents of the University of
6+
* Wisconsin-Madison.
7+
* %%
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
* 2. Redistributions in binary form must reproduce the above copyright notice,
14+
* this list of conditions and the following disclaimer in the documentation
15+
* and/or other materials provided with the distribution.
16+
*
17+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
21+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
* #L%
29+
*/
30+
31+
package org.scijava.ui.swing.console;
32+
33+
import java.awt.*;
34+
35+
import javax.swing.*;
36+
import javax.swing.text.BadLocationException;
37+
import javax.swing.text.Style;
38+
import javax.swing.text.StyleConstants;
39+
import javax.swing.text.StyledDocument;
40+
41+
import net.miginfocom.swing.MigLayout;
42+
43+
import org.scijava.console.OutputEvent;
44+
import org.scijava.console.OutputListener;
45+
import org.scijava.log.IgnoreAsCallingClass;
46+
import org.scijava.thread.ThreadService;
47+
import org.scijava.ui.swing.StaticSwingUtils;
48+
49+
/**
50+
* {@link ConsolePanel} is a {@link JPanel} holding a {@link JTextArea}. It can
51+
* be used to display text written to System.out and System.err. Therefor it can
52+
* be added as {@link OutputListener} to
53+
* {@link org.scijava.console.ConsoleService}.
54+
*
55+
* @author Matthias Arzt
56+
*/
57+
@IgnoreAsCallingClass
58+
public class ConsolePanel extends JPanel implements OutputListener
59+
{
60+
private JTextPane textPane;
61+
private JScrollPane scrollPane;
62+
63+
private StyledDocument doc;
64+
private Style stdoutLocal;
65+
private Style stderrLocal;
66+
private Style stdoutGlobal;
67+
private Style stderrGlobal;
68+
69+
private final ThreadService threadService;
70+
71+
public ConsolePanel(ThreadService threadService) {
72+
this.threadService = threadService;
73+
initGui();
74+
}
75+
76+
public void clear() {
77+
textPane.setText("");
78+
}
79+
80+
@Override
81+
public void outputOccurred(OutputEvent event) {
82+
threadService.queue(new Runnable() {
83+
84+
@Override
85+
public void run() {
86+
final boolean atBottom =
87+
StaticSwingUtils.isScrolledToBottom(scrollPane);
88+
try {
89+
doc.insertString(doc.getLength(), event.getOutput(), getStyle(event));
90+
}
91+
catch (final BadLocationException exc) {
92+
throw new RuntimeException(exc);
93+
}
94+
if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane);
95+
}
96+
});
97+
}
98+
99+
private synchronized void initGui() {
100+
setLayout(new MigLayout("inset 0", "[grow,fill]", "[grow,fill,align top]"));
101+
102+
textPane = new JTextPane();
103+
textPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
104+
textPane.setEditable(false);
105+
106+
doc = textPane.getStyledDocument();
107+
108+
stdoutLocal = createStyle("stdoutLocal", null, Color.black, null, null);
109+
stderrLocal = createStyle("stderrLocal", null, Color.red, null, null);
110+
stdoutGlobal = createStyle("stdoutGlobal", stdoutLocal, null, null, true);
111+
stderrGlobal = createStyle("stderrGlobal", stderrLocal, null, null, true);
112+
113+
// NB: We wrap the JTextPane in a JPanel to disable
114+
// the text pane's intelligent line wrapping behavior.
115+
// I.e.: we want console lines _not_ to wrap, but instead
116+
// for the scroll pane to show a horizontal scroll bar.
117+
// Thanks to: https://tips4java.wordpress.com/2009/01/25/no-wrap-text-pane/
118+
final JPanel textPanel = new JPanel();
119+
textPanel.setLayout(new BorderLayout());
120+
textPanel.add(textPane);
121+
122+
scrollPane = new JScrollPane(textPanel);
123+
scrollPane.setPreferredSize(new Dimension(600, 600));
124+
125+
// Make the scroll bars move at a reasonable pace.
126+
final FontMetrics fm = scrollPane.getFontMetrics(scrollPane.getFont());
127+
final int charWidth = fm.charWidth('a');
128+
final int lineHeight = fm.getHeight();
129+
scrollPane.getHorizontalScrollBar().setUnitIncrement(charWidth);
130+
scrollPane.getVerticalScrollBar().setUnitIncrement(2 * lineHeight);
131+
132+
add(scrollPane);
133+
}
134+
// -- Helper methods --
135+
136+
private Style createStyle(final String name, final Style parent,
137+
final Color foreground, final Boolean bold, final Boolean italic)
138+
{
139+
final Style style = textPane.addStyle(name, parent);
140+
if (foreground != null) StyleConstants.setForeground(style, foreground);
141+
if (bold != null) StyleConstants.setBold(style, bold);
142+
if (italic != null) StyleConstants.setItalic(style, italic);
143+
return style;
144+
}
145+
146+
private Style getStyle(final OutputEvent event) {
147+
final boolean stderr = event.getSource() == OutputEvent.Source.STDERR;
148+
final boolean contextual = event.isContextual();
149+
if (stderr) return contextual ? stderrLocal : stderrGlobal;
150+
return contextual ? stdoutLocal : stdoutGlobal;
151+
}
152+
153+
public JTextPane getTextPane() {
154+
return textPane;
155+
}
156+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* #%L
3+
* SciJava UI components for Java Swing.
4+
* %%
5+
* Copyright (C) 2010 - 2017 Board of Regents of the University of
6+
* Wisconsin-Madison.
7+
* %%
8+
* Redistribution and use in source and binary forms, with or without
9+
* modification, are permitted provided that the following conditions are met:
10+
*
11+
* 1. Redistributions of source code must retain the above copyright notice,
12+
* this list of conditions and the following disclaimer.
13+
* 2. Redistributions in binary form must reproduce the above copyright notice,
14+
* this list of conditions and the following disclaimer in the documentation
15+
* and/or other materials provided with the distribution.
16+
*
17+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
21+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
* POSSIBILITY OF SUCH DAMAGE.
28+
* #L%
29+
*/
30+
31+
package org.scijava.ui.swing.console;
32+
33+
import java.awt.Font;
34+
import java.util.Collections;
35+
import java.util.Iterator;
36+
import java.util.Objects;
37+
38+
import javax.swing.*;
39+
import javax.swing.text.AttributeSet;
40+
import javax.swing.text.BadLocationException;
41+
import javax.swing.text.DefaultStyledDocument;
42+
import javax.swing.text.StyledDocument;
43+
44+
import org.scijava.thread.ThreadService;
45+
import org.scijava.ui.swing.StaticSwingUtils;
46+
47+
/**
48+
* {@link ItemTextPane} provides a {@link JTextPane} in a {@link JScrollPane}.
49+
* The content is provided as {@link Iterator} of {@link ItemTextPane.Item}.
50+
* ItemTextPane is used to display a large list of items, which is often
51+
* replaced and extended.
52+
* <p>
53+
* Replacing the entire list requires an update to the {@link StyledDocument}.
54+
* This is performed in a worker thread; as a result, the event dispatch thread
55+
* will not be blocked.
56+
* <p>
57+
* An {@link Item} can be tagged and incomplete. If this is the case the item
58+
* will be removed, when the next item with the same tag is displayed.
59+
* <p>
60+
* {@link ItemTextPane} is used in {@link LoggingPanel}.
61+
*
62+
* @author Matthias Arzt
63+
*/
64+
class ItemTextPane {
65+
66+
private final ThreadService threadService;
67+
68+
private DocumentCalculator initialCalculator = null;
69+
70+
private JTextPane textPane = new JTextPane();
71+
72+
private JScrollPane scrollPane = new JScrollPane(textPane);
73+
74+
private boolean waitingForProcessNewItems = false;
75+
76+
private DocumentCalculator calculator =
77+
new DocumentCalculator(Collections.<Item> emptyList().iterator());
78+
79+
// -- constructor --
80+
81+
ItemTextPane(ThreadService threadService) {
82+
this.threadService = Objects.requireNonNull(threadService);
83+
textPane.setEditable(false);
84+
textPane.setFont(new Font("monospaced", Font.PLAIN, 12));
85+
}
86+
87+
// -- ItemTextPane methods --
88+
89+
JComponent getJComponent() {
90+
return scrollPane;
91+
}
92+
93+
public void setPopupMenu(JPopupMenu menu) {
94+
textPane.setComponentPopupMenu(menu);
95+
}
96+
97+
/**
98+
* Set the {@link ItemTextPane.Item}s to be displayed in the
99+
* {@link JTextPane}.
100+
*
101+
* @param data The iterator will be used by a SwingWorker, and the
102+
* SwingThread. NB: Each time {@link ItemTextPane#update()} is called
103+
* {@link Iterator#hasNext()} will be called again (even if it
104+
* returned false before) to check if maybe the iterator provides new
105+
* items.
106+
*/
107+
public void setData(Iterator<Item> data) {
108+
calculator.cancel();
109+
if (initialCalculator != null) initialCalculator.cancel();
110+
DocumentCalculator calculator = new DocumentCalculator(data);
111+
initialCalculator = calculator;
112+
threadService.run(() -> initCalculator(calculator));
113+
}
114+
115+
/**
116+
* This initiates to check {@link Iterator#hasNext()} of iterator previously
117+
* set with {@link #setData(Iterator)} again. If {@link Iterator#hasNext()}
118+
* returns true, the new items will be red from the Iterator, and displayed.
119+
*/
120+
public void update() {
121+
if (waitingForProcessNewItems) return;
122+
waitingForProcessNewItems = true;
123+
threadService.queue(this::processNewItemsInSwingThread);
124+
}
125+
126+
/** Copy selected text to the clipboard. */
127+
public void copySelectionToClipboard() {
128+
textPane.copy();
129+
}
130+
131+
// -- Helper methods --
132+
133+
private void processNewItemsInSwingThread() {
134+
if (calculator.isCanceled()) return;
135+
136+
waitingForProcessNewItems = false;
137+
boolean atBottom = StaticSwingUtils.isScrolledToBottom(scrollPane);
138+
calculator.update();
139+
if (atBottom) StaticSwingUtils.scrollToBottom(scrollPane);
140+
}
141+
142+
private void initCalculator(DocumentCalculator calculator) {
143+
calculator.update();
144+
if (calculator.isCanceled()) return;
145+
threadService.queue(() -> applyCalculator(calculator));
146+
}
147+
148+
private void applyCalculator(DocumentCalculator calculator) {
149+
if (initialCalculator != calculator) return;
150+
this.calculator = calculator;
151+
textPane.setDocument(calculator.document());
152+
processNewItemsInSwingThread();
153+
threadService.queue(() -> StaticSwingUtils.scrollToBottom(scrollPane));
154+
}
155+
156+
// -- Helper methods - testing --
157+
158+
JTextPane getTextPane() {
159+
return textPane;
160+
}
161+
162+
// -- public Helper classes --
163+
164+
public static class Item {
165+
166+
private final String text;
167+
private final AttributeSet style;
168+
169+
Item(AttributeSet style, String text) {
170+
this.style = style;
171+
this.text = text;
172+
}
173+
174+
final String text() {
175+
return text;
176+
}
177+
178+
final AttributeSet style() {
179+
return style;
180+
}
181+
}
182+
183+
// -- Helper classes --
184+
185+
/**
186+
* {@link DocumentCalculator} is used to calculate a {@link StyledDocument}
187+
* for a given {@link Iterator} of {@link Item}s. NB: Items can be incomplete
188+
* and tagged. Such incomplete tagged Item will be replaced by the following
189+
* item with the same tag.
190+
*/
191+
static class DocumentCalculator {
192+
193+
private final Iterator<Item> data;
194+
195+
private final StyledDocument document = new DefaultStyledDocument();
196+
197+
private boolean canceled = false;
198+
199+
DocumentCalculator(Iterator<Item> data) {
200+
this.data = data;
201+
}
202+
203+
public StyledDocument document() {
204+
return document;
205+
}
206+
207+
public boolean isCanceled() {
208+
return canceled;
209+
}
210+
211+
public void cancel() {
212+
canceled = true;
213+
}
214+
215+
public synchronized void update() {
216+
while (data.hasNext() && !canceled)
217+
addText(data.next());
218+
}
219+
220+
private void addText(Item item) {
221+
try {
222+
document.insertString(document.getLength(), item.text(), item.style());
223+
} catch (BadLocationException e) {
224+
// ignore
225+
}
226+
}
227+
}
228+
}

0 commit comments

Comments
 (0)