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    }