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

Revision 5200, 65.5 KB checked in by akks, 5 weeks ago (diff)

see #7626, fix #7463: keys Ctrl-Shift-Up/Down, Enter, Spacebar work better in toggle dialogs
Enter and Spacebar = useful actions for list items (select, toggle, etc.)

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