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

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

fix #7671 - Fix (last ?) problem with recently added tags (unwanted autocompletion when the key already exists)

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