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 * Returns the opposite/inverted user choice. 043 * @return the opposite/inverted user choice 044 */ 045 public ExistingBothNew opposite() { 046 return equals(OLD) ? NEW : equals(NEW) ? OLD : this; 047 } 048 } 049 050 /** 051 * Provides toggle buttons to allow the user choose the existing node, the new nodes, or all of them. 052 */ 053 private static class ExistingBothNewChoice { 054 /** The "Existing node" button */ 055 final AbstractButton oldNode = new JToggleButton(tr("Existing node"), ImageProvider.get("dialogs/conflict/tagkeeptheir")); 056 /** The "Both nodes" button */ 057 final AbstractButton bothNodes = new JToggleButton(tr("Both nodes"), ImageProvider.get("dialogs/conflict/tagundecide")); 058 /** The "New node" button */ 059 final AbstractButton newNode = new JToggleButton(tr("New node"), ImageProvider.get("dialogs/conflict/tagkeepmine")); 060 061 ExistingBothNewChoice(final boolean preselectNew) { 062 final ButtonGroup tagsGroup = new ButtonGroup(); 063 tagsGroup.add(oldNode); 064 tagsGroup.add(bothNodes); 065 tagsGroup.add(newNode); 066 tagsGroup.setSelected((preselectNew ? newNode : oldNode).getModel(), true); 067 } 068 069 void add(JPanel content, int gridy) { 070 content.add(oldNode, GBC.std(1, gridy)); 071 content.add(bothNodes, GBC.std(2, gridy)); 072 content.add(newNode, GBC.std(3, gridy)); 073 } 074 075 ExistingBothNew getSelected() { 076 if (oldNode.isSelected()) { 077 return ExistingBothNew.OLD; 078 } else if (bothNodes.isSelected()) { 079 return ExistingBothNew.BOTH; 080 } else if (newNode.isSelected()) { 081 return ExistingBothNew.NEW; 082 } else { 083 throw new IllegalStateException(); 084 } 085 } 086 } 087 088 private PropertiesMembershipChoiceDialog(boolean preselectNew, boolean queryTags, boolean queryMemberships) { 089 super(MainApplication.getMainFrame(), tr("Tags/Memberships"), tr("Unglue"), tr("Cancel")); 090 setButtonIcons("unglueways", "cancel"); 091 092 final JPanel content = new JPanel(new GridBagLayout()); 093 094 if (queryTags) { 095 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)); 096 tags = new ExistingBothNewChoice(preselectNew); 097 tags.add(content, 2); 098 } else { 099 tags = null; 100 } 101 102 if (queryMemberships) { 103 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)); 104 memberships = new ExistingBothNewChoice(preselectNew); 105 memberships.add(content, 4); 106 } else { 107 memberships = null; 108 } 109 110 setContent(content); 111 setResizable(false); 112 } 113 114 /** 115 * Returns the tags choice. 116 * @return the tags choice 117 */ 118 public Optional<ExistingBothNew> getTags() { 119 return Optional.ofNullable(tags).map(ExistingBothNewChoice::getSelected); 120 } 121 122 /** 123 * Returns the memberships choice. 124 * @return the memberships choice 125 */ 126 public Optional<ExistingBothNew> getMemberships() { 127 return Optional.ofNullable(memberships).map(ExistingBothNewChoice::getSelected); 128 } 129 130 /** 131 * Creates and shows a new {@code PropertiesMembershipChoiceDialog} if necessary. Otherwise does nothing. 132 * @param selectedNodes selected nodes 133 * @param preselectNew if {@code true}, pre-select "new node" as default choice 134 * @return A new {@code PropertiesMembershipChoiceDialog} that has been shown to user, or {@code null} 135 * @throws UserCancelException if user cancels choice 136 */ 137 public static PropertiesMembershipChoiceDialog showIfNecessary(Collection<Node> selectedNodes, boolean preselectNew) 138 throws UserCancelException { 139 final boolean queryTags = isTagged(selectedNodes); 140 final boolean queryMemberships = isUsedInRelations(selectedNodes); 141 if (queryTags || queryMemberships) { 142 final PropertiesMembershipChoiceDialog dialog = new PropertiesMembershipChoiceDialog(preselectNew, queryTags, queryMemberships); 143 dialog.showDialog(); 144 if (dialog.getValue() != 1) { 145 throw new UserCancelException(); 146 } 147 return dialog; 148 } 149 return null; 150 } 151 152 private static boolean isTagged(final Collection<Node> existingNodes) { 153 return existingNodes.stream().anyMatch(Node::hasKeys); 154 } 155 156 private static boolean isUsedInRelations(final Collection<Node> existingNodes) { 157 return existingNodes.stream().anyMatch( 158 selectedNode -> selectedNode.referrers(Relation.class).anyMatch(Objects::nonNull)); 159 } 160}