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

Last change on this file since 12891 was 12809, checked in by bastiK, 7 years ago

replace abstract class AbstractVisitor by interface OsmPrimitiveVisitor; deprecate Visitor

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