001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.event.KeyEvent; 010import java.awt.event.WindowEvent; 011import java.awt.event.WindowListener; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.EnumSet; 016import java.util.LinkedList; 017import java.util.List; 018import java.util.stream.Collectors; 019 020import javax.swing.BorderFactory; 021import javax.swing.GroupLayout; 022import javax.swing.JLabel; 023import javax.swing.JOptionPane; 024import javax.swing.JPanel; 025import javax.swing.KeyStroke; 026import javax.swing.border.EtchedBorder; 027import javax.swing.plaf.basic.BasicComboBoxEditor; 028 029import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 030import org.openstreetmap.josm.data.osm.PrimitiveId; 031import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 032import org.openstreetmap.josm.gui.ExtendedDialog; 033import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils; 034import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 035import org.openstreetmap.josm.gui.widgets.HtmlPanel; 036import org.openstreetmap.josm.gui.widgets.JosmTextField; 037import org.openstreetmap.josm.gui.widgets.OsmIdTextField; 038import org.openstreetmap.josm.gui.widgets.OsmPrimitiveTypesComboBox; 039import org.openstreetmap.josm.spi.preferences.Config; 040import org.openstreetmap.josm.tools.Logging; 041import org.openstreetmap.josm.tools.Utils; 042 043/** 044 * Dialog prompt to user to let him choose OSM primitives by specifying their type and IDs. 045 * @since 6448, split from DownloadObjectDialog 046 */ 047public class OsmIdSelectionDialog extends ExtendedDialog implements WindowListener { 048 049 protected final JPanel panel = new JPanel(); 050 protected final OsmPrimitiveTypesComboBox cbType = new OsmPrimitiveTypesComboBox(); 051 protected final OsmIdTextField tfId = new OsmIdTextField(); 052 protected final HistoryComboBox cbId = new HistoryComboBox(); 053 protected final transient GroupLayout layout = new GroupLayout(panel); 054 055 /** 056 * Creates a new OsmIdSelectionDialog 057 * @param parent The parent element that will be used for position and maximum size 058 * @param title The text that will be shown in the window titlebar 059 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 060 */ 061 public OsmIdSelectionDialog(Component parent, String title, String... buttonTexts) { 062 super(parent, title, buttonTexts); 063 } 064 065 /** 066 * Creates a new OsmIdSelectionDialog 067 * @param parent The parent element that will be used for position and maximum size 068 * @param title The text that will be shown in the window titlebar 069 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 070 * @param modal Set it to {@code true} if you want the dialog to be modal 071 */ 072 public OsmIdSelectionDialog(Component parent, String title, String[] buttonTexts, boolean modal) { 073 super(parent, title, buttonTexts, modal); 074 } 075 076 /** 077 * Creates a new OsmIdSelectionDialog 078 * @param parent The parent element that will be used for position and maximum size 079 * @param title The text that will be shown in the window titlebar 080 * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one. 081 * @param modal Set it to {@code true} if you want the dialog to be modal 082 * @param disposeOnClose whether to call {@link #dispose} when closing the dialog 083 */ 084 public OsmIdSelectionDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) { 085 super(parent, title, buttonTexts, modal, disposeOnClose); 086 } 087 088 protected void init() { 089 panel.setLayout(layout); 090 layout.setAutoCreateGaps(true); 091 layout.setAutoCreateContainerGaps(true); 092 093 JLabel lbl1 = new JLabel(tr("Object type:")); 094 lbl1.setLabelFor(cbType); 095 096 cbType.addItem(trc("osm object types", "mixed")); 097 cbType.setToolTipText(tr("Choose the OSM object type")); 098 JLabel lbl2 = new JLabel(tr("Object ID:")); 099 lbl2.setLabelFor(cbId); 100 101 cbId.setEditor(new BasicComboBoxEditor() { 102 @Override 103 protected JosmTextField createEditorComponent() { 104 return tfId; 105 } 106 }); 107 cbId.setToolTipText(tr("Enter the ID of the object that should be downloaded")); 108 restorePrimitivesHistory(cbId); 109 110 // forward the enter key stroke to the download button 111 tfId.getKeymap().removeKeyStrokeBinding(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false)); 112 tfId.setPreferredSize(new Dimension(400, tfId.getPreferredSize().height)); 113 114 final String help1 = /* I18n: {0} and contains example strings not meant for translation. */ 115 tr("Object IDs can be separated by comma or space, for instance: {0}", 116 "<b>" + Utils.joinAsHtmlUnorderedList(Arrays.asList("1 2 5", "1,2,5")) + "</b>"); 117 final String help2 = /* I18n: {0} and contains example strings not meant for translation. {1}=n, {2}=w, {3}=r. */ 118 tr("In mixed mode, specify objects like this: {0}<br/>" 119 + "({1} stands for <i>node</i>, {2} for <i>way</i>, and {3} for <i>relation</i>)", 120 "<b>w123, n110, w12, r15</b>", "<b>n</b>", "<b>w</b>", "<b>r</b>"); 121 final String help3 = /* I18n: {0} and contains example strings not meant for translation. */ 122 tr("Ranges of object IDs are specified with a hyphen, for instance: {0}", 123 "<b>" + Utils.joinAsHtmlUnorderedList(Arrays.asList("w1-5", "n30-37", "r501-5")) + "</b>"); 124 HtmlPanel help = new HtmlPanel(help1 + "<br/>" + help2 + "<br/><br/>" + help3); 125 help.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED)); 126 127 cbType.addItemListener(e -> { 128 tfId.setType(cbType.getType()); 129 tfId.performValidation(); 130 }); 131 132 final GroupLayout.SequentialGroup sequentialGroup = layout.createSequentialGroup() 133 .addGroup(layout.createParallelGroup() 134 .addComponent(lbl1) 135 .addComponent(cbType, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)) 136 .addGroup(layout.createParallelGroup() 137 .addComponent(lbl2) 138 .addComponent(cbId, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)); 139 140 final GroupLayout.ParallelGroup parallelGroup = layout.createParallelGroup() 141 .addGroup(layout.createSequentialGroup() 142 .addGroup(layout.createParallelGroup() 143 .addComponent(lbl1) 144 .addComponent(lbl2) 145 ) 146 .addGroup(layout.createParallelGroup() 147 .addComponent(cbType) 148 .addComponent(cbId)) 149 ); 150 151 for (Component i : getComponentsBeforeHelp()) { 152 sequentialGroup.addComponent(i); 153 parallelGroup.addComponent(i); 154 } 155 156 layout.setVerticalGroup(sequentialGroup.addComponent(help)); 157 layout.setHorizontalGroup(parallelGroup.addComponent(help)); 158 } 159 160 /** 161 * Let subclasses add custom components between the id input field and the help text 162 * @return the collections to add 163 */ 164 protected Collection<Component> getComponentsBeforeHelp() { 165 return Collections.emptySet(); 166 } 167 168 /** 169 * Allows subclasses to specify a different continue button index. If this button is pressed, the history is updated. 170 * @return the button index 171 */ 172 public int getContinueButtonIndex() { 173 return 1; 174 } 175 176 /** 177 * Restore the current history from the preferences 178 * 179 * @param cbHistory the {@link HistoryComboBox} to which the history is restored to 180 */ 181 protected void restorePrimitivesHistory(HistoryComboBox cbHistory) { 182 List<String> cmtHistory = new LinkedList<>( 183 Config.getPref().getList(getClass().getName() + ".primitivesHistory", new LinkedList<String>())); 184 // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement() 185 Collections.reverse(cmtHistory); 186 cbHistory.setPossibleItems(cmtHistory); 187 } 188 189 /** 190 * Remind the current history in the preferences 191 * 192 * @param cbHistory the {@link HistoryComboBox} of which to restore the history 193 */ 194 protected void remindPrimitivesHistory(HistoryComboBox cbHistory) { 195 cbHistory.addCurrentItemToHistory(); 196 Config.getPref().putList(getClass().getName() + ".primitivesHistory", cbHistory.getHistory()); 197 } 198 199 /** 200 * Gets the requested OSM object IDs. 201 * 202 * @return The list of requested OSM object IDs 203 */ 204 public final List<PrimitiveId> getOsmIds() { 205 return tfId.getIds(); 206 } 207 208 @Override 209 public void setupDialog() { 210 setContent(panel, false); 211 try { 212 cbType.setSelectedIndex(Config.getPref().getInt("downloadprimitive.lasttype", 0)); 213 } catch (IllegalArgumentException e) { 214 cbType.setSelectedIndex(0); 215 Logging.warn(e); 216 } 217 tfId.setType(cbType.getType()); 218 if (Config.getPref().getBoolean("downloadprimitive.autopaste", true)) { 219 tryToPasteFromClipboard(tfId, cbType); 220 } 221 setDefaultButton(getContinueButtonIndex()); 222 addWindowListener(this); 223 super.setupDialog(); 224 } 225 226 protected void tryToPasteFromClipboard(OsmIdTextField tfId, OsmPrimitiveTypesComboBox cbType) { 227 String buf = ClipboardUtils.getClipboardStringContent(); 228 if (buf == null || buf.isEmpty()) return; 229 if (buf.length() > Config.getPref().getInt("downloadprimitive.max-autopaste-length", 2000)) return; 230 final List<SimplePrimitiveId> ids = SimplePrimitiveId.fuzzyParse(buf); 231 if (!ids.isEmpty()) { 232 final String parsedText = ids.stream().map(x -> x.getType().getAPIName().charAt(0) + String.valueOf(x.getUniqueId())) 233 .collect(Collectors.joining(", ")); 234 tfId.tryToPasteFrom(parsedText); 235 final EnumSet<OsmPrimitiveType> types = ids.stream().map(SimplePrimitiveId::getType).collect( 236 Collectors.toCollection(() -> EnumSet.noneOf(OsmPrimitiveType.class))); 237 if (types.size() == 1) { 238 // select corresponding type 239 cbType.setSelectedItem(types.iterator().next()); 240 } else { 241 // select "mixed" 242 cbType.setSelectedIndex(3); 243 } 244 } else if (buf.matches("[\\d,v\\s]+")) { 245 //fallback solution for id1,id2,id3 format 246 tfId.tryToPasteFrom(buf); 247 } 248 } 249 250 @Override public void windowClosed(WindowEvent e) { 251 if (e != null && e.getComponent() == this && getValue() == getContinueButtonIndex()) { 252 Config.getPref().putInt("downloadprimitive.lasttype", cbType.getSelectedIndex()); 253 254 if (!tfId.readIds()) { 255 JOptionPane.showMessageDialog(getParent(), 256 tr("Invalid ID list specified\n" 257 + "Cannot continue."), 258 tr("Information"), 259 JOptionPane.INFORMATION_MESSAGE 260 ); 261 return; 262 } 263 264 remindPrimitivesHistory(cbId); 265 } 266 } 267 268 @Override public void windowOpened(WindowEvent e) { 269 // Do nothing 270 } 271 272 @Override public void windowClosing(WindowEvent e) { 273 // Do nothing 274 } 275 276 @Override public void windowIconified(WindowEvent e) { 277 // Do nothing 278 } 279 280 @Override public void windowDeiconified(WindowEvent e) { 281 // Do nothing 282 } 283 284 @Override public void windowActivated(WindowEvent e) { 285 // Do nothing 286 } 287 288 @Override public void windowDeactivated(WindowEvent e) { 289 // Do nothing 290 } 291}