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 }