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