001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.Color; 010import java.awt.Graphics; 011import java.awt.Point; 012import java.awt.event.ActionEvent; 013import java.awt.event.KeyEvent; 014import java.awt.event.MouseEvent; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.Collection; 018import java.util.HashSet; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Set; 022import java.util.concurrent.CopyOnWriteArrayList; 023 024import javax.swing.AbstractAction; 025import javax.swing.JList; 026import javax.swing.JMenuItem; 027import javax.swing.JOptionPane; 028import javax.swing.JPopupMenu; 029import javax.swing.ListModel; 030import javax.swing.ListSelectionModel; 031import javax.swing.event.ListDataEvent; 032import javax.swing.event.ListDataListener; 033import javax.swing.event.ListSelectionEvent; 034import javax.swing.event.ListSelectionListener; 035import javax.swing.event.PopupMenuEvent; 036import javax.swing.event.PopupMenuListener; 037 038import org.openstreetmap.josm.Main; 039import org.openstreetmap.josm.actions.AbstractSelectAction; 040import org.openstreetmap.josm.actions.ExpertToggleAction; 041import org.openstreetmap.josm.command.Command; 042import org.openstreetmap.josm.command.SequenceCommand; 043import org.openstreetmap.josm.data.SelectionChangedListener; 044import org.openstreetmap.josm.data.conflict.Conflict; 045import org.openstreetmap.josm.data.conflict.ConflictCollection; 046import org.openstreetmap.josm.data.conflict.IConflictListener; 047import org.openstreetmap.josm.data.osm.DataSet; 048import org.openstreetmap.josm.data.osm.Node; 049import org.openstreetmap.josm.data.osm.OsmPrimitive; 050import org.openstreetmap.josm.data.osm.Relation; 051import org.openstreetmap.josm.data.osm.RelationMember; 052import org.openstreetmap.josm.data.osm.Way; 053import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; 054import org.openstreetmap.josm.data.osm.visitor.Visitor; 055import org.openstreetmap.josm.data.preferences.ColorProperty; 056import org.openstreetmap.josm.gui.HelpAwareOptionPane; 057import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 058import org.openstreetmap.josm.gui.NavigatableComponent; 059import org.openstreetmap.josm.gui.OsmPrimitivRenderer; 060import org.openstreetmap.josm.gui.PopupMenuHandler; 061import org.openstreetmap.josm.gui.SideButton; 062import org.openstreetmap.josm.gui.conflict.pair.ConflictResolver; 063import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 064import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 065import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 066import org.openstreetmap.josm.gui.layer.OsmDataLayer; 067import org.openstreetmap.josm.gui.util.GuiHelper; 068import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 069import org.openstreetmap.josm.tools.ImageProvider; 070import org.openstreetmap.josm.tools.Shortcut; 071 072/** 073 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle 074 * dialog on the right of the main frame. 075 * @since 86 076 */ 077public final class ConflictDialog extends ToggleDialog implements ActiveLayerChangeListener, IConflictListener, SelectionChangedListener { 078 079 private static final ColorProperty CONFLICT_COLOR = new ColorProperty(marktr("conflict"), Color.GRAY); 080 private static final ColorProperty BACKGROUND_COLOR = new ColorProperty(marktr("background"), Color.BLACK); 081 082 /** the collection of conflicts displayed by this conflict dialog */ 083 private transient ConflictCollection conflicts; 084 085 /** the model for the list of conflicts */ 086 private transient ConflictListModel model; 087 /** the list widget for the list of conflicts */ 088 private JList<OsmPrimitive> lstConflicts; 089 090 private final JPopupMenu popupMenu = new JPopupMenu(); 091 private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu); 092 093 private final ResolveAction actResolve = new ResolveAction(); 094 private final SelectAction actSelect = new SelectAction(); 095 096 /** 097 * Constructs a new {@code ConflictDialog}. 098 */ 099 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}