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

Last change on this file since 5200 was 5200, checked in by akks, 12 years ago

see #7626, fix #7463: keys Ctrl-Shift-Up/Down, Enter, Spacebar work better in toggle dialogs
Enter and Spacebar = useful actions for list items (select, toggle, etc.)

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