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

Last change on this file since 12464 was 12353, checked in by michael2402, 7 years ago

ConflictDialog: Do not refresh twice during show

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