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