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, 6 years 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.