001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagLayout; 007import java.util.Collection; 008import java.util.Objects; 009import java.util.Optional; 010 011import javax.swing.AbstractButton; 012import javax.swing.ButtonGroup; 013import javax.swing.JLabel; 014import javax.swing.JPanel; 015import javax.swing.JToggleButton; 016 017import org.openstreetmap.josm.data.osm.Node; 018import org.openstreetmap.josm.data.osm.Relation; 019import org.openstreetmap.josm.gui.ExtendedDialog; 020import org.openstreetmap.josm.gui.MainApplication; 021import org.openstreetmap.josm.tools.GBC; 022import org.openstreetmap.josm.tools.ImageProvider; 023import org.openstreetmap.josm.tools.UserCancelException; 024 025/** 026 * A dialog allowing the user decide whether the tags/memberships of the existing node should afterwards be at 027 * the existing node, the new nodes, or all of them. 028 * @since 14320 (extracted from UnglueAction) 029 */ 030public final class PropertiesMembershipChoiceDialog extends ExtendedDialog { 031 032 private final transient ExistingBothNewChoice tags; 033 private final transient ExistingBothNewChoice memberships; 034 035 /** 036 * Represents the user choice: the existing node, the new nodes, or all of them 037 */ 038 public enum ExistingBothNew { 039 OLD, BOTH, NEW; 040 } 041 042 /** 043 * Provides toggle buttons to allow the user choose the existing node, the new nodes, or all of them. 044 */ 045 private static class ExistingBothNewChoice { 046 /** The "Existing node" button */ 047 final AbstractButton oldNode = new JToggleButton(tr("Existing node"), ImageProvider.get("dialogs/conflict/tagkeeptheir")); 048 /** The "Both nodes" button */ 049 final AbstractButton bothNodes = new JToggleButton(tr("Both nodes"), ImageProvider.get("dialogs/conflict/tagundecide")); 050 /** The "New node" button */ 051 final AbstractButton newNode = new JToggleButton(tr("New node"), ImageProvider.get("dialogs/conflict/tagkeepmine")); 052 053 ExistingBothNewChoice(final boolean preselectNew) { 054 final ButtonGroup tagsGroup = new ButtonGroup(); 055 tagsGroup.add(oldNode); 056 tagsGroup.add(bothNodes); 057 tagsGroup.add(newNode); 058 tagsGroup.setSelected((preselectNew ? newNode : oldNode).getModel(), true); 059 } 060 061 void add(JPanel content, int gridy) { 062 content.add(oldNode, GBC.std(1, gridy)); 063 content.add(bothNodes, GBC.std(2, gridy)); 064 content.add(newNode, GBC.std(3, gridy)); 065 } 066 067 ExistingBothNew getSelected() { 068 if (oldNode.isSelected()) { 069 return ExistingBothNew.OLD; 070 } else if (bothNodes.isSelected()) { 071 return ExistingBothNew.BOTH; 072 } else if (newNode.isSelected()) { 073 return ExistingBothNew.NEW; 074 } else { 075 throw new IllegalStateException(); 076 } 077 } 078 } 079 080 private PropertiesMembershipChoiceDialog(boolean preselectNew, boolean queryTags, boolean queryMemberships) { 081 super(MainApplication.getMainFrame(), tr("Tags/Memberships"), tr("Unglue"), tr("Cancel")); 082 setButtonIcons("unglueways", "cancel"); 083 084 final JPanel content = new JPanel(new GridBagLayout()); 085 086 if (queryTags) { 087 content.add(new JLabel(tr("Where should the tags of the node be put?")), GBC.std(1, 1).span(3).insets(0, 20, 0, 0)); 088 tags = new ExistingBothNewChoice(preselectNew); 089 tags.add(content, 2); 090 } else { 091 tags = null; 092 } 093 094 if (queryMemberships) { 095 content.add(new JLabel(tr("Where should the memberships of this node be put?")), GBC.std(1, 3).span(3).insets(0, 20, 0, 0)); 096 memberships = new ExistingBothNewChoice(preselectNew); 097 memberships.add(content, 4); 098 } else { 099 memberships = null; 100 } 101 102 setContent(content); 103 setResizable(false); 104 } 105 106 /** 107 * Returns the tags choice. 108 * @return the tags choice 109 */ 110 public Optional<ExistingBothNew> getTags() { 111 return Optional.ofNullable(tags).map(ExistingBothNewChoice::getSelected); 112 } 113 114 /** 115 * Returns the memberships choice. 116 * @return the memberships choice 117 */ 118 public Optional<ExistingBothNew> getMemberships() { 119 return Optional.ofNullable(memberships).map(ExistingBothNewChoice::getSelected); 120 } 121 122 /** 123 * Creates and shows a new {@code PropertiesMembershipChoiceDialog} if necessary. Otherwise does nothing. 124 * @param selectedNodes selected nodes 125 * @param preselectNew if {@code true}, pre-select "new node" as default choice 126 * @return A new {@code PropertiesMembershipChoiceDialog} that has been shown to user, or {@code null} 127 * @throws UserCancelException if user cancels choice 128 */ 129 public static PropertiesMembershipChoiceDialog showIfNecessary(Collection<Node> selectedNodes, boolean preselectNew) 130 throws UserCancelException { 131 final boolean queryTags = isTagged(selectedNodes); 132 final boolean queryMemberships = isUsedInRelations(selectedNodes); 133 if (queryTags || queryMemberships) { 134 final PropertiesMembershipChoiceDialog dialog = new PropertiesMembershipChoiceDialog(preselectNew, queryTags, queryMemberships); 135 dialog.showDialog(); 136 if (dialog.getValue() != 1) { 137 throw new UserCancelException(); 138 } 139 return dialog; 140 } 141 return null; 142 } 143 144 private static boolean isTagged(final Collection<Node> existingNodes) { 145 return existingNodes.stream().anyMatch(Node::isTagged); 146 } 147 148 private static boolean isUsedInRelations(final Collection<Node> existingNodes) { 149 return existingNodes.stream().anyMatch( 150 selectedNode -> selectedNode.referrers(Relation.class).anyMatch(Objects::nonNull)); 151 } 152}