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

Revision 5200, 21.6 KB checked in by akks, 5 weeks ago (diff)

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
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.