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

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

see #13479 - make ConflictResolutionDialog inherit from ExtendedDialog

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