source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/ConflictDialog.java @ 14134

Last change on this file since 14134 was 14134, checked in by Don-vip, 4 months ago

see #15229 - deprecate Main*.undoRedo - make UndoRedoHandler a singleton

  • Property svn:eol-style set to native
File size: 22.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.marktr;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.Color;
10import java.awt.Graphics;
11import java.awt.Point;
12import java.awt.event.ActionEvent;
13import java.awt.event.KeyEvent;
14import java.awt.event.MouseEvent;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.Collection;
18import java.util.HashSet;
19import java.util.LinkedList;
20import java.util.List;
21import java.util.Set;
22import java.util.concurrent.CopyOnWriteArrayList;
23
24import javax.swing.AbstractAction;
25import javax.swing.JList;
26import javax.swing.JMenuItem;
27import javax.swing.JOptionPane;
28import javax.swing.JPopupMenu;
29import javax.swing.ListModel;
30import javax.swing.ListSelectionModel;
31import javax.swing.event.ListDataEvent;
32import javax.swing.event.ListDataListener;
33import javax.swing.event.ListSelectionEvent;
34import javax.swing.event.ListSelectionListener;
35import javax.swing.event.PopupMenuEvent;
36import javax.swing.event.PopupMenuListener;
37
38import org.openstreetmap.josm.Main;
39import org.openstreetmap.josm.actions.AbstractSelectAction;
40import org.openstreetmap.josm.actions.ExpertToggleAction;
41import org.openstreetmap.josm.command.Command;
42import org.openstreetmap.josm.command.SequenceCommand;
43import org.openstreetmap.josm.data.UndoRedoHandler;
44import org.openstreetmap.josm.data.conflict.Conflict;
45import org.openstreetmap.josm.data.conflict.ConflictCollection;
46import org.openstreetmap.josm.data.conflict.IConflictListener;
47import org.openstreetmap.josm.data.osm.DataSelectionListener;
48import org.openstreetmap.josm.data.osm.DataSet;
49import org.openstreetmap.josm.data.osm.Node;
50import org.openstreetmap.josm.data.osm.OsmPrimitive;
51import org.openstreetmap.josm.data.osm.Relation;
52import org.openstreetmap.josm.data.osm.RelationMember;
53import org.openstreetmap.josm.data.osm.Way;
54import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
55import org.openstreetmap.josm.data.preferences.NamedColorProperty;
56import org.openstreetmap.josm.gui.HelpAwareOptionPane;
57import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
58import org.openstreetmap.josm.gui.MainApplication;
59import org.openstreetmap.josm.gui.NavigatableComponent;
60import org.openstreetmap.josm.gui.PopupMenuHandler;
61import org.openstreetmap.josm.gui.PrimitiveRenderer;
62import org.openstreetmap.josm.gui.SideButton;
63import org.openstreetmap.josm.gui.conflict.pair.ConflictResolver;
64import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
65import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
66import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
67import org.openstreetmap.josm.gui.layer.OsmDataLayer;
68import org.openstreetmap.josm.gui.util.GuiHelper;
69import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
70import org.openstreetmap.josm.tools.ImageProvider;
71import org.openstreetmap.josm.tools.Logging;
72import org.openstreetmap.josm.tools.Shortcut;
73
74/**
75 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle
76 * dialog on the right of the main frame.
77 * @since 86
78 */
79public final class ConflictDialog extends ToggleDialog implements ActiveLayerChangeListener, IConflictListener, DataSelectionListener {
80
81    private static final NamedColorProperty CONFLICT_COLOR = new NamedColorProperty(marktr("conflict"), Color.GRAY);
82    private static final NamedColorProperty BACKGROUND_COLOR = new NamedColorProperty(marktr("background"), Color.BLACK);
83
84    /** the collection of conflicts displayed by this conflict dialog */
85    private transient ConflictCollection conflicts;
86
87    /** the model for the list of conflicts */
88    private transient ConflictListModel model;
89    /** the list widget for the list of conflicts */
90    private JList<OsmPrimitive> lstConflicts;
91
92    private final JPopupMenu popupMenu = new JPopupMenu();
93    private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
94
95    private final ResolveAction actResolve = new ResolveAction();
96    private final SelectAction actSelect = new SelectAction();
97
98    /**
99     * Constructs a new {@code ConflictDialog}.
100     */
101    public ConflictDialog() {
102        super(tr("Conflict"), "conflict", tr("Resolve conflicts"),
103                Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")),
104                KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100);
105
106        build();
107        refreshView();
108    }
109
110    /**
111     * Replies the color used to paint conflicts.
112     *
113     * @return the color used to paint conflicts
114     * @see #paintConflicts
115     * @since 1221
116     */
117    public static Color getColor() {
118        return CONFLICT_COLOR.get();
119    }
120
121    /**
122     * builds the GUI
123     */
124    private void build() {
125        synchronized (this) {
126            model = new ConflictListModel();
127
128            lstConflicts = new JList<>(model);
129            lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
130            lstConflicts.setCellRenderer(new PrimitiveRenderer());
131            lstConflicts.addMouseListener(new MouseEventHandler());
132        }
133        addListSelectionListener(e -> MainApplication.getMap().mapView.repaint());
134
135        SideButton btnResolve = new SideButton(actResolve);
136        addListSelectionListener(actResolve);
137
138        SideButton btnSelect = new SideButton(actSelect);
139        addListSelectionListener(actSelect);
140
141        createLayout(lstConflicts, true, Arrays.asList(btnResolve, btnSelect));
142
143        popupMenuHandler.addAction(MainApplication.getMenu().autoScaleActions.get("conflict"));
144
145        ResolveToMyVersionAction resolveToMyVersionAction = new ResolveToMyVersionAction();
146        ResolveToTheirVersionAction resolveToTheirVersionAction = new ResolveToTheirVersionAction();
147        addListSelectionListener(resolveToMyVersionAction);
148        addListSelectionListener(resolveToTheirVersionAction);
149        JMenuItem btnResolveMy = popupMenuHandler.addAction(resolveToMyVersionAction);
150        JMenuItem btnResolveTheir = popupMenuHandler.addAction(resolveToTheirVersionAction);
151
152        popupMenuHandler.addListener(new ResolveButtonsPopupMenuListener(btnResolveTheir, btnResolveMy));
153    }
154
155    @Override
156    public void showNotify() {
157        MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this);
158    }
159
160    @Override
161    public void hideNotify() {
162        MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
163        removeDataLayerListeners(MainApplication.getLayerManager().getEditLayer());
164    }
165
166    /**
167     * Add a list selection listener to the conflicts list.
168     * @param listener the ListSelectionListener
169     * @since 5958
170     */
171    public synchronized void addListSelectionListener(ListSelectionListener listener) {
172        lstConflicts.getSelectionModel().addListSelectionListener(listener);
173    }
174
175    /**
176     * Remove the given list selection listener from the conflicts list.
177     * @param listener the ListSelectionListener
178     * @since 5958
179     */
180    public synchronized void removeListSelectionListener(ListSelectionListener listener) {
181        lstConflicts.getSelectionModel().removeListSelectionListener(listener);
182    }
183
184    /**
185     * Replies the popup menu handler.
186     * @return The popup menu handler
187     * @since 5958
188     */
189    public PopupMenuHandler getPopupMenuHandler() {
190        return popupMenuHandler;
191    }
192
193    /**
194     * Launches a conflict resolution dialog for the first selected conflict
195     */
196    private void resolve() {
197        synchronized (this) {
198            if (conflicts == null || model.getSize() == 0)
199                return;
200
201            int index = lstConflicts.getSelectedIndex();
202            if (index < 0) {
203                index = 0;
204            }
205
206            Conflict<? extends OsmPrimitive> c = conflicts.get(index);
207            ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent);
208            dialog.getConflictResolver().populate(c);
209            dialog.showDialog();
210
211            if (index < conflicts.size() - 1) {
212                lstConflicts.setSelectedIndex(index);
213            } else {
214                lstConflicts.setSelectedIndex(index - 1);
215            }
216        }
217        MainApplication.getMap().mapView.repaint();
218    }
219
220    /**
221     * refreshes the view of this dialog
222     */
223    public void refreshView() {
224        DataSet editDs = MainApplication.getLayerManager().getEditDataSet();
225        synchronized (this) {
226            conflicts = editDs == null ? new ConflictCollection() : editDs.getConflicts();
227        }
228        GuiHelper.runInEDT(() -> {
229            model.fireContentChanged();
230            updateTitle();
231        });
232    }
233
234    private synchronized void updateTitle() {
235        int conflictsCount = conflicts.size();
236        if (conflictsCount > 0) {
237            setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) +
238                    " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}",
239                            conflicts.getRelationConflicts().size(),
240                            conflicts.getWayConflicts().size(),
241                            conflicts.getNodeConflicts().size())+')');
242        } else {
243            setTitle(tr("Conflict"));
244        }
245    }
246
247    /**
248     * Paints all conflicts that can be expressed on the main window.
249     *
250     * @param g The {@code Graphics} used to paint
251     * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
252     * @since 86
253     */
254    public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
255        Color preferencesColor = getColor();
256        if (preferencesColor.equals(BACKGROUND_COLOR.get()))
257            return;
258        g.setColor(preferencesColor);
259        OsmPrimitiveVisitor conflictPainter = new ConflictPainter(nc, g);
260        synchronized (this) {
261            for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
262                if (conflicts == null || !conflicts.hasConflictForMy(o)) {
263                    continue;
264                }
265                conflicts.getConflictForMy(o).getTheir().accept(conflictPainter);
266            }
267        }
268    }
269
270    @Override
271    public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
272        removeDataLayerListeners(e.getPreviousDataLayer());
273        addDataLayerListeners(e.getSource().getActiveDataLayer());
274        refreshView();
275    }
276
277    private void addDataLayerListeners(OsmDataLayer newLayer) {
278        if (newLayer != null) {
279            newLayer.getConflicts().addConflictListener(this);
280            newLayer.data.addSelectionListener(this);
281        }
282    }
283
284    private void removeDataLayerListeners(OsmDataLayer oldLayer) {
285        if (oldLayer != null) {
286            oldLayer.getConflicts().removeConflictListener(this);
287            oldLayer.data.removeSelectionListener(this);
288        }
289    }
290
291    /**
292     * replies the conflict collection currently held by this dialog; may be null
293     *
294     * @return the conflict collection currently held by this dialog; may be null
295     */
296    public synchronized ConflictCollection getConflicts() {
297        return conflicts;
298    }
299
300    /**
301     * returns the first selected item of the conflicts list
302     *
303     * @return Conflict
304     */
305    public synchronized Conflict<? extends OsmPrimitive> getSelectedConflict() {
306        if (conflicts == null || model.getSize() == 0)
307            return null;
308
309        int index = lstConflicts.getSelectedIndex();
310
311        return index >= 0 && index < conflicts.size() ? conflicts.get(index) : null;
312    }
313
314    private synchronized boolean isConflictSelected() {
315        final ListSelectionModel selModel = lstConflicts.getSelectionModel();
316        return selModel.getMinSelectionIndex() >= 0 && selModel.getMaxSelectionIndex() >= selModel.getMinSelectionIndex();
317    }
318
319    @Override
320    public void onConflictsAdded(ConflictCollection conflicts) {
321        refreshView();
322    }
323
324    @Override
325    public void onConflictsRemoved(ConflictCollection conflicts) {
326        Logging.info("1 conflict has been resolved.");
327        refreshView();
328    }
329
330    @Override
331    public synchronized void selectionChanged(SelectionChangeEvent event) {
332        lstConflicts.setValueIsAdjusting(true);
333        lstConflicts.clearSelection();
334        for (OsmPrimitive osm : event.getSelection()) {
335            if (conflicts != null && conflicts.hasConflictForMy(osm)) {
336                int pos = model.indexOf(osm);
337                if (pos >= 0) {
338                    lstConflicts.addSelectionInterval(pos, pos);
339                }
340            }
341        }
342        lstConflicts.setValueIsAdjusting(false);
343    }
344
345    @Override
346    public String helpTopic() {
347        return ht("/Dialog/ConflictList");
348    }
349
350    static final class ResolveButtonsPopupMenuListener implements PopupMenuListener {
351        private final JMenuItem btnResolveTheir;
352        private final JMenuItem btnResolveMy;
353
354        ResolveButtonsPopupMenuListener(JMenuItem btnResolveTheir, JMenuItem btnResolveMy) {
355            this.btnResolveTheir = btnResolveTheir;
356            this.btnResolveMy = btnResolveMy;
357        }
358
359        @Override
360        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
361            btnResolveMy.setVisible(ExpertToggleAction.isExpert());
362            btnResolveTheir.setVisible(ExpertToggleAction.isExpert());
363        }
364
365        @Override
366        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
367            // Do nothing
368        }
369
370        @Override
371        public void popupMenuCanceled(PopupMenuEvent e) {
372            // Do nothing
373        }
374    }
375
376    class MouseEventHandler extends PopupMenuLauncher {
377        /**
378         * Constructs a new {@code MouseEventHandler}.
379         */
380        MouseEventHandler() {
381            super(popupMenu);
382        }
383
384        @Override public void mouseClicked(MouseEvent e) {
385            if (isDoubleClick(e)) {
386                resolve();
387            }
388        }
389    }
390
391    /**
392     * The {@link ListModel} for conflicts
393     *
394     */
395    class ConflictListModel implements ListModel<OsmPrimitive> {
396
397        private final CopyOnWriteArrayList<ListDataListener> listeners;
398
399        /**
400         * Constructs a new {@code ConflictListModel}.
401         */
402        ConflictListModel() {
403            listeners = new CopyOnWriteArrayList<>();
404        }
405
406        @Override
407        public void addListDataListener(ListDataListener l) {
408            if (l != null) {
409                listeners.addIfAbsent(l);
410            }
411        }
412
413        @Override
414        public void removeListDataListener(ListDataListener l) {
415            listeners.remove(l);
416        }
417
418        protected void fireContentChanged() {
419            ListDataEvent evt = new ListDataEvent(
420                    this,
421                    ListDataEvent.CONTENTS_CHANGED,
422                    0,
423                    getSize()
424            );
425            for (ListDataListener listener : listeners) {
426                listener.contentsChanged(evt);
427            }
428        }
429
430        @Override
431        public synchronized OsmPrimitive getElementAt(int index) {
432            if (index < 0 || index >= getSize())
433                return null;
434            return conflicts.get(index).getMy();
435        }
436
437        @Override
438        public synchronized int getSize() {
439            return conflicts != null ? conflicts.size() : 0;
440        }
441
442        public synchronized int indexOf(OsmPrimitive my) {
443            if (conflicts != null) {
444                for (int i = 0; i < conflicts.size(); i++) {
445                    if (conflicts.get(i).isMatchingMy(my))
446                        return i;
447                }
448            }
449            return -1;
450        }
451
452        public synchronized OsmPrimitive get(int idx) {
453            return conflicts != null ? conflicts.get(idx).getMy() : null;
454        }
455    }
456
457    class ResolveAction extends AbstractAction implements ListSelectionListener {
458        ResolveAction() {
459            putValue(NAME, tr("Resolve"));
460            putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above."));
461            new ImageProvider("dialogs", "conflict").getResource().attachImageIcon(this, true);
462            putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
463        }
464
465        @Override
466        public void actionPerformed(ActionEvent e) {
467            resolve();
468        }
469
470        @Override
471        public void valueChanged(ListSelectionEvent e) {
472            setEnabled(isConflictSelected());
473        }
474    }
475
476    final class SelectAction extends AbstractSelectAction implements ListSelectionListener {
477        private SelectAction() {
478            putValue("help", ht("/Dialog/ConflictList#SelectAction"));
479        }
480
481        @Override
482        public void actionPerformed(ActionEvent e) {
483            Collection<OsmPrimitive> sel = new LinkedList<>();
484            synchronized (this) {
485                for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
486                    sel.add(o);
487                }
488            }
489            DataSet ds = MainApplication.getLayerManager().getEditDataSet();
490            if (ds != null) { // Can't see how it is possible but it happened in #7942
491                ds.setSelected(sel);
492            }
493        }
494
495        @Override
496        public void valueChanged(ListSelectionEvent e) {
497            setEnabled(isConflictSelected());
498        }
499    }
500
501    abstract class ResolveToAction extends ResolveAction {
502        private final String name;
503        private final MergeDecisionType type;
504
505        ResolveToAction(String name, String description, MergeDecisionType type) {
506            this.name = name;
507            this.type = type;
508            putValue(NAME, name);
509            putValue(SHORT_DESCRIPTION, description);
510        }
511
512        @Override
513        public void actionPerformed(ActionEvent e) {
514            final ConflictResolver resolver = new ConflictResolver();
515            final List<Command> commands = new ArrayList<>();
516            synchronized (this) {
517                for (OsmPrimitive osmPrimitive : lstConflicts.getSelectedValuesList()) {
518                    Conflict<? extends OsmPrimitive> c = conflicts.getConflictForMy(osmPrimitive);
519                    if (c != null) {
520                        resolver.populate(c);
521                        resolver.decideRemaining(type);
522                        commands.add(resolver.buildResolveCommand());
523                    }
524                }
525            }
526            UndoRedoHandler.getInstance().add(new SequenceCommand(name, commands));
527            refreshView();
528        }
529    }
530
531    class ResolveToMyVersionAction extends ResolveToAction {
532        ResolveToMyVersionAction() {
533            super(tr("Resolve to my versions"), tr("Resolves all unresolved conflicts to ''my'' version"),
534                    MergeDecisionType.KEEP_MINE);
535        }
536    }
537
538    class ResolveToTheirVersionAction extends ResolveToAction {
539        ResolveToTheirVersionAction() {
540            super(tr("Resolve to their versions"), tr("Resolves all unresolved conflicts to ''their'' version"),
541                    MergeDecisionType.KEEP_THEIR);
542        }
543    }
544
545    /**
546     * Paints conflicts.
547     */
548    public static class ConflictPainter implements OsmPrimitiveVisitor {
549        // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
550        private final Set<Relation> visited = new HashSet<>();
551        private final NavigatableComponent nc;
552        private final Graphics g;
553
554        ConflictPainter(NavigatableComponent nc, Graphics g) {
555            this.nc = nc;
556            this.g = g;
557        }
558
559        @Override
560        public void visit(Node n) {
561            Point p = nc.getPoint(n);
562            g.drawRect(p.x-1, p.y-1, 2, 2);
563        }
564
565        private void visit(Node n1, Node n2) {
566            Point p1 = nc.getPoint(n1);
567            Point p2 = nc.getPoint(n2);
568            g.drawLine(p1.x, p1.y, p2.x, p2.y);
569        }
570
571        @Override
572        public void visit(Way w) {
573            Node lastN = null;
574            for (Node n : w.getNodes()) {
575                if (lastN == null) {
576                    lastN = n;
577                    continue;
578                }
579                visit(lastN, n);
580                lastN = n;
581            }
582        }
583
584        @Override
585        public void visit(Relation e) {
586            if (!visited.contains(e)) {
587                visited.add(e);
588                try {
589                    for (RelationMember em : e.getMembers()) {
590                        em.getMember().accept(this);
591                    }
592                } finally {
593                    visited.remove(e);
594                }
595            }
596        }
597    }
598
599    /**
600     * Warns the user about the number of detected conflicts
601     *
602     * @param numNewConflicts the number of detected conflicts
603     * @since 5775
604     */
605    public void warnNumNewConflicts(int numNewConflicts) {
606        if (numNewConflicts == 0)
607            return;
608
609        String msg1 = trn(
610                "There was {0} conflict detected.",
611                "There were {0} conflicts detected.",
612                numNewConflicts,
613                numNewConflicts
614        );
615
616        final StringBuilder sb = new StringBuilder();
617        sb.append("<html>").append(msg1).append("</html>");
618        if (numNewConflicts > 0) {
619            final ButtonSpec[] options = new ButtonSpec[] {
620                    new ButtonSpec(
621                            tr("OK"),
622                            new ImageProvider("ok"),
623                            tr("Click to close this dialog and continue editing"),
624                            null /* no specific help */
625                    )
626            };
627            GuiHelper.runInEDT(() -> {
628                HelpAwareOptionPane.showOptionDialog(
629                        Main.parent,
630                        sb.toString(),
631                        tr("Conflicts detected"),
632                        JOptionPane.WARNING_MESSAGE,
633                        null, /* no icon */
634                        options,
635                        options[0],
636                        ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
637                );
638                unfurlDialog();
639                MainApplication.getMap().repaint();
640            });
641        }
642    }
643}
Note: See TracBrowser for help on using the repository browser.