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.event.ActionEvent; 013import java.awt.event.HierarchyBoundsListener; 014import java.awt.event.HierarchyEvent; 015import java.awt.event.WindowAdapter; 016import java.awt.event.WindowEvent; 017import java.beans.PropertyChangeEvent; 018import java.beans.PropertyChangeListener; 019import java.util.Collection; 020import java.util.HashSet; 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.corrector.UserCancelException; 038import org.openstreetmap.josm.data.osm.Node; 039import org.openstreetmap.josm.data.osm.OsmPrimitive; 040import org.openstreetmap.josm.data.osm.Relation; 041import org.openstreetmap.josm.data.osm.TagCollection; 042import org.openstreetmap.josm.data.osm.Way; 043import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 044import org.openstreetmap.josm.gui.DefaultNameFormatter; 045import org.openstreetmap.josm.gui.SideButton; 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.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 #isCanceled()} to check whether the user canceled 082 * the dialog. If it wasn't canceled 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 static private 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 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 private RelationMemberConflictResolver pnlRelationMemberConflictResolver; 112 private boolean canceled; 113 private JPanel pnlButtons; 114 private 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 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(); 186 return pnlRelationMemberConflictResolver; 187 } 188 189 protected JPanel buildButtonPanel() { 190 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER)); 191 192 // -- apply button 193 ApplyAction applyAction = new ApplyAction(); 194 pnlTagConflictResolver.getModel().addPropertyChangeListener(applyAction); 195 pnlRelationMemberConflictResolver.getModel().addPropertyChangeListener(applyAction); 196 btnApply = new SideButton(applyAction); 197 btnApply.setFocusable(true); 198 pnl.add(btnApply); 199 200 // -- cancel button 201 CancelAction cancelAction = new CancelAction(); 202 pnl.add(new SideButton(cancelAction)); 203 204 // -- help button 205 helpAction = new ContextSensitiveHelpAction(); 206 pnl.add(new SideButton(helpAction)); 207 208 return pnl; 209 } 210 211 /** 212 * Constructs a new {@code CombinePrimitiveResolverDialog}. 213 * @param parent The parent component in which this dialog will be displayed. 214 */ 215 public CombinePrimitiveResolverDialog(Component parent) { 216 super(JOptionPane.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL); 217 build(); 218 } 219 220 /** 221 * Replies the tag conflict resolver model. 222 * @return The tag conflict resolver model. 223 */ 224 public TagConflictResolverModel getTagConflictResolverModel() { 225 return pnlTagConflictResolver.getModel(); 226 } 227 228 /** 229 * Replies the relation membership conflict resolver model. 230 * @return The relation membership conflict resolver model. 231 */ 232 public RelationMemberConflictResolverModel getRelationMemberConflictResolverModel() { 233 return pnlRelationMemberConflictResolver.getModel(); 234 } 235 236 protected List<Command> buildTagChangeCommand(OsmPrimitive primitive, TagCollection tc) { 237 LinkedList<Command> cmds = new LinkedList<Command>(); 238 for (String key : tc.getKeys()) { 239 if (tc.hasUniqueEmptyValue(key)) { 240 if (primitive.get(key) != null) { 241 cmds.add(new ChangePropertyCommand(primitive, key, null)); 242 } 243 } else { 244 String value = tc.getJoinedValues(key); 245 if (!value.equals(primitive.get(key))) { 246 cmds.add(new ChangePropertyCommand(primitive, key, value)); 247 } 248 } 249 } 250 return cmds; 251 } 252 253 /** 254 * Replies the list of {@link Command commands} needed to apply resolution choices. 255 * @return The list of {@link Command commands} needed to apply resolution choices. 256 */ 257 public List<Command> buildResolutionCommands() { 258 List<Command> cmds = new LinkedList<Command>(); 259 260 TagCollection allResolutions = getTagConflictResolverModel().getAllResolutions(); 261 if (!allResolutions.isEmpty()) { 262 cmds.addAll(buildTagChangeCommand(targetPrimitive, allResolutions)); 263 } 264 for(String p : OsmPrimitive.getDiscardableKeys()) { 265 if (targetPrimitive.get(p) != null) { 266 cmds.add(new ChangePropertyCommand(targetPrimitive, p, null)); 267 } 268 } 269 270 if (getRelationMemberConflictResolverModel().getNumDecisions() > 0) { 271 cmds.addAll(getRelationMemberConflictResolverModel().buildResolutionCommands(targetPrimitive)); 272 } 273 274 Command cmd = pnlRelationMemberConflictResolver.buildTagApplyCommands(getRelationMemberConflictResolverModel() 275 .getModifiedRelations(targetPrimitive)); 276 if (cmd != null) { 277 cmds.add(cmd); 278 } 279 return cmds; 280 } 281 282 protected void prepareDefaultTagDecisions() { 283 TagConflictResolverModel model = getTagConflictResolverModel(); 284 for (int i = 0; i < model.getRowCount(); i++) { 285 MultiValueResolutionDecision decision = model.getDecision(i); 286 List<String> values = decision.getValues(); 287 values.remove(""); 288 if (values.size() == 1) { 289 decision.keepOne(values.get(0)); 290 } else { 291 decision.keepAll(); 292 } 293 } 294 model.rebuild(); 295 } 296 297 protected void prepareDefaultRelationDecisions() { 298 RelationMemberConflictResolverModel model = getRelationMemberConflictResolverModel(); 299 Set<Relation> relations = new HashSet<Relation>(); 300 for (int i = 0; i < model.getNumDecisions(); i++) { 301 RelationMemberConflictDecision decision = model.getDecision(i); 302 if (!relations.contains(decision.getRelation())) { 303 decision.decide(RelationMemberConflictDecisionType.KEEP); 304 relations.add(decision.getRelation()); 305 } else { 306 decision.decide(RelationMemberConflictDecisionType.REMOVE); 307 } 308 } 309 model.refresh(); 310 } 311 312 /** 313 * Prepares the default decisions for populated tag and relation membership conflicts. 314 */ 315 public void prepareDefaultDecisions() { 316 prepareDefaultTagDecisions(); 317 prepareDefaultRelationDecisions(); 318 } 319 320 protected JPanel buildEmptyConflictsPanel() { 321 JPanel pnl = new JPanel(new BorderLayout()); 322 pnl.add(new JLabel(tr("No conflicts to resolve"))); 323 return pnl; 324 } 325 326 protected void prepareGUIBeforeConflictResolutionStarts() { 327 RelationMemberConflictResolverModel relModel = getRelationMemberConflictResolverModel(); 328 TagConflictResolverModel tagModel = getTagConflictResolverModel(); 329 getContentPane().removeAll(); 330 331 if (relModel.getNumDecisions() > 0 && tagModel.getNumDecisions() > 0) { 332 // display both, the dialog for resolving relation conflicts and for resolving tag conflicts 333 spTagConflictTypes.setTopComponent(pnlTagConflictResolver); 334 spTagConflictTypes.setBottomComponent(pnlRelationMemberConflictResolver); 335 getContentPane().add(spTagConflictTypes, BorderLayout.CENTER); 336 } else if (relModel.getNumDecisions() > 0) { 337 // relation conflicts only 338 getContentPane().add(pnlRelationMemberConflictResolver, BorderLayout.CENTER); 339 } else if (tagModel.getNumDecisions() > 0) { 340 // tag conflicts only 341 getContentPane().add(pnlTagConflictResolver, BorderLayout.CENTER); 342 } else { 343 getContentPane().add(buildEmptyConflictsPanel(), BorderLayout.CENTER); 344 } 345 346 getContentPane().add(pnlButtons, BorderLayout.SOUTH); 347 validate(); 348 int numTagDecisions = getTagConflictResolverModel().getNumDecisions(); 349 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions(); 350 if (numTagDecisions > 0 && numRelationDecisions > 0) { 351 spTagConflictTypes.setDividerLocation(0.5); 352 } 353 pnlRelationMemberConflictResolver.prepareForEditing(); 354 } 355 356 protected void setCanceled(boolean canceled) { 357 this.canceled = canceled; 358 } 359 360 /** 361 * Determines if this dialog has been cancelled. 362 * @return true if this dialog has been cancelled, false otherwise. 363 */ 364 public boolean isCanceled() { 365 return canceled; 366 } 367 368 @Override 369 public void setVisible(boolean visible) { 370 if (visible) { 371 prepareGUIBeforeConflictResolutionStarts(); 372 new WindowGeometry(getClass().getName() + ".geometry", WindowGeometry.centerInWindow(Main.parent, 373 new Dimension(600, 400))).applySafe(this); 374 setCanceled(false); 375 btnApply.requestFocusInWindow(); 376 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775 377 new WindowGeometry(this).remember(getClass().getName() + ".geometry"); 378 } 379 super.setVisible(visible); 380 } 381 382 class CancelAction extends AbstractAction { 383 384 public CancelAction() { 385 putValue(Action.SHORT_DESCRIPTION, tr("Cancel conflict resolution")); 386 putValue(Action.NAME, tr("Cancel")); 387 putValue(Action.SMALL_ICON, ImageProvider.get("", "cancel")); 388 setEnabled(true); 389 } 390 391 @Override 392 public void actionPerformed(ActionEvent arg0) { 393 setCanceled(true); 394 setVisible(false); 395 } 396 } 397 398 class ApplyAction extends AbstractAction implements PropertyChangeListener { 399 400 public ApplyAction() { 401 putValue(Action.SHORT_DESCRIPTION, tr("Apply resolved conflicts")); 402 putValue(Action.NAME, tr("Apply")); 403 putValue(Action.SMALL_ICON, ImageProvider.get("ok")); 404 updateEnabledState(); 405 } 406 407 @Override 408 public void actionPerformed(ActionEvent arg0) { 409 setVisible(false); 410 pnlTagConflictResolver.rememberPreferences(); 411 } 412 413 protected void updateEnabledState() { 414 setEnabled(pnlTagConflictResolver.getModel().getNumConflicts() == 0 415 && pnlRelationMemberConflictResolver.getModel().getNumConflicts() == 0); 416 } 417 418 @Override 419 public void propertyChange(PropertyChangeEvent evt) { 420 if (evt.getPropertyName().equals(TagConflictResolverModel.NUM_CONFLICTS_PROP)) { 421 updateEnabledState(); 422 } 423 if (evt.getPropertyName().equals(RelationMemberConflictResolverModel.NUM_CONFLICTS_PROP)) { 424 updateEnabledState(); 425 } 426 } 427 } 428 429 class AdjustDividerLocationAction extends WindowAdapter { 430 @Override 431 public void windowOpened(WindowEvent e) { 432 int numTagDecisions = getTagConflictResolverModel().getNumDecisions(); 433 int numRelationDecisions = getRelationMemberConflictResolverModel().getNumDecisions(); 434 if (numTagDecisions > 0 && numRelationDecisions > 0) { 435 spTagConflictTypes.setDividerLocation(0.5); 436 } 437 } 438 } 439 440 static class AutoAdjustingSplitPane extends JSplitPane implements PropertyChangeListener, HierarchyBoundsListener { 441 private double dividerLocation; 442 443 public AutoAdjustingSplitPane(int newOrientation) { 444 super(newOrientation); 445 addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this); 446 addHierarchyBoundsListener(this); 447 } 448 449 @Override 450 public void ancestorResized(HierarchyEvent e) { 451 setDividerLocation((int) (dividerLocation * getHeight())); 452 } 453 454 @Override 455 public void ancestorMoved(HierarchyEvent e) { 456 // do nothing 457 } 458 459 @Override 460 public void propertyChange(PropertyChangeEvent evt) { 461 if (evt.getPropertyName().equals(JSplitPane.DIVIDER_LOCATION_PROPERTY)) { 462 int newVal = (Integer) evt.getNewValue(); 463 if (getHeight() != 0) { 464 dividerLocation = (double) newVal / (double) getHeight(); 465 } 466 } 467 } 468 } 469 470 /** 471 * Replies the list of {@link Command commands} needed to resolve specified conflicts, 472 * by displaying if necessary a {@link CombinePrimitiveResolverDialog} to the user. 473 * This dialog will allow the user to choose conflict resolution actions. 474 * 475 * Non-expert users are informed first of the meaning of these operations, allowing them to cancel. 476 * 477 * @param tagsOfPrimitives The tag collection of the primitives to be combined. 478 * Should generally be equal to {@code TagCollection.unionOfAllPrimitives(primitives)} 479 * @param primitives The primitives to be combined 480 * @param targetPrimitives The primitives the collection of primitives are merged or combined to. 481 * @return The list of {@link Command commands} needed to apply resolution actions. 482 * @throws UserCancelException If the user cancelled a dialog. 483 */ 484 public static List<Command> launchIfNecessary( 485 final TagCollection tagsOfPrimitives, 486 final Collection<? extends OsmPrimitive> primitives, 487 final Collection<? extends OsmPrimitive> targetPrimitives) throws UserCancelException { 488 489 CheckParameterUtil.ensureParameterNotNull(tagsOfPrimitives, "tagsOfPrimitives"); 490 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 491 CheckParameterUtil.ensureParameterNotNull(targetPrimitives, "targetPrimitives"); 492 493 final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives); 494 TagConflictResolutionUtil.combineTigerTags(completeWayTags); 495 TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives); 496 final TagCollection tagsToEdit = new TagCollection(completeWayTags); 497 TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit); 498 499 final Set<Relation> parentRelations = OsmPrimitive.getParentRelations(primitives); 500 501 // Show information dialogs about conflicts to non-experts 502 if (!ExpertToggleAction.isExpert()) { 503 // Tag conflicts 504 if (!completeWayTags.isApplicableToPrimitive()) { 505 informAboutTagConflicts(primitives, completeWayTags); 506 } 507 // Relation membership conflicts 508 if (!parentRelations.isEmpty()) { 509 informAboutRelationMembershipConflicts(primitives, parentRelations); 510 } 511 } 512 513 // Build conflict resolution dialog 514 final CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance(); 515 516 dialog.getTagConflictResolverModel().populate(tagsToEdit, completeWayTags.getKeysWithMultipleValues()); 517 dialog.getRelationMemberConflictResolverModel().populate(parentRelations, primitives); 518 dialog.prepareDefaultDecisions(); 519 520 // Ensure a proper title is displayed instead of a previous target (fix #7925) 521 if (targetPrimitives.size() == 1) { 522 dialog.setTargetPrimitive(targetPrimitives.iterator().next()); 523 } else { 524 dialog.setTargetPrimitive(null); 525 } 526 527 // Resolve tag conflicts if necessary 528 if (!completeWayTags.isApplicableToPrimitive() || !parentRelations.isEmpty()) { 529 dialog.setVisible(true); 530 if (dialog.isCanceled()) { 531 throw new UserCancelException(); 532 } 533 } 534 List<Command> cmds = new LinkedList<Command>(); 535 for (OsmPrimitive i : targetPrimitives) { 536 dialog.setTargetPrimitive(i); 537 cmds.addAll(dialog.buildResolutionCommands()); 538 } 539 return cmds; 540 } 541 542 /** 543 * Inform a non-expert user about what relation membership conflict resolution means. 544 * @param primitives The primitives to be combined 545 * @param parentRelations The parent relations of the primitives 546 * @throws UserCancelException If the user cancels the dialog. 547 */ 548 protected static void informAboutRelationMembershipConflicts( 549 final Collection<? extends OsmPrimitive> primitives, 550 final Set<Relation> parentRelations) throws UserCancelException { 551 String msg = trn("You are about to combine {1} objects, " 552 + "which are part of {0} relation:<br/>{2}" 553 + "Combining these objects may break this relation. If you are unsure, please cancel this operation.<br/>" 554 + "If you want to continue, you are shown a dialog to decide how to adapt the relation.<br/><br/>" 555 + "Do you want to continue?", 556 "You are about to combine {1} objects, " 557 + "which are part of {0} relations:<br/>{2}" 558 + "Combining these objects may break these relations. If you are unsure, please cancel this operation.<br/>" 559 + "If you want to continue, you are shown a dialog to decide how to adapt the relations.<br/><br/>" 560 + "Do you want to continue?", 561 parentRelations.size(), parentRelations.size(), primitives.size(), 562 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(parentRelations)); 563 564 if (!ConditionalOptionPaneUtil.showConfirmationDialog( 565 "combine_tags", 566 Main.parent, 567 "<html>" + msg + "</html>", 568 tr("Combine confirmation"), 569 JOptionPane.YES_NO_OPTION, 570 JOptionPane.QUESTION_MESSAGE, 571 JOptionPane.YES_OPTION)) { 572 throw new UserCancelException(); 573 } 574 } 575 576 /** 577 * Inform a non-expert user about what tag conflict resolution means. 578 * @param primitives The primitives to be combined 579 * @param normalizedTags The normalized tag collection of the primitives to be combined 580 * @throws UserCancelException If the user cancels the dialog. 581 */ 582 protected static void informAboutTagConflicts( 583 final Collection<? extends OsmPrimitive> primitives, 584 final TagCollection normalizedTags) throws UserCancelException { 585 String conflicts = Utils.joinAsHtmlUnorderedList(Utils.transform(normalizedTags.getKeysWithMultipleValues(), new Function<String, String>() { 586 587 @Override 588 public String apply(String key) { 589 return tr("{0} ({1})", key, Utils.join(tr(", "), Utils.transform(normalizedTags.getValues(key), new Function<String, String>() { 590 591 @Override 592 public String apply(String x) { 593 return x == null || x.isEmpty() ? tr("<i>missing</i>") : x; 594 } 595 }))); 596 } 597 })); 598 String msg = tr("You are about to combine {0} objects, " 599 + "but the following tags are used conflictingly:<br/>{1}" 600 + "If these objects are combined, the resulting object may have unwanted tags.<br/>" 601 + "If you want to continue, you are shown a dialog to fix the conflicting tags.<br/><br/>" 602 + "Do you want to continue?", 603 primitives.size(), conflicts); 604 605 if (!ConditionalOptionPaneUtil.showConfirmationDialog( 606 "combine_tags", 607 Main.parent, 608 "<html>" + msg + "</html>", 609 tr("Combine confirmation"), 610 JOptionPane.YES_NO_OPTION, 611 JOptionPane.QUESTION_MESSAGE, 612 JOptionPane.YES_OPTION)) { 613 throw new UserCancelException(); 614 } 615 } 616}