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.gui.HelpAwareOptionPane; 056import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 057import org.openstreetmap.josm.gui.NavigatableComponent; 058import org.openstreetmap.josm.gui.OsmPrimitivRenderer; 059import org.openstreetmap.josm.gui.PopupMenuHandler; 060import org.openstreetmap.josm.gui.SideButton; 061import org.openstreetmap.josm.gui.conflict.pair.ConflictResolver; 062import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 063import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent; 064import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener; 065import org.openstreetmap.josm.gui.layer.OsmDataLayer; 066import org.openstreetmap.josm.gui.util.GuiHelper; 067import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 068import org.openstreetmap.josm.tools.ImageProvider; 069import org.openstreetmap.josm.tools.Shortcut; 070 071/** 072 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle 073 * dialog on the right of the main frame. 074 * @since 86 075 */ 076public final class ConflictDialog extends ToggleDialog implements ActiveLayerChangeListener, IConflictListener, SelectionChangedListener { 077 078 /** the collection of conflicts displayed by this conflict dialog */ 079 private transient ConflictCollection conflicts; 080 081 /** the model for the list of conflicts */ 082 private transient ConflictListModel model; 083 /** the list widget for the list of conflicts */ 084 private JList<OsmPrimitive> lstConflicts; 085 086 private final JPopupMenu popupMenu = new JPopupMenu(); 087 private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu); 088 089 private final ResolveAction actResolve = new ResolveAction(); 090 private final SelectAction actSelect = new SelectAction(); 091 092 /** 093 * Constructs a new {@code ConflictDialog}. 094 */ 095 public ConflictDialog() { 096 super(tr("Conflict"), "conflict", tr("Resolve conflicts."), 097 Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")), 098 KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100); 099 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.getLayerManager().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().attachImageIcon(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.getLayerManager().getEditDataSet(); 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}