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

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

sonar - pmd:AccessorClassGeneration

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