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.Arrays; 016import java.util.Collection; 017import java.util.HashSet; 018import java.util.LinkedList; 019import java.util.Set; 020import java.util.concurrent.CopyOnWriteArrayList; 021 022import javax.swing.AbstractAction; 023import javax.swing.JList; 024import javax.swing.JOptionPane; 025import javax.swing.JPopupMenu; 026import javax.swing.ListModel; 027import javax.swing.ListSelectionModel; 028import javax.swing.event.ListDataEvent; 029import javax.swing.event.ListDataListener; 030import javax.swing.event.ListSelectionEvent; 031import javax.swing.event.ListSelectionListener; 032 033import org.openstreetmap.josm.Main; 034import org.openstreetmap.josm.actions.AbstractSelectAction; 035import org.openstreetmap.josm.data.SelectionChangedListener; 036import org.openstreetmap.josm.data.conflict.Conflict; 037import org.openstreetmap.josm.data.conflict.ConflictCollection; 038import org.openstreetmap.josm.data.conflict.IConflictListener; 039import org.openstreetmap.josm.data.osm.DataSet; 040import org.openstreetmap.josm.data.osm.Node; 041import org.openstreetmap.josm.data.osm.OsmPrimitive; 042import org.openstreetmap.josm.data.osm.Relation; 043import org.openstreetmap.josm.data.osm.RelationMember; 044import org.openstreetmap.josm.data.osm.Way; 045import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; 046import org.openstreetmap.josm.data.osm.visitor.Visitor; 047import org.openstreetmap.josm.gui.HelpAwareOptionPane; 048import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 049import org.openstreetmap.josm.gui.MapView; 050import org.openstreetmap.josm.gui.NavigatableComponent; 051import org.openstreetmap.josm.gui.OsmPrimitivRenderer; 052import org.openstreetmap.josm.gui.PopupMenuHandler; 053import org.openstreetmap.josm.gui.SideButton; 054import org.openstreetmap.josm.gui.layer.OsmDataLayer; 055import org.openstreetmap.josm.gui.util.GuiHelper; 056import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 057import org.openstreetmap.josm.tools.ImageProvider; 058import org.openstreetmap.josm.tools.Shortcut; 059 060/** 061 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle 062 * dialog on the right of the main frame. 063 * 064 */ 065public final class ConflictDialog extends ToggleDialog implements MapView.EditLayerChangeListener, IConflictListener, SelectionChangedListener { 066 067 /** 068 * Replies the color used to paint conflicts. 069 * 070 * @return the color used to paint conflicts 071 * @see #paintConflicts 072 * @since 1221 073 */ 074 public static Color getColor() { 075 return Main.pref.getColor(marktr("conflict"), Color.gray); 076 } 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 ResolveAction actResolve; 090 private SelectAction actSelect; 091 092 /** 093 * builds the GUI 094 */ 095 protected void build() { 096 model = new ConflictListModel(); 097 098 lstConflicts = new JList<>(model); 099 lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 100 lstConflicts.setCellRenderer(new OsmPrimitivRenderer()); 101 lstConflicts.addMouseListener(new MouseEventHandler()); 102 addListSelectionListener(new ListSelectionListener() { 103 @Override 104 public void valueChanged(ListSelectionEvent e) { 105 Main.map.mapView.repaint(); 106 } 107 }); 108 109 SideButton btnResolve = new SideButton(actResolve = new ResolveAction()); 110 addListSelectionListener(actResolve); 111 112 SideButton btnSelect = new SideButton(actSelect = new SelectAction()); 113 addListSelectionListener(actSelect); 114 115 createLayout(lstConflicts, true, Arrays.asList(new SideButton[] { 116 btnResolve, btnSelect 117 })); 118 119 popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("conflict")); 120 } 121 122 /** 123 * constructor 124 */ 125 public ConflictDialog() { 126 super(tr("Conflict"), "conflict", tr("Resolve conflicts."), 127 Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")), 128 KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100); 129 130 build(); 131 refreshView(); 132 } 133 134 @Override 135 public void showNotify() { 136 DataSet.addSelectionListener(this); 137 MapView.addEditLayerChangeListener(this, true); 138 refreshView(); 139 } 140 141 @Override 142 public void hideNotify() { 143 MapView.removeEditLayerChangeListener(this); 144 DataSet.removeSelectionListener(this); 145 } 146 147 /** 148 * Add a list selection listener to the conflicts list. 149 * @param listener the ListSelectionListener 150 * @since 5958 151 */ 152 public void addListSelectionListener(ListSelectionListener listener) { 153 lstConflicts.getSelectionModel().addListSelectionListener(listener); 154 } 155 156 /** 157 * Remove the given list selection listener from the conflicts list. 158 * @param listener the ListSelectionListener 159 * @since 5958 160 */ 161 public void removeListSelectionListener(ListSelectionListener listener) { 162 lstConflicts.getSelectionModel().removeListSelectionListener(listener); 163 } 164 165 /** 166 * Replies the popup menu handler. 167 * @return The popup menu handler 168 * @since 5958 169 */ 170 public PopupMenuHandler getPopupMenuHandler() { 171 return popupMenuHandler; 172 } 173 174 /** 175 * Launches a conflict resolution dialog for the first selected conflict 176 * 177 */ 178 private void resolve() { 179 if (conflicts == null || model.getSize() == 0) return; 180 181 int index = lstConflicts.getSelectedIndex(); 182 if (index < 0) { 183 index = 0; 184 } 185 186 Conflict<? extends OsmPrimitive> c = conflicts.get(index); 187 ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent); 188 dialog.getConflictResolver().populate(c); 189 dialog.setVisible(true); 190 191 lstConflicts.setSelectedIndex(index); 192 193 Main.map.mapView.repaint(); 194 } 195 196 /** 197 * refreshes the view of this dialog 198 */ 199 public void refreshView() { 200 OsmDataLayer editLayer = Main.main.getEditLayer(); 201 conflicts = (editLayer == null ? new ConflictCollection() : editLayer.getConflicts()); 202 GuiHelper.runInEDT(new Runnable() { 203 @Override 204 public void run() { 205 model.fireContentChanged(); 206 updateTitle(); 207 } 208 }); 209 } 210 211 private void updateTitle() { 212 int conflictsCount = conflicts.size(); 213 if (conflictsCount > 0) { 214 setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) + 215 " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}", 216 conflicts.getRelationConflicts().size(), 217 conflicts.getWayConflicts().size(), 218 conflicts.getNodeConflicts().size())+')'); 219 } else { 220 setTitle(tr("Conflict")); 221 } 222 } 223 224 /** 225 * Paints all conflicts that can be expressed on the main window. 226 * 227 * @param g The {@code Graphics} used to paint 228 * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes 229 * @since 86 230 */ 231 public void paintConflicts(final Graphics g, final NavigatableComponent nc) { 232 Color preferencesColor = getColor(); 233 if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black))) 234 return; 235 g.setColor(preferencesColor); 236 Visitor conflictPainter = new AbstractVisitor() { 237 // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938) 238 private final Set<Relation> visited = new HashSet<>(); 239 @Override 240 public void visit(Node n) { 241 Point p = nc.getPoint(n); 242 g.drawRect(p.x-1, p.y-1, 2, 2); 243 } 244 245 public void visit(Node n1, Node n2) { 246 Point p1 = nc.getPoint(n1); 247 Point p2 = nc.getPoint(n2); 248 g.drawLine(p1.x, p1.y, p2.x, p2.y); 249 } 250 251 @Override 252 public void visit(Way w) { 253 Node lastN = null; 254 for (Node n : w.getNodes()) { 255 if (lastN == null) { 256 lastN = n; 257 continue; 258 } 259 visit(lastN, n); 260 lastN = n; 261 } 262 } 263 264 @Override 265 public void visit(Relation e) { 266 if (!visited.contains(e)) { 267 visited.add(e); 268 try { 269 for (RelationMember em : e.getMembers()) { 270 em.getMember().accept(this); 271 } 272 } finally { 273 visited.remove(e); 274 } 275 } 276 } 277 }; 278 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) { 279 if (conflicts == null || !conflicts.hasConflictForMy(o)) { 280 continue; 281 } 282 conflicts.getConflictForMy(o).getTheir().accept(conflictPainter); 283 } 284 } 285 286 @Override 287 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 288 if (oldLayer != null) { 289 oldLayer.getConflicts().removeConflictListener(this); 290 } 291 if (newLayer != null) { 292 newLayer.getConflicts().addConflictListener(this); 293 } 294 refreshView(); 295 } 296 297 298 /** 299 * replies the conflict collection currently held by this dialog; may be null 300 * 301 * @return the conflict collection currently held by this dialog; may be null 302 */ 303 public ConflictCollection getConflicts() { 304 return conflicts; 305 } 306 307 /** 308 * returns the first selected item of the conflicts list 309 * 310 * @return Conflict 311 */ 312 public Conflict<? extends OsmPrimitive> getSelectedConflict() { 313 if (conflicts == null || model.getSize() == 0) return null; 314 315 int index = lstConflicts.getSelectedIndex(); 316 if (index < 0) return null; 317 318 return conflicts.get(index); 319 } 320 321 @Override 322 public void onConflictsAdded(ConflictCollection conflicts) { 323 refreshView(); 324 } 325 326 @Override 327 public void onConflictsRemoved(ConflictCollection conflicts) { 328 Main.info("1 conflict has been resolved."); 329 refreshView(); 330 } 331 332 @Override 333 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 334 lstConflicts.clearSelection(); 335 for (OsmPrimitive osm : newSelection) { 336 if (conflicts != null && conflicts.hasConflictForMy(osm)) { 337 int pos = model.indexOf(osm); 338 if (pos >= 0) { 339 lstConflicts.addSelectionInterval(pos, pos); 340 } 341 } 342 } 343 } 344 345 @Override 346 public String helpTopic() { 347 return ht("/Dialog/ConflictList"); 348 } 349 350 class MouseEventHandler extends PopupMenuLauncher { 351 /** 352 * Constructs a new {@code MouseEventHandler}. 353 */ 354 MouseEventHandler() { 355 super(popupMenu); 356 } 357 358 @Override public void mouseClicked(MouseEvent e) { 359 if (isDoubleClick(e)) { 360 resolve(); 361 } 362 } 363 } 364 365 /** 366 * The {@link ListModel} for conflicts 367 * 368 */ 369 class ConflictListModel implements ListModel<OsmPrimitive> { 370 371 private final CopyOnWriteArrayList<ListDataListener> listeners; 372 373 /** 374 * Constructs a new {@code ConflictListModel}. 375 */ 376 ConflictListModel() { 377 listeners = new CopyOnWriteArrayList<>(); 378 } 379 380 @Override 381 public void addListDataListener(ListDataListener l) { 382 if (l != null) { 383 listeners.addIfAbsent(l); 384 } 385 } 386 387 @Override 388 public void removeListDataListener(ListDataListener l) { 389 listeners.remove(l); 390 } 391 392 protected void fireContentChanged() { 393 ListDataEvent evt = new ListDataEvent( 394 this, 395 ListDataEvent.CONTENTS_CHANGED, 396 0, 397 getSize() 398 ); 399 for (ListDataListener listener : listeners) { 400 listener.contentsChanged(evt); 401 } 402 } 403 404 @Override 405 public OsmPrimitive getElementAt(int index) { 406 if (index < 0) return null; 407 if (index >= getSize()) return null; 408 return conflicts.get(index).getMy(); 409 } 410 411 @Override 412 public int getSize() { 413 if (conflicts == null) return 0; 414 return conflicts.size(); 415 } 416 417 public int indexOf(OsmPrimitive my) { 418 if (conflicts == null) return -1; 419 for (int i = 0; i < conflicts.size(); i++) { 420 if (conflicts.get(i).isMatchingMy(my)) 421 return i; 422 } 423 return -1; 424 } 425 426 public OsmPrimitive get(int idx) { 427 if (conflicts == null) return null; 428 return conflicts.get(idx).getMy(); 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 putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict")); 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 ListSelectionModel model = (ListSelectionModel) e.getSource(); 448 boolean enabled = model.getMinSelectionIndex() >= 0 449 && model.getMaxSelectionIndex() >= model.getMinSelectionIndex(); 450 setEnabled(enabled); 451 } 452 } 453 454 final class SelectAction extends AbstractSelectAction implements ListSelectionListener { 455 private SelectAction() { 456 putValue("help", ht("/Dialog/ConflictList#SelectAction")); 457 } 458 459 @Override 460 public void actionPerformed(ActionEvent e) { 461 Collection<OsmPrimitive> sel = new LinkedList<>(); 462 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) { 463 sel.add(o); 464 } 465 DataSet ds = Main.main.getCurrentDataSet(); 466 if (ds != null) { // Can't see how it is possible but it happened in #7942 467 ds.setSelected(sel); 468 } 469 } 470 471 @Override 472 public void valueChanged(ListSelectionEvent e) { 473 ListSelectionModel model = (ListSelectionModel) e.getSource(); 474 boolean enabled = model.getMinSelectionIndex() >= 0 475 && model.getMaxSelectionIndex() >= model.getMinSelectionIndex(); 476 setEnabled(enabled); 477 } 478 } 479 480 /** 481 * Warns the user about the number of detected conflicts 482 * 483 * @param numNewConflicts the number of detected conflicts 484 * @since 5775 485 */ 486 public void warnNumNewConflicts(int numNewConflicts) { 487 if (numNewConflicts == 0) return; 488 489 String msg1 = trn( 490 "There was {0} conflict detected.", 491 "There were {0} conflicts detected.", 492 numNewConflicts, 493 numNewConflicts 494 ); 495 496 final StringBuilder sb = new StringBuilder(); 497 sb.append("<html>").append(msg1).append("</html>"); 498 if (numNewConflicts > 0) { 499 final ButtonSpec[] options = new ButtonSpec[] { 500 new ButtonSpec( 501 tr("OK"), 502 ImageProvider.get("ok"), 503 tr("Click to close this dialog and continue editing"), 504 null /* no specific help */ 505 ) 506 }; 507 GuiHelper.runInEDT(new Runnable() { 508 @Override 509 public void run() { 510 HelpAwareOptionPane.showOptionDialog( 511 Main.parent, 512 sb.toString(), 513 tr("Conflicts detected"), 514 JOptionPane.WARNING_MESSAGE, 515 null, /* no icon */ 516 options, 517 options[0], 518 ht("/Concepts/Conflict#WarningAboutDetectedConflicts") 519 ); 520 unfurlDialog(); 521 Main.map.repaint(); 522 } 523 }); 524 } 525 } 526}