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