Skip to content

Commit 3b9e30d

Browse files
committed
LoggingPanel: add gui to filter log messages by source and level
1 parent 99527d5 commit 3b9e30d

File tree

2 files changed

+409
-12
lines changed

2 files changed

+409
-12
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
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+
import java.awt.event.FocusEvent;
35+
import java.awt.event.FocusListener;
36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.EnumSet;
39+
import java.util.HashMap;
40+
import java.util.HashSet;
41+
import java.util.List;
42+
import java.util.Map;
43+
import java.util.Set;
44+
import java.util.StringJoiner;
45+
import java.util.function.Consumer;
46+
import java.util.function.Function;
47+
import java.util.function.Predicate;
48+
import java.util.stream.Collectors;
49+
import java.util.stream.Stream;
50+
51+
import javax.swing.*;
52+
import javax.swing.event.TreeSelectionEvent;
53+
import javax.swing.tree.DefaultMutableTreeNode;
54+
import javax.swing.tree.DefaultTreeCellRenderer;
55+
import javax.swing.tree.DefaultTreeModel;
56+
import javax.swing.tree.TreePath;
57+
58+
import net.miginfocom.swing.MigLayout;
59+
60+
import org.scijava.log.LogLevel;
61+
import org.scijava.log.LogMessage;
62+
import org.scijava.log.LogSource;
63+
64+
/**
65+
* {@link LogSourcesPanel} is a {@link JPanel}, that contains a tree view, where
66+
* {@link LogSource}s are listed. For each log source the user may select a set
67+
* of visible log levels.
68+
*
69+
* @author Matthias Arzt
70+
*/
71+
class LogSourcesPanel extends JPanel {
72+
73+
private static final EnumSet<Level> VALID_LEVELS = EnumSet.range(Level.ERROR,
74+
Level.TRACE);
75+
76+
private final JPopupMenu menu = new JPopupMenu();
77+
78+
private final Map<LogSource, Item> sourceItems = new HashMap<>();
79+
private JTree tree;
80+
private DefaultTreeModel treeModel;
81+
private DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Log Sources:");
82+
83+
private Predicate<LogMessage> filter = message -> true;
84+
private Runnable changeListener = null;
85+
private List<LogSource> selected;
86+
87+
// -- constructor --
88+
89+
LogSourcesPanel(JButton reloadButton) {
90+
initMenu();
91+
initTreeView();
92+
JButton visibilityButton = initVisibilityButton();
93+
setLayout(new MigLayout("inset 0", "[grow]", "[][grow][]"));
94+
add(new JLabel("Log Sources:"), "grow, wrap");
95+
add(new JScrollPane(tree), "grow, wrap");
96+
add(new JLabel(""), "split 3, grow");
97+
add(reloadButton);
98+
add(visibilityButton);
99+
actionWhenFocusLost(() -> tree.clearSelection(), Arrays.asList(tree, visibilityButton));
100+
}
101+
102+
// -- LogLevelPanel methods --
103+
104+
public void setChangeListener(Runnable changeListener) {
105+
this.changeListener = changeListener;
106+
}
107+
108+
public Predicate<LogMessage> getFilter() {
109+
if (filter == null) updateFilter();
110+
return filter;
111+
}
112+
113+
public void updateSources(Set<LogSource> logSources) {
114+
for (LogSource sources : logSources)
115+
getItem(sources);
116+
treeModel.reload();
117+
}
118+
119+
// -- Helper methods --
120+
121+
private void initTreeView() {
122+
treeModel = new DefaultTreeModel(rootNode);
123+
tree = new JTree(treeModel);
124+
tree.setRootVisible(false);
125+
DefaultTreeCellRenderer cellRenderer = new DefaultTreeCellRenderer();
126+
cellRenderer.setIcon(null);
127+
cellRenderer.setLeafIcon(null);
128+
cellRenderer.setOpenIcon(null);
129+
tree.setCellRenderer(cellRenderer);
130+
tree.setComponentPopupMenu(menu);
131+
tree.setEditable(false);
132+
tree.setShowsRootHandles(true);
133+
tree.addTreeSelectionListener(this::selectionChanged);
134+
}
135+
136+
private void selectionChanged(TreeSelectionEvent treeSelectionEvent) {
137+
selected = getSelectedItems().map(item -> item.source).collect(Collectors.toList());
138+
settingsChanged();
139+
}
140+
141+
private JButton initVisibilityButton() {
142+
JButton button = new JButton("visibility");
143+
button.addActionListener(a -> menu.show(button, 0, button.getHeight()));
144+
return button;
145+
}
146+
147+
private void initMenu() {
148+
EnumSet<Level> levels = EnumSet.range(Level.ERROR, Level.TRACE);
149+
addMenuItemPerLevel(EnumSet.of(Level.TRACE), level -> "show all",
150+
this::onShowErrorUpToClicked);
151+
addMenuItemPerLevel(EnumSet.of(Level.NONE), level -> "hide all",
152+
this::onShowNoneClicked);
153+
menu.addSeparator();
154+
addMenuItemPerLevel(levels, level -> "show " + level.toString(),
155+
this::onShowLogLevelClicked);
156+
menu.addSeparator();
157+
addMenuItemPerLevel(levels, level -> "hide " + level.toString(),
158+
this::onHideLogLevelClicked);
159+
menu.addSeparator();
160+
addMenuItemPerLevel(levels, LogSourcesPanel::listLevelsErrorTo,
161+
this::onShowErrorUpToClicked);
162+
}
163+
164+
private void addMenuItemPerLevel(EnumSet<Level> levels,
165+
Function<Level, String> title, Consumer<Level> consumer)
166+
{
167+
for (Level level : levels) {
168+
JMenuItem menuItem = new JMenuItem(title.apply(level));
169+
menuItem.addActionListener(a -> consumer.accept(level));
170+
menu.add(menuItem);
171+
}
172+
}
173+
174+
private void onShowLogLevelClicked(Level level) {
175+
modifyFilterOnSelection(filter -> {
176+
filter.add(level);
177+
return filter;
178+
});
179+
}
180+
181+
private void onHideLogLevelClicked(Level level) {
182+
modifyFilterOnSelection(filter -> {
183+
filter.remove(level);
184+
return filter;
185+
});
186+
}
187+
188+
private void onShowErrorUpToClicked(Level level) {
189+
EnumSet<Level> enumSet = EnumSet.range(Level.ERROR, level);
190+
modifyFilterOnSelection(ignore -> enumSet);
191+
}
192+
193+
private void onShowNoneClicked(Level level) {
194+
EnumSet<Level> enumSet = EnumSet.noneOf(Level.class);
195+
modifyFilterOnSelection(ignore -> enumSet);
196+
}
197+
198+
private void modifyFilterOnSelection(
199+
Function<EnumSet<Level>, EnumSet<Level>> itemConsumer)
200+
{
201+
getSelectedItems().forEach(item -> {
202+
item.setLevelSet(itemConsumer.apply(item.levels));
203+
treeModel.nodeChanged(item.node);
204+
});
205+
settingsChanged();
206+
}
207+
208+
private Stream<Item> getSelectedItems() {
209+
TreePath[] selectionPaths = tree.getSelectionPaths();
210+
if (selectionPaths == null) return Collections.<Item> emptyList().stream();
211+
return Stream.of(selectionPaths).map(path -> getItem(
212+
(DefaultMutableTreeNode) path.getLastPathComponent()));
213+
}
214+
215+
private void settingsChanged() {
216+
filter = null;
217+
if (changeListener != null) changeListener.run();
218+
}
219+
220+
private Item getItem(DefaultMutableTreeNode node) {
221+
return (Item) node.getUserObject();
222+
}
223+
224+
private Item getItem(LogSource source) {
225+
Item existing = sourceItems.get(source);
226+
return existing == null ? initItem(source) : existing;
227+
}
228+
229+
private Item initItem(LogSource source) {
230+
Item item = new Item(source);
231+
sourceItems.put(item.source, item);
232+
DefaultMutableTreeNode parent = source.isRoot() ?
233+
rootNode : getItem(source.parent()).node;
234+
parent.add(item.node);
235+
return item;
236+
}
237+
238+
private void updateFilter() {
239+
Map<LogSource, EnumSet<Level>> filterData = new HashMap<>();
240+
EnumSet<Level> none = EnumSet.noneOf(Level.class);
241+
sourceItems.forEach((name, item) -> filterData.put(name, item.visible
242+
? item.levels : none));
243+
Set<LogSource> selectedSources = new HashSet<>(selected);
244+
filter = message -> {
245+
if(!selectedSources.isEmpty() && !selectedSources.contains(message.source()))
246+
return false;
247+
EnumSet<Level> logLevels = filterData.get(message.source());
248+
if (logLevels == null) return true;
249+
return logLevels.contains(Level.of(message.level()));
250+
};
251+
}
252+
253+
private static String listLevelsErrorTo(Level max) {
254+
return enumSetToString(EnumSet.range(Level.ERROR, max));
255+
}
256+
257+
private static String enumSetToString(EnumSet<Level> levels) {
258+
StringJoiner s = new StringJoiner(", ");
259+
for (Level level : levels)
260+
s.add(level.toString());
261+
return s.toString();
262+
}
263+
264+
private static void actionWhenFocusLost(Runnable action, List<JComponent> components) {
265+
FocusListener l = new FocusListener() {
266+
@Override
267+
public void focusGained(FocusEvent e) {
268+
}
269+
270+
@Override
271+
public void focusLost(FocusEvent e) {
272+
boolean keepSelection = components.contains(e.getOppositeComponent()) || e.isTemporary();
273+
if (!keepSelection) action.run();
274+
}
275+
};
276+
components.forEach(c -> c.addFocusListener(l));
277+
}
278+
279+
// -- Helper classes --
280+
281+
private static class Item {
282+
283+
DefaultMutableTreeNode node;
284+
LogSource source;
285+
EnumSet<Level> levels;
286+
boolean visible;
287+
288+
public Item(LogSource source) {
289+
this.levels = EnumSet.range(Level.ERROR, Level.TRACE);
290+
this.source = source;
291+
this.visible = true;
292+
this.node = new DefaultMutableTreeNode(this);
293+
}
294+
295+
public String toString() {
296+
String name = source.isRoot() ? "ROOT" : source.name();
297+
return name + " " + getLevelString();
298+
}
299+
300+
private String getLevelString() {
301+
if (levels.equals(VALID_LEVELS)) return "";
302+
if (levels.isEmpty()) return "[hidden]";
303+
return levels.toString();
304+
}
305+
306+
public void setLevelSet(EnumSet<Level> value) {
307+
levels = value.clone();
308+
}
309+
}
310+
311+
private enum Level {
312+
NONE, ERROR, WARN, INFO, DEBUG, TRACE;
313+
314+
static Level of(int x) {
315+
switch (x) {
316+
case LogLevel.NONE:
317+
return NONE;
318+
case LogLevel.ERROR:
319+
return ERROR;
320+
case LogLevel.WARN:
321+
return WARN;
322+
case LogLevel.INFO:
323+
return INFO;
324+
case LogLevel.DEBUG:
325+
return DEBUG;
326+
default:
327+
if (x >= LogLevel.TRACE) return TRACE;
328+
else throw new IllegalArgumentException();
329+
}
330+
}
331+
}
332+
333+
public static void main(String... args) {
334+
JFrame frame = new JFrame();
335+
LogSourcesPanel logLevelPanel = new LogSourcesPanel(new JButton("dummy"));
336+
LogSource root = LogSource.newRoot();
337+
Set<LogSource> loggers = new HashSet<>(Arrays.asList(
338+
root.subSource("Hello:World"),
339+
root.subSource("Hello:Universe"),
340+
root.subSource("Hello:foo:bar")
341+
));
342+
logLevelPanel.updateSources(loggers);
343+
frame.getContentPane().add(logLevelPanel, BorderLayout.CENTER);
344+
frame.pack();
345+
frame.setVisible(true);
346+
}
347+
}

0 commit comments

Comments
 (0)