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