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

Last change on this file since 12630 was 12630, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.map and Main.isDisplayingMapView(). Replacements: gui.MainApplication.getMap() / gui.MainApplication.isDisplayingMapView()

  • 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.AbstractVisitor;
54import org.openstreetmap.josm.data.osm.visitor.Visitor;
55import org.openstreetmap.josm.data.preferences.ColorProperty;
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.OsmPrimitivRenderer;
61import org.openstreetmap.josm.gui.PopupMenuHandler;
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 ColorProperty CONFLICT_COLOR = new ColorProperty(marktr("conflict"), Color.GRAY);
82 private static final ColorProperty BACKGROUND_COLOR = new ColorProperty(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 OsmPrimitivRenderer());
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(Main.main.menu.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 Main.getLayerManager().addAndFireActiveLayerChangeListener(this);
158 }
159
160 @Override
161 public void hideNotify() {
162 Main.getLayerManager().removeActiveLayerChangeListener(this);
163 removeEditLayerListeners(Main.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 lstConflicts.setSelectedIndex(index);
212 }
213 MainApplication.getMap().mapView.repaint();
214 }
215
216 /**
217 * refreshes the view of this dialog
218 */
219 public void refreshView() {
220 OsmDataLayer editLayer = Main.getLayerManager().getEditLayer();
221 synchronized (this) {
222 conflicts = editLayer == null ? new ConflictCollection() : editLayer.getConflicts();
223 }
224 GuiHelper.runInEDT(() -> {
225 model.fireContentChanged();
226 updateTitle();
227 });
228 }
229
230 private synchronized void updateTitle() {
231 int conflictsCount = conflicts.size();
232 if (conflictsCount > 0) {
233 setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) +
234 " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}",
235 conflicts.getRelationConflicts().size(),
236 conflicts.getWayConflicts().size(),
237 conflicts.getNodeConflicts().size())+')');
238 } else {
239 setTitle(tr("Conflict"));
240 }
241 }
242
243 /**
244 * Paints all conflicts that can be expressed on the main window.
245 *
246 * @param g The {@code Graphics} used to paint
247 * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
248 * @since 86
249 */
250 public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
251 Color preferencesColor = getColor();
252 if (preferencesColor.equals(BACKGROUND_COLOR.get()))
253 return;
254 g.setColor(preferencesColor);
255 Visitor conflictPainter = new ConflictPainter(nc, g);
256 synchronized (this) {
257 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
258 if (conflicts == null || !conflicts.hasConflictForMy(o)) {
259 continue;
260 }
261 conflicts.getConflictForMy(o).getTheir().accept(conflictPainter);
262 }
263 }
264 }
265
266 @Override
267 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
268 removeEditLayerListeners(e.getPreviousEditLayer());
269 addEditLayerListeners(e.getSource().getEditLayer());
270 refreshView();
271 }
272
273 private void addEditLayerListeners(OsmDataLayer newLayer) {
274 if (newLayer != null) {
275 newLayer.getConflicts().addConflictListener(this);
276 newLayer.data.addSelectionListener(this);
277 }
278 }
279
280 private void removeEditLayerListeners(OsmDataLayer oldLayer) {
281 if (oldLayer != null) {
282 oldLayer.getConflicts().removeConflictListener(this);
283 oldLayer.data.removeSelectionListener(this);
284 }
285 }
286
287 /**
288 * replies the conflict collection currently held by this dialog; may be null
289 *
290 * @return the conflict collection currently held by this dialog; may be null
291 */
292 public synchronized ConflictCollection getConflicts() {
293 return conflicts;
294 }
295
296 /**
297 * returns the first selected item of the conflicts list
298 *
299 * @return Conflict
300 */
301 public synchronized Conflict<? extends OsmPrimitive> getSelectedConflict() {
302 if (conflicts == null || model.getSize() == 0)
303 return null;
304
305 int index = lstConflicts.getSelectedIndex();
306
307 return index >= 0 ? conflicts.get(index) : null;
308 }
309
310 private synchronized boolean isConflictSelected() {
311 final ListSelectionModel selModel = lstConflicts.getSelectionModel();
312 return selModel.getMinSelectionIndex() >= 0 && selModel.getMaxSelectionIndex() >= selModel.getMinSelectionIndex();
313 }
314
315 @Override
316 public void onConflictsAdded(ConflictCollection conflicts) {
317 refreshView();
318 }
319
320 @Override
321 public void onConflictsRemoved(ConflictCollection conflicts) {
322 Logging.info("1 conflict has been resolved.");
323 refreshView();
324 }
325
326 @Override
327 public synchronized void selectionChanged(SelectionChangeEvent event) {
328 lstConflicts.setValueIsAdjusting(true);
329 lstConflicts.clearSelection();
330 for (OsmPrimitive osm : event.getSelection()) {
331 if (conflicts != null && conflicts.hasConflictForMy(osm)) {
332 int pos = model.indexOf(osm);
333 if (pos >= 0) {
334 lstConflicts.addSelectionInterval(pos, pos);
335 }
336 }
337 }
338 lstConflicts.setValueIsAdjusting(false);
339 }
340
341 @Override
342 public String helpTopic() {
343 return ht("/Dialog/ConflictList");
344 }
345
346 static final class ResolveButtonsPopupMenuListener implements PopupMenuListener {
347 private final JMenuItem btnResolveTheir;
348 private final JMenuItem btnResolveMy;
349
350 ResolveButtonsPopupMenuListener(JMenuItem btnResolveTheir, JMenuItem btnResolveMy) {
351 this.btnResolveTheir = btnResolveTheir;
352 this.btnResolveMy = btnResolveMy;
353 }
354
355 @Override
356 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
357 btnResolveMy.setVisible(ExpertToggleAction.isExpert());
358 btnResolveTheir.setVisible(ExpertToggleAction.isExpert());
359 }
360
361 @Override
362 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
363 // Do nothing
364 }
365
366 @Override
367 public void popupMenuCanceled(PopupMenuEvent e) {
368 // Do nothing
369 }
370 }
371
372 class MouseEventHandler extends PopupMenuLauncher {
373 /**
374 * Constructs a new {@code MouseEventHandler}.
375 */
376 MouseEventHandler() {
377 super(popupMenu);
378 }
379
380 @Override public void mouseClicked(MouseEvent e) {
381 if (isDoubleClick(e)) {
382 resolve();
383 }
384 }
385 }
386
387 /**
388 * The {@link ListModel} for conflicts
389 *
390 */
391 class ConflictListModel implements ListModel<OsmPrimitive> {
392
393 private final CopyOnWriteArrayList<ListDataListener> listeners;
394
395 /**
396 * Constructs a new {@code ConflictListModel}.
397 */
398 ConflictListModel() {
399 listeners = new CopyOnWriteArrayList<>();
400 }
401
402 @Override
403 public void addListDataListener(ListDataListener l) {
404 if (l != null) {
405 listeners.addIfAbsent(l);
406 }
407 }
408
409 @Override
410 public void removeListDataListener(ListDataListener l) {
411 listeners.remove(l);
412 }
413
414 protected void fireContentChanged() {
415 ListDataEvent evt = new ListDataEvent(
416 this,
417 ListDataEvent.CONTENTS_CHANGED,
418 0,
419 getSize()
420 );
421 for (ListDataListener listener : listeners) {
422 listener.contentsChanged(evt);
423 }
424 }
425
426 @Override
427 public synchronized OsmPrimitive getElementAt(int index) {
428 if (index < 0 || index >= getSize())
429 return null;
430 return conflicts.get(index).getMy();
431 }
432
433 @Override
434 public synchronized int getSize() {
435 return conflicts != null ? conflicts.size() : 0;
436 }
437
438 public synchronized int indexOf(OsmPrimitive my) {
439 if (conflicts != null) {
440 for (int i = 0; i < conflicts.size(); i++) {
441 if (conflicts.get(i).isMatchingMy(my))
442 return i;
443 }
444 }
445 return -1;
446 }
447
448 public synchronized OsmPrimitive get(int idx) {
449 return conflicts != null ? conflicts.get(idx).getMy() : null;
450 }
451 }
452
453 class ResolveAction extends AbstractAction implements ListSelectionListener {
454 ResolveAction() {
455 putValue(NAME, tr("Resolve"));
456 putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above."));
457 new ImageProvider("dialogs", "conflict").getResource().attachImageIcon(this, true);
458 putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
459 }
460
461 @Override
462 public void actionPerformed(ActionEvent e) {
463 resolve();
464 }
465
466 @Override
467 public void valueChanged(ListSelectionEvent e) {
468 setEnabled(isConflictSelected());
469 }
470 }
471
472 final class SelectAction extends AbstractSelectAction implements ListSelectionListener {
473 private SelectAction() {
474 putValue("help", ht("/Dialog/ConflictList#SelectAction"));
475 }
476
477 @Override
478 public void actionPerformed(ActionEvent e) {
479 Collection<OsmPrimitive> sel = new LinkedList<>();
480 synchronized (this) {
481 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) {
482 sel.add(o);
483 }
484 }
485 DataSet ds = Main.getLayerManager().getEditDataSet();
486 if (ds != null) { // Can't see how it is possible but it happened in #7942
487 ds.setSelected(sel);
488 }
489 }
490
491 @Override
492 public void valueChanged(ListSelectionEvent e) {
493 setEnabled(isConflictSelected());
494 }
495 }
496
497 abstract class ResolveToAction extends ResolveAction {
498 private final String name;
499 private final MergeDecisionType type;
500
501 ResolveToAction(String name, String description, MergeDecisionType type) {
502 this.name = name;
503 this.type = type;
504 putValue(NAME, name);
505 putValue(SHORT_DESCRIPTION, description);
506 }
507
508 @Override
509 public void actionPerformed(ActionEvent e) {
510 final ConflictResolver resolver = new ConflictResolver();
511 final List<Command> commands = new ArrayList<>();
512 synchronized (this) {
513 for (OsmPrimitive osmPrimitive : lstConflicts.getSelectedValuesList()) {
514 Conflict<? extends OsmPrimitive> c = conflicts.getConflictForMy(osmPrimitive);
515 if (c != null) {
516 resolver.populate(c);
517 resolver.decideRemaining(type);
518 commands.add(resolver.buildResolveCommand());
519 }
520 }
521 }
522 Main.main.undoRedo.add(new SequenceCommand(name, commands));
523 refreshView();
524 }
525 }
526
527 class ResolveToMyVersionAction extends ResolveToAction {
528 ResolveToMyVersionAction() {
529 super(tr("Resolve to my versions"), tr("Resolves all unresolved conflicts to ''my'' version"),
530 MergeDecisionType.KEEP_MINE);
531 }
532 }
533
534 class ResolveToTheirVersionAction extends ResolveToAction {
535 ResolveToTheirVersionAction() {
536 super(tr("Resolve to their versions"), tr("Resolves all unresolved conflicts to ''their'' version"),
537 MergeDecisionType.KEEP_THEIR);
538 }
539 }
540
541 /**
542 * Paints conflicts.
543 */
544 public static class ConflictPainter extends AbstractVisitor {
545 // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
546 private final Set<Relation> visited = new HashSet<>();
547 private final NavigatableComponent nc;
548 private final Graphics g;
549
550 ConflictPainter(NavigatableComponent nc, Graphics g) {
551 this.nc = nc;
552 this.g = g;
553 }
554
555 @Override
556 public void visit(Node n) {
557 Point p = nc.getPoint(n);
558 g.drawRect(p.x-1, p.y-1, 2, 2);
559 }
560
561 private void visit(Node n1, Node n2) {
562 Point p1 = nc.getPoint(n1);
563 Point p2 = nc.getPoint(n2);
564 g.drawLine(p1.x, p1.y, p2.x, p2.y);
565 }
566
567 @Override
568 public void visit(Way w) {
569 Node lastN = null;
570 for (Node n : w.getNodes()) {
571 if (lastN == null) {
572 lastN = n;
573 continue;
574 }
575 visit(lastN, n);
576 lastN = n;
577 }
578 }
579
580 @Override
581 public void visit(Relation e) {
582 if (!visited.contains(e)) {
583 visited.add(e);
584 try {
585 for (RelationMember em : e.getMembers()) {
586 em.getMember().accept(this);
587 }
588 } finally {
589 visited.remove(e);
590 }
591 }
592 }
593 }
594
595 /**
596 * Warns the user about the number of detected conflicts
597 *
598 * @param numNewConflicts the number of detected conflicts
599 * @since 5775
600 */
601 public void warnNumNewConflicts(int numNewConflicts) {
602 if (numNewConflicts == 0)
603 return;
604
605 String msg1 = trn(
606 "There was {0} conflict detected.",
607 "There were {0} conflicts detected.",
608 numNewConflicts,
609 numNewConflicts
610 );
611
612 final StringBuilder sb = new StringBuilder();
613 sb.append("<html>").append(msg1).append("</html>");
614 if (numNewConflicts > 0) {
615 final ButtonSpec[] options = new ButtonSpec[] {
616 new ButtonSpec(
617 tr("OK"),
618 ImageProvider.get("ok"),
619 tr("Click to close this dialog and continue editing"),
620 null /* no specific help */
621 )
622 };
623 GuiHelper.runInEDT(() -> {
624 HelpAwareOptionPane.showOptionDialog(
625 Main.parent,
626 sb.toString(),
627 tr("Conflicts detected"),
628 JOptionPane.WARNING_MESSAGE,
629 null, /* no icon */
630 options,
631 options[0],
632 ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
633 );
634 unfurlDialog();
635 MainApplication.getMap().repaint();
636 });
637 }
638 }
639}
Note: See TracBrowser for help on using the repository browser.