001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.tags; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.BorderLayout; 009import java.awt.Component; 010import java.awt.Dimension; 011import java.awt.FlowLayout; 012import java.awt.GraphicsEnvironment; 013import java.awt.event.ActionEvent; 014import java.awt.event.HierarchyBoundsListener; 015import java.awt.event.HierarchyEvent; 016import java.awt.event.WindowAdapter; 017import java.awt.event.WindowEvent; 018import java.beans.PropertyChangeEvent; 019import java.beans.PropertyChangeListener; 020import java.util.Collection; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Set; 024import java.util.stream.Collectors; 025 026import javax.swing.AbstractAction; 027import javax.swing.Action; 028import javax.swing.JButton; 029import javax.swing.JDialog; 030import javax.swing.JLabel; 031import javax.swing.JOptionPane; 032import javax.swing.JPanel; 033import javax.swing.JSplitPane; 034 035import org.openstreetmap.josm.Main; 036import org.openstreetmap.josm.actions.ExpertToggleAction; 037import org.openstreetmap.josm.command.ChangePropertyCommand; 038import org.openstreetmap.josm.command.Command; 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.TagCollection; 043import org.openstreetmap.josm.data.osm.Way; 044import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 045import org.openstreetmap.josm.gui.DefaultNameFormatter; 046import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 047import org.openstreetmap.josm.gui.help.HelpUtil; 048import org.openstreetmap.josm.gui.util.GuiHelper; 049import org.openstreetmap.josm.tools.CheckParameterUtil; 050import org.openstreetmap.josm.tools.ImageProvider; 051import org.openstreetmap.josm.tools.InputMapUtils; 052import org.openstreetmap.josm.tools.StreamUtils; 053import org.openstreetmap.josm.tools.UserCancelException; 054import org.openstreetmap.josm.tools.WindowGeometry; 055 056/** 057 * This dialog helps to resolve conflicts occurring when ways are combined or 058 * nodes are merged. 059 * 060 * Usage: {@link #launchIfNecessary} followed by {@link #buildResolutionCommands}. 061 * 062 * Prior to {@link #launchIfNecessary}, the following usage sequence was needed: 063 * 064 * There is a singleton instance of this dialog which can be retrieved using 065 * {@link #getInstance()}. 066 * 067 * The dialog uses two models: one for resolving tag conflicts, the other 068 * for resolving conflicts in relation memberships. For both models there are accessors, 069 * i.e {@link #getTagConflictResolverModel()} and {@link #getRelationMemberConflictResolverModel()}. 070 * 071 * Models have to be <strong>populated</strong> before the dialog is launched. Example: 072 * <pre> 073 * CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance(); 074 * dialog.getTagConflictResolverModel().populate(aTagCollection); 075 * dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection); 076 * dialog.prepareDefaultDecisions(); 077 * </pre> 078 * 079 * You should also set the target primitive which other primitives (ways or nodes) are 080 * merged to, see {@link #setTargetPrimitive(OsmPrimitive)}. 081 * 082 * After the dialog is closed use {@link #isApplied()} to check whether the dialog has been 083 * applied. If it was applied you may build a collection of {@link Command} objects 084 * which reflect the conflict resolution decisions the user made in the dialog: 085 * see {@link #buildResolutionCommands()} 086 */ 087public class CombinePrimitiveResolverDialog extends JDialog { 088 089 /** the unique instance of the dialog */ 090 private static CombinePrimitiveResolverDialog instance; 091 092 /** 093 * Replies the unique instance of the dialog 094 * 095 * @return the unique instance of the dialog 096 * @deprecated use {@link #launchIfNecessary} instead. 097 */ 098 @Deprecated 099 public static synchronized CombinePrimitiveResolverDialog getInstance() { 100 if (instance == null) { 101 GuiHelper.runInEDTAndWait(() -> instance = new CombinePrimitiveResolverDialog(Main.parent)); 102 } 103 return instance; 104 } 105 106 private AutoAdjustingSplitPane spTagConflictTypes; 107 private TagConflictResolver pnlTagConflictResolver; 108 protected RelationMemberConflictResolver pnlRelationMemberConflictResolver; 109 private boolean applied; 110 private JPanel pnlButtons; 111 protected transient OsmPrimitive targetPrimitive; 112 113 /** the private help action */ 114 private ContextSensitiveHelpAction helpAction; 115 /** the apply button */ 116 private JButton btnApply; 117 118 /** 119 * Replies the target primitive the collection of primitives is merged 120 * or combined to. 121 * 122 * @return the target primitive 123 */ 124 public OsmPrimitive getTargetPrimitmive() { 125 return targetPrimitive; 126 } 127 128 /** 129 * Sets the primitive the collection of primitives is merged or combined to. 130 * 131 * @param primitive the target primitive 132 */ 133 public void setTargetPrimitive(final OsmPrimitive primitive) { 134 this.targetPrimitive = primitive; 135 GuiHelper.runInEDTAndWait(() -> { 136 updateTitle(); 137 if (primitive instanceof Way) { 138 pnlRelationMemberConflictResolver.initForWayCombining(); 139 } else if (primitive instanceof Node) { 140 pnlRelationMemberConflictResolver.initForNodeMerging(); 141 } 142 }); 143 } 144 145 protected void updateTitle() { 146 if (targetPrimitive == null) { 147 setTitle(tr("Conflicts when combining primitives")); 148 return; 149 } 150 if (targetPrimitive instanceof Way) { 151 setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive 152 .getDisplayName(DefaultNameFormatter.getInstance()))); 153 helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts")); 154 getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts")); 155 } else if (targetPrimitive instanceof Node) { 156 setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive 157 .getDisplayName(DefaultNameFormatter.getInstance()))); 158 helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts")); 159 getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts")); 160 } 161 } 162 163 protected final void build() { 164 getContentPane().setLayout(new BorderLayout()); 165 updateTitle(); 166 spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT); 167 spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel()); 168 spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel()); 169 pnlButtons = buildButtonPanel(); 170 getContentPane().add(pnlButtons, BorderLayout.SOUTH); 171 addWindowListener(new AdjustDividerLocationAction()); 172 HelpUtil.setHelpContext(getRootPane(), ht("/")); 173 InputMapUtils.addEscapeAction(getRootPane(), new CancelAction()); 174 } 175 176 protected JPanel buildTagConflictResolverPanel() { 177 pnlTagConflictResolver = new TagConflictResolver(); 178 return pnlTagConflictResolver; 179 } 180 181 protected JPanel buildRelationMemberConflictResolverPanel() { 182 pnlRelationMemberConflictResolver = new RelationMemberConflictResolver(new RelationMemberConflictResolverModel()); 183 return pnlRelationMemberConflictResolver; 184 } 185 186 protected ApplyAction buildApplyAction() { 187 return new ApplyAction(); 188 } 189 190 protected JPanel buildButtonPanel() { 191 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 192 193 // -- apply button 194 ApplyAction applyAction = buildApplyAction(); 195 pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction); 196 pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction); 197 btnApply = new JButton(applyAction); 198 btnApply.setFocusable(true); 199 pnl.add(btnApply); 200 201 // -- cancel button 202 CancelAction cancelAction = new CancelAction(); 203 pnl.add(new JButton(cancelAction)); 204 205 // -- help button 206 helpAction = new ContextSensitiveHelpAction(); 207 pnl.add(new JButton(helpAction)); 208 209 return pnl; 210 } 211 212 /** 213 * Constructs a new {@code CombinePrimitiveResolverDialog}. 214 * @param parent The parent component in which this dialog will be displayed. 215 */ 216 public CombinePrimitiveResolverDialog(Component parent) { 217 super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL); 218 build(); 219 } 220 221 /** 222 * Replies the tag conflict resolver model. 223 * @return The tag conflict resolver model. 224 */ 225 public TagConflictResolverModel getTagConflictResolverModel() { 226 return pnlTagConflictResolver.getModel(); 227 } 228 229 /** 230 * Replies the relation membership conflict resolver model. 231 * @return The relation membership conflict resolver model. 232 */ 233 public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() { 234 return pnlRelationMemberConflictResolver.getModel(); 235 } 236 237 /** 238 * Replies true if all tag and relation member conflicts have been decided. 239 * 240 * @return true if all tag and relation member conflicts have been decided; false otherwise 241 */ 242 public boolean isResolvedCompletely() { 243 return getTagConflictResolverModel().isResolvedCompletely() 244 && getRelationMemberConflictResolverModel().isResolvedCompletely(); 245 } 246 247 protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) { 248 List<Command> cmds = new LinkedList<>(); 249 for (String key : tc.getKeys()) { 250 if (tc.hasUniqueEmptyValue(key)) { 251 if (primitive.get(key) != null) { 252 cmds.add(new ChangePropertyCommand(primitive, key, null)); 253 } 254 } else { 255 String value = tc.getJoinedValues(key); 256 if (!value.equals(primitive.get(key))) { 257 cmds.add(new ChangePropertyCommand(primitive, key, value)); 258 } 259 } 260 } 261 return cmds; 262 } 263 264 /** 265 * Replies the list of {@link Command commands} needed to apply resolution choices. 266 * @return The list of {@link Command commands} needed to apply resolution choices. 267 */ 268 public List<Command> buildResolutionCommands() { 269 List<Command> cmds = new LinkedList<>(); 270 271 TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions(); 272 if (!allResolutions.isEmpty()) { 273 cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions)); 274 } 275 for (String p : OsmPrimitive.getDiscardableKeys()) { 276 if (targetPrimitive.get(p) != null) { 277 cmds.add(new ChangePropertyCommand(targetPrimitive, p, null)); 278 } 279 } 280 281 if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) { 282 cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive)); 283 } 284 285 Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel() 286 .getModifiedRelations(targetPrimitive)); 287 if (cmd != null) { 288 cmds.add(cmd); 289 } 290 return cmds; 291 } 292 293 /** 294 * Prepares the default decisions for populated tag and relation membership conflicts. 295 */ 296 public void prepareDefaultDecisions() { 297 getTagConflictResolverModel().prepareDefaultTagDecisions(); 298 getRelationMemberConflictResolverModel().prepareDefaultRelationDecisions(); 299 } 300 301 protected JPanel buildEmptyConflictsPanel() { 302 JPanel pnl = new JPanel(new BorderLayout()); 303 pnl.add(new JLabel(tr("No conflicts to resolve"))); 304 return pnl; 305 } 306 307 protected void prepareGUIBeforeConflictResolutionStarts() { 308 RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel(); 309 TagConflictResolverModel tagModel = getTagConflictResolverModel(); 310 getContentPane().removeAll(); 311 312 if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) { 313 // display both, the dialog for resolving relation conflicts and for resolving tag conflicts 314 spTagConflictTypes.setTopComponent(pnlTagConflictResolver); 315 spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver); 316 getContentPane().add(spTagConflictTypes, BorderLayout.CENTER); 317 } else if (relModel.getNumDecisions() > 0) { 318 // relation conflicts only 319 getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER); 320 } else if (tagModel.getNumDecisions() > 0) { 321 // tag conflicts only 322 getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER); 323 } else { 324 getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER); 325 } 326 327 getContentPane().add(pnlButtons, BorderLayout.SOUTH); 328 validate(); 329 adjustDividerLocation(); 330 pnlRelationMemberConflictResolver.prepareForEditing(); 331 } 332 333 protected void setApplied(boolean applied) { 334 this.applied = applied; 335 } 336 337 /** 338 * Determines if this dialog has been closed with "Apply". 339 * @return true if this dialog has been closed with "Apply", false otherwise. 340 */ 341 public boolean isApplied() { 342 return applied; 343 } 344 345 @Override 346 public void setVisible(boolean visible) { 347 if (visible) { 348 prepareGUIBeforeConflictResolutionStarts(); 349 setMinimumSize(new Dimension(400, 400)); 350 new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, 351 new Dimension(800, 600))).applySafe(this); 352 setApplied(false); 353 btnApply.requestFocusInWindow(); 354 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 355 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 356 } 357 super.setVisible(visible); 358 } 359 360 class CancelAction extends AbstractAction { 361 362 CancelAction() { 363 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution")); 364 putValue(Action.NAME, tr("Cancel")); 365 new ImageProvider("cancel").getResource().attachImageIcon(this); 366 setEnabled(true); 367 } 368 369 @Override 370 public void actionPerformed(ActionEvent arg0) { 371 setVisible(false); 372 } 373 } 374 375 protected class ApplyAction extends AbstractAction implements PropertyChangeListener { 376 377 public ApplyAction() { 378 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts")); 379 putValue(Action.NAME, tr("Apply")); 380 new ImageProvider("ok").getResource().attachImageIcon(this); 381 updateEnabledState(); 382 } 383 384 @Override 385 public void actionPerformed(ActionEvent arg0) { 386 setApplied(true); 387 setVisible(false); 388 pnlTagConflictResolver.rememberPreferences(); 389 } 390 391 protected final void updateEnabledState() { 392 setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0 393 && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0); 394 } 395 396 @Override 397 public void propertyChange(PropertyChangeEvent evt) { 398 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 399 updateEnabledState(); 400 } 401 if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) { 402 updateEnabledState(); 403 } 404 } 405 } 406 407 private void adjustDividerLocation() { 408 int numTagDecisions = getTagConflictResolverModel().getNumDecisions(); 409 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions(); 410 if (numTagDecisions > 0 && numRelationDecisions > 0) { 411 double nTop = 1.0 + numTagDecisions; 412 double nBottom = 2.5 + numRelationDecisions; 413 spTagConflictTypes.setDividerLocation(nTop/(nTop+nBottom)); 414 } 415 } 416 417 class AdjustDividerLocationAction extends WindowAdapter { 418 @Override 419 public void windowOpened(WindowEvent e) { 420 adjustDividerLocation(); 421 } 422 } 423 424 static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener { 425 private double dividerLocation; 426 427 AutoAdjustingSplitPane(int newOrientation) { 428 super(newOrientation); 429 addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this); 430 addHierarchyBoundsListener(this); 431 } 432 433 @Override 434 public void ancestorResized(HierarchyEvent e) { 435 setDividerLocation((int) (dividerLocation * getHeight())); 436 } 437 438 @Override 439 public void ancestorMoved(HierarchyEvent e) { 440 // do nothing 441 } 442 443 @Override 444 public void propertyChange(PropertyChangeEvent evt) { 445 if (JSplitPane.DIVIDER_LOCATION_PROPERTY.equals(evt.getPropertyName())) { 446 int newVal = (Integer) evt.getNewValue(); 447 if (getHeight() != 0) { 448 dividerLocation = (double) newVal / (double) getHeight(); 449 } 450 } 451 } 452 } 453 454 /** 455 * Replies the list of {@link Command commands} needed to resolve specified conflicts, 456 * by displaying if necessary a {@link CombinePrimitiveResolverDialog} to the user. 457 * This dialog will allow the user to choose conflict resolution actions. 458 * 459 * Non-expert users are informed first of the meaning of these operations, allowing them to cancel. 460 * 461 * @param tagsOfPrimitives The tag collection of the primitives to be combined. 462 * Should generally be equal to {@code TagCollection.unionOfAllPrimitives(primitives)} 463 * @param primitives The primitives to be combined 464 * @param targetPrimitives The primitives the collection of primitives are merged or combined to. 465 * @return The list of {@link Command commands} needed to apply resolution actions. 466 * @throws UserCancelException If the user cancelled a dialog. 467 */ 468 public static List<Command> launchIfNecessary( 469 final TagCollection tagsOfPrimitives, 470 final Collection<? extends OsmPrimitive> primitives, 471 final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException { 472 473 CheckParameterUtil.ensureParameterNotNull(tagsOfPrimitives, "tagsOfPrimitives"); 474 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 475 CheckParameterUtil.ensureParameterNotNull(targetPrimitives, "targetPrimitives"); 476 477 final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives); 478 TagConflictResolutionUtil.combineTigerTags(completeWayTags); 479 TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives); 480 final TagCollection tagsToEdit = new TagCollection(completeWayTags); 481 TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit); 482 483 final Set<Relation> parentRelations = OsmPrimitive.getParentRelations(primitives); 484 485 // Show information dialogs about conflicts to non-experts 486 if (!ExpertToggleAction.isExpert()) { 487 // Tag conflicts 488 if (!completeWayTags.isApplicableToPrimitive()) { 489 informAboutTagConflicts(primitives, completeWayTags); 490 } 491 // Relation membership conflicts 492 if (!parentRelations.isEmpty()) { 493 informAboutRelationMembershipConflicts(primitives, parentRelations); 494 } 495 } 496 497 List<Command> cmds = new LinkedList<>(); 498 499 if (!GraphicsEnvironment.isHeadless()) { 500 // Build conflict resolution dialog 501 final CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance(); 502 503 dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues()); 504 dialog.getRelationMemberConflictResolverModel().populate(parentRelations, primitives); 505 dialog.prepareDefaultDecisions(); 506 507 // Ensure a proper title is displayed instead of a previous target (fix #7925) 508 if (targetPrimitives.size() == 1) { 509 dialog.setTargetPrimitive(targetPrimitives.iterator().next()); 510 } else { 511 dialog.setTargetPrimitive(null); 512 } 513 514 // Resolve tag conflicts if necessary 515 if (!dialog.isResolvedCompletely()) { 516 dialog.setVisible(true); 517 if (!dialog.isApplied()) { 518 throw new UserCancelException(); 519 } 520 } 521 for (OsmPrimitive i : targetPrimitives) { 522 dialog.setTargetPrimitive(i); 523 cmds.addAll(dialog.buildResolutionCommands()); 524 } 525 } 526 return cmds; 527 } 528 529 /** 530 * Inform a non-expert user about what relation membership conflict resolution means. 531 * @param primitives The primitives to be combined 532 * @param parentRelations The parent relations of the primitives 533 * @throws UserCancelException If the user cancels the dialog. 534 */ 535 protected static void informAboutRelationMembershipConflicts( 536 final Collection<? extends OsmPrimitive> primitives, 537 final Set<Relation> parentRelations) throws UserCancelException { 538 /* I18n: object count < 2 is not possible */ 539 String msg = trn("You are about to combine {1} object, " 540 + "which is part of {0} relation:<br/>{2}" 541 + "Combining these objects may break this relation. If you are unsure, please cancel this operation.<br/>" 542 + "If you want to continue, you are shown a dialog to decide how to adapt the relation.<br/><br/>" 543 + "Do you want to continue?", 544 "You are about to combine {1} objects, " 545 + "which are part of {0} relations:<br/>{2}" 546 + "Combining these objects may break these relations. If you are unsure, please cancel this operation.<br/>" 547 + "If you want to continue, you are shown a dialog to decide how to adapt the relations.<br/><br/>" 548 + "Do you want to continue?", 549 parentRelations.size(), parentRelations.size(), primitives.size(), 550 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(parentRelations, 20)); 551 552 if (!ConditionalOptionPaneUtil.showConfirmationDialog( 553 "combine_tags", 554 Main.parent, 555 "<html>" + msg + "</html>", 556 tr("Combine confirmation"), 557 JOptionPane.YES_NO_OPTION, 558 JOptionPane.QUESTION_MESSAGE, 559 JOptionPane.YES_OPTION)) { 560 throw new UserCancelException(); 561 } 562 } 563 564 /** 565 * Inform a non-expert user about what tag conflict resolution means. 566 * @param primitives The primitives to be combined 567 * @param normalizedTags The normalized tag collection of the primitives to be combined 568 * @throws UserCancelException If the user cancels the dialog. 569 */ 570 protected static void informAboutTagConflicts( 571 final Collection<? extends OsmPrimitive> primitives, 572 final TagCollection normalizedTags) throws UserCancelException { 573 String conflicts = normalizedTags.getKeysWithMultipleValues().stream().map( 574 key -> getKeyDescription(key, normalizedTags)).collect(StreamUtils.toHtmlList()); 575 String msg = /* for correct i18n of plural forms - see #9110 */ trn("You are about to combine {0} objects, " 576 + "but the following tags are used conflictingly:<br/>{1}" 577 + "If these objects are combined, the resulting object may have unwanted tags.<br/>" 578 + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>" 579 + "Do you want to continue?", "You are about to combine {0} objects, " 580 + "but the following tags are used conflictingly:<br/>{1}" 581 + "If these objects are combined, the resulting object may have unwanted tags.<br/>" 582 + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>" 583 + "Do you want to continue?", 584 primitives.size(), primitives.size(), conflicts); 585 586 if (!ConditionalOptionPaneUtil.showConfirmationDialog( 587 "combine_tags", 588 Main.parent, 589 "<html>" + msg + "</html>", 590 tr("Combine confirmation"), 591 JOptionPane.YES_NO_OPTION, 592 JOptionPane.QUESTION_MESSAGE, 593 JOptionPane.YES_OPTION)) { 594 throw new UserCancelException(); 595 } 596 } 597 598 private static String getKeyDescription(String key, TagCollection normalizedTags) { 599 String values = normalizedTags.getValues(key) 600 .stream() 601 .map(x -> (x == null || x.isEmpty()) ? tr("<i>missing</i>") : x) 602 .collect(Collectors.joining(tr(", "))); 603 return tr("{0} ({1})", key, values); 604 } 605}