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.MapView; 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.OsmDataLayer; 065import org.openstreetmap.josm.gui.util.GuiHelper; 066import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 067import org.openstreetmap.josm.tools.ImageProvider; 068import org.openstreetmap.josm.tools.Shortcut; 069 070/** 071 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle 072 * dialog on the right of the main frame. 073 * @since 86 074 */ 075public final class ConflictDialog extends ToggleDialog implements MapView.EditLayerChangeListener, IConflictListener, SelectionChangedListener { 076 077 /** the collection of conflicts displayed by this conflict dialog */ 078 private transient ConflictCollection conflicts; 079 080 /** the model for the list of conflicts */ 081 private transient ConflictListModel model; 082 /** the list widget for the list of conflicts */ 083 private JList<OsmPrimitive> lstConflicts; 084 085 private final JPopupMenu popupMenu = new JPopupMenu(); 086 private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu); 087 088 private final ResolveAction actResolve = new ResolveAction(); 089 private final SelectAction actSelect = new SelectAction(); 090 091 /** 092 * Constructs a new {@code ConflictDialog}. 093 */ 094 public ConflictDialog() { 095 super(tr("Conflict"), "conflict", tr("Resolve conflicts."), 096 Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")), 097 KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100); 098 099 build(); 100 refreshView(); 101 } 102 103 /** 104 * Replies the color used to paint conflicts. 105 * 106 * @return the color used to paint conflicts 107 * @see #paintConflicts 108 * @since 1221 109 */ 110 public static Color getColor() { 111 return Main.pref.getColor(marktr("conflict"), Color.gray); 112 } 113 114 /** 115 * builds the GUI 116 */ 117 protected void build() { 118 model = new ConflictListModel(); 119 120 lstConflicts = new JList<>(model); 121 lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 122 lstConflicts.setCellRenderer(new OsmPrimitivRenderer()); 123 lstConflicts.addMouseListener(new MouseEventHandler()); 124 addListSelectionListener(new ListSelectionListener() { 125 @Override 126 public void valueChanged(ListSelectionEvent e) { 127 Main.map.mapView.repaint(); 128 } 129 }); 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 MapView.addEditLayerChangeListener(this, true); 173 refreshView(); 174 } 175 176 @Override 177 public void hideNotify() { 178 MapView.removeEditLayerChangeListener(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.setVisible(true); 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.main.getEditLayer(); 236 conflicts = editLayer == null ? new ConflictCollection() : editLayer.getConflicts(); 237 GuiHelper.runInEDT(new Runnable() { 238 @Override 239 public void run() { 240 model.fireContentChanged(); 241 updateTitle(); 242 } 243 }); 244 } 245 246 private void updateTitle() { 247 int conflictsCount = conflicts.size(); 248 if (conflictsCount > 0) { 249 setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) + 250 " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}", 251 conflicts.getRelationConflicts().size(), 252 conflicts.getWayConflicts().size(), 253 conflicts.getNodeConflicts().size())+')'); 254 } else { 255 setTitle(tr("Conflict")); 256 } 257 } 258 259 /** 260 * Paints all conflicts that can be expressed on the main window. 261 * 262 * @param g The {@code Graphics} used to paint 263 * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes 264 * @since 86 265 */ 266 public void paintConflicts(final Graphics g, final NavigatableComponent nc) { 267 Color preferencesColor = getColor(); 268 if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black))) 269 return; 270 g.setColor(preferencesColor); 271 Visitor conflictPainter = new ConflictPainter(nc, g); 272 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) { 273 if (conflicts == null || !conflicts.hasConflictForMy(o)) { 274 continue; 275 } 276 conflicts.getConflictForMy(o).getTheir().accept(conflictPainter); 277 } 278 } 279 280 @Override 281 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 282 if (oldLayer != null) { 283 oldLayer.getConflicts().removeConflictListener(this); 284 } 285 if (newLayer != null) { 286 newLayer.getConflicts().addConflictListener(this); 287 } 288 refreshView(); 289 } 290 291 /** 292 * replies the conflict collection currently held by this dialog; may be null 293 * 294 * @return the conflict collection currently held by this dialog; may be null 295 */ 296 public ConflictCollection getConflicts() { 297 return conflicts; 298 } 299 300 /** 301 * returns the first selected item of the conflicts list 302 * 303 * @return Conflict 304 */ 305 public Conflict<? extends OsmPrimitive> getSelectedConflict() { 306 if (conflicts == null || model.getSize() == 0) 307 return null; 308 309 int index = lstConflicts.getSelectedIndex(); 310 311 return index >= 0 ? conflicts.get(index) : null; 312 } 313 314 private boolean isConflictSelected() { 315 final ListSelectionModel selModel = lstConflicts.getSelectionModel(); 316 return selModel.getMinSelectionIndex() >= 0 && selModel.getMaxSelectionIndex() >= selModel.getMinSelectionIndex(); 317 } 318 319 @Override 320 public void onConflictsAdded(ConflictCollection conflicts) { 321 refreshView(); 322 } 323 324 @Override 325 public void onConflictsRemoved(ConflictCollection conflicts) { 326 Main.info("1 conflict has been resolved."); 327 refreshView(); 328 } 329 330 @Override 331 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 332 lstConflicts.clearSelection(); 333 for (OsmPrimitive osm : newSelection) { 334 if (conflicts != null && conflicts.hasConflictForMy(osm)) { 335 int pos = model.indexOf(osm); 336 if (pos >= 0) { 337 lstConflicts.addSelectionInterval(pos, pos); 338 } 339 } 340 } 341 } 342 343 @Override 344 public String helpTopic() { 345 return ht("/Dialog/ConflictList"); 346 } 347 348 class MouseEventHandler extends PopupMenuLauncher { 349 /** 350 * Constructs a new {@code MouseEventHandler}. 351 */ 352 MouseEventHandler() { 353 super(popupMenu); 354 } 355 356 @Override public void mouseClicked(MouseEvent e) { 357 if (isDoubleClick(e)) { 358 resolve(); 359 } 360 } 361 } 362 363 /** 364 * The {@link ListModel} for conflicts 365 * 366 */ 367 class ConflictListModel implements ListModel<OsmPrimitive> { 368 369 private final CopyOnWriteArrayList<ListDataListener> listeners; 370 371 /** 372 * Constructs a new {@code ConflictListModel}. 373 */ 374 ConflictListModel() { 375 listeners = new CopyOnWriteArrayList<>(); 376 } 377 378 @Override 379 public void addListDataListener(ListDataListener l) { 380 if (l != null) { 381 listeners.addIfAbsent(l); 382 } 383 } 384 385 @Override 386 public void removeListDataListener(ListDataListener l) { 387 listeners.remove(l); 388 } 389 390 protected void fireContentChanged() { 391 ListDataEvent evt = new ListDataEvent( 392 this, 393 ListDataEvent.CONTENTS_CHANGED, 394 0, 395 getSize() 396 ); 397 for (ListDataListener listener : listeners) { 398 listener.contentsChanged(evt); 399 } 400 } 401 402 @Override 403 public OsmPrimitive getElementAt(int index) { 404 if (index < 0 || index >= getSize()) 405 return null; 406 return conflicts.get(index).getMy(); 407 } 408 409 @Override 410 public int getSize() { 411 return conflicts != null ? conflicts.size() : 0; 412 } 413 414 public int indexOf(OsmPrimitive my) { 415 if (conflicts != null) { 416 for (int i = 0; i < conflicts.size(); i++) { 417 if (conflicts.get(i).isMatchingMy(my)) 418 return i; 419 } 420 } 421 return -1; 422 } 423 424 public OsmPrimitive get(int idx) { 425 return conflicts != null ? conflicts.get(idx).getMy() : null; 426 } 427 } 428 429 class ResolveAction extends AbstractAction implements ListSelectionListener { 430 ResolveAction() { 431 putValue(NAME, tr("Resolve")); 432 putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above.")); 433 putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict")); 434 putValue("help", ht("/Dialog/ConflictList#ResolveAction")); 435 } 436 437 @Override 438 public void actionPerformed(ActionEvent e) { 439 resolve(); 440 } 441 442 @Override 443 public void valueChanged(ListSelectionEvent e) { 444 setEnabled(isConflictSelected()); 445 } 446 } 447 448 final class SelectAction extends AbstractSelectAction implements ListSelectionListener { 449 private SelectAction() { 450 putValue("help", ht("/Dialog/ConflictList#SelectAction")); 451 } 452 453 @Override 454 public void actionPerformed(ActionEvent e) { 455 Collection<OsmPrimitive> sel = new LinkedList<>(); 456 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) { 457 sel.add(o); 458 } 459 DataSet ds = Main.main.getCurrentDataSet(); 460 if (ds != null) { // Can't see how it is possible but it happened in #7942 461 ds.setSelected(sel); 462 } 463 } 464 465 @Override 466 public void valueChanged(ListSelectionEvent e) { 467 setEnabled(isConflictSelected()); 468 } 469 } 470 471 abstract class ResolveToAction extends ResolveAction { 472 private final String name; 473 private final MergeDecisionType type; 474 475 ResolveToAction(String name, String description, MergeDecisionType type) { 476 this.name = name; 477 this.type = type; 478 putValue(NAME, name); 479 putValue(SHORT_DESCRIPTION, description); 480 } 481 482 @Override 483 public void actionPerformed(ActionEvent e) { 484 final ConflictResolver resolver = new ConflictResolver(); 485 final List<Command> commands = new ArrayList<>(); 486 for (OsmPrimitive osmPrimitive : lstConflicts.getSelectedValuesList()) { 487 Conflict<? extends OsmPrimitive> c = conflicts.getConflictForMy(osmPrimitive); 488 if (c != null) { 489 resolver.populate(c); 490 resolver.decideRemaining(type); 491 commands.add(resolver.buildResolveCommand()); 492 } 493 } 494 Main.main.undoRedo.add(new SequenceCommand(name, commands)); 495 refreshView(); 496 Main.map.mapView.repaint(); 497 } 498 } 499 500 class ResolveToMyVersionAction extends ResolveToAction { 501 ResolveToMyVersionAction() { 502 super(tr("Resolve to my versions"), tr("Resolves all unresolved conflicts to ''my'' version"), 503 MergeDecisionType.KEEP_MINE); 504 } 505 } 506 507 class ResolveToTheirVersionAction extends ResolveToAction { 508 ResolveToTheirVersionAction() { 509 super(tr("Resolve to their versions"), tr("Resolves all unresolved conflicts to ''their'' version"), 510 MergeDecisionType.KEEP_THEIR); 511 } 512 } 513 514 /** 515 * Paints conflicts. 516 */ 517 public static class ConflictPainter extends AbstractVisitor { 518 // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938) 519 private final Set<Relation> visited = new HashSet<>(); 520 private final NavigatableComponent nc; 521 private final Graphics g; 522 523 ConflictPainter(NavigatableComponent nc, Graphics g) { 524 this.nc = nc; 525 this.g = g; 526 } 527 528 @Override 529 public void visit(Node n) { 530 Point p = nc.getPoint(n); 531 g.drawRect(p.x-1, p.y-1, 2, 2); 532 } 533 534 private void visit(Node n1, Node n2) { 535 Point p1 = nc.getPoint(n1); 536 Point p2 = nc.getPoint(n2); 537 g.drawLine(p1.x, p1.y, p2.x, p2.y); 538 } 539 540 @Override 541 public void visit(Way w) { 542 Node lastN = null; 543 for (Node n : w.getNodes()) { 544 if (lastN == null) { 545 lastN = n; 546 continue; 547 } 548 visit(lastN, n); 549 lastN = n; 550 } 551 } 552 553 @Override 554 public void visit(Relation e) { 555 if (!visited.contains(e)) { 556 visited.add(e); 557 try { 558 for (RelationMember em : e.getMembers()) { 559 em.getMember().accept(this); 560 } 561 } finally { 562 visited.remove(e); 563 } 564 } 565 } 566 } 567 568 /** 569 * Warns the user about the number of detected conflicts 570 * 571 * @param numNewConflicts the number of detected conflicts 572 * @since 5775 573 */ 574 public void warnNumNewConflicts(int numNewConflicts) { 575 if (numNewConflicts == 0) 576 return; 577 578 String msg1 = trn( 579 "There was {0} conflict detected.", 580 "There were {0} conflicts detected.", 581 numNewConflicts, 582 numNewConflicts 583 ); 584 585 final StringBuilder sb = new StringBuilder(); 586 sb.append("<html>").append(msg1).append("</html>"); 587 if (numNewConflicts > 0) { 588 final ButtonSpec[] options = new ButtonSpec[] { 589 new ButtonSpec( 590 tr("OK"), 591 ImageProvider.get("ok"), 592 tr("Click to close this dialog and continue editing"), 593 null /* no specific help */ 594 ) 595 }; 596 GuiHelper.runInEDT(new Runnable() { 597 @Override 598 public void run() { 599 HelpAwareOptionPane.showOptionDialog( 600 Main.parent, 601 sb.toString(), 602 tr("Conflicts detected"), 603 JOptionPane.WARNING_MESSAGE, 604 null, /* no icon */ 605 options, 606 options[0], 607 ht("/Concepts/Conflict#WarningAboutDetectedConflicts") 608 ); 609 unfurlDialog(); 610 Main.map.repaint(); 611 } 612 }); 613 } 614 } 615}