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

Last change on this file was 18784, checked in by taylor.smock, 3 years ago

See #23081: NoSuchElementException in ConflictDialog.ResolveToAction#actionPerformed, fix extra indentation

  • Property svn:eol-style set to native
File size: 23.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;
23import java.util.stream.IntStream;
24
25import javax.swing.AbstractAction;
26import javax.swing.JList;
27import javax.swing.JMenuItem;
28import javax.swing.JOptionPane;
29import javax.swing.JPopupMenu;
30import javax.swing.ListModel;
31import javax.swing.ListSelectionModel;
32import javax.swing.event.ListDataEvent;
33import javax.swing.event.ListDataListener;
34import javax.swing.event.ListSelectionEvent;
35import javax.swing.event.ListSelectionListener;
36import javax.swing.event.PopupMenuEvent;
37import javax.swing.event.PopupMenuListener;
38
39import org.openstreetmap.josm.actions.AbstractSelectAction;
40import org.openstreetmap.josm.actions.AutoScaleAction;
41import org.openstreetmap.josm.actions.ExpertToggleAction;
42import org.openstreetmap.josm.command.Command;
43import org.openstreetmap.josm.command.SequenceCommand;
44import org.openstreetmap.josm.data.UndoRedoHandler;
45import org.openstreetmap.josm.data.conflict.Conflict;
46import org.openstreetmap.josm.data.conflict.ConflictCollection;
47import org.openstreetmap.josm.data.conflict.IConflictListener;
48import org.openstreetmap.josm.data.osm.DataSelectionListener;
49import org.openstreetmap.josm.data.osm.DataSet;
50import org.openstreetmap.josm.data.osm.Node;
51import org.openstreetmap.josm.data.osm.OsmPrimitive;
52import org.openstreetmap.josm.data.osm.Relation;
53import org.openstreetmap.josm.data.osm.RelationMember;
54import org.openstreetmap.josm.data.osm.Way;
55import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
56import org.openstreetmap.josm.data.preferences.NamedColorProperty;
57import org.openstreetmap.josm.gui.HelpAwareOptionPane;
58import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
59import org.openstreetmap.josm.gui.MainApplication;
60import org.openstreetmap.josm.gui.NavigatableComponent;
61import org.openstreetmap.josm.gui.PopupMenuHandler;
62import org.openstreetmap.josm.gui.PrimitiveRenderer;
63import org.openstreetmap.josm.gui.SideButton;
64import org.openstreetmap.josm.gui.conflict.pair.ConflictResolver;
65import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType;
66import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
67import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
68import org.openstreetmap.josm.gui.layer.OsmDataLayer;
69import org.openstreetmap.josm.gui.util.GuiHelper;
70import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
71import org.openstreetmap.josm.tools.ImageProvider;
72import org.openstreetmap.josm.tools.Logging;
73import org.openstreetmap.josm.tools.Shortcut;
74
75/**
76 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle
77 * dialog on the right of the main frame.
78 * @since 86
79 */
80public final class ConflictDialog extends ToggleDialog implements ActiveLayerChangeListener, IConflictListener, DataSelectionListener {
81
82 private static final NamedColorProperty CONFLICT_COLOR = new NamedColorProperty(marktr("conflict"), Color.GRAY);
83 private static final NamedColorProperty BACKGROUND_COLOR = new NamedColorProperty(marktr("background"), Color.BLACK);
84
85 /** the collection of conflicts displayed by this conflict dialog */
86 private transient ConflictCollection conflicts;
87
88 /** the model for the list of conflicts */
89 private transient ConflictListModel model;
90 /** the list widget for the list of conflicts */
91 private JList<OsmPrimitive> lstConflicts;
92
93 private final JPopupMenu popupMenu = new JPopupMenu();
94 private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
95
96 private final ResolveAction actResolve = new ResolveAction();
97 private final SelectAction actSelect = new SelectAction();
98
99 /**
100 * Constructs a new {@code ConflictDialog}.
101 */
102 public ConflictDialog() {
103 super(tr("Conflict"), "conflict", tr("Resolve conflicts"),
104 Shortcut.registerShortcut("subwindow:conflict", tr("Windows: {0}", tr("Conflict")),
105 KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100);
106
107 build();
108 refreshView();
109 }
110
111 /**
112 * Replies the color used to paint conflicts.
113 *
114 * @return the color used to paint conflicts
115 * @see #paintConflicts
116 * @since 1221
117 */
118 public static Color getColor() {
119 return CONFLICT_COLOR.get();
120 }
121
122 /**
123 * builds the GUI
124 */
125 private void build() {
126 synchronized (this) {
127 model = new ConflictListModel();
128
129 lstConflicts = new JList<>(model);
130 lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
131 lstConflicts.setCellRenderer(new PrimitiveRenderer());
132 lstConflicts.addMouseListener(new MouseEventHandler());
133 }
134 addListSelectionListener(e -> MainApplication.getMap().mapView.repaint());
135
136 SideButton btnResolve = new SideButton(actResolve);
137 addListSelectionListener(actResolve);
138
139 SideButton btnSelect = new SideButton(actSelect);
140 addListSelectionListener(actSelect);
141
142 createLayout(lstConflicts, true, Arrays.asList(btnResolve, btnSelect));
143
144 popupMenuHandler.addAction(MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.CONFLICT));
145
146 ResolveToMyVersionAction resolveToMyVersionAction = new ResolveToMyVersionAction();
147 ResolveToTheirVersionAction resolveToTheirVersionAction = new ResolveToTheirVersionAction();
148 addListSelectionListener(resolveToMyVersionAction);
149 addListSelectionListener(resolveToTheirVersionAction);
150 JMenuItem btnResolveMy = popupMenuHandler.addAction(resolveToMyVersionAction);
151 JMenuItem btnResolveTheir = popupMenuHandler.addAction(resolveToTheirVersionAction);
152
153 popupMenuHandler.addListener(new ResolveButtonsPopupMenuListener(btnResolveTheir, btnResolveMy));
154 }
155
156 @Override
157 public void showNotify() {
158 MainApplication.getLayerManager().addAndFireActiveLayerChangeListener(this);
159 }
160
161 @Override
162 public void hideNotify() {
163 MainApplication.getLayerManager().removeActiveLayerChangeListener(this);
164 removeDataLayerListeners(MainApplication.getLayerManager().getEditLayer());
165 }
166
167 /**
168 * Add a list selection listener to the conflicts list.
169 * @param listener the ListSelectionListener
170 * @since 5958
171 */
172 public synchronized void addListSelectionListener(ListSelectionListener listener) {
173 lstConflicts.getSelectionModel().addListSelectionListener(listener);
174 }
175
176 /**
177 * Remove the given list selection listener from the conflicts list.
178 * @param listener the ListSelectionListener
179 * @since 5958
180 */
181 public synchronized void removeListSelectionListener(ListSelectionListener listener) {
182 lstConflicts.getSelectionModel().removeListSelectionListener(listener);
183 }
184
185 /**
186 * Replies the popup menu handler.
187 * @return The popup menu handler
188 * @since 5958
189 */
190 public PopupMenuHandler getPopupMenuHandler() {
191 return popupMenuHandler;
192 }
193
194 /**
195 * Launches a conflict resolution dialog for the first selected conflict
196 */
197 private void resolve() {
198 final ConflictResolutionDialog dialog;
199 int index;
200 synchronized (this) {
201 if (conflicts == null || model.getSize() == 0)
202 return;
203
204 index = lstConflicts.getSelectedIndex();
205 if (index < 0) {
206 index = 0;
207 }
208
209 Conflict<? extends OsmPrimitive> c = conflicts.get(index);
210 dialog = new ConflictResolutionDialog(MainApplication.getMainFrame());
211 dialog.getConflictResolver().populate(c);
212 }
213 // This must not be synchronized. See #23079.
214 // On macOS, under some instances, the AppKit thread may want to lock this (`ConflictDialog`) in order to add a
215 // property change listener. This dialog currently locks the UI thread, so it *should* be safe to have outside
216 // of the synchronized lock.
217 dialog.showDialog();
218 synchronized (this) {
219 if (index < conflicts.size() - 1) {
220 lstConflicts.setSelectedIndex(index);
221 } else {
222 lstConflicts.setSelectedIndex(index - 1);
223 }
224 }
225 MainApplication.getMap().mapView.repaint();
226 }
227
228 /**
229 * refreshes the view of this dialog
230 */
231 public void refreshView() {
232 DataSet editDs = MainApplication.getLayerManager().getEditDataSet();
233 synchronized (this) {
234 conflicts = editDs == null ? new ConflictCollection() : editDs.getConflicts();
235 }
236 GuiHelper.runInEDT(() -> {
237 model.fireContentChanged();
238 updateTitle();
239 });
240 }
241
242 private synchronized void updateTitle() {
243 int conflictsCount = conflicts.size();
244 if (conflictsCount > 0) {
245 setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) +
246 " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}",
247 conflicts.getNumberOfRelationConflicts(),
248 conflicts.getNumberOfWayConflicts(),
249 conflicts.getNumberOfNodeConflicts())+')');
250 } else {
251 setTitle(tr("Conflict"));
252 }
253 }
254
255 /**
256 * Paints all conflicts that can be expressed on the main window.
257 *
258 * @param g The {@code Graphics} used to paint
259 * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes
260 * @since 86
261 */
262 public void paintConflicts(final Graphics g, final NavigatableComponent nc) {
263 Color preferencesColor = getColor();
264 if (preferencesColor.equals(BACKGROUND_COLOR.get()))
265 return;
266 g.setColor(preferencesColor);
267 OsmPrimitiveVisitor conflictPainter = new ConflictPainter(nc, g);
268 synchronized (this) {
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
278 @Override
279 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
280 removeDataLayerListeners(e.getPreviousDataLayer());
281 addDataLayerListeners(e.getSource().getActiveDataLayer());
282 refreshView();
283 }
284
285 private void addDataLayerListeners(OsmDataLayer newLayer) {
286 if (newLayer != null) {
287 newLayer.getConflicts().addConflictListener(this);
288 newLayer.data.addSelectionListener(this);
289 }
290 }
291
292 private void removeDataLayerListeners(OsmDataLayer oldLayer) {
293 if (oldLayer != null) {
294 oldLayer.getConflicts().removeConflictListener(this);
295 oldLayer.data.removeSelectionListener(this);
296 }
297 }
298
299 /**
300 * replies the conflict collection currently held by this dialog; may be null
301 *
302 * @return the conflict collection currently held by this dialog; may be null
303 */
304 public synchronized ConflictCollection getConflicts() {
305 return conflicts;
306 }
307
308 /**
309 * returns the first selected item of the conflicts list
310 *
311 * @return Conflict
312 */
313 public synchronized Conflict<? extends OsmPrimitive> getSelectedConflict() {
314 if (conflicts == null || model.getSize() == 0)
315 return null;
316
317 int index = lstConflicts.getSelectedIndex();
318
319 return index >= 0 && index < conflicts.size() ? conflicts.get(index) : null;
320 }
321
322 private synchronized boolean isConflictSelected() {
323 final ListSelectionModel selModel = lstConflicts.getSelectionModel();
324 final int minSelectionIndex = selModel.getMinSelectionIndex();
325 final int maxSelectionIndex = selModel.getMaxSelectionIndex();
326 final int maxIndex = conflicts.size();
327 // if minSelectionIndex < 0, nothing is selected
328 // if minSelectionIndex > maxIndex, then nothing is selected (we are operating with an old selection context, most likely)
329 // if maxSelectionIndex < minSelectionIndex, _something_ funny is going on. Or there was a typo in the original code.
330 return minSelectionIndex >= 0 && maxIndex > minSelectionIndex && maxSelectionIndex >= minSelectionIndex;
331 }
332
333 @Override
334 public void onConflictsAdded(ConflictCollection conflicts) {
335 refreshView();
336 }
337
338 @Override
339 public void onConflictsRemoved(ConflictCollection conflicts) {
340 Logging.debug("1 conflict has been resolved.");
341 refreshView();
342 }
343
344 @Override
345 public synchronized void selectionChanged(SelectionChangeEvent event) {
346 lstConflicts.setValueIsAdjusting(true);
347 lstConflicts.clearSelection();
348 for (OsmPrimitive osm : event.getSelection()) {
349 if (conflicts != null && conflicts.hasConflictForMy(osm)) {
350 int pos = model.indexOf(osm);
351 if (pos >= 0) {
352 lstConflicts.addSelectionInterval(pos, pos);
353 }
354 }
355 }
356 lstConflicts.setValueIsAdjusting(false);
357 }
358
359 @Override
360 public String helpTopic() {
361 return ht("/Dialog/ConflictList");
362 }
363
364 static final class ResolveButtonsPopupMenuListener implements PopupMenuListener {
365 private final JMenuItem btnResolveTheir;
366 private final JMenuItem btnResolveMy;
367
368 ResolveButtonsPopupMenuListener(JMenuItem btnResolveTheir, JMenuItem btnResolveMy) {
369 this.btnResolveTheir = btnResolveTheir;
370 this.btnResolveMy = btnResolveMy;
371 }
372
373 @Override
374 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
375 btnResolveMy.setVisible(ExpertToggleAction.isExpert());
376 btnResolveTheir.setVisible(ExpertToggleAction.isExpert());
377 ((ResolveAction) btnResolveMy.getAction()).valueChanged(null);
378 ((ResolveAction) btnResolveTheir.getAction()).valueChanged(null);
379 }
380
381 @Override
382 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
383 // Do nothing
384 }
385
386 @Override
387 public void popupMenuCanceled(PopupMenuEvent e) {
388 // Do nothing
389 }
390 }
391
392 class MouseEventHandler extends PopupMenuLauncher {
393 /**
394 * Constructs a new {@code MouseEventHandler}.
395 */
396 MouseEventHandler() {
397 super(popupMenu);
398 }
399
400 @Override public void mouseClicked(MouseEvent e) {
401 if (isDoubleClick(e)) {
402 resolve();
403 }
404 }
405 }
406
407 /**
408 * The {@link ListModel} for conflicts
409 *
410 */
411 class ConflictListModel implements ListModel<OsmPrimitive> {
412
413 private final CopyOnWriteArrayList<ListDataListener> listeners;
414
415 /**
416 * Constructs a new {@code ConflictListModel}.
417 */
418 ConflictListModel() {
419 listeners = new CopyOnWriteArrayList<>();
420 }
421
422 @Override
423 public void addListDataListener(ListDataListener l) {
424 if (l != null) {
425 listeners.addIfAbsent(l);
426 }
427 }
428
429 @Override
430 public void removeListDataListener(ListDataListener l) {
431 listeners.remove(l);
432 }
433
434 protected void fireContentChanged() {
435 ListDataEvent evt = new ListDataEvent(
436 this,
437 ListDataEvent.CONTENTS_CHANGED,
438 0,
439 getSize()
440 );
441 for (ListDataListener listener : listeners) {
442 listener.contentsChanged(evt);
443 }
444 }
445
446 @Override
447 public synchronized OsmPrimitive getElementAt(int index) {
448 if (index < 0 || index >= getSize())
449 return null;
450 return conflicts.get(index).getMy();
451 }
452
453 @Override
454 public synchronized int getSize() {
455 return conflicts != null ? conflicts.size() : 0;
456 }
457
458 public synchronized int indexOf(OsmPrimitive my) {
459 if (conflicts != null) {
460 return IntStream.range(0, conflicts.size())
461 .filter(i -> conflicts.get(i).isMatchingMy(my))
462 .findFirst().orElse(-1);
463 }
464 return -1;
465 }
466
467 public synchronized OsmPrimitive get(int idx) {
468 return conflicts != null ? conflicts.get(idx).getMy() : null;
469 }
470 }
471
472 class ResolveAction extends AbstractAction implements ListSelectionListener {
473 ResolveAction() {
474 putValue(NAME, tr("Resolve"));
475 putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above."));
476 new ImageProvider("dialogs", "conflict").getResource().attachImageIcon(this, true);
477 putValue("help", ht("/Dialog/ConflictList#ResolveAction"));
478 }
479
480 @Override
481 public void actionPerformed(ActionEvent e) {
482 resolve();
483 }
484
485 @Override
486 public void valueChanged(ListSelectionEvent e) {
487 setEnabled(isConflictSelected());
488 }
489 }
490
491 final class SelectAction extends AbstractSelectAction implements ListSelectionListener {
492 private SelectAction() {
493 putValue("help", ht("/Dialog/ConflictList#SelectAction"));
494 }
495
496 @Override
497 public void actionPerformed(ActionEvent e) {
498 Collection<OsmPrimitive> sel = new LinkedList<>();
499 synchronized (this) {
500 sel.addAll(lstConflicts.getSelectedValuesList());
501 }
502 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
503 if (ds != null) { // Can't see how it is possible but it happened in #7942
504 ds.setSelected(sel);
505 }
506 }
507
508 @Override
509 public void valueChanged(ListSelectionEvent e) {
510 setEnabled(isConflictSelected());
511 }
512 }
513
514 abstract class ResolveToAction extends ResolveAction {
515 private final String name;
516 private final MergeDecisionType type;
517
518 ResolveToAction(String name, String description, MergeDecisionType type) {
519 this.name = name;
520 this.type = type;
521 putValue(NAME, name);
522 putValue(SHORT_DESCRIPTION, description);
523 }
524
525 @Override
526 public void actionPerformed(ActionEvent e) {
527 final ConflictResolver resolver = new ConflictResolver();
528 final List<Command> commands = new ArrayList<>();
529 synchronized (this) {
530 for (OsmPrimitive osmPrimitive : lstConflicts.getSelectedValuesList()) {
531 Conflict<? extends OsmPrimitive> c = conflicts.getConflictForMy(osmPrimitive);
532 if (c != null) {
533 resolver.populate(c);
534 resolver.decideRemaining(type);
535 commands.add(resolver.buildResolveCommand());
536 }
537 }
538 }
539 UndoRedoHandler.getInstance().add(new SequenceCommand(name, commands));
540 refreshView();
541 }
542 }
543
544 class ResolveToMyVersionAction extends ResolveToAction {
545 ResolveToMyVersionAction() {
546 super(tr("Resolve to my versions"), tr("Resolves all unresolved conflicts to ''my'' version"),
547 MergeDecisionType.KEEP_MINE);
548 }
549 }
550
551 class ResolveToTheirVersionAction extends ResolveToAction {
552 ResolveToTheirVersionAction() {
553 super(tr("Resolve to their versions"), tr("Resolves all unresolved conflicts to ''their'' version"),
554 MergeDecisionType.KEEP_THEIR);
555 }
556 }
557
558 /**
559 * Paints conflicts.
560 */
561 public static class ConflictPainter implements OsmPrimitiveVisitor {
562 // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938)
563 private final Set<Relation> visited = new HashSet<>();
564 private final NavigatableComponent nc;
565 private final Graphics g;
566
567 ConflictPainter(NavigatableComponent nc, Graphics g) {
568 this.nc = nc;
569 this.g = g;
570 }
571
572 @Override
573 public void visit(Node n) {
574 Point p = nc.getPoint(n);
575 g.drawRect(p.x-1, p.y-1, 2, 2);
576 }
577
578 private void visit(Node n1, Node n2) {
579 Point p1 = nc.getPoint(n1);
580 Point p2 = nc.getPoint(n2);
581 g.drawLine(p1.x, p1.y, p2.x, p2.y);
582 }
583
584 @Override
585 public void visit(Way w) {
586 Node lastN = null;
587 for (Node n : w.getNodes()) {
588 if (lastN == null) {
589 lastN = n;
590 continue;
591 }
592 visit(lastN, n);
593 lastN = n;
594 }
595 }
596
597 @Override
598 public void visit(Relation e) {
599 if (!visited.contains(e)) {
600 visited.add(e);
601 try {
602 for (RelationMember em : e.getMembers()) {
603 em.getMember().accept(this);
604 }
605 } finally {
606 visited.remove(e);
607 }
608 }
609 }
610 }
611
612 /**
613 * Warns the user about the number of detected conflicts
614 *
615 * @param numNewConflicts the number of detected conflicts
616 * @since 5775
617 */
618 public void warnNumNewConflicts(int numNewConflicts) {
619 if (numNewConflicts == 0)
620 return;
621
622 String msg1 = trn(
623 "There was {0} conflict detected.",
624 "There were {0} conflicts detected.",
625 numNewConflicts,
626 numNewConflicts
627 );
628
629 final StringBuilder sb = new StringBuilder();
630 sb.append("<html>").append(msg1).append("</html>");
631 if (numNewConflicts > 0) {
632 final ButtonSpec[] options = {
633 new ButtonSpec(
634 tr("OK"),
635 new ImageProvider("ok"),
636 tr("Click to close this dialog and continue editing"),
637 null /* no specific help */
638 )
639 };
640 GuiHelper.runInEDT(() -> {
641 HelpAwareOptionPane.showOptionDialog(
642 MainApplication.getMainFrame(),
643 sb.toString(),
644 tr("Conflicts detected"),
645 JOptionPane.WARNING_MESSAGE,
646 null, /* no icon */
647 options,
648 options[0],
649 ht("/Concepts/Conflict#WarningAboutDetectedConflicts")
650 );
651 unfurlDialog();
652 MainApplication.getMap().repaint();
653 });
654 }
655 }
656}
Note: See TracBrowser for help on using the repository browser.