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