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

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

sonar - squid:S2156 - "final" classes should not have "protected" members

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