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; 024 025import javax.swing.AbstractAction; 026import javax.swing.Action; 027import javax.swing.JDialog; 028import javax.swing.JLabel; 029import javax.swing.JOptionPane; 030import javax.swing.JPanel; 031import javax.swing.JSplitPane; 032 033import org.openstreetmap.josm.Main; 034import org.openstreetmap.josm.actions.ExpertToggleAction; 035import org.openstreetmap.josm.command.ChangePropertyCommand; 036import org.openstreetmap.josm.command.Command; 037import org.openstreetmap.josm.data.osm.Node; 038import org.openstreetmap.josm.data.osm.OsmPrimitive; 039import org.openstreetmap.josm.data.osm.Relation; 040import org.openstreetmap.josm.data.osm.TagCollection; 041import org.openstreetmap.josm.data.osm.Way; 042import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 043import org.openstreetmap.josm.gui.DefaultNameFormatter; 044import org.openstreetmap.josm.gui.SideButton; 045import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction; 046import org.openstreetmap.josm.gui.help.HelpUtil; 047import org.openstreetmap.josm.gui.util.GuiHelper; 048import org.openstreetmap.josm.tools.CheckParameterUtil; 049import org.openstreetmap.josm.tools.ImageProvider; 050import org.openstreetmap.josm.tools.UserCancelException; 051import org.openstreetmap.josm.tools.Utils; 052import org.openstreetmap.josm.tools.Utils.Function; 053import org.openstreetmap.josm.tools.WindowGeometry; 054 055/** 056 * This dialog helps to resolve conflicts occurring when ways are combined or 057 * nodes are merged. 058 * 059 * Usage: {@link #launchIfNecessary} followed by {@link #buildResolutionCommands}. 060 * 061 * Prior to {@link #launchIfNecessary}, the following usage sequence was needed: 062 * 063 * There is a singleton instance of this dialog which can be retrieved using 064 * {@link #getInstance()}. 065 * 066 * The dialog uses two models: one for resolving tag conflicts, the other 067 * for resolving conflicts in relation memberships. For both models there are accessors, 068 * i.e {@link #getTagConflictResolverModel()} and {@link #getRelationMemberConflictResolverModel()}. 069 * 070 * Models have to be <strong>populated</strong> before the dialog is launched. Example: 071 * <pre> 072 * CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance(); 073 * dialog.getTagConflictResolverModel().populate(aTagCollection); 074 * dialog.getRelationMemberConflictResolverModel().populate(aRelationLinkCollection); 075 * dialog.prepareDefaultDecisions(); 076 * </pre> 077 * 078 * You should also set the target primitive which other primitives (ways or nodes) are 079 * merged to, see {@link #setTargetPrimitive(OsmPrimitive)}. 080 * 081 * After the dialog is closed use {@link #isApplied()} to check whether the dialog has been 082 * applied. If it was applied you may build a collection of {@link Command} objects 083 * which reflect the conflict resolution decisions the user made in the dialog: 084 * see {@link #buildResolutionCommands()} 085 */ 086public class CombinePrimitiveResolverDialog extends JDialog { 087 088 /** the unique instance of the dialog */ 089 private static CombinePrimitiveResolverDialog instance; 090 091 /** 092 * Replies the unique instance of the dialog 093 * 094 * @return the unique instance of the dialog 095 * @deprecated use {@link #launchIfNecessary} instead. 096 */ 097 @Deprecated 098 public static synchronized CombinePrimitiveResolverDialog getInstance() { 099 if (instance == null) { 100 GuiHelper.runInEDTAndWait(new Runnable() { 101 @Override public void run() { 102 instance = new CombinePrimitiveResolverDialog(Main.parent); 103 } 104 }); 105 } 106 return instance; 107 } 108 109 private AutoAdjustingSplitPane spTagConflictTypes; 110 private TagConflictResolver pnlTagConflictResolver; 111 protected RelationMemberConflictResolver pnlRelationMemberConflictResolver; 112 private boolean applied; 113 private JPanel pnlButtons; 114 protected transient OsmPrimitive targetPrimitive; 115 116 /** the private help action */ 117 private ContextSensitiveHelpAction helpAction; 118 /** the apply button */ 119 private SideButton btnApply; 120 121 /** 122 * Replies the target primitive the collection of primitives is merged 123 * or combined to. 124 * 125 * @return the target primitive 126 */ 127 public OsmPrimitive getTargetPrimitmive() { 128 return targetPrimitive; 129 } 130 131 /** 132 * Sets the primitive the collection of primitives is merged or combined to. 133 * 134 * @param primitive the target primitive 135 */ 136 public void setTargetPrimitive(final OsmPrimitive primitive) { 137 this.targetPrimitive = primitive; 138 GuiHelper.runInEDTAndWait(new Runnable() { 139 @Override public void run() { 140 updateTitle(); 141 if (primitive instanceof Way) { 142 pnlRelationMemberConflictResolver.initForWayCombining(); 143 } else if (primitive instanceof Node) { 144 pnlRelationMemberConflictResolver.initForNodeMerging(); 145 } 146 } 147 }); 148 } 149 150 protected void updateTitle() { 151 if (targetPrimitive == null) { 152 setTitle(tr("Conflicts when combining primitives")); 153 return; 154 } 155 if (targetPrimitive instanceof Way) { 156 setTitle(tr("Conflicts when combining ways - combined way is ''{0}''", targetPrimitive 157 .getDisplayName(DefaultNameFormatter.getInstance()))); 158 helpAction.setHelpTopic(ht("/Action/CombineWay#ResolvingConflicts")); 159 getRootPane().putClientProperty("help", ht("/Action/CombineWay#ResolvingConflicts")); 160 } else if (targetPrimitive instanceof Node) { 161 setTitle(tr("Conflicts when merging nodes - target node is ''{0}''", targetPrimitive 162 .getDisplayName(DefaultNameFormatter.getInstance()))); 163 helpAction.setHelpTopic(ht("/Action/MergeNodes#ResolvingConflicts")); 164 getRootPane().putClientProperty("help", ht("/Action/MergeNodes#ResolvingConflicts")); 165 } 166 } 167 168 protected final void build() { 169 getContentPane().setLayout(new BorderLayout()); 170 updateTitle(); 171 spTagConflictTypes = new AutoAdjustingSplitPane(JSplitPane.VERTICAL_SPLIT); 172 spTagConflictTypes.setTopComponent(buildTagConflictResolverPanel()); 173 spTagConflictTypes.setBottomComponent(buildRelationMemberConflictResolverPanel()); 174 getContentPane().add(pnlButtons = buildButtonPanel(), BorderLayout.SOUTH); 175 addWindowListener(new AdjustDividerLocationAction()); 176 HelpUtil.setHelpContext(getRootPane(), ht("/")); 177 } 178 179 protected JPanel buildTagConflictResolverPanel() { 180 pnlTagConflictResolver = new TagConflictResolver(); 181 return pnlTagConflictResolver; 182 } 183 184 protected JPanel buildRelationMemberConflictResolverPanel() { 185 pnlRelationMemberConflictResolver = new RelationMemberConflictResolver(new RelationMemberConflictResolverModel()); 186 return pnlRelationMemberConflictResolver; 187 } 188 189 protected ApplyAction buildApplyAction() { 190 return new ApplyAction(); 191 } 192 193 protected JPanel buildButtonPanel() { 194 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 195 196 // -- apply button 197 ApplyAction applyAction = buildApplyAction(); 198 pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction); 199 pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction); 200 btnApply = new SideButton(applyAction); 201 btnApply.setFocusable(true); 202 pnl.add(btnApply); 203 204 // -- cancel button 205 CancelAction cancelAction = new CancelAction(); 206 pnl.add(new SideButton(cancelAction)); 207 208 // -- help button 209 helpAction = new ContextSensitiveHelpAction(); 210 pnl.add(new SideButton(helpAction)); 211 212 return pnl; 213 } 214 215 /** 216 * Constructs a new {@code CombinePrimitiveResolverDialog}. 217 * @param parent The parent component in which this dialog will be displayed. 218 */ 219 public CombinePrimitiveResolverDialog(Component parent) { 220 super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL); 221 build(); 222 } 223 224 /** 225 * Replies the tag conflict resolver model. 226 * @return The tag conflict resolver model. 227 */ 228 public TagConflictResolverModel getTagConflictResolverModel() { 229 return pnlTagConflictResolver.getModel(); 230 } 231 232 /** 233 * Replies the relation membership conflict resolver model. 234 * @return The relation membership conflict resolver model. 235 */ 236 public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() { 237 return pnlRelationMemberConflictResolver.getModel(); 238 } 239 240 /** 241 * Replies true if all tag and relation member conflicts have been decided. 242 * 243 * @return true if all tag and relation member conflicts have been decided; false otherwise 244 */ 245 public boolean isResolvedCompletely() { 246 return getTagConflictResolverModel().isResolvedCompletely() 247 && getRelationMemberConflictResolverModel().isResolvedCompletely(); 248 } 249 250 protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) { 251 List<Command> cmds = new LinkedList<>(); 252 for (String key : tc.getKeys()) { 253 if (tc.hasUniqueEmptyValue(key)) { 254 if (primitive.get(key) != null) { 255 cmds.add(new ChangePropertyCommand(primitive, key, null)); 256 } 257 } else { 258 String value = tc.getJoinedValues(key); 259 if (!value.equals(primitive.get(key))) { 260 cmds.add(new ChangePropertyCommand(primitive, key, value)); 261 } 262 } 263 } 264 return cmds; 265 } 266 267 /** 268 * Replies the list of {@link Command commands} needed to apply resolution choices. 269 * @return The list of {@link Command commands} needed to apply resolution choices. 270 */ 271 public List<Command> buildResolutionCommands() { 272 List<Command> cmds = new LinkedList<>(); 273 274 TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions(); 275 if (!allResolutions.isEmpty()) { 276 cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions)); 277 } 278 for (String p : OsmPrimitive.getDiscardableKeys()) { 279 if (targetPrimitive.get(p) != null) { 280 cmds.add(new ChangePropertyCommand(targetPrimitive, p, null)); 281 } 282 } 283 284 if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) { 285 cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive)); 286 } 287 288 Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel() 289 .getModifiedRelations(targetPrimitive)); 290 if (cmd != null) { 291 cmds.add(cmd); 292 } 293 return cmds; 294 } 295 296 /** 297 * Prepares the default decisions for populated tag and relation membership conflicts. 298 */ 299 public void prepareDefaultDecisions() { 300 getTagConflictResolverModel().prepareDefaultTagDecisions(); 301 getRelationMemberConflictResolverModel().prepareDefaultRelationDecisions(); 302 } 303 304 protected JPanel buildEmptyConflictsPanel() { 305 JPanel pnl = new JPanel(new BorderLayout()); 306 pnl.add(new JLabel(tr("No conflicts to resolve"))); 307 return pnl; 308 } 309 310 protected void prepareGUIBeforeConflictResolutionStarts() { 311 RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel(); 312 TagConflictResolverModel tagModel = getTagConflictResolverModel(); 313 getContentPane().removeAll(); 314 315 if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) { 316 // display both, the dialog for resolving relation conflicts and for resolving tag conflicts 317 spTagConflictTypes.setTopComponent(pnlTagConflictResolver); 318 spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver); 319 getContentPane().add(spTagConflictTypes, BorderLayout.CENTER); 320 } else if (relModel.getNumDecisions() > 0) { 321 // relation conflicts only 322 getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER); 323 } else if (tagModel.getNumDecisions() > 0) { 324 // tag conflicts only 325 getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER); 326 } else { 327 getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER); 328 } 329 330 getContentPane().add(pnlButtons, BorderLayout.SOUTH); 331 validate(); 332 int numTagDecisions = getTagConflictResolverModel().getNumDecisions(); 333 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions(); 334 if (numTagDecisions > 0 && numRelationDecisions > 0) { 335 spTagConflictTypes.setDividerLocation(0.5); 336 } 337 pnlRelationMemberConflictResolver.prepareForEditing(); 338 } 339 340 protected void setApplied(boolean applied) { 341 this.applied = applied; 342 } 343 344 /** 345 * Determines if this dialog has been closed with "Apply". 346 * @return true if this dialog has been closed with "Apply", false otherwise. 347 */ 348 public boolean isApplied() { 349 return applied; 350 } 351 352 @Override 353 public void setVisible(boolean visible) { 354 if (visible) { 355 prepareGUIBeforeConflictResolutionStarts(); 356 new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, 357 new Dimension(600, 400))).applySafe(this); 358 setApplied(false); 359 btnApply.requestFocusInWindow(); 360 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 361 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 362 } 363 super.setVisible(visible); 364 } 365 366 class CancelAction extends AbstractAction { 367 368 CancelAction() { 369 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution")); 370 putValue(Action.NAME, tr("Cancel")); 371 putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel")); 372 setEnabled(true); 373 } 374 375 @Override 376 public void actionPerformed(ActionEvent arg0) { 377 setVisible(false); 378 } 379 } 380 381 protected class ApplyAction extends AbstractAction implements PropertyChangeListener { 382 383 public ApplyAction() { 384 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts")); 385 putValue(Action.NAME, tr("Apply")); 386 putValue(Action.SMALL_ICON, ImageProvider.get("ok")); 387 updateEnabledState(); 388 } 389 390 @Override 391 public void actionPerformed(ActionEvent arg0) { 392 setApplied(true); 393 setVisible(false); 394 pnlTagConflictResolver.rememberPreferences(); 395 } 396 397 protected final void updateEnabledState() { 398 setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0 399 && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0); 400 } 401 402 @Override 403 public void propertyChange(PropertyChangeEvent evt) { 404 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 405 updateEnabledState(); 406 } 407 if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) { 408 updateEnabledState(); 409 } 410 } 411 } 412 413 class AdjustDividerLocationAction extends WindowAdapter { 414 @Override 415 public void windowOpened(WindowEvent e) { 416 int numTagDecisions = getTagConflictResolverModel().getNumDecisions(); 417 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions(); 418 if (numTagDecisions > 0 && numRelationDecisions > 0) { 419 spTagConflictTypes.setDividerLocation(0.5); 420 } 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)); 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 = Utils.joinAsHtmlUnorderedList(Utils.transform(normalizedTags.getKeysWithMultipleValues(), 574 new Function<String, String>() { 575 @Override 576 public String apply(String key) { 577 return tr("{0} ({1})", key, Utils.join(tr(", "), Utils.transform(normalizedTags.getValues(key), 578 new Function<String, String>() { 579 @Override 580 public String apply(String x) { 581 return x == null || x.isEmpty() ? tr("<i>missing</i>") : x; 582 } 583 }))); 584 } 585 })); 586 String msg = /* for correct i18n of plural forms - see #9110 */ trn("You are about to combine {0} objects, " 587 + "but the following tags are used conflictingly:<br/>{1}" 588 + "If these objects are combined, the resulting object may have unwanted tags.<br/>" 589 + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>" 590 + "Do you want to continue?", "You are about to combine {0} objects, " 591 + "but the following tags are used conflictingly:<br/>{1}" 592 + "If these objects are combined, the resulting object may have unwanted tags.<br/>" 593 + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>" 594 + "Do you want to continue?", 595 primitives.size(), primitives.size(), conflicts); 596 597 if (!ConditionalOptionPaneUtil.showConfirmationDialog( 598 "combine_tags", 599 Main.parent, 600 "<html>" + msg + "</html>", 601 tr("Combine confirmation"), 602 JOptionPane.YES_NO_OPTION, 603 JOptionPane.QUESTION_MESSAGE, 604 JOptionPane.YES_OPTION)) { 605 throw new UserCancelException(); 606 } 607 } 608}