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

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

ConflictDialog: fix Sonar/Coverity issues + add basic unit test

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