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

Last change on this file since 10356 was 10356, checked in by stoecker, 8 years ago

see #9995, see #10684 - remove more hardcoded places of images

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