source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java@ 5587

Last change on this file since 5587 was 5587, checked in by Don-vip, 11 years ago

Single entry point in Utils to open HTTP connections

  • Property svn:eol-style set to native
File size: 72.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.properties;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.Container;
10import java.awt.Cursor;
11import java.awt.Dialog.ModalityType;
12import java.awt.Dimension;
13import java.awt.FlowLayout;
14import java.awt.Font;
15import java.awt.GridBagConstraints;
16import java.awt.GridBagLayout;
17import java.awt.Point;
18import java.awt.Toolkit;
19import java.awt.datatransfer.Clipboard;
20import java.awt.datatransfer.Transferable;
21import java.awt.event.ActionEvent;
22import java.awt.event.ActionListener;
23import java.awt.event.FocusAdapter;
24import java.awt.event.FocusEvent;
25import java.awt.event.KeyEvent;
26import java.awt.event.MouseAdapter;
27import java.awt.event.MouseEvent;
28import java.awt.image.BufferedImage;
29import java.net.HttpURLConnection;
30import java.net.URI;
31import java.net.URLEncoder;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.Comparator;
37import java.util.EnumSet;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.Iterator;
41import java.util.LinkedHashMap;
42import java.util.LinkedList;
43import java.util.List;
44import java.util.Map;
45import java.util.Map.Entry;
46import java.util.Set;
47import java.util.TreeMap;
48import java.util.TreeSet;
49import java.util.Vector;
50
51import javax.swing.AbstractAction;
52import javax.swing.Action;
53import javax.swing.Box;
54import javax.swing.DefaultListCellRenderer;
55import javax.swing.ImageIcon;
56import javax.swing.JComponent;
57import javax.swing.JDialog;
58import javax.swing.JLabel;
59import javax.swing.JList;
60import javax.swing.JMenuItem;
61import javax.swing.JOptionPane;
62import javax.swing.JPanel;
63import javax.swing.JPopupMenu;
64import javax.swing.JScrollPane;
65import javax.swing.JTable;
66import javax.swing.KeyStroke;
67import javax.swing.ListSelectionModel;
68import javax.swing.event.ListSelectionEvent;
69import javax.swing.event.ListSelectionListener;
70import javax.swing.event.PopupMenuListener;
71import javax.swing.table.DefaultTableCellRenderer;
72import javax.swing.table.DefaultTableModel;
73import javax.swing.table.TableColumnModel;
74import javax.swing.table.TableModel;
75import javax.swing.text.JTextComponent;
76
77import org.openstreetmap.josm.Main;
78import org.openstreetmap.josm.actions.JosmAction;
79import org.openstreetmap.josm.actions.mapmode.DrawAction;
80import org.openstreetmap.josm.actions.search.SearchAction.SearchMode;
81import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
82import org.openstreetmap.josm.command.ChangeCommand;
83import org.openstreetmap.josm.command.ChangePropertyCommand;
84import org.openstreetmap.josm.command.Command;
85import org.openstreetmap.josm.command.SequenceCommand;
86import org.openstreetmap.josm.data.SelectionChangedListener;
87import org.openstreetmap.josm.data.osm.DataSet;
88import org.openstreetmap.josm.data.osm.IRelation;
89import org.openstreetmap.josm.data.osm.Node;
90import org.openstreetmap.josm.data.osm.OsmPrimitive;
91import org.openstreetmap.josm.data.osm.Relation;
92import org.openstreetmap.josm.data.osm.RelationMember;
93import org.openstreetmap.josm.data.osm.Tag;
94import org.openstreetmap.josm.data.osm.Way;
95import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
96import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
97import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
98import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
99import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
100import org.openstreetmap.josm.gui.DefaultNameFormatter;
101import org.openstreetmap.josm.gui.ExtendedDialog;
102import org.openstreetmap.josm.gui.MapFrame;
103import org.openstreetmap.josm.gui.MapView;
104import org.openstreetmap.josm.gui.SideButton;
105import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
106import org.openstreetmap.josm.gui.dialogs.properties.PresetListPanel.PresetHandler;
107import org.openstreetmap.josm.gui.dialogs.relation.DownloadRelationMemberTask;
108import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
109import org.openstreetmap.josm.gui.layer.OsmDataLayer;
110import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
111import org.openstreetmap.josm.gui.tagging.TaggingPreset;
112import org.openstreetmap.josm.gui.tagging.TaggingPreset.PresetType;
113import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingComboBox;
114import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionListItem;
115import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
116import org.openstreetmap.josm.gui.util.GuiHelper;
117import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
118import org.openstreetmap.josm.tools.GBC;
119import org.openstreetmap.josm.tools.ImageProvider;
120import org.openstreetmap.josm.tools.InputMapUtils;
121import org.openstreetmap.josm.tools.LanguageInfo;
122import org.openstreetmap.josm.tools.OpenBrowser;
123import org.openstreetmap.josm.tools.Shortcut;
124import org.openstreetmap.josm.tools.Utils;
125
126/**
127 * This dialog displays the properties of the current selected primitives.
128 *
129 * If no object is selected, the dialog list is empty.
130 * If only one is selected, all properties of this object are selected.
131 * If more than one object are selected, the sum of all properties are displayed. If the
132 * different objects share the same property, the shared value is displayed. If they have
133 * different values, all of them are put in a combo box and the string "<different>"
134 * is displayed in italic.
135 *
136 * Below the list, the user can click on an add, modify and delete property button to
137 * edit the table selection value.
138 *
139 * The command is applied to all selected entries.
140 *
141 * @author imi
142 */
143public class PropertiesDialog extends ToggleDialog implements SelectionChangedListener, MapView.EditLayerChangeListener, DataSetListenerAdapter.Listener {
144 /**
145 * Watches for mouse clicks
146 * @author imi
147 */
148 public class MouseClickWatch extends MouseAdapter {
149 @Override public void mouseClicked(MouseEvent e) {
150 if (e.getClickCount() < 2)
151 {
152 // single click, clear selection in other table not clicked in
153 if (e.getSource() == propertyTable) {
154 membershipTable.clearSelection();
155 } else if (e.getSource() == membershipTable) {
156 propertyTable.clearSelection();
157 }
158 }
159 // double click, edit or add property
160 else if (e.getSource() == propertyTable)
161 {
162 int row = propertyTable.rowAtPoint(e.getPoint());
163 if (row > -1) {
164 editProperty(row);
165 } else {
166 addProperty();
167 }
168 } else if (e.getSource() == membershipTable) {
169 int row = membershipTable.rowAtPoint(e.getPoint());
170 if (row > -1) {
171 editMembership(row);
172 }
173 }
174 else
175 {
176 addProperty();
177 }
178 }
179 @Override public void mousePressed(MouseEvent e) {
180 if (e.getSource() == propertyTable) {
181 membershipTable.clearSelection();
182 } else if (e.getSource() == membershipTable) {
183 propertyTable.clearSelection();
184 }
185 }
186 }
187
188 // hook for roadsigns plugin to display a small
189 // button in the upper right corner of this dialog
190 public static final JPanel pluginHook = new JPanel();
191
192 private JPopupMenu propertyMenu;
193 private JPopupMenu membershipMenu;
194
195 private final Map<String, Map<String, Integer>> valueCount = new TreeMap<String, Map<String, Integer>>();
196
197 Comparator<AutoCompletionListItem> defaultACItemComparator = new Comparator<AutoCompletionListItem>() {
198 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) {
199 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue());
200 }
201 };
202
203 private final DataSetListenerAdapter dataChangedAdapter = new DataSetListenerAdapter(this);
204 private final HelpAction helpAction = new HelpAction();
205 private final CopyValueAction copyValueAction = new CopyValueAction();
206 private final CopyKeyValueAction copyKeyValueAction = new CopyKeyValueAction();
207 private final CopyAllKeyValueAction copyAllKeyValueAction = new CopyAllKeyValueAction();
208 private final SearchAction searchActionSame = new SearchAction(true);
209 private final SearchAction searchActionAny = new SearchAction(false);
210 private final AddAction addAction = new AddAction();
211 private final EditAction editAction = new EditAction();
212 private final DeleteAction deleteAction = new DeleteAction();
213 private final JosmAction[] josmActions = new JosmAction[]{addAction, editAction, deleteAction};
214
215 @Override
216 public void showNotify() {
217 DatasetEventManager.getInstance().addDatasetListener(dataChangedAdapter, FireMode.IN_EDT_CONSOLIDATED);
218 SelectionEventManager.getInstance().addSelectionListener(this, FireMode.IN_EDT_CONSOLIDATED);
219 MapView.addEditLayerChangeListener(this);
220 for (JosmAction action : josmActions) {
221 Main.registerActionShortcut(action);
222 }
223 updateSelection();
224 }
225
226 @Override
227 public void hideNotify() {
228 DatasetEventManager.getInstance().removeDatasetListener(dataChangedAdapter);
229 SelectionEventManager.getInstance().removeSelectionListener(this);
230 MapView.removeEditLayerChangeListener(this);
231 for (JosmAction action : josmActions) {
232 Main.unregisterActionShortcut(action);
233 }
234 }
235
236 /**
237 * Edit the value in the properties table row
238 * @param row The row of the table from which the value is edited.
239 */
240 @SuppressWarnings("unchecked")
241 private void editProperty(int row) {
242 Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getSelected();
243 if (sel.isEmpty()) return;
244
245 String key = propertyData.getValueAt(row, 0).toString();
246 objKey=key;
247
248 String msg = "<html>"+trn("This will change {0} object.",
249 "This will change up to {0} objects.", sel.size(), sel.size())
250 +"<br><br>("+tr("An empty value deletes the tag.", key)+")</html>";
251
252 JPanel panel = new JPanel(new BorderLayout());
253 panel.add(new JLabel(msg), BorderLayout.NORTH);
254
255 JPanel p = new JPanel(new GridBagLayout());
256 panel.add(p, BorderLayout.CENTER);
257
258 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager();
259 List<AutoCompletionListItem> keyList = autocomplete.getKeys();
260 Collections.sort(keyList, defaultACItemComparator);
261
262 final AutoCompletingComboBox keys = new AutoCompletingComboBox(key);
263 keys.setPossibleACItems(keyList);
264 keys.setEditable(true);
265 keys.setSelectedItem(key);
266
267 p.add(new JLabel(tr("Key")), GBC.std());
268 p.add(Box.createHorizontalStrut(10), GBC.std());
269 p.add(keys, GBC.eol().fill(GBC.HORIZONTAL));
270
271 final Map<String, Integer> m = (Map<String, Integer>) propertyData.getValueAt(row, 1);
272
273 Comparator<AutoCompletionListItem> usedValuesAwareComparator = new Comparator<AutoCompletionListItem>() {
274
275 @Override
276 public int compare(AutoCompletionListItem o1, AutoCompletionListItem o2) {
277 boolean c1 = m.containsKey(o1.getValue());
278 boolean c2 = m.containsKey(o2.getValue());
279 if (c1 == c2)
280 return String.CASE_INSENSITIVE_ORDER.compare(o1.getValue(), o2.getValue());
281 else if (c1)
282 return -1;
283 else
284 return +1;
285 }
286 };
287
288 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key));
289 Collections.sort(valueList, usedValuesAwareComparator);
290
291 final String selection= m.size()!=1?tr("<different>"):m.entrySet().iterator().next().getKey();
292
293 final AutoCompletingComboBox values = new AutoCompletingComboBox(selection);
294 values.setRenderer(new DefaultListCellRenderer() {
295 @Override public Component getListCellRendererComponent(JList list,
296 Object value, int index, boolean isSelected, boolean cellHasFocus){
297 Component c = super.getListCellRendererComponent(list, value,
298 index, isSelected, cellHasFocus);
299 if (c instanceof JLabel) {
300 String str = ((AutoCompletionListItem) value).getValue();
301 if (valueCount.containsKey(objKey)) {
302 Map<String, Integer> m = valueCount.get(objKey);
303 if (m.containsKey(str)) {
304 str = tr("{0} ({1})", str, m.get(str));
305 c.setFont(c.getFont().deriveFont(Font.ITALIC + Font.BOLD));
306 }
307 }
308 ((JLabel) c).setText(str);
309 }
310 return c;
311 }
312 });
313
314 values.setEditable(true);
315 values.setPossibleACItems(valueList);
316 values.setSelectedItem(selection);
317 values.getEditor().setItem(selection);
318 p.add(new JLabel(tr("Value")), GBC.std());
319 p.add(Box.createHorizontalStrut(10), GBC.std());
320 p.add(values, GBC.eol().fill(GBC.HORIZONTAL));
321 addFocusAdapter(keys, values, autocomplete, usedValuesAwareComparator);
322
323 final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION) {
324 @Override public void selectInitialValue() {
325 // save unix system selection (middle mouse paste)
326 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
327 if(sysSel != null) {
328 Transferable old = sysSel.getContents(null);
329 values.requestFocusInWindow();
330 values.getEditor().selectAll();
331 sysSel.setContents(old, null);
332 } else {
333 values.requestFocusInWindow();
334 values.getEditor().selectAll();
335 }
336 }
337 };
338 final JDialog dlg = optionPane.createDialog(Main.parent, trn("Change value?", "Change values?", m.size()));
339 dlg.setModalityType(ModalityType.DOCUMENT_MODAL);
340 Dimension dlgSize = dlg.getSize();
341 if(dlgSize.width > Main.parent.getSize().width) {
342 dlgSize.width = Math.max(250, Main.parent.getSize().width);
343 dlg.setSize(dlgSize);
344 }
345 dlg.setLocationRelativeTo(Main.parent);
346 values.getEditor().addActionListener(new ActionListener() {
347 public void actionPerformed(ActionEvent e) {
348 dlg.setVisible(false);
349 optionPane.setValue(JOptionPane.OK_OPTION);
350 }
351 });
352
353 String oldValue = values.getEditor().getItem().toString();
354 dlg.setVisible(true);
355
356 Object answer = optionPane.getValue();
357 if (answer == null || answer == JOptionPane.UNINITIALIZED_VALUE ||
358 (answer instanceof Integer && (Integer)answer != JOptionPane.OK_OPTION)) {
359 values.getEditor().setItem(oldValue);
360 return;
361 }
362
363 String value = values.getEditor().getItem().toString().trim();
364 // is not Java 1.5
365 //value = java.text.Normalizer.normalize(value, java.text.Normalizer.Form.NFC);
366 if (value.equals("")) {
367 value = null; // delete the key
368 }
369 String newkey = keys.getEditor().getItem().toString().trim();
370 //newkey = java.text.Normalizer.normalize(newkey, java.text.Normalizer.Form.NFC);
371 if (newkey.equals("")) {
372 newkey = key;
373 value = null; // delete the key instead
374 }
375 if (key.equals(newkey) && tr("<different>").equals(value))
376 return;
377 if (key.equals(newkey) || value == null) {
378 Main.main.undoRedo.add(new ChangePropertyCommand(sel, newkey, value));
379 } else {
380 for (OsmPrimitive osm: sel) {
381 if(osm.get(newkey) != null) {
382 ExtendedDialog ed = new ExtendedDialog(
383 Main.parent,
384 tr("Overwrite key"),
385 new String[]{tr("Replace"), tr("Cancel")});
386 ed.setButtonIcons(new String[]{"purge", "cancel"});
387 ed.setContent(tr("You changed the key from ''{0}'' to ''{1}''.\n"
388 + "The new key is already used, overwrite values?", key, newkey));
389 ed.setCancelButton(2);
390 ed.toggleEnable("overwriteEditKey");
391 ed.showDialog();
392
393 if (ed.getValue() != 1)
394 return;
395 break;
396 }
397 }
398 Collection<Command> commands=new Vector<Command>();
399 commands.add(new ChangePropertyCommand(sel, key, null));
400 if (value.equals(tr("<different>"))) {
401 HashMap<String, Vector<OsmPrimitive>> map=new HashMap<String, Vector<OsmPrimitive>>();
402 for (OsmPrimitive osm: sel) {
403 String val=osm.get(key);
404 if(val != null)
405 {
406 if (map.containsKey(val)) {
407 map.get(val).add(osm);
408 } else {
409 Vector<OsmPrimitive> v = new Vector<OsmPrimitive>();
410 v.add(osm);
411 map.put(val, v);
412 }
413 }
414 }
415 for (Entry<String, Vector<OsmPrimitive>> e: map.entrySet()) {
416 commands.add(new ChangePropertyCommand(e.getValue(), newkey, e.getKey()));
417 }
418 } else {
419 commands.add(new ChangePropertyCommand(sel, newkey, value));
420 }
421 Main.main.undoRedo.add(new SequenceCommand(
422 trn("Change properties of up to {0} object",
423 "Change properties of up to {0} objects", sel.size(), sel.size()),
424 commands));
425 }
426
427 if(!key.equals(newkey)) {
428 for(int i=0; i < propertyTable.getRowCount(); i++)
429 if(propertyData.getValueAt(i, 0).toString().equals(newkey)) {
430 row=i;
431 break;
432 }
433 }
434 propertyTable.changeSelection(row, 0, false, false);
435 }
436
437 /**
438 * For a given key k, return a list of keys which are used as keys for
439 * auto-completing values to increase the search space.
440 * @param key the key k
441 * @return a list of keys
442 */
443 private static List<String> getAutocompletionKeys(String key) {
444 if ("name".equals(key) || "addr:street".equals(key))
445 return Arrays.asList("addr:street", "name");
446 else
447 return Arrays.asList(key);
448 }
449
450 /**
451 * This simply fires up an {@link RelationEditor} for the relation shown; everything else
452 * is the editor's business.
453 *
454 * @param row
455 */
456 private void editMembership(int row) {
457 Relation relation = (Relation)membershipData.getValueAt(row, 0);
458 Main.map.relationListDialog.selectRelation(relation);
459 RelationEditor.getEditor(
460 Main.map.mapView.getEditLayer(),
461 relation,
462 ((MemberInfo) membershipData.getValueAt(row, 1)).role).setVisible(true);
463 }
464
465 private static String lastAddKey = null;
466 private static String lastAddValue = null;
467
468 public static final int DEFAULT_LRU_TAGS_NUMBER = 5;
469 public static final int MAX_LRU_TAGS_NUMBER = 9;
470
471 // LRU cache for recently added tags (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html)
472 private static final Map<Tag, Void> recentTags = new LinkedHashMap<Tag, Void>(MAX_LRU_TAGS_NUMBER+1, 1.1f, true) {
473 @Override
474 protected boolean removeEldestEntry(Entry<Tag, Void> eldest) {
475 return size() > MAX_LRU_TAGS_NUMBER;
476 }
477 };
478
479 /**
480 * Open the add selection dialog and add a new key/value to the table (and
481 * to the dataset, of course).
482 */
483 private void addProperty() {
484 Collection<OsmPrimitive> sel;
485 if (Main.map.mapMode instanceof DrawAction) {
486 sel = ((DrawAction) Main.map.mapMode).getInProgressSelection();
487 } else {
488 DataSet ds = Main.main.getCurrentDataSet();
489 if (ds == null) return;
490 sel = ds.getSelected();
491 }
492 if (sel.isEmpty()) return;
493
494 JPanel p = new JPanel(new GridBagLayout());
495 p.add(new JLabel("<html>"+trn("This will change up to {0} object.",
496 "This will change up to {0} objects.", sel.size(),sel.size())
497 +"<br><br>"+tr("Please select a key")), GBC.eol().fill(GBC.HORIZONTAL));
498 final AutoCompletingComboBox keys = new AutoCompletingComboBox();
499 AutoCompletionManager autocomplete = Main.main.getEditLayer().data.getAutoCompletionManager();
500 List<AutoCompletionListItem> keyList = autocomplete.getKeys();
501
502 AutoCompletionListItem itemToSelect = null;
503 // remove the object's tag keys from the list
504 Iterator<AutoCompletionListItem> iter = keyList.iterator();
505 while (iter.hasNext()) {
506 AutoCompletionListItem item = iter.next();
507 if (item.getValue().equals(lastAddKey)) {
508 itemToSelect = item;
509 }
510 for (int i = 0; i < propertyData.getRowCount(); ++i) {
511 if (item.getValue().equals(propertyData.getValueAt(i, 0))) {
512 if (itemToSelect == item) {
513 itemToSelect = null;
514 }
515 iter.remove();
516 break;
517 }
518 }
519 }
520
521 Collections.sort(keyList, defaultACItemComparator);
522 keys.setPossibleACItems(keyList);
523 keys.setEditable(true);
524
525 p.add(keys, GBC.eop().fill());
526
527 p.add(new JLabel(tr("Please select a value")), GBC.eol());
528 final AutoCompletingComboBox values = new AutoCompletingComboBox();
529 values.setEditable(true);
530 p.add(values, GBC.eop().fill());
531 if (itemToSelect != null) {
532 keys.setSelectedItem(itemToSelect);
533 if (lastAddValue != null) {
534 values.setSelectedItem(lastAddValue);
535 }
536 }
537
538 FocusAdapter focus = addFocusAdapter(keys, values, autocomplete, defaultACItemComparator);
539 // fire focus event in advance or otherwise the popup list will be too small at first
540 focus.focusGained(null);
541
542 int recentTagsToShow = Main.pref.getInteger("properties.recently-added-tags", DEFAULT_LRU_TAGS_NUMBER);
543 if (recentTagsToShow > MAX_LRU_TAGS_NUMBER) {
544 recentTagsToShow = MAX_LRU_TAGS_NUMBER;
545 }
546 List<JosmAction> recentTagsActions = new ArrayList<JosmAction>();
547 suggestRecentlyAddedTags(p, keys, values, recentTagsActions, recentTagsToShow, focus);
548
549 JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION){
550 @Override public void selectInitialValue() {
551 // save unix system selection (middle mouse paste)
552 Clipboard sysSel = Toolkit.getDefaultToolkit().getSystemSelection();
553 if(sysSel != null) {
554 Transferable old = sysSel.getContents(null);
555 keys.requestFocusInWindow();
556 keys.getEditor().selectAll();
557 sysSel.setContents(old, null);
558 } else {
559 keys.requestFocusInWindow();
560 keys.getEditor().selectAll();
561 }
562 }
563 };
564 JDialog dialog = pane.createDialog(Main.parent, tr("Add value?"));
565 dialog.setModalityType(ModalityType.DOCUMENT_MODAL);
566 dialog.setVisible(true);
567
568 for (JosmAction action : recentTagsActions) {
569 action.destroy();
570 }
571
572 if (!Integer.valueOf(JOptionPane.OK_OPTION).equals(pane.getValue()))
573 return;
574 String key = keys.getEditor().getItem().toString().trim();
575 String value = values.getEditor().getItem().toString().trim();
576 if (key.isEmpty() || value.isEmpty())
577 return;
578 lastAddKey = key;
579 lastAddValue = value;
580 recentTags.put(new Tag(key, value), null);
581 Main.main.undoRedo.add(new ChangePropertyCommand(sel, key, value));
582 btnAdd.requestFocusInWindow();
583 }
584
585 private void suggestRecentlyAddedTags(JPanel p, final AutoCompletingComboBox keys, final AutoCompletingComboBox values, List<JosmAction> tagsActions, int tagsToShow, final FocusAdapter focus) {
586 if (tagsToShow > 0 && !recentTags.isEmpty()) {
587 p.add(new JLabel(tr("Recently added tags")), GBC.eol());
588
589 int count = 1;
590 // We store the maximum number (9) of recent tags to allow dynamic change of number of tags shown in the preferences.
591 // This implies to iterate in descending order, as the oldest elements will only be removed after we reach the maximum numbern and not the number of tags to show.
592 // However, as Set does not allow to iterate in descending order, we need to copy its elements into a List we can access in reverse order.
593 List<Tag> tags = new LinkedList<Tag>(recentTags.keySet());
594 for (int i = tags.size()-1; i >= 0 && count <= tagsToShow; i--, count++) {
595 final Tag t = tags.get(i);
596 // Create action for reusing the tag, with keyboard shortcut Ctrl+(1-5)
597 String actionShortcutKey = "properties:recent:"+count;
598 Shortcut sc = Shortcut.registerShortcut(actionShortcutKey, null, KeyEvent.VK_0+count, Shortcut.CTRL);
599 final JosmAction action = new JosmAction(actionShortcutKey, null, tr("Use this tag again"), sc, false) {
600 @Override
601 public void actionPerformed(ActionEvent e) {
602 keys.setSelectedItem(t.getKey());
603 values.setSelectedItem(t.getValue());
604 // Update list of values (fix #7951)
605 focus.focusGained(null);
606 }
607 };
608 tagsActions.add(action);
609 // Disable action if its key is already set on the object (the key being absent from the keys list for this reason
610 // performing this action leads to autocomplete to the next key (see #7671 comments)
611 for (int j = 0; j < propertyData.getRowCount(); ++j) {
612 if (t.getKey().equals(propertyData.getValueAt(j, 0))) {
613 action.setEnabled(false);
614 break;
615 }
616 }
617 // Find and display icon
618 ImageIcon icon = MapPaintStyles.getNodeIcon(t, false); // Filters deprecated icon
619 if (icon == null) {
620 icon = new ImageIcon(new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB));
621 }
622 GridBagConstraints gbc = new GridBagConstraints();
623 gbc.ipadx = 5;
624 p.add(new JLabel(action.isEnabled() ? icon : GuiHelper.getDisabledIcon(icon)), gbc);
625 // Create tag label
626 final String color = action.isEnabled() ? "" : "; color:gray";
627 final JLabel tagLabel = new JLabel("<html>"
628 + "<style>td{border:1px solid gray; font-weight:normal"+color+"}</style>"
629 + "<table><tr><td>" + t.toString() + "</td></tr></table></html>");
630 if (action.isEnabled()) {
631 // Register action
632 p.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(sc.getKeyStroke(), actionShortcutKey);
633 p.getActionMap().put(actionShortcutKey, action);
634 // Make the tag label clickable and set tooltip to the action description (this displays also the keyboard shortcut)
635 tagLabel.setToolTipText((String) action.getValue(Action.SHORT_DESCRIPTION));
636 tagLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
637 tagLabel.addMouseListener(new MouseAdapter() {
638 @Override
639 public void mouseClicked(MouseEvent e) {
640 action.actionPerformed(null);
641 }
642 });
643 } else {
644 // Disable tag label
645 tagLabel.setEnabled(false);
646 // Explain in the tooltip why
647 tagLabel.setToolTipText(tr("The key ''{0}'' is already used", t.getKey()));
648 }
649 // Finally add label to the resulting panel
650 JPanel tagPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
651 tagPanel.add(tagLabel);
652 p.add(tagPanel, GBC.eol().fill(GBC.HORIZONTAL));
653 }
654 }
655 }
656
657 /**
658 * Create a focus handling adapter and apply in to the editor component of value
659 * autocompletion box.
660 * @param keys Box for keys entering and autocompletion
661 * @param values Box for values entering and autocompletion
662 * @param autocomplete Manager handling the autocompletion
663 * @param comparator Class to decide what values are offered on autocompletion
664 * @return The created adapter
665 */
666 private FocusAdapter addFocusAdapter(final AutoCompletingComboBox keys, final AutoCompletingComboBox values,
667 final AutoCompletionManager autocomplete, final Comparator<AutoCompletionListItem> comparator) {
668 // get the combo box' editor component
669 JTextComponent editor = (JTextComponent)values.getEditor()
670 .getEditorComponent();
671 // Refresh the values model when focus is gained
672 FocusAdapter focus = new FocusAdapter() {
673 @Override public void focusGained(FocusEvent e) {
674 String key = keys.getEditor().getItem().toString();
675
676 List<AutoCompletionListItem> valueList = autocomplete.getValues(getAutocompletionKeys(key));
677 Collections.sort(valueList, comparator);
678
679 values.setPossibleACItems(valueList);
680 objKey=key;
681 }
682 };
683 editor.addFocusListener(focus);
684 return focus;
685 }
686 private String objKey;
687
688 /**
689 * The property data of selected objects.
690 */
691 private final DefaultTableModel propertyData = new DefaultTableModel() {
692 @Override public boolean isCellEditable(int row, int column) {
693 return false;
694 }
695 @Override public Class<?> getColumnClass(int columnIndex) {
696 return String.class;
697 }
698 };
699
700 /**
701 * The membership data of selected objects.
702 */
703 private final DefaultTableModel membershipData = new DefaultTableModel() {
704 @Override public boolean isCellEditable(int row, int column) {
705 return false;
706 }
707 @Override public Class<?> getColumnClass(int columnIndex) {
708 return String.class;
709 }
710 };
711
712 /**
713 * The properties table.
714 */
715 private final JTable propertyTable = new JTable(propertyData);
716 /**
717 * The membership table.
718 */
719 private final JTable membershipTable = new JTable(membershipData);
720
721 /**
722 * The Add button (needed to be able to disable it)
723 */
724 private final SideButton btnAdd;
725 /**
726 * The Edit button (needed to be able to disable it)
727 */
728 private final SideButton btnEdit;
729 /**
730 * The Delete button (needed to be able to disable it)
731 */
732 private final SideButton btnDel;
733 /**
734 * Matching preset display class
735 */
736 private final PresetListPanel presets = new PresetListPanel();
737
738 /**
739 * Text to display when nothing selected.
740 */
741 private final JLabel selectSth = new JLabel("<html><p>"
742 + tr("Select objects for which to change properties.") + "</p></html>");
743
744 static class MemberInfo {
745 List<RelationMember> role = new ArrayList<RelationMember>();
746 List<Integer> position = new ArrayList<Integer>();
747 private String positionString = null;
748 void add(RelationMember r, Integer p) {
749 role.add(r);
750 position.add(p);
751 }
752 String getPositionString() {
753 if (positionString == null) {
754 Collections.sort(position);
755 positionString = String.valueOf(position.get(0));
756 int cnt = 0;
757 int last = position.get(0);
758 for (int i = 1; i < position.size(); ++i) {
759 int cur = position.get(i);
760 if (cur == last + 1) {
761 ++cnt;
762 } else if (cnt == 0) {
763 positionString += "," + String.valueOf(cur);
764 } else {
765 positionString += "-" + String.valueOf(last);
766 positionString += "," + String.valueOf(cur);
767 cnt = 0;
768 }
769 last = cur;
770 }
771 if (cnt >= 1) {
772 positionString += "-" + String.valueOf(last);
773 }
774 }
775 if (positionString.length() > 20) {
776 positionString = positionString.substring(0, 17) + "...";
777 }
778 return positionString;
779 }
780 }
781
782 /**
783 * Create a new PropertiesDialog
784 */
785 public PropertiesDialog(MapFrame mapFrame) {
786 super(tr("Properties/Memberships"), "propertiesdialog", tr("Properties for selected objects."),
787 Shortcut.registerShortcut("subwindow:properties", tr("Toggle: {0}", tr("Properties/Memberships")), KeyEvent.VK_P,
788 Shortcut.ALT_SHIFT), 150, true);
789
790 // setting up the properties table
791 propertyMenu = new JPopupMenu();
792 propertyMenu.add(copyValueAction);
793 propertyMenu.add(copyKeyValueAction);
794 propertyMenu.add(copyAllKeyValueAction);
795 propertyMenu.addSeparator();
796 propertyMenu.add(searchActionAny);
797 propertyMenu.add(searchActionSame);
798 propertyMenu.addSeparator();
799 propertyMenu.add(helpAction);
800
801 propertyData.setColumnIdentifiers(new String[]{tr("Key"),tr("Value")});
802 propertyTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
803 propertyTable.getTableHeader().setReorderingAllowed(false);
804 propertyTable.addMouseListener(new PopupMenuLauncher() {
805 @Override
806 public void launch(MouseEvent evt) {
807 Point p = evt.getPoint();
808 int row = propertyTable.rowAtPoint(p);
809 if (row > -1) {
810 propertyTable.changeSelection(row, 0, false, false);
811 propertyMenu.show(propertyTable, p.x, p.y-3);
812 }
813 }
814 });
815
816 propertyTable.getColumnModel().getColumn(1).setCellRenderer(new DefaultTableCellRenderer(){
817 @Override public Component getTableCellRendererComponent(JTable table, Object value,
818 boolean isSelected, boolean hasFocus, int row, int column) {
819 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
820 if (value == null)
821 return this;
822 if (c instanceof JLabel) {
823 String str = null;
824 if (value instanceof String) {
825 str = (String) value;
826 } else if (value instanceof Map<?, ?>) {
827 Map<?, ?> v = (Map<?, ?>) value;
828 if (v.size() != 1) {
829 str=tr("<different>");
830 c.setFont(c.getFont().deriveFont(Font.ITALIC));
831 } else {
832 final Map.Entry<?, ?> entry = v.entrySet().iterator().next();
833 str = (String) entry.getKey();
834 }
835 }
836 ((JLabel)c).setText(str);
837 }
838 return c;
839 }
840 });
841
842 // setting up the membership table
843 membershipMenu = new JPopupMenu();
844 membershipMenu.add(new SelectRelationAction(true));
845 membershipMenu.add(new SelectRelationAction(false));
846 membershipMenu.add(new SelectRelationMembersAction());
847 membershipMenu.add(new DownloadIncompleteMembersAction());
848 membershipMenu.addSeparator();
849 membershipMenu.add(helpAction);
850
851 membershipData.setColumnIdentifiers(new String[]{tr("Member Of"),tr("Role"),tr("Position")});
852 membershipTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
853 membershipTable.addMouseListener(new PopupMenuLauncher() {
854 @Override
855 public void launch(MouseEvent evt) {
856 Point p = evt.getPoint();
857 int row = membershipTable.rowAtPoint(p);
858 if (row > -1) {
859 membershipTable.changeSelection(row, 0, false, false);
860 Relation relation = (Relation)membershipData.getValueAt(row, 0);
861 for (Component c : membershipMenu.getComponents()) {
862 if (c instanceof JMenuItem) {
863 Action action = ((JMenuItem) c).getAction();
864 if (action instanceof RelationRelated) {
865 ((RelationRelated)action).setRelation(relation);
866 }
867 }
868 }
869 membershipMenu.show(membershipTable, p.x, p.y-3);
870 }
871 }
872 });
873
874 TableColumnModel mod = membershipTable.getColumnModel();
875 membershipTable.getTableHeader().setReorderingAllowed(false);
876 mod.getColumn(0).setCellRenderer(new DefaultTableCellRenderer() {
877 @Override public Component getTableCellRendererComponent(JTable table, Object value,
878 boolean isSelected, boolean hasFocus, int row, int column) {
879 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
880 if (value == null)
881 return this;
882 if (c instanceof JLabel) {
883 JLabel label = (JLabel)c;
884 Relation r = (Relation)value;
885 label.setText(r.getDisplayName(DefaultNameFormatter.getInstance()));
886 if (r.isDisabledAndHidden()) {
887 label.setFont(label.getFont().deriveFont(Font.ITALIC));
888 }
889 }
890 return c;
891 }
892 });
893
894 mod.getColumn(1).setCellRenderer(new DefaultTableCellRenderer() {
895 @Override public Component getTableCellRendererComponent(JTable table, Object value,
896 boolean isSelected, boolean hasFocus, int row, int column) {
897 if (value == null)
898 return this;
899 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
900 boolean isDisabledAndHidden = (((Relation)table.getValueAt(row, 0))).isDisabledAndHidden();
901 if (c instanceof JLabel) {
902 JLabel label = (JLabel)c;
903 MemberInfo col = (MemberInfo) value;
904
905 String text = null;
906 for (RelationMember r : col.role) {
907 if (text == null) {
908 text = r.getRole();
909 }
910 else if (!text.equals(r.getRole())) {
911 text = tr("<different>");
912 break;
913 }
914 }
915
916 label.setText(text);
917 if (isDisabledAndHidden) {
918 label.setFont(label.getFont().deriveFont(Font.ITALIC));
919 }
920 }
921 return c;
922 }
923 });
924
925 mod.getColumn(2).setCellRenderer(new DefaultTableCellRenderer() {
926 @Override public Component getTableCellRendererComponent(JTable table, Object value,
927 boolean isSelected, boolean hasFocus, int row, int column) {
928 Component c = super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
929 boolean isDisabledAndHidden = (((Relation)table.getValueAt(row, 0))).isDisabledAndHidden();
930 if (c instanceof JLabel) {
931 JLabel label = (JLabel)c;
932 label.setText(((MemberInfo) table.getValueAt(row, 1)).getPositionString());
933 if (isDisabledAndHidden) {
934 label.setFont(label.getFont().deriveFont(Font.ITALIC));
935 }
936 }
937 return c;
938 }
939 });
940 mod.getColumn(2).setPreferredWidth(20);
941 mod.getColumn(1).setPreferredWidth(40);
942 mod.getColumn(0).setPreferredWidth(200);
943
944 // combine both tables and wrap them in a scrollPane
945 JPanel bothTables = new JPanel();
946 boolean top = Main.pref.getBoolean("properties.presets.top", true);
947 bothTables.setLayout(new GridBagLayout());
948 if(top) {
949 bothTables.add(presets, GBC.std().fill(GBC.HORIZONTAL).insets(5, 2, 5, 2).anchor(GBC.NORTHWEST));
950 double epsilon = Double.MIN_VALUE; // need to set a weight or else anchor value is ignored
951 bothTables.add(pluginHook, GBC.eol().insets(0,1,1,1).anchor(GBC.NORTHEAST).weight(epsilon, epsilon));
952 }
953 bothTables.add(selectSth, GBC.eol().fill().insets(10, 10, 10, 10));
954 bothTables.add(propertyTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
955 bothTables.add(propertyTable, GBC.eol().fill(GBC.BOTH));
956 bothTables.add(membershipTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
957 bothTables.add(membershipTable, GBC.eol().fill(GBC.BOTH));
958 if(!top) {
959 bothTables.add(presets, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 2, 5, 2));
960 }
961
962 // Open edit dialog whe enter pressed in tables
963 propertyTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
964 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),"onTableEnter");
965 propertyTable.getActionMap().put("onTableEnter",editAction);
966 membershipTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
967 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),"onTableEnter");
968 membershipTable.getActionMap().put("onTableEnter",editAction);
969
970 // Open add property dialog when INS is pressed in tables
971 propertyTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
972 .put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0),"onTableInsert");
973 propertyTable.getActionMap().put("onTableInsert",addAction);
974
975 // unassign some standard shortcuts for JTable to allow upload / download
976 InputMapUtils.unassignCtrlShiftUpDown(propertyTable, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
977
978 // -- add action and shortcut
979 this.btnAdd = new SideButton(addAction);
980 InputMapUtils.enableEnter(this.btnAdd);
981
982 // -- edit action
983 //
984 propertyTable.getSelectionModel().addListSelectionListener(editAction);
985 membershipTable.getSelectionModel().addListSelectionListener(editAction);
986 this.btnEdit = new SideButton(editAction);
987
988 // -- delete action
989 //
990 this.btnDel = new SideButton(deleteAction);
991 membershipTable.getSelectionModel().addListSelectionListener(deleteAction);
992 propertyTable.getSelectionModel().addListSelectionListener(deleteAction);
993 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
994 KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),"delete"
995 );
996 getActionMap().put("delete", deleteAction);
997
998 JScrollPane scrollPane = (JScrollPane) createLayout(bothTables, true, Arrays.asList(new SideButton[] {
999 this.btnAdd, this.btnEdit, this.btnDel
1000 }));
1001
1002 MouseClickWatch mouseClickWatch = new MouseClickWatch();
1003 propertyTable.addMouseListener(mouseClickWatch);
1004 membershipTable.addMouseListener(mouseClickWatch);
1005 scrollPane.addMouseListener(mouseClickWatch);
1006
1007 selectSth.setPreferredSize(scrollPane.getSize());
1008 presets.setSize(scrollPane.getSize());
1009
1010 // -- help action
1011 //
1012 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
1013 KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0), "onHelp");
1014 getActionMap().put("onHelp", helpAction);
1015 }
1016
1017 @Override
1018 public void setVisible(boolean b) {
1019 super.setVisible(b);
1020 if (b && Main.main.getCurrentDataSet() != null) {
1021 selectionChanged(Main.main.getCurrentDataSet().getSelected());
1022 }
1023 }
1024
1025 private int findRow(TableModel model, Object value) {
1026 for (int i=0; i<model.getRowCount(); i++) {
1027 if (model.getValueAt(i, 0).equals(value))
1028 return i;
1029 }
1030 return -1;
1031 }
1032
1033 private PresetHandler presetHandler = new PresetHandler() {
1034
1035 @Override
1036 public void updateTags(List<Tag> tags) {
1037 Command command = TaggingPreset.createCommand(getSelection(), tags);
1038 if (command != null) {
1039 Main.main.undoRedo.add(command);
1040 }
1041 }
1042
1043 @Override
1044 public Collection<OsmPrimitive> getSelection() {
1045 if (Main.main == null) return null;
1046 if (Main.main.getCurrentDataSet() == null) return null;
1047
1048 return Main.main.getCurrentDataSet().getSelected();
1049 }
1050 };
1051
1052 @Override
1053 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
1054 if (!isVisible())
1055 return;
1056 if (propertyTable == null)
1057 return; // selection changed may be received in base class constructor before init
1058 if (propertyTable.getCellEditor() != null) {
1059 propertyTable.getCellEditor().cancelCellEditing();
1060 }
1061
1062 String selectedTag = null;
1063 Relation selectedRelation = null;
1064 if (propertyTable.getSelectedRowCount() == 1) {
1065 selectedTag = (String)propertyData.getValueAt(propertyTable.getSelectedRow(), 0);
1066 }
1067 if (membershipTable.getSelectedRowCount() == 1) {
1068 selectedRelation = (Relation)membershipData.getValueAt(membershipTable.getSelectedRow(), 0);
1069 }
1070
1071 // re-load property data
1072 propertyData.setRowCount(0);
1073
1074 final Map<String, Integer> keyCount = new HashMap<String, Integer>();
1075 final Map<String, String> tags = new HashMap<String, String>();
1076 valueCount.clear();
1077 EnumSet<PresetType> types = EnumSet.noneOf(TaggingPreset.PresetType.class);
1078 for (OsmPrimitive osm : newSelection) {
1079 types.add(PresetType.forPrimitive(osm));
1080 for (String key : osm.keySet()) {
1081 String value = osm.get(key);
1082 keyCount.put(key, keyCount.containsKey(key) ? keyCount.get(key) + 1 : 1);
1083 if (valueCount.containsKey(key)) {
1084 Map<String, Integer> v = valueCount.get(key);
1085 v.put(value, v.containsKey(value) ? v.get(value) + 1 : 1);
1086 } else {
1087 TreeMap<String, Integer> v = new TreeMap<String, Integer>();
1088 v.put(value, 1);
1089 valueCount.put(key, v);
1090 }
1091 }
1092 }
1093 for (Entry<String, Map<String, Integer>> e : valueCount.entrySet()) {
1094 int count = 0;
1095 for (Entry<String, Integer> e1 : e.getValue().entrySet()) {
1096 count += e1.getValue();
1097 }
1098 if (count < newSelection.size()) {
1099 e.getValue().put("", newSelection.size() - count);
1100 }
1101 propertyData.addRow(new Object[]{e.getKey(), e.getValue()});
1102 tags.put(e.getKey(), e.getValue().size() == 1
1103 ? e.getValue().keySet().iterator().next() : tr("<different>"));
1104 }
1105
1106 membershipData.setRowCount(0);
1107
1108 Map<Relation, MemberInfo> roles = new HashMap<Relation, MemberInfo>();
1109 for (OsmPrimitive primitive: newSelection) {
1110 for (OsmPrimitive ref: primitive.getReferrers()) {
1111 if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
1112 Relation r = (Relation) ref;
1113 MemberInfo mi = roles.get(r);
1114 if(mi == null) {
1115 mi = new MemberInfo();
1116 }
1117 roles.put(r, mi);
1118 int i = 1;
1119 for (RelationMember m : r.getMembers()) {
1120 if (m.getMember() == primitive) {
1121 mi.add(m, i);
1122 }
1123 ++i;
1124 }
1125 }
1126 }
1127 }
1128
1129 List<Relation> sortedRelations = new ArrayList<Relation>(roles.keySet());
1130 Collections.sort(sortedRelations, new Comparator<Relation>() {
1131 public int compare(Relation o1, Relation o2) {
1132 int comp = Boolean.valueOf(o1.isDisabledAndHidden()).compareTo(o2.isDisabledAndHidden());
1133 if (comp == 0) {
1134 comp = o1.getDisplayName(DefaultNameFormatter.getInstance()).compareTo(o2.getDisplayName(DefaultNameFormatter.getInstance()));
1135 }
1136 return comp;
1137 }}
1138 );
1139
1140 for (Relation r: sortedRelations) {
1141 membershipData.addRow(new Object[]{r, roles.get(r)});
1142 }
1143
1144 presets.updatePresets(types, tags, presetHandler);
1145
1146 membershipTable.getTableHeader().setVisible(membershipData.getRowCount() > 0);
1147 membershipTable.setVisible(membershipData.getRowCount() > 0);
1148
1149 boolean hasSelection = !newSelection.isEmpty();
1150 boolean hasTags = hasSelection && propertyData.getRowCount() > 0;
1151 boolean hasMemberships = hasSelection && membershipData.getRowCount() > 0;
1152 btnAdd.setEnabled(hasSelection);
1153 btnEdit.setEnabled(hasTags || hasMemberships);
1154 btnDel.setEnabled(hasTags || hasMemberships);
1155 propertyTable.setVisible(hasTags);
1156 propertyTable.getTableHeader().setVisible(hasTags);
1157 selectSth.setVisible(!hasSelection);
1158 pluginHook.setVisible(hasSelection);
1159
1160 int selectedIndex;
1161 if (selectedTag != null && (selectedIndex = findRow(propertyData, selectedTag)) != -1) {
1162 propertyTable.changeSelection(selectedIndex, 0, false, false);
1163 } else if (selectedRelation != null && (selectedIndex = findRow(membershipData, selectedRelation)) != -1) {
1164 membershipTable.changeSelection(selectedIndex, 0, false, false);
1165 } else if(hasTags) {
1166 propertyTable.changeSelection(0, 0, false, false);
1167 } else if(hasMemberships) {
1168 membershipTable.changeSelection(0, 0, false, false);
1169 }
1170
1171 if(propertyData.getRowCount() != 0 || membershipData.getRowCount() != 0) {
1172 setTitle(tr("Properties: {0} / Memberships: {1}",
1173 propertyData.getRowCount(), membershipData.getRowCount()));
1174 } else {
1175 setTitle(tr("Properties / Memberships"));
1176 }
1177 }
1178
1179 /**
1180 * Update selection status, call @{link #selectionChanged} function.
1181 */
1182 private void updateSelection() {
1183 if (Main.main.getCurrentDataSet() == null) {
1184 selectionChanged(Collections.<OsmPrimitive>emptyList());
1185 } else {
1186 selectionChanged(Main.main.getCurrentDataSet().getSelected());
1187 }
1188 }
1189
1190 /* ---------------------------------------------------------------------------------- */
1191 /* EditLayerChangeListener */
1192 /* ---------------------------------------------------------------------------------- */
1193 @Override
1194 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) {
1195 updateSelection();
1196 }
1197
1198 @Override
1199 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
1200 updateSelection();
1201 }
1202
1203 /**
1204 * Action handling delete button press in properties dialog.
1205 */
1206 class DeleteAction extends JosmAction implements ListSelectionListener {
1207
1208 public DeleteAction() {
1209 super(tr("Delete"), "dialogs/delete", tr("Delete the selected key in all objects"),
1210 Shortcut.registerShortcut("properties:delete", tr("Delete Properties"), KeyEvent.VK_D,
1211 Shortcut.ALT_CTRL_SHIFT), false);
1212 updateEnabledState();
1213 }
1214
1215 protected void deleteProperties(int[] rows){
1216 // convert list of rows to HashMap (and find gap for nextKey)
1217 HashMap<String, String> tags = new HashMap<String, String>(rows.length);
1218 int nextKeyIndex = rows[0];
1219 for (int row : rows) {
1220 String key = propertyData.getValueAt(row, 0).toString();
1221 if (row == nextKeyIndex + 1) {
1222 nextKeyIndex = row; // no gap yet
1223 }
1224 tags.put(key, null);
1225 }
1226
1227 // find key to select after deleting other properties
1228 String nextKey = null;
1229 int rowCount = propertyData.getRowCount();
1230 if (rowCount > rows.length) {
1231 if (nextKeyIndex == rows[rows.length-1]) {
1232 // no gap found, pick next or previous key in list
1233 nextKeyIndex = (nextKeyIndex + 1 < rowCount ? nextKeyIndex + 1 : rows[0] - 1);
1234 } else {
1235 // gap found
1236 nextKeyIndex++;
1237 }
1238 nextKey = (String)propertyData.getValueAt(nextKeyIndex, 0);
1239 }
1240
1241 Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getSelected();
1242 Main.main.undoRedo.add(new ChangePropertyCommand(sel, tags));
1243
1244 membershipTable.clearSelection();
1245 if (nextKey != null) {
1246 propertyTable.changeSelection(findRow(propertyData, nextKey), 0, false, false);
1247 }
1248 }
1249
1250 protected void deleteFromRelation(int row) {
1251 Relation cur = (Relation)membershipData.getValueAt(row, 0);
1252
1253 Relation nextRelation = null;
1254 int rowCount = membershipTable.getRowCount();
1255 if (rowCount > 1) {
1256 nextRelation = (Relation)membershipData.getValueAt((row + 1 < rowCount ? row + 1 : row - 1), 0);
1257 }
1258
1259 ExtendedDialog ed = new ExtendedDialog(Main.parent,
1260 tr("Change relation"),
1261 new String[] {tr("Delete from relation"), tr("Cancel")});
1262 ed.setButtonIcons(new String[] {"dialogs/delete.png", "cancel.png"});
1263 ed.setContent(tr("Really delete selection from relation {0}?", cur.getDisplayName(DefaultNameFormatter.getInstance())));
1264 ed.toggleEnable("delete_from_relation");
1265 ed.showDialog();
1266
1267 if(ed.getValue() != 1)
1268 return;
1269
1270 Relation rel = new Relation(cur);
1271 Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getSelected();
1272 for (OsmPrimitive primitive: sel) {
1273 rel.removeMembersFor(primitive);
1274 }
1275 Main.main.undoRedo.add(new ChangeCommand(cur, rel));
1276
1277 propertyTable.clearSelection();
1278 if (nextRelation != null) {
1279 membershipTable.changeSelection(findRow(membershipData, nextRelation), 0, false, false);
1280 }
1281 }
1282
1283 @Override
1284 public void actionPerformed(ActionEvent e) {
1285 if (propertyTable.getSelectedRowCount() > 0) {
1286 int[] rows = propertyTable.getSelectedRows();
1287 deleteProperties(rows);
1288 } else if (membershipTable.getSelectedRowCount() > 0) {
1289 int row = membershipTable.getSelectedRow();
1290 deleteFromRelation(row);
1291 }
1292 }
1293
1294 @Override
1295 protected void updateEnabledState() {
1296 setEnabled(
1297 (propertyTable != null && propertyTable.getSelectedRowCount() >= 1)
1298 || (membershipTable != null && membershipTable.getSelectedRowCount() == 1)
1299 );
1300 }
1301
1302 @Override
1303 public void valueChanged(ListSelectionEvent e) {
1304 updateEnabledState();
1305 }
1306 }
1307
1308 /**
1309 * Action handling add button press in properties dialog.
1310 */
1311 class AddAction extends JosmAction {
1312 public AddAction() {
1313 super(tr("Add"), "dialogs/add", tr("Add a new key/value pair to all objects"),
1314 Shortcut.registerShortcut("properties:add", tr("Add Property"), KeyEvent.VK_A,
1315 Shortcut.ALT), false);
1316 }
1317
1318 @Override
1319 public void actionPerformed(ActionEvent e) {
1320 addProperty();
1321 }
1322 }
1323
1324 /**
1325 * Action handling edit button press in properties dialog.
1326 */
1327 class EditAction extends JosmAction implements ListSelectionListener {
1328 public EditAction() {
1329 super(tr("Edit"), "dialogs/edit", tr("Edit the value of the selected key for all objects"),
1330 Shortcut.registerShortcut("properties:edit", tr("Edit Properties"), KeyEvent.VK_S,
1331 Shortcut.ALT), false);
1332 updateEnabledState();
1333 }
1334
1335 @Override
1336 public void actionPerformed(ActionEvent e) {
1337 if (!isEnabled())
1338 return;
1339 if (propertyTable.getSelectedRowCount() == 1) {
1340 int row = propertyTable.getSelectedRow();
1341 editProperty(row);
1342 } else if (membershipTable.getSelectedRowCount() == 1) {
1343 int row = membershipTable.getSelectedRow();
1344 editMembership(row);
1345 }
1346 }
1347
1348 @Override
1349 protected void updateEnabledState() {
1350 setEnabled(
1351 (propertyTable != null && propertyTable.getSelectedRowCount() == 1)
1352 ^ (membershipTable != null && membershipTable.getSelectedRowCount() == 1)
1353 );
1354 }
1355
1356 @Override
1357 public void valueChanged(ListSelectionEvent e) {
1358 updateEnabledState();
1359 }
1360 }
1361
1362 class HelpAction extends AbstractAction {
1363 public HelpAction() {
1364 putValue(NAME, tr("Go to OSM wiki for tag help (F1)"));
1365 putValue(SHORT_DESCRIPTION, tr("Launch browser with wiki help for selected object"));
1366 putValue(SMALL_ICON, ImageProvider.get("dialogs", "search"));
1367 }
1368
1369 public void actionPerformed(ActionEvent e) {
1370 try {
1371 String base = Main.pref.get("url.openstreetmap-wiki", "http://wiki.openstreetmap.org/wiki/");
1372 String lang = LanguageInfo.getWikiLanguagePrefix();
1373 final List<URI> uris = new ArrayList<URI>();
1374 int row;
1375 if (propertyTable.getSelectedRowCount() == 1) {
1376 row = propertyTable.getSelectedRow();
1377 String key = URLEncoder.encode(propertyData.getValueAt(row, 0).toString(), "UTF-8");
1378 String val = URLEncoder.encode(
1379 ((Map<String,Integer>)propertyData.getValueAt(row, 1))
1380 .entrySet().iterator().next().getKey(), "UTF-8"
1381 );
1382
1383 uris.add(new URI(String.format("%s%sTag:%s=%s", base, lang, key, val)));
1384 uris.add(new URI(String.format("%sTag:%s=%s", base, key, val)));
1385 uris.add(new URI(String.format("%s%sKey:%s", base, lang, key)));
1386 uris.add(new URI(String.format("%sKey:%s", base, key)));
1387 uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
1388 uris.add(new URI(String.format("%sMap_Features", base)));
1389 } else if (membershipTable.getSelectedRowCount() == 1) {
1390 row = membershipTable.getSelectedRow();
1391 String type = URLEncoder.encode(
1392 ((Relation)membershipData.getValueAt(row, 0)).get("type"), "UTF-8"
1393 );
1394
1395 if (type != null && !type.equals("")) {
1396 uris.add(new URI(String.format("%s%sRelation:%s", base, lang, type)));
1397 uris.add(new URI(String.format("%sRelation:%s", base, type)));
1398 }
1399
1400 uris.add(new URI(String.format("%s%sRelations", base, lang)));
1401 uris.add(new URI(String.format("%sRelations", base)));
1402 } else {
1403 // give the generic help page, if more than one element is selected
1404 uris.add(new URI(String.format("%s%sMap_Features", base, lang)));
1405 uris.add(new URI(String.format("%sMap_Features", base)));
1406 }
1407
1408 Main.worker.execute(new Runnable(){
1409 public void run() {
1410 try {
1411 // find a page that actually exists in the wiki
1412 HttpURLConnection conn;
1413 for (URI u : uris) {
1414 conn = Utils.openHttpConnection(u.toURL());
1415 conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
1416
1417 if (conn.getResponseCode() != 200) {
1418 Main.info("INFO: {0} does not exist", u);
1419 conn.disconnect();
1420 } else {
1421 int osize = conn.getContentLength();
1422 conn.disconnect();
1423
1424 conn = Utils.openHttpConnection(new URI(u.toString()
1425 .replace("=", "%3D") /* do not URLencode whole string! */
1426 .replaceFirst("/wiki/", "/w/index.php?redirect=no&title=")
1427 ).toURL());
1428 conn.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
1429
1430 /* redirect pages have different content length, but retrieving a "nonredirect"
1431 * page using index.php and the direct-link method gives slightly different
1432 * content lengths, so we have to be fuzzy.. (this is UGLY, recode if u know better)
1433 */
1434 if (Math.abs(conn.getContentLength() - osize) > 200) {
1435 Main.info("INFO: {0} is a mediawiki redirect", u);
1436 conn.disconnect();
1437 } else {
1438 Main.info("INFO: browsing to {0}", u);
1439 conn.disconnect();
1440
1441 OpenBrowser.displayUrl(u.toString());
1442 break;
1443 }
1444 }
1445 }
1446 } catch (Exception e) {
1447 e.printStackTrace();
1448 }
1449 }
1450 });
1451 } catch (Exception e1) {
1452 e1.printStackTrace();
1453 }
1454 }
1455 }
1456
1457 public void addPropertyPopupMenuSeparator() {
1458 propertyMenu.addSeparator();
1459 }
1460
1461 public JMenuItem addPropertyPopupMenuAction(Action a) {
1462 return propertyMenu.add(a);
1463 }
1464
1465 public void addPropertyPopupMenuListener(PopupMenuListener l) {
1466 propertyMenu.addPopupMenuListener(l);
1467 }
1468
1469 public void removePropertyPopupMenuListener(PopupMenuListener l) {
1470 propertyMenu.addPopupMenuListener(l);
1471 }
1472
1473 @SuppressWarnings("unchecked")
1474 public Tag getSelectedProperty() {
1475 int row = propertyTable.getSelectedRow();
1476 if (row == -1) return null;
1477 TreeMap<String, Integer> map = (TreeMap<String, Integer>) propertyData.getValueAt(row, 1);
1478 return new Tag(
1479 propertyData.getValueAt(row, 0).toString(),
1480 map.size() > 1 ? "" : map.keySet().iterator().next());
1481 }
1482
1483 public void addMembershipPopupMenuSeparator() {
1484 membershipMenu.addSeparator();
1485 }
1486
1487 public JMenuItem addMembershipPopupMenuAction(Action a) {
1488 return membershipMenu.add(a);
1489 }
1490
1491 public void addMembershipPopupMenuListener(PopupMenuListener l) {
1492 membershipMenu.addPopupMenuListener(l);
1493 }
1494
1495 public void removeMembershipPopupMenuListener(PopupMenuListener l) {
1496 membershipMenu.addPopupMenuListener(l);
1497 }
1498
1499 public IRelation getSelectedMembershipRelation() {
1500 int row = membershipTable.getSelectedRow();
1501 return row > -1 ? (IRelation) membershipData.getValueAt(row, 0) : null;
1502 }
1503
1504 public static interface RelationRelated {
1505 public Relation getRelation();
1506 public void setRelation(Relation relation);
1507 }
1508
1509 static abstract class AbstractRelationAction extends AbstractAction implements RelationRelated {
1510 protected Relation relation;
1511 public Relation getRelation() {
1512 return this.relation;
1513 }
1514 public void setRelation(Relation relation) {
1515 this.relation = relation;
1516 }
1517 }
1518
1519 static class SelectRelationAction extends AbstractRelationAction {
1520 boolean selectionmode;
1521 public SelectRelationAction(boolean select) {
1522 selectionmode = select;
1523 if(select) {
1524 putValue(NAME, tr("Select relation"));
1525 putValue(SHORT_DESCRIPTION, tr("Select relation in main selection."));
1526 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select"));
1527 } else {
1528 putValue(NAME, tr("Select in relation list"));
1529 putValue(SHORT_DESCRIPTION, tr("Select relation in relation list."));
1530 putValue(SMALL_ICON, ImageProvider.get("dialogs", "relationlist"));
1531 }
1532 }
1533
1534 public void actionPerformed(ActionEvent e) {
1535 if(selectionmode) {
1536 Main.map.mapView.getEditLayer().data.setSelected(relation);
1537 } else {
1538 Main.map.relationListDialog.selectRelation(relation);
1539 Main.map.relationListDialog.unfurlDialog();
1540 }
1541 }
1542 }
1543
1544
1545 /**
1546 * Sets the current selection to the members of selected relation
1547 *
1548 */
1549 class SelectRelationMembersAction extends AbstractRelationAction {
1550 public SelectRelationMembersAction() {
1551 putValue(SHORT_DESCRIPTION,tr("Select the members of selected relation"));
1552 putValue(SMALL_ICON, ImageProvider.get("selectall"));
1553 putValue(NAME, tr("Select members"));
1554 }
1555
1556 public void actionPerformed(ActionEvent e) {
1557 HashSet<OsmPrimitive> members = new HashSet<OsmPrimitive>();
1558 members.addAll(relation.getMemberPrimitives());
1559 Main.map.mapView.getEditLayer().data.setSelected(members);
1560 }
1561
1562 }
1563
1564 /**
1565 * Action for downloading incomplete members of selected relation
1566 *
1567 */
1568 class DownloadIncompleteMembersAction extends AbstractRelationAction {
1569 public DownloadIncompleteMembersAction() {
1570 putValue(SHORT_DESCRIPTION, tr("Download incomplete members of selected relations"));
1571 putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected"));
1572 putValue(NAME, tr("Download incomplete members"));
1573 }
1574
1575 public Set<OsmPrimitive> buildSetOfIncompleteMembers(Relation r) {
1576 Set<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
1577 ret.addAll(r.getIncompleteMembers());
1578 return ret;
1579 }
1580
1581 public void actionPerformed(ActionEvent e) {
1582 if (!relation.hasIncompleteMembers()) return;
1583 ArrayList<Relation> rels = new ArrayList<Relation>();
1584 rels.add(relation);
1585 Main.worker.submit(new DownloadRelationMemberTask(
1586 rels,
1587 buildSetOfIncompleteMembers(relation),
1588 Main.map.mapView.getEditLayer()
1589 ));
1590 }
1591 }
1592
1593 abstract class AbstractCopyAction extends AbstractAction {
1594
1595 protected abstract Collection<String> getString(OsmPrimitive p, String key);
1596
1597 @Override
1598 public void actionPerformed(ActionEvent ae) {
1599 if (propertyTable.getSelectedRowCount() != 1)
1600 return;
1601 String key = propertyData.getValueAt(propertyTable.getSelectedRow(), 0).toString();
1602 Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getSelected();
1603 if (sel.isEmpty())
1604 return;
1605 Set<String> values = new TreeSet<String>();
1606 for (OsmPrimitive p : sel) {
1607 Collection<String> s = getString(p,key);
1608 if (s != null) {
1609 values.addAll(s);
1610 }
1611 }
1612 Utils.copyToClipboard(Utils.join("\n", values));
1613 }
1614 }
1615
1616 class CopyValueAction extends AbstractCopyAction {
1617
1618 public CopyValueAction() {
1619 putValue(NAME, tr("Copy Value"));
1620 putValue(SHORT_DESCRIPTION, tr("Copy the value of the selected tag to clipboard"));
1621 }
1622
1623 @Override
1624 protected Collection<String> getString(OsmPrimitive p, String key) {
1625 String v = p.get(key);
1626 return v == null ? null : Collections.singleton(v);
1627 }
1628 }
1629
1630 class CopyKeyValueAction extends AbstractCopyAction {
1631
1632 public CopyKeyValueAction() {
1633 putValue(NAME, tr("Copy Key/Value"));
1634 putValue(SHORT_DESCRIPTION, tr("Copy the key and value of the selected tag to clipboard"));
1635 }
1636
1637 @Override
1638 protected Collection<String> getString(OsmPrimitive p, String key) {
1639 String v = p.get(key);
1640 return v == null ? null : Collections.singleton(new Tag(key, v).toString());
1641 }
1642 }
1643
1644 class CopyAllKeyValueAction extends AbstractCopyAction {
1645
1646 public CopyAllKeyValueAction() {
1647 putValue(NAME, tr("Copy all Keys/Values"));
1648 putValue(SHORT_DESCRIPTION, tr("Copy the key and value of the all tags to clipboard"));
1649 }
1650
1651 @Override
1652 protected Collection<String> getString(OsmPrimitive p, String key) {
1653 List<String> r = new LinkedList<String>();
1654 for (Entry<String, String> kv : p.getKeys().entrySet()) {
1655 r.add(new Tag(kv.getKey(), kv.getValue()).toString());
1656 }
1657 return r;
1658 }
1659 }
1660
1661 class SearchAction extends AbstractAction {
1662 final boolean sameType;
1663
1664 public SearchAction(boolean sameType) {
1665 this.sameType = sameType;
1666 if (sameType) {
1667 putValue(NAME, tr("Search Key/Value/Type"));
1668 putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag, restrict to type (i.e., node/way/relation)"));
1669 } else {
1670 putValue(NAME, tr("Search Key/Value"));
1671 putValue(SHORT_DESCRIPTION, tr("Search with the key and value of the selected tag"));
1672 }
1673 }
1674
1675 public void actionPerformed(ActionEvent e) {
1676 if (propertyTable.getSelectedRowCount() != 1)
1677 return;
1678 String key = propertyData.getValueAt(propertyTable.getSelectedRow(), 0).toString();
1679 Collection<OsmPrimitive> sel = Main.main.getCurrentDataSet().getSelected();
1680 if (sel.isEmpty())
1681 return;
1682 String sep = "";
1683 String s = "";
1684 for (OsmPrimitive p : sel) {
1685 String val = p.get(key);
1686 if (val == null) {
1687 continue;
1688 }
1689 String t = "";
1690 if (!sameType) {
1691 t = "";
1692 } else if (p instanceof Node) {
1693 t = "type:node ";
1694 } else if (p instanceof Way) {
1695 t = "type:way ";
1696 } else if (p instanceof Relation) {
1697 t = "type:relation ";
1698 }
1699 s += sep + "(" + t + "\"" +
1700 org.openstreetmap.josm.actions.search.SearchAction.escapeStringForSearch(key) + "\"=\"" +
1701 org.openstreetmap.josm.actions.search.SearchAction.escapeStringForSearch(val) + "\")";
1702 sep = " OR ";
1703 }
1704
1705 SearchSetting ss = new SearchSetting(s, SearchMode.replace, true, false, false);
1706 org.openstreetmap.josm.actions.search.SearchAction.searchWithoutHistory(ss);
1707 }
1708 }
1709
1710 @Override
1711 public void destroy() {
1712 super.destroy();
1713 for (JosmAction action : josmActions) {
1714 action.destroy();
1715 }
1716 Container parent = pluginHook.getParent();
1717 if (parent != null) {
1718 parent.remove(pluginHook);
1719 }
1720 }
1721}
Note: See TracBrowser for help on using the repository browser.