source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java@ 16079

Last change on this file since 16079 was 16079, checked in by GerdP, 4 years ago

see #18810: extend delay for tool tip in validator tree

  • Property svn:eol-style set to native
File size: 21.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.validator;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.KeyListener;
7import java.awt.event.MouseEvent;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Enumeration;
11import java.util.HashSet;
12import java.util.List;
13import java.util.Map;
14import java.util.Map.Entry;
15import java.util.Set;
16import java.util.function.Consumer;
17import java.util.function.Predicate;
18
19import javax.swing.JTree;
20import javax.swing.ToolTipManager;
21import javax.swing.tree.DefaultMutableTreeNode;
22import javax.swing.tree.DefaultTreeModel;
23import javax.swing.tree.TreeNode;
24import javax.swing.tree.TreePath;
25import javax.swing.tree.TreeSelectionModel;
26
27import org.openstreetmap.josm.data.osm.OsmPrimitive;
28import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
29import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
30import org.openstreetmap.josm.data.osm.event.DataSetListener;
31import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
32import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
33import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
34import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
35import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
36import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
37import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
38import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
39import org.openstreetmap.josm.data.validation.OsmValidator;
40import org.openstreetmap.josm.data.validation.Severity;
41import org.openstreetmap.josm.data.validation.TestError;
42import org.openstreetmap.josm.gui.util.GuiHelper;
43import org.openstreetmap.josm.tools.Destroyable;
44import org.openstreetmap.josm.tools.ListenerList;
45
46/**
47 * A panel that displays the error tree. The selection manager
48 * respects clicks into the selection list. Ctrl-click will remove entries from
49 * the list while single click will make the clicked entry the only selection.
50 *
51 * @author frsantos
52 */
53public class ValidatorTreePanel extends JTree implements Destroyable, DataSetListener {
54
55 private static final class GroupTreeNode extends DefaultMutableTreeNode {
56
57 GroupTreeNode(Object userObject) {
58 super(userObject);
59 }
60
61 @Override
62 public String toString() {
63 return tr("{0} ({1})", super.toString(), getLeafCount());
64 }
65 }
66
67 /**
68 * The validation data.
69 */
70 protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
71
72 /** The list of errors shown in the tree, normally identical to field validationErrors in current edit layer*/
73 private transient List<TestError> errors;
74
75 /**
76 * If {@link #filter} is not <code>null</code> only errors are displayed
77 * that refer to one of the primitives in the filter.
78 */
79 private transient Set<? extends OsmPrimitive> filter;
80
81 private final transient ListenerList<Runnable> invalidationListeners = ListenerList.create();
82
83 /** if true, buildTree() does nothing */
84 private boolean resetScheduled;
85
86 /**
87 * Constructor
88 * @param errors The list of errors
89 */
90 public ValidatorTreePanel(List<TestError> errors) {
91 setErrorList(errors);
92 ToolTipManager.sharedInstance().registerComponent(this);
93 GuiHelper.extendTooltipDelay(this);
94
95 this.setModel(valTreeModel);
96 this.setRootVisible(false);
97 this.setShowsRootHandles(true);
98 this.expandRow(0);
99 this.setVisibleRowCount(8);
100 this.setCellRenderer(new ValidatorTreeRenderer());
101 this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
102 for (KeyListener keyListener : getKeyListeners()) {
103 // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands
104 if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) {
105 removeKeyListener(keyListener);
106 }
107 }
108 DatasetEventManager.getInstance().addDatasetListener(this, DatasetEventManager.FireMode.IN_EDT);
109 }
110
111 @Override
112 public String getToolTipText(MouseEvent e) {
113 String res = null;
114 TreePath path = getPathForLocation(e.getX(), e.getY());
115 if (path != null) {
116 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
117 Object nodeInfo = node.getUserObject();
118
119 if (nodeInfo instanceof TestError) {
120 TestError error = (TestError) nodeInfo;
121 res = error.getNameVisitor().getText() + "<br>" + error.getMessage();
122 String d = error.getDescription();
123 if (d != null)
124 res += "<br>" + d;
125 res += "<br>" + tr("Test: {0}", getTesterDetails(error));
126 } else {
127 Set<String> tests = new HashSet<>();
128 visitTestErrors(node, err -> tests.add(getTesterDetails(err)), null);
129 String source = (tests.size() == 1) ? tr("Test: {0}", tests.iterator().next()) : tr("Different tests");
130 res = node.toString() + "<br>" + source;
131 }
132 }
133 return res == null ? null : "<html>" + res + "</html>";
134 }
135
136 private static String getTesterDetails(TestError e) {
137 return e.getTester().getName() + "<br>" + e.getTester().getSource();
138 }
139
140 /** Constructor */
141 public ValidatorTreePanel() {
142 this(null);
143 }
144
145 @Override
146 public void setVisible(boolean v) {
147 if (v) {
148 buildTree();
149 } else {
150 valTreeModel.setRoot(new DefaultMutableTreeNode());
151 }
152 super.setVisible(v);
153 invalidationListeners.fireEvent(Runnable::run);
154 }
155
156 /**
157 * Builds the errors tree
158 */
159 public void buildTree() {
160 buildTree(true);
161 }
162
163 /**
164 * Builds the errors tree
165 * @param expandAgain if true, try to expand the same rows as before
166 */
167 public void buildTree(boolean expandAgain) {
168 if (resetScheduled)
169 return;
170 final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
171
172 if (errors == null || errors.isEmpty()) {
173 GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode));
174 return;
175 }
176
177 // Remember first selected tree row
178 TreePath selPath = getSelectionPath();
179 int selRow = selPath == null ? -1 : getRowForPath(selPath);
180
181 // Remember the currently expanded rows
182 Set<Object> oldExpandedRows = new HashSet<>();
183 if (expandAgain) {
184 Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
185 if (expanded != null) {
186 while (expanded.hasMoreElements()) {
187 TreePath path = expanded.nextElement();
188 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
189 Object userObject = node.getUserObject();
190 if (userObject instanceof Severity) {
191 oldExpandedRows.add(userObject);
192 } else if (userObject instanceof String) {
193 String msg = removeSize((String) userObject);
194 oldExpandedRows.add(msg);
195 }
196 }
197 }
198 }
199
200 Predicate<TestError> filterToUse = e -> !e.isIgnored();
201 if (!ValidatorPrefHelper.PREF_OTHER.get()) {
202 filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
203 }
204 if (filter != null) {
205 filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
206 }
207 Map<Severity, Map<String, Map<String, List<TestError>>>> errorsBySeverityMessageDescription
208 = OsmValidator.getErrorsBySeverityMessageDescription(errors, filterToUse);
209
210 final List<TreePath> expandedPaths = new ArrayList<>();
211 for (Entry<Severity, Map<String, Map<String, List<TestError>>>> entry: errorsBySeverityMessageDescription.entrySet()) {
212 Severity severity = entry.getKey();
213 Map<String, Map<String, List<TestError>>> errorsByMessageDescription = entry.getValue();
214
215 // Severity node
216 final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity);
217 rootNode.add(severityNode);
218
219 if (oldExpandedRows.contains(severity)) {
220 expandedPaths.add(new TreePath(severityNode.getPath()));
221 }
222
223 final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get("");
224 if (errorsWithEmptyMessageByDescription != null) {
225 errorsWithEmptyMessageByDescription.forEach((description, noDescriptionErrors) -> {
226 final String msg = addSize(description, noDescriptionErrors);
227 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
228 severityNode.add(messageNode);
229
230 if (oldExpandedRows.contains(description)) {
231 expandedPaths.add(new TreePath(messageNode.getPath()));
232 }
233 // add the matching errors to the current node
234 noDescriptionErrors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
235 });
236 }
237
238 errorsByMessageDescription.forEach((message, errorsByDescription) -> {
239 if (message.isEmpty()) {
240 return;
241 }
242 // Group node
243 final DefaultMutableTreeNode groupNode;
244 if (errorsByDescription.size() > 1) {
245 groupNode = new GroupTreeNode(message);
246 severityNode.add(groupNode);
247 if (oldExpandedRows.contains(message)) {
248 expandedPaths.add(new TreePath(groupNode.getPath()));
249 }
250 } else {
251 groupNode = null;
252 }
253
254 errorsByDescription.forEach((description, errorsWithDescription) -> {
255 // Message node
256 final String searchMsg;
257 if (groupNode != null) {
258 searchMsg = description;
259 } else if (description == null || description.isEmpty()) {
260 searchMsg = message;
261 } else {
262 searchMsg = message + " - " + description;
263 }
264 final String msg = addSize(searchMsg, errorsWithDescription);
265
266 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
267 DefaultMutableTreeNode currNode = groupNode != null ? groupNode : severityNode;
268 currNode.add(messageNode);
269 if (oldExpandedRows.contains(searchMsg)) {
270 expandedPaths.add(new TreePath(messageNode.getPath()));
271 }
272
273 // add the matching errors to the current node
274 errorsWithDescription.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
275 });
276 });
277 }
278
279 valTreeModel.setRoot(rootNode);
280 for (TreePath path : expandedPaths) {
281 this.expandPath(path);
282 }
283
284 if (selPath != null) {
285 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent();
286 Object userObject = node.getUserObject();
287 if (userObject instanceof TestError && ((TestError) userObject).isIgnored()) {
288 // don't try to find ignored error
289 selPath = null;
290 }
291 }
292 if (selPath != null) {
293 // try to reselect previously selected row. May not work if tree structure changed too much.
294 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent();
295 Object searchObject = node.getUserObject();
296 String msg = null;
297 if (searchObject instanceof String) {
298 msg = removeSize((String) searchObject);
299 }
300 String searchString = msg;
301 visitTreeNodes(getRoot(), n -> {
302 boolean found = false;
303 final Object userInfo = n.getUserObject();
304 if (searchObject instanceof TestError && userInfo instanceof TestError) {
305 TestError e1 = (TestError) searchObject;
306 TestError e2 = (TestError) userInfo;
307 found |= e1.getCode() == e2.getCode() && e1.getMessage().equals(e2.getMessage())
308 && e1.getPrimitives().size() == e2.getPrimitives().size()
309 && e1.getPrimitives().containsAll(e2.getPrimitives());
310 } else if (searchObject instanceof String && userInfo instanceof String) {
311 found |= ((String) userInfo).startsWith(searchString);
312 } else if (searchObject instanceof Severity) {
313 found |= searchObject.equals(userInfo);
314 }
315
316 if (found) {
317 TreePath path = new TreePath(n.getPath());
318 setSelectionPath(path);
319 scrollPathToVisible(path);
320 }
321 });
322 }
323 if (selRow >= 0 && selRow < getRowCount() && getSelectionCount() == 0) {
324 // fall back: if we cannot find the previously selected entry, select the row by position
325 setSelectionRow(selRow);
326 scrollRowToVisible(selRow);
327 }
328
329 invalidationListeners.fireEvent(Runnable::run);
330 }
331
332 private static String addSize(String msg, Collection<?> coll) {
333 return msg + " (" + coll.size() + ")";
334 }
335
336 private static String removeSize(String msg) {
337 int index = msg.lastIndexOf(" (");
338 return index > 0 ? msg.substring(0, index) : msg;
339 }
340
341 /**
342 * Add a new invalidation listener
343 * @param listener The listener
344 */
345 public void addInvalidationListener(Runnable listener) {
346 invalidationListeners.addListener(listener);
347 }
348
349 /**
350 * Remove an invalidation listener
351 * @param listener The listener
352 * @since 10880
353 */
354 public void removeInvalidationListener(Runnable listener) {
355 invalidationListeners.removeListener(listener);
356 }
357
358 /**
359 * Sets the errors list used by a data layer
360 * @param errors The error list that is used by a data layer
361 */
362 public final void setErrorList(List<TestError> errors) {
363 if (errors != null && errors == this.errors)
364 return;
365 this.errors = errors != null ? errors : new ArrayList<>();
366 if (isVisible()) {
367 //TODO: If list is changed because another layer was activated it would be good to store/restore
368 // the expanded / selected paths.
369 clearSelection();
370 buildTree(false);
371 }
372 }
373
374 /**
375 * Clears the current error list and adds these errors to it
376 * @param newerrors The validation errors
377 */
378 public void setErrors(List<TestError> newerrors) {
379 errors.clear();
380 for (TestError error : newerrors) {
381 if (!error.isIgnored()) {
382 errors.add(error);
383 }
384 }
385 if (isVisible()) {
386 buildTree();
387 }
388 }
389
390 /**
391 * Returns the errors of the tree
392 * @return the errors of the tree
393 */
394 public List<TestError> getErrors() {
395 return errors;
396 }
397
398 /**
399 * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
400 * returns a primitive present in {@code primitives}.
401 * @param primitives collection of primitives
402 */
403 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
404 final List<TreePath> paths = new ArrayList<>();
405 walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
406 clearSelection();
407 setSelectionPaths(paths.toArray(new TreePath[0]));
408 // make sure that first path is visible
409 if (!paths.isEmpty()) {
410 scrollPathToVisible(paths.get(0));
411 }
412 }
413
414 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
415 final int count = getModel().getChildCount(p.getLastPathComponent());
416 for (int i = 0; i < count; i++) {
417 final Object child = getModel().getChild(p.getLastPathComponent(), i);
418 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
419 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
420 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
421 if (error.getPrimitives().stream().anyMatch(isRelevant)) {
422 paths.add(p.pathByAddingChild(child));
423 }
424 } else {
425 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
426 }
427 }
428 }
429
430 /**
431 * Returns the filter list
432 * @return the list of primitives used for filtering
433 */
434 public Set<? extends OsmPrimitive> getFilter() {
435 return filter;
436 }
437
438 /**
439 * Set the filter list to a set of primitives
440 * @param filter the list of primitives used for filtering
441 */
442 public void setFilter(Set<? extends OsmPrimitive> filter) {
443 if (filter != null && filter.isEmpty()) {
444 this.filter = null;
445 } else {
446 this.filter = filter;
447 }
448 if (isVisible()) {
449 buildTree();
450 }
451 }
452
453 /**
454 * Updates the current errors list
455 */
456 public void resetErrors() {
457 resetScheduled = false;
458 filterRemovedPrimitives();
459 setErrors(new ArrayList<>(errors));
460 }
461
462 /**
463 * Expands complete tree
464 */
465 public void expandAll() {
466 visitTreeNodes(getRoot(), x -> expandPath(new TreePath(x.getPath())));
467 }
468
469 /**
470 * Returns the root node model.
471 * @return The root node model
472 */
473 public DefaultMutableTreeNode getRoot() {
474 return (DefaultMutableTreeNode) valTreeModel.getRoot();
475 }
476
477 @Override
478 public void destroy() {
479 DatasetEventManager.getInstance().removeDatasetListener(this);
480 ToolTipManager.sharedInstance().unregisterComponent(this);
481 errors.clear();
482 }
483
484 /**
485 * Visitor call for all tree nodes children of root, in breadth-first order.
486 * @param root Root node
487 * @param visitor Visitor
488 * @since 13940
489 */
490 public static void visitTreeNodes(DefaultMutableTreeNode root, Consumer<DefaultMutableTreeNode> visitor) {
491 @SuppressWarnings("unchecked")
492 Enumeration<TreeNode> errorMessages = root.breadthFirstEnumeration();
493 while (errorMessages.hasMoreElements()) {
494 visitor.accept(((DefaultMutableTreeNode) errorMessages.nextElement()));
495 }
496 }
497
498 /**
499 * Visitor call for all {@link TestError} nodes children of root, in breadth-first order.
500 * @param root Root node
501 * @param visitor Visitor
502 * @since 13940
503 */
504 public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor) {
505 visitTestErrors(root, visitor, null);
506 }
507
508 /**
509 * Visitor call for all {@link TestError} nodes children of root, in breadth-first order.
510 * @param root Root node
511 * @param visitor Visitor
512 * @param processedNodes Set of already visited nodes (optional)
513 * @since 13940
514 */
515 public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor,
516 Set<DefaultMutableTreeNode> processedNodes) {
517 visitTreeNodes(root, n -> {
518 if (processedNodes == null || !processedNodes.contains(n)) {
519 if (processedNodes != null) {
520 processedNodes.add(n);
521 }
522 Object o = n.getUserObject();
523 if (o instanceof TestError) {
524 visitor.accept((TestError) o);
525 }
526 }
527 });
528 }
529
530 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
531 // Remove purged primitives (fix #8639)
532 if (filterRemovedPrimitives()) {
533 buildTree();
534 }
535 }
536
537 @Override public void primitivesAdded(PrimitivesAddedEvent event) {
538 // Do nothing
539 }
540
541 @Override public void tagsChanged(TagsChangedEvent event) {
542 // Do nothing
543 }
544
545 @Override public void nodeMoved(NodeMovedEvent event) {
546 // Do nothing
547 }
548
549 @Override public void wayNodesChanged(WayNodesChangedEvent event) {
550 // Do nothing
551 }
552
553 @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
554 // Do nothing
555 }
556
557 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
558 // Do nothing
559 }
560
561 @Override public void dataChanged(DataChangedEvent event) {
562 if (filterRemovedPrimitives()) {
563 buildTree();
564 }
565 }
566
567 /**
568 * Can be called to suppress execution of buildTree() while doing multiple updates. Caller must
569 * call resetErrors() to end this state.
570 * @since 14849
571 */
572 public void setResetScheduled() {
573 resetScheduled = true;
574 }
575
576 /**
577 * Remove errors which refer to removed or purged primitives.
578 * @return true if error list was changed
579 */
580 private boolean filterRemovedPrimitives() {
581 return errors.removeIf(
582 error -> error.getPrimitives().stream().anyMatch(p -> p.isDeleted() || p.getDataSet() == null));
583 }
584
585}
Note: See TracBrowser for help on using the repository browser.