source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java@ 7949

Last change on this file since 7949 was 7949, checked in by Don-vip, 9 years ago

fix #10933 - keep the same selection in selection dialog after cliking the "select" button + code refactoring/cleanup

  • Property svn:eol-style set to native
File size: 23.0 KB
Line 
1// License: GPL. See LICENSE file for details.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.awt.event.MouseEvent;
9import java.io.IOException;
10import java.lang.reflect.InvocationTargetException;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Enumeration;
14import java.util.HashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.JComponent;
21import javax.swing.JOptionPane;
22import javax.swing.JPopupMenu;
23import javax.swing.SwingUtilities;
24import javax.swing.event.TreeSelectionEvent;
25import javax.swing.event.TreeSelectionListener;
26import javax.swing.tree.DefaultMutableTreeNode;
27import javax.swing.tree.TreePath;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.actions.AbstractSelectAction;
31import org.openstreetmap.josm.actions.AutoScaleAction;
32import org.openstreetmap.josm.actions.relation.EditRelationAction;
33import org.openstreetmap.josm.command.Command;
34import org.openstreetmap.josm.data.SelectionChangedListener;
35import org.openstreetmap.josm.data.osm.DataSet;
36import org.openstreetmap.josm.data.osm.Node;
37import org.openstreetmap.josm.data.osm.OsmPrimitive;
38import org.openstreetmap.josm.data.osm.WaySegment;
39import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
40import org.openstreetmap.josm.data.validation.OsmValidator;
41import org.openstreetmap.josm.data.validation.TestError;
42import org.openstreetmap.josm.data.validation.ValidatorVisitor;
43import org.openstreetmap.josm.gui.MapView;
44import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
45import org.openstreetmap.josm.gui.PleaseWaitRunnable;
46import org.openstreetmap.josm.gui.PopupMenuHandler;
47import org.openstreetmap.josm.gui.SideButton;
48import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
49import org.openstreetmap.josm.gui.layer.Layer;
50import org.openstreetmap.josm.gui.layer.OsmDataLayer;
51import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
52import org.openstreetmap.josm.gui.progress.ProgressMonitor;
53import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
54import org.openstreetmap.josm.io.OsmTransferException;
55import org.openstreetmap.josm.tools.ImageProvider;
56import org.openstreetmap.josm.tools.InputMapUtils;
57import org.openstreetmap.josm.tools.Shortcut;
58import org.xml.sax.SAXException;
59
60/**
61 * A small tool dialog for displaying the current errors. The selection manager
62 * respects clicks into the selection list. Ctrl-click will remove entries from
63 * the list while single click will make the clicked entry the only selection.
64 *
65 * @author frsantos
66 */
67public class ValidatorDialog extends ToggleDialog implements SelectionChangedListener, LayerChangeListener {
68
69 /** The display tree */
70 public ValidatorTreePanel tree;
71
72 /** The fix button */
73 private SideButton fixButton;
74 /** The ignore button */
75 private SideButton ignoreButton;
76 /** The select button */
77 private SideButton selectButton;
78
79 private final JPopupMenu popupMenu = new JPopupMenu();
80 private final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
81
82 /** Last selected element */
83 private DefaultMutableTreeNode lastSelectedNode = null;
84
85 private OsmDataLayer linkedLayer;
86
87 /**
88 * Constructor
89 */
90 public ValidatorDialog() {
91 super(tr("Validation Results"), "validator", tr("Open the validation window."),
92 Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation results")),
93 KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150, false, ValidatorPreference.class);
94
95 popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("problem"));
96 popupMenuHandler.addAction(new EditRelationAction());
97
98 tree = new ValidatorTreePanel();
99 tree.addMouseListener(new MouseEventHandler());
100 addTreeSelectionListener(new SelectionWatch());
101 InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
102
103 List<SideButton> buttons = new LinkedList<>();
104
105 selectButton = new SideButton(new AbstractSelectAction() {
106 @Override
107 public void actionPerformed(ActionEvent e) {
108 setSelectedItems();
109 }
110 });
111 InputMapUtils.addEnterAction(tree, selectButton.getAction());
112
113 selectButton.setEnabled(false);
114 buttons.add(selectButton);
115
116 buttons.add(new SideButton(Main.main.validator.validateAction));
117
118 fixButton = new SideButton(new AbstractAction() {
119 {
120 putValue(NAME, tr("Fix"));
121 putValue(SHORT_DESCRIPTION, tr("Fix the selected issue."));
122 putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
123 }
124 @Override
125 public void actionPerformed(ActionEvent e) {
126 fixErrors();
127 }
128 });
129 fixButton.setEnabled(false);
130 buttons.add(fixButton);
131
132 if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
133 ignoreButton = new SideButton(new AbstractAction() {
134 {
135 putValue(NAME, tr("Ignore"));
136 putValue(SHORT_DESCRIPTION, tr("Ignore the selected issue next time."));
137 putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
138 }
139 @Override
140 public void actionPerformed(ActionEvent e) {
141 ignoreErrors();
142 }
143 });
144 ignoreButton.setEnabled(false);
145 buttons.add(ignoreButton);
146 } else {
147 ignoreButton = null;
148 }
149 createLayout(tree, true, buttons);
150 }
151
152 @Override
153 public void showNotify() {
154 DataSet.addSelectionListener(this);
155 DataSet ds = Main.main.getCurrentDataSet();
156 if (ds != null) {
157 updateSelection(ds.getAllSelected());
158 }
159 MapView.addLayerChangeListener(this);
160 Layer activeLayer = Main.map.mapView.getActiveLayer();
161 if (activeLayer != null) {
162 activeLayerChange(null, activeLayer);
163 }
164 }
165
166 @Override
167 public void hideNotify() {
168 MapView.removeLayerChangeListener(this);
169 DataSet.removeSelectionListener(this);
170 }
171
172 @Override
173 public void setVisible(boolean v) {
174 if (tree != null) {
175 tree.setVisible(v);
176 }
177 super.setVisible(v);
178 Main.map.repaint();
179 }
180
181 /**
182 * Fix selected errors
183 */
184 @SuppressWarnings("unchecked")
185 private void fixErrors() {
186 TreePath[] selectionPaths = tree.getSelectionPaths();
187 if (selectionPaths == null)
188 return;
189
190 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
191
192 LinkedList<TestError> errorsToFix = new LinkedList<>();
193 for (TreePath path : selectionPaths) {
194 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
195 if (node == null) {
196 continue;
197 }
198
199 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
200 while (children.hasMoreElements()) {
201 DefaultMutableTreeNode childNode = children.nextElement();
202 if (processedNodes.contains(childNode)) {
203 continue;
204 }
205
206 processedNodes.add(childNode);
207 Object nodeInfo = childNode.getUserObject();
208 if (nodeInfo instanceof TestError) {
209 errorsToFix.add((TestError)nodeInfo);
210 }
211 }
212 }
213
214 // run fix task asynchronously
215 //
216 FixTask fixTask = new FixTask(errorsToFix);
217 Main.worker.submit(fixTask);
218 }
219
220 /**
221 * Set selected errors to ignore state
222 */
223 @SuppressWarnings("unchecked")
224 private void ignoreErrors() {
225 int asked = JOptionPane.DEFAULT_OPTION;
226 boolean changed = false;
227 TreePath[] selectionPaths = tree.getSelectionPaths();
228 if (selectionPaths == null)
229 return;
230
231 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
232 for (TreePath path : selectionPaths) {
233 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
234 if (node == null) {
235 continue;
236 }
237
238 Object mainNodeInfo = node.getUserObject();
239 if (!(mainNodeInfo instanceof TestError)) {
240 Set<String> state = new HashSet<>();
241 // ask if the whole set should be ignored
242 if (asked == JOptionPane.DEFAULT_OPTION) {
243 String[] a = new String[] { tr("Whole group"), tr("Single elements"), tr("Nothing") };
244 asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
245 tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
246 a, a[1]);
247 }
248 if (asked == JOptionPane.YES_NO_OPTION) {
249 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
250 while (children.hasMoreElements()) {
251 DefaultMutableTreeNode childNode = children.nextElement();
252 if (processedNodes.contains(childNode)) {
253 continue;
254 }
255
256 processedNodes.add(childNode);
257 Object nodeInfo = childNode.getUserObject();
258 if (nodeInfo instanceof TestError) {
259 TestError err = (TestError) nodeInfo;
260 err.setIgnored(true);
261 changed = true;
262 state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
263 }
264 }
265 for (String s : state) {
266 OsmValidator.addIgnoredError(s);
267 }
268 continue;
269 } else if (asked == JOptionPane.CANCEL_OPTION || asked == JOptionPane.CLOSED_OPTION) {
270 continue;
271 }
272 }
273
274 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
275 while (children.hasMoreElements()) {
276 DefaultMutableTreeNode childNode = children.nextElement();
277 if (processedNodes.contains(childNode)) {
278 continue;
279 }
280
281 processedNodes.add(childNode);
282 Object nodeInfo = childNode.getUserObject();
283 if (nodeInfo instanceof TestError) {
284 TestError error = (TestError) nodeInfo;
285 String state = error.getIgnoreState();
286 if (state != null) {
287 OsmValidator.addIgnoredError(state);
288 }
289 changed = true;
290 error.setIgnored(true);
291 }
292 }
293 }
294 if (changed) {
295 tree.resetErrors();
296 OsmValidator.saveIgnoredErrors();
297 Main.map.repaint();
298 }
299 }
300
301 /**
302 * Sets the selection of the map to the current selected items.
303 */
304 @SuppressWarnings("unchecked")
305 private void setSelectedItems() {
306 if (tree == null)
307 return;
308
309 Collection<OsmPrimitive> sel = new HashSet<>(40);
310
311 TreePath[] selectedPaths = tree.getSelectionPaths();
312 if (selectedPaths == null)
313 return;
314
315 for (TreePath path : selectedPaths) {
316 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
317 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
318 while (children.hasMoreElements()) {
319 DefaultMutableTreeNode childNode = children.nextElement();
320 Object nodeInfo = childNode.getUserObject();
321 if (nodeInfo instanceof TestError) {
322 TestError error = (TestError) nodeInfo;
323 sel.addAll(error.getSelectablePrimitives());
324 }
325 }
326 }
327 DataSet ds = Main.main.getCurrentDataSet();
328 if (ds != null) {
329 ds.setSelected(sel);
330 }
331 }
332
333 /**
334 * Checks for fixes in selected element and, if needed, adds to the sel
335 * parameter all selected elements
336 *
337 * @param sel
338 * The collection where to add all selected elements
339 * @param addSelected
340 * if true, add all selected elements to collection
341 * @return whether the selected elements has any fix
342 */
343 @SuppressWarnings("unchecked")
344 private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
345 boolean hasFixes = false;
346
347 DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
348 if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
349 Enumeration<DefaultMutableTreeNode> children = lastSelectedNode.breadthFirstEnumeration();
350 while (children.hasMoreElements()) {
351 DefaultMutableTreeNode childNode = children.nextElement();
352 Object nodeInfo = childNode.getUserObject();
353 if (nodeInfo instanceof TestError) {
354 TestError error = (TestError) nodeInfo;
355 error.setSelected(false);
356 }
357 }
358 }
359
360 lastSelectedNode = node;
361 if (node == null)
362 return hasFixes;
363
364 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
365 while (children.hasMoreElements()) {
366 DefaultMutableTreeNode childNode = children.nextElement();
367 Object nodeInfo = childNode.getUserObject();
368 if (nodeInfo instanceof TestError) {
369 TestError error = (TestError) nodeInfo;
370 error.setSelected(true);
371
372 hasFixes = hasFixes || error.isFixable();
373 if (addSelected) {
374 sel.addAll(error.getSelectablePrimitives());
375 }
376 }
377 }
378 selectButton.setEnabled(true);
379 if (ignoreButton != null) {
380 ignoreButton.setEnabled(true);
381 }
382
383 return hasFixes;
384 }
385
386 @Override
387 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
388 if (newLayer instanceof OsmDataLayer) {
389 linkedLayer = (OsmDataLayer)newLayer;
390 tree.setErrorList(linkedLayer.validationErrors);
391 }
392 }
393
394 @Override
395 public void layerAdded(Layer newLayer) {}
396
397 @Override
398 public void layerRemoved(Layer oldLayer) {
399 if (oldLayer == linkedLayer) {
400 tree.setErrorList(new ArrayList<TestError>());
401 }
402 }
403
404 /**
405 * Add a tree selection listener to the validator tree.
406 * @param listener the TreeSelectionListener
407 * @since 5958
408 */
409 public void addTreeSelectionListener(TreeSelectionListener listener) {
410 tree.addTreeSelectionListener(listener);
411 }
412
413 /**
414 * Remove the given tree selection listener from the validator tree.
415 * @param listener the TreeSelectionListener
416 * @since 5958
417 */
418 public void removeTreeSelectionListener(TreeSelectionListener listener) {
419 tree.removeTreeSelectionListener(listener);
420 }
421
422 /**
423 * Replies the popup menu handler.
424 * @return The popup menu handler
425 * @since 5958
426 */
427 public PopupMenuHandler getPopupMenuHandler() {
428 return popupMenuHandler;
429 }
430
431 /**
432 * Replies the currently selected error, or {@code null}.
433 * @return The selected error, if any.
434 * @since 5958
435 */
436 public TestError getSelectedError() {
437 Object comp = tree.getLastSelectedPathComponent();
438 if (comp instanceof DefaultMutableTreeNode) {
439 Object object = ((DefaultMutableTreeNode)comp).getUserObject();
440 if (object instanceof TestError) {
441 return (TestError) object;
442 }
443 }
444 return null;
445 }
446
447 /**
448 * Watches for double clicks and launches the popup menu.
449 */
450 class MouseEventHandler extends PopupMenuLauncher {
451
452 public MouseEventHandler() {
453 super(popupMenu);
454 }
455
456 @Override
457 public void mouseClicked(MouseEvent e) {
458 fixButton.setEnabled(false);
459 if (ignoreButton != null) {
460 ignoreButton.setEnabled(false);
461 }
462 selectButton.setEnabled(false);
463
464 boolean isDblClick = isDoubleClick(e);
465
466 Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
467
468 boolean hasFixes = setSelection(sel, isDblClick);
469 fixButton.setEnabled(hasFixes);
470
471 if (isDblClick) {
472 Main.main.getCurrentDataSet().setSelected(sel);
473 if (Main.pref.getBoolean("validator.autozoom", false)) {
474 AutoScaleAction.zoomTo(sel);
475 }
476 }
477 }
478
479 @Override public void launch(MouseEvent e) {
480 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
481 if (selPath == null)
482 return;
483 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
484 if (!(node.getUserObject() instanceof TestError))
485 return;
486 super.launch(e);
487 }
488
489 }
490
491 /**
492 * Watches for tree selection.
493 */
494 public class SelectionWatch implements TreeSelectionListener {
495 @Override
496 public void valueChanged(TreeSelectionEvent e) {
497 fixButton.setEnabled(false);
498 if (ignoreButton != null) {
499 ignoreButton.setEnabled(false);
500 }
501 selectButton.setEnabled(false);
502
503 Collection<OsmPrimitive> sel = new HashSet<>();
504 boolean hasFixes = setSelection(sel, true);
505 fixButton.setEnabled(hasFixes);
506 popupMenuHandler.setPrimitives(sel);
507 if (Main.map != null) {
508 Main.map.repaint();
509 }
510 }
511 }
512
513 public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
514 @Override
515 public void visit(OsmPrimitive p) {
516 if (p.isUsable()) {
517 p.accept(this);
518 }
519 }
520
521 @Override
522 public void visit(WaySegment ws) {
523 if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
524 return;
525 visit(ws.way.getNodes().get(ws.lowerIndex));
526 visit(ws.way.getNodes().get(ws.lowerIndex + 1));
527 }
528
529 @Override
530 public void visit(List<Node> nodes) {
531 for (Node n: nodes) {
532 visit(n);
533 }
534 }
535
536 @Override
537 public void visit(TestError error) {
538 if (error != null) {
539 error.visitHighlighted(this);
540 }
541 }
542 }
543
544 public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
545 if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false))
546 return;
547 if (newSelection.isEmpty()) {
548 tree.setFilter(null);
549 }
550 HashSet<OsmPrimitive> filter = new HashSet<>(newSelection);
551 tree.setFilter(filter);
552 }
553
554 @Override
555 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
556 updateSelection(newSelection);
557 }
558
559 /**
560 * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
561 *
562 *
563 */
564 class FixTask extends PleaseWaitRunnable {
565 private Collection<TestError> testErrors;
566 private boolean canceled;
567
568 public FixTask(Collection<TestError> testErrors) {
569 super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
570 this.testErrors = testErrors == null ? new ArrayList<TestError> (): testErrors;
571 }
572
573 @Override
574 protected void cancel() {
575 this.canceled = true;
576 }
577
578 @Override
579 protected void finish() {
580 // do nothing
581 }
582
583 protected void fixError(TestError error) throws InterruptedException, InvocationTargetException {
584 if (error.isFixable()) {
585 final Command fixCommand = error.getFix();
586 if (fixCommand != null) {
587 SwingUtilities.invokeAndWait(new Runnable() {
588 @Override
589 public void run() {
590 Main.main.undoRedo.addNoRedraw(fixCommand);
591 }
592 });
593 }
594 // It is wanted to ignore an error if it said fixable, even if fixCommand was null
595 // This is to fix #5764 and #5773: a delete command, for example, may be null if all concerned primitives have already been deleted
596 error.setIgnored(true);
597 }
598 }
599
600 @Override
601 protected void realRun() throws SAXException, IOException,
602 OsmTransferException {
603 ProgressMonitor monitor = getProgressMonitor();
604 try {
605 monitor.setTicksCount(testErrors.size());
606 int i=0;
607 SwingUtilities.invokeAndWait(new Runnable() {
608 @Override
609 public void run() {
610 Main.main.getCurrentDataSet().beginUpdate();
611 }
612 });
613 try {
614 for (TestError error: testErrors) {
615 i++;
616 monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(),error.getMessage()));
617 if (this.canceled)
618 return;
619 fixError(error);
620 monitor.worked(1);
621 }
622 } finally {
623 SwingUtilities.invokeAndWait(new Runnable() {
624 @Override
625 public void run() {
626 Main.main.getCurrentDataSet().endUpdate();
627 }
628 });
629 }
630 monitor.subTask(tr("Updating map ..."));
631 SwingUtilities.invokeAndWait(new Runnable() {
632 @Override
633 public void run() {
634 Main.main.undoRedo.afterAdd();
635 Main.map.repaint();
636 tree.resetErrors();
637 Main.main.getCurrentDataSet().fireSelectionChanged();
638 }
639 });
640 } catch(InterruptedException | InvocationTargetException e) {
641 // FIXME: signature of realRun should have a generic checked exception we
642 // could throw here
643 throw new RuntimeException(e);
644 } finally {
645 monitor.finishTask();
646 }
647 }
648 }
649}
Note: See TracBrowser for help on using the repository browser.