001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Container;
008import java.awt.Dimension;
009import java.awt.GridBagLayout;
010import java.awt.GridLayout;
011import java.awt.LayoutManager;
012import java.awt.Rectangle;
013import java.awt.datatransfer.DataFlavor;
014import java.awt.datatransfer.Transferable;
015import java.awt.datatransfer.UnsupportedFlavorException;
016import java.awt.event.ActionEvent;
017import java.awt.event.ActionListener;
018import java.awt.event.InputEvent;
019import java.awt.event.KeyEvent;
020import java.beans.PropertyChangeEvent;
021import java.beans.PropertyChangeListener;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.ConcurrentHashMap;
031
032import javax.swing.AbstractAction;
033import javax.swing.Action;
034import javax.swing.DefaultListCellRenderer;
035import javax.swing.DefaultListModel;
036import javax.swing.Icon;
037import javax.swing.ImageIcon;
038import javax.swing.JButton;
039import javax.swing.JCheckBoxMenuItem;
040import javax.swing.JComponent;
041import javax.swing.JLabel;
042import javax.swing.JList;
043import javax.swing.JMenuItem;
044import javax.swing.JPanel;
045import javax.swing.JPopupMenu;
046import javax.swing.JScrollPane;
047import javax.swing.JTable;
048import javax.swing.JToolBar;
049import javax.swing.JTree;
050import javax.swing.ListCellRenderer;
051import javax.swing.MenuElement;
052import javax.swing.TransferHandler;
053import javax.swing.event.ListSelectionEvent;
054import javax.swing.event.ListSelectionListener;
055import javax.swing.event.PopupMenuEvent;
056import javax.swing.event.PopupMenuListener;
057import javax.swing.event.TreeSelectionEvent;
058import javax.swing.event.TreeSelectionListener;
059import javax.swing.table.AbstractTableModel;
060import javax.swing.tree.DefaultMutableTreeNode;
061import javax.swing.tree.DefaultTreeCellRenderer;
062import javax.swing.tree.DefaultTreeModel;
063import javax.swing.tree.TreePath;
064
065import org.openstreetmap.josm.Main;
066import org.openstreetmap.josm.actions.ActionParameter;
067import org.openstreetmap.josm.actions.AdaptableAction;
068import org.openstreetmap.josm.actions.JosmAction;
069import org.openstreetmap.josm.actions.ParameterizedAction;
070import org.openstreetmap.josm.actions.ParameterizedActionDecorator;
071import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
072import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
073import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
074import org.openstreetmap.josm.tools.GBC;
075import org.openstreetmap.josm.tools.ImageProvider;
076import org.openstreetmap.josm.tools.Shortcut;
077
078public class ToolbarPreferences implements PreferenceSettingFactory {
079
080    private static final String EMPTY_TOOLBAR_MARKER = "<!-empty-!>";
081
082    public static class ActionDefinition {
083        private final Action action;
084        private String name = "";
085        private String icon = "";
086        private ImageIcon ico;
087        private final Map<String, Object> parameters = new ConcurrentHashMap<>();
088
089        public ActionDefinition(Action action) {
090            this.action = action;
091        }
092
093        public Map<String, Object> getParameters() {
094            return parameters;
095        }
096
097        public Action getParametrizedAction() {
098            if (getAction() instanceof ParameterizedAction)
099                return new ParameterizedActionDecorator((ParameterizedAction) getAction(), parameters);
100            else
101                return getAction();
102        }
103
104        public Action getAction() {
105            return action;
106        }
107
108        public String getName() {
109            return name;
110        }
111
112        public String getDisplayName() {
113            return name.isEmpty() ? (String) action.getValue(Action.NAME) : name;
114        }
115
116        public String getDisplayTooltip() {
117            if (!name.isEmpty())
118                return name;
119
120            Object tt = action.getValue(TaggingPreset.OPTIONAL_TOOLTIP_TEXT);
121            if (tt != null)
122                return (String) tt;
123
124            return (String) action.getValue(Action.SHORT_DESCRIPTION);
125        }
126
127        public Icon getDisplayIcon() {
128            if (ico != null)
129                return ico;
130            Object o = action.getValue(Action.LARGE_ICON_KEY);
131            if (o == null)
132                o = action.getValue(Action.SMALL_ICON);
133            return (Icon) o;
134        }
135
136        public void setName(String name) {
137            this.name = name;
138        }
139
140        public String getIcon() {
141            return icon;
142        }
143
144        public void setIcon(String icon) {
145            this.icon = icon;
146            ico = ImageProvider.getIfAvailable("", icon);
147        }
148
149        public boolean isSeparator() {
150            return action == null;
151        }
152
153        public static ActionDefinition getSeparator() {
154            return new ActionDefinition(null);
155        }
156
157        public boolean hasParameters() {
158            if (!(getAction() instanceof ParameterizedAction)) return false;
159            for (Object o: parameters.values()) {
160                if (o != null) return true;
161            }
162            return false;
163        }
164    }
165
166    public static class ActionParser {
167        private final Map<String, Action> actions;
168        private final StringBuilder result = new StringBuilder();
169        private int index;
170        private char[] s;
171
172        public ActionParser(Map<String, Action> actions) {
173            this.actions = actions;
174        }
175
176        private String readTillChar(char ch1, char ch2) {
177            result.setLength(0);
178            while (index < s.length && s[index] != ch1 && s[index] != ch2) {
179                if (s[index] == '\\') {
180                    index++;
181                    if (index >= s.length) {
182                        break;
183                    }
184                }
185                result.append(s[index]);
186                index++;
187            }
188            return result.toString();
189        }
190
191        private void skip(char ch) {
192            if (index < s.length && s[index] == ch) {
193                index++;
194            }
195        }
196
197        public ActionDefinition loadAction(String actionName) {
198            index = 0;
199            this.s = actionName.toCharArray();
200
201            String name = readTillChar('(', '{');
202            Action action = actions.get(name);
203
204            if (action == null)
205                return null;
206
207            ActionDefinition result = new ActionDefinition(action);
208
209            if (action instanceof ParameterizedAction) {
210                skip('(');
211
212                ParameterizedAction parametrizedAction = (ParameterizedAction) action;
213                Map<String, ActionParameter<?>> actionParams = new ConcurrentHashMap<>();
214                for (ActionParameter<?> param: parametrizedAction.getActionParameters()) {
215                    actionParams.put(param.getName(), param);
216                }
217
218                while (index < s.length && s[index] != ')') {
219                    String paramName = readTillChar('=', '=');
220                    skip('=');
221                    String paramValue = readTillChar(',', ')');
222                    if (!paramName.isEmpty() && !paramValue.isEmpty()) {
223                        ActionParameter<?> actionParam = actionParams.get(paramName);
224                        if (actionParam != null) {
225                            result.getParameters().put(paramName, actionParam.readFromString(paramValue));
226                        }
227                    }
228                    skip(',');
229                }
230                skip(')');
231            }
232            if (action instanceof AdaptableAction) {
233                skip('{');
234
235                while (index < s.length && s[index] != '}') {
236                    String paramName = readTillChar('=', '=');
237                    skip('=');
238                    String paramValue = readTillChar(',', '}');
239                    if ("icon".equals(paramName) && !paramValue.isEmpty()) {
240                        result.setIcon(paramValue);
241                    } else if ("name".equals(paramName) && !paramValue.isEmpty()) {
242                        result.setName(paramValue);
243                    }
244                    skip(',');
245                }
246                skip('}');
247            }
248
249            return result;
250        }
251
252        private void escape(String s) {
253            for (int i = 0; i < s.length(); i++) {
254                char ch = s.charAt(i);
255                if (ch == '\\' || ch == '(' || ch == '{' || ch == ',' || ch == ')' || ch == '}' || ch == '=') {
256                    result.append('\\');
257                    result.append(ch);
258                } else {
259                    result.append(ch);
260                }
261            }
262        }
263
264        @SuppressWarnings("unchecked")
265        public String saveAction(ActionDefinition action) {
266            result.setLength(0);
267
268            String val = (String) action.getAction().getValue("toolbar");
269            if (val == null)
270                return null;
271            escape(val);
272            if (action.getAction() instanceof ParameterizedAction) {
273                result.append('(');
274                List<ActionParameter<?>> params = ((ParameterizedAction) action.getAction()).getActionParameters();
275                for (int i = 0; i < params.size(); i++) {
276                    ActionParameter<Object> param = (ActionParameter<Object>) params.get(i);
277                    escape(param.getName());
278                    result.append('=');
279                    Object value = action.getParameters().get(param.getName());
280                    if (value != null) {
281                        escape(param.writeToString(value));
282                    }
283                    if (i < params.size() - 1) {
284                        result.append(',');
285                    } else {
286                        result.append(')');
287                    }
288                }
289            }
290            if (action.getAction() instanceof AdaptableAction) {
291                boolean first = true;
292                String tmp = action.getName();
293                if (!tmp.isEmpty()) {
294                    result.append(first ? "{" : ",");
295                    result.append("name=");
296                    escape(tmp);
297                    first = false;
298                }
299                tmp = action.getIcon();
300                if (!tmp.isEmpty()) {
301                    result.append(first ? "{" : ",");
302                    result.append("icon=");
303                    escape(tmp);
304                    first = false;
305                }
306                if (!first) {
307                    result.append('}');
308            }
309            }
310
311            return result.toString();
312        }
313    }
314
315    private static class ActionParametersTableModel extends AbstractTableModel {
316
317        private transient ActionDefinition currentAction = ActionDefinition.getSeparator();
318
319        @Override
320        public int getColumnCount() {
321            return 2;
322        }
323
324        @Override
325        public int getRowCount() {
326            int adaptable = (currentAction.getAction() instanceof AdaptableAction) ? 2 : 0;
327            if (currentAction.isSeparator() || !(currentAction.getAction() instanceof ParameterizedAction))
328                return adaptable;
329            ParameterizedAction pa = (ParameterizedAction) currentAction.getAction();
330            return pa.getActionParameters().size() + adaptable;
331        }
332
333        @SuppressWarnings("unchecked")
334        private ActionParameter<Object> getParam(int index) {
335            ParameterizedAction pa = (ParameterizedAction) currentAction.getAction();
336            return (ActionParameter<Object>) pa.getActionParameters().get(index);
337        }
338
339        @Override
340        public Object getValueAt(int rowIndex, int columnIndex) {
341            if (currentAction.getAction() instanceof AdaptableAction) {
342                if (rowIndex < 2) {
343                    switch (columnIndex) {
344                    case 0:
345                        return rowIndex == 0 ? tr("Tooltip") : tr("Icon");
346                    case 1:
347                        return rowIndex == 0 ? currentAction.getName() : currentAction.getIcon();
348                    default:
349                        return null;
350                    }
351                } else {
352                    rowIndex -= 2;
353                }
354            }
355            ActionParameter<Object> param = getParam(rowIndex);
356            switch (columnIndex) {
357            case 0:
358                return param.getName();
359            case 1:
360                return param.writeToString(currentAction.getParameters().get(param.getName()));
361            default:
362                return null;
363            }
364        }
365
366        @Override
367        public boolean isCellEditable(int row, int column) {
368            return column == 1;
369        }
370
371        @Override
372        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
373            String val = (String) aValue;
374            int paramIndex = rowIndex;
375
376            if (currentAction.getAction() instanceof AdaptableAction) {
377                if (rowIndex == 0) {
378                     currentAction.setName(val);
379                     return;
380                } else if (rowIndex == 1) {
381                     currentAction.setIcon(val);
382                     return;
383                } else {
384                    paramIndex -= 2;
385                }
386            }
387            ActionParameter<Object> param = getParam(paramIndex);
388
389            if (param != null && !val.isEmpty()) {
390                currentAction.getParameters().put(param.getName(), param.readFromString((String) aValue));
391            }
392        }
393
394        public void setCurrentAction(ActionDefinition currentAction) {
395            this.currentAction = currentAction;
396            fireTableDataChanged();
397        }
398    }
399
400    private class ToolbarPopupMenu extends JPopupMenu  {
401        private transient ActionDefinition act;
402
403        private void setActionAndAdapt(ActionDefinition action) {
404            this.act = action;
405            doNotHide.setSelected(Main.pref.getBoolean("toolbar.always-visible", true));
406            remove.setVisible(act != null);
407            shortcutEdit.setVisible(act != null);
408        }
409
410        private final JMenuItem remove = new JMenuItem(new AbstractAction(tr("Remove from toolbar")) {
411            @Override
412            public void actionPerformed(ActionEvent e) {
413                Collection<String> t = new LinkedList<>(getToolString());
414                ActionParser parser = new ActionParser(null);
415                // get text definition of current action
416                String res = parser.saveAction(act);
417                // remove the button from toolbar preferences
418                t.remove(res);
419                Main.pref.putCollection("toolbar", t);
420                Main.toolbar.refreshToolbarControl();
421            }
422        });
423
424        private final JMenuItem configure = new JMenuItem(new AbstractAction(tr("Configure toolbar")) {
425            @Override
426            public void actionPerformed(ActionEvent e) {
427                final PreferenceDialog p = new PreferenceDialog(Main.parent);
428                p.selectPreferencesTabByName("toolbar");
429                p.setVisible(true);
430            }
431        });
432
433        private final JMenuItem shortcutEdit = new JMenuItem(new AbstractAction(tr("Edit shortcut")) {
434            @Override
435            public void actionPerformed(ActionEvent e) {
436                final PreferenceDialog p = new PreferenceDialog(Main.parent);
437                p.getTabbedPane().getShortcutPreference().setDefaultFilter(act.getDisplayName());
438                p.selectPreferencesTabByName("shortcuts");
439                p.setVisible(true);
440                // refresh toolbar to try using changed shortcuts without restart
441                Main.toolbar.refreshToolbarControl();
442            }
443        });
444
445        private final JCheckBoxMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide toolbar and menu")) {
446            @Override
447            public void actionPerformed(ActionEvent e) {
448                boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
449                Main.pref.put("toolbar.always-visible", sel);
450                Main.pref.put("menu.always-visible", sel);
451            }
452        });
453
454        {
455            addPopupMenuListener(new PopupMenuListener() {
456                @Override
457                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
458                    setActionAndAdapt(buttonActions.get(
459                            ((JPopupMenu) e.getSource()).getInvoker()
460                    ));
461                }
462
463                @Override
464                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
465                    // Do nothing
466                }
467
468                @Override
469                public void popupMenuCanceled(PopupMenuEvent e) {
470                    // Do nothing
471                }
472            });
473            add(remove);
474            add(configure);
475            add(shortcutEdit);
476            add(doNotHide);
477        }
478    }
479
480    private final ToolbarPopupMenu popupMenu = new ToolbarPopupMenu();
481
482    /**
483     * Key: Registered name (property "toolbar" of action).
484     * Value: The action to execute.
485     */
486    private final Map<String, Action> actions = new ConcurrentHashMap<>();
487    private final Map<String, Action> regactions = new ConcurrentHashMap<>();
488
489    private final DefaultMutableTreeNode rootActionsNode = new DefaultMutableTreeNode(tr("Actions"));
490
491    public final JToolBar control = new JToolBar();
492    private final Map<Object, ActionDefinition> buttonActions = new ConcurrentHashMap<>(30);
493
494    @Override
495    public PreferenceSetting createPreferenceSetting() {
496        return new Settings(rootActionsNode);
497    }
498
499    public class Settings extends DefaultTabPreferenceSetting {
500
501        private final class SelectedListTransferHandler extends TransferHandler {
502            @Override
503            @SuppressWarnings("unchecked")
504            protected Transferable createTransferable(JComponent c) {
505                List<ActionDefinition> actions = new ArrayList<>();
506                for (ActionDefinition o: ((JList<ActionDefinition>) c).getSelectedValuesList()) {
507                    actions.add(o);
508                }
509                return new ActionTransferable(actions);
510            }
511
512            @Override
513            public int getSourceActions(JComponent c) {
514                return TransferHandler.MOVE;
515            }
516
517            @Override
518            public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
519                for (DataFlavor f : transferFlavors) {
520                    if (ACTION_FLAVOR.equals(f))
521                        return true;
522                }
523                return false;
524            }
525
526            @Override
527            public void exportAsDrag(JComponent comp, InputEvent e, int action) {
528                super.exportAsDrag(comp, e, action);
529                movingComponent = "list";
530            }
531
532            @Override
533            public boolean importData(JComponent comp, Transferable t) {
534                try {
535                    int dropIndex = selectedList.locationToIndex(selectedList.getMousePosition(true));
536                    @SuppressWarnings("unchecked")
537                    List<ActionDefinition> draggedData = (List<ActionDefinition>) t.getTransferData(ACTION_FLAVOR);
538
539                    Object leadItem = dropIndex >= 0 ? selected.elementAt(dropIndex) : null;
540                    int dataLength = draggedData.size();
541
542                    if (leadItem != null) {
543                        for (Object o: draggedData) {
544                            if (leadItem.equals(o))
545                                return false;
546                        }
547                    }
548
549                    int dragLeadIndex = -1;
550                    boolean localDrop = "list".equals(movingComponent);
551
552                    if (localDrop) {
553                        dragLeadIndex = selected.indexOf(draggedData.get(0));
554                        for (Object o: draggedData) {
555                            selected.removeElement(o);
556                        }
557                    }
558                    int[] indices = new int[dataLength];
559
560                    if (localDrop) {
561                        int adjustedLeadIndex = selected.indexOf(leadItem);
562                        int insertionAdjustment = dragLeadIndex <= adjustedLeadIndex ? 1 : 0;
563                        for (int i = 0; i < dataLength; i++) {
564                            selected.insertElementAt(draggedData.get(i), adjustedLeadIndex + insertionAdjustment + i);
565                            indices[i] = adjustedLeadIndex + insertionAdjustment + i;
566                        }
567                    } else {
568                        for (int i = 0; i < dataLength; i++) {
569                            selected.add(dropIndex, draggedData.get(i));
570                            indices[i] = dropIndex + i;
571                        }
572                    }
573                    selectedList.clearSelection();
574                    selectedList.setSelectedIndices(indices);
575                    movingComponent = "";
576                    return true;
577                } catch (IOException | UnsupportedFlavorException e) {
578                    Main.error(e);
579                }
580                return false;
581            }
582
583            @Override
584            protected void exportDone(JComponent source, Transferable data, int action) {
585                if ("list".equals(movingComponent)) {
586                    try {
587                        List<?> draggedData = (List<?>) data.getTransferData(ACTION_FLAVOR);
588                        boolean localDrop = selected.contains(draggedData.get(0));
589                        if (localDrop) {
590                            int[] indices = selectedList.getSelectedIndices();
591                            Arrays.sort(indices);
592                            for (int i = indices.length - 1; i >= 0; i--) {
593                                selected.remove(indices[i]);
594                            }
595                        }
596                    } catch (IOException | UnsupportedFlavorException e) {
597                        Main.error(e);
598                    }
599                    movingComponent = "";
600                }
601            }
602        }
603
604        private final class Move implements ActionListener {
605            @Override
606            public void actionPerformed(ActionEvent e) {
607                if ("<".equals(e.getActionCommand()) && actionsTree.getSelectionCount() > 0) {
608
609                    int leadItem = selected.getSize();
610                    if (selectedList.getSelectedIndex() != -1) {
611                        int[] indices = selectedList.getSelectedIndices();
612                        leadItem = indices[indices.length - 1];
613                    }
614                    for (TreePath selectedAction : actionsTree.getSelectionPaths()) {
615                        DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectedAction.getLastPathComponent();
616                        if (node.getUserObject() == null) {
617                            selected.add(leadItem++, ActionDefinition.getSeparator());
618                        } else if (node.getUserObject() instanceof Action) {
619                            selected.add(leadItem++, new ActionDefinition((Action) node.getUserObject()));
620                        }
621                    }
622                } else if (">".equals(e.getActionCommand()) && selectedList.getSelectedIndex() != -1) {
623                    while (selectedList.getSelectedIndex() != -1) {
624                        selected.remove(selectedList.getSelectedIndex());
625                    }
626                } else if ("up".equals(e.getActionCommand())) {
627                    int i = selectedList.getSelectedIndex();
628                    ActionDefinition o = selected.get(i);
629                    if (i != 0) {
630                        selected.remove(i);
631                        selected.add(i-1, o);
632                        selectedList.setSelectedIndex(i-1);
633                    }
634                } else if ("down".equals(e.getActionCommand())) {
635                    int i = selectedList.getSelectedIndex();
636                    ActionDefinition o = selected.get(i);
637                    if (i != selected.size()-1) {
638                        selected.remove(i);
639                        selected.add(i+1, o);
640                        selectedList.setSelectedIndex(i+1);
641                    }
642                }
643            }
644        }
645
646        private class ActionTransferable implements Transferable {
647
648            private final DataFlavor[] flavors = new DataFlavor[] {ACTION_FLAVOR};
649
650            private final List<ActionDefinition> actions;
651
652            ActionTransferable(List<ActionDefinition> actions) {
653                this.actions = actions;
654            }
655
656            @Override
657            public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
658                return actions;
659            }
660
661            @Override
662            public DataFlavor[] getTransferDataFlavors() {
663                return flavors;
664            }
665
666            @Override
667            public boolean isDataFlavorSupported(DataFlavor flavor) {
668                return flavors[0] == flavor;
669            }
670        }
671
672        private final Move moveAction = new Move();
673
674        private final DefaultListModel<ActionDefinition> selected = new DefaultListModel<>();
675        private final JList<ActionDefinition> selectedList = new JList<>(selected);
676
677        private final DefaultTreeModel actionsTreeModel;
678        private final JTree actionsTree;
679
680        private final ActionParametersTableModel actionParametersModel = new ActionParametersTableModel();
681        private final JTable actionParametersTable = new JTable(actionParametersModel);
682        private JPanel actionParametersPanel;
683
684        private final JButton upButton = createButton("up");
685        private final JButton downButton = createButton("down");
686        private final JButton removeButton = createButton(">");
687        private final JButton addButton = createButton("<");
688
689        private String movingComponent;
690
691        /**
692         * Constructs a new {@code Settings}.
693         * @param rootActionsNode root actions node
694         */
695        public Settings(DefaultMutableTreeNode rootActionsNode) {
696            super(/* ICON(preferences/) */ "toolbar", tr("Toolbar customization"), tr("Customize the elements on the toolbar."));
697            actionsTreeModel = new DefaultTreeModel(rootActionsNode);
698            actionsTree = new JTree(actionsTreeModel);
699        }
700
701        private JButton createButton(String name) {
702            JButton b = new JButton();
703            if ("up".equals(name)) {
704                b.setIcon(ImageProvider.get("dialogs", "up"));
705            } else if ("down".equals(name)) {
706                b.setIcon(ImageProvider.get("dialogs", "down"));
707            } else {
708                b.setText(name);
709            }
710            b.addActionListener(moveAction);
711            b.setActionCommand(name);
712            return b;
713        }
714
715        private void updateEnabledState() {
716            int index = selectedList.getSelectedIndex();
717            upButton.setEnabled(index > 0);
718            downButton.setEnabled(index != -1 && index < selectedList.getModel().getSize() - 1);
719            removeButton.setEnabled(index != -1);
720            addButton.setEnabled(actionsTree.getSelectionCount() > 0);
721        }
722
723        @Override
724        public void addGui(PreferenceTabbedPane gui) {
725            actionsTree.setCellRenderer(new DefaultTreeCellRenderer() {
726                @Override
727                public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
728                        boolean leaf, int row, boolean hasFocus) {
729                    DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
730                    JLabel comp = (JLabel) super.getTreeCellRendererComponent(
731                            tree, value, sel, expanded, leaf, row, hasFocus);
732                    if (node.getUserObject() == null) {
733                        comp.setText(tr("Separator"));
734                        comp.setIcon(ImageProvider.get("preferences/separator"));
735                    } else if (node.getUserObject() instanceof Action) {
736                        Action action = (Action) node.getUserObject();
737                        comp.setText((String) action.getValue(Action.NAME));
738                        comp.setIcon((Icon) action.getValue(Action.SMALL_ICON));
739                    }
740                    return comp;
741                }
742            });
743
744            ListCellRenderer<ActionDefinition> renderer = new ListCellRenderer<ActionDefinition>() {
745                private final DefaultListCellRenderer def = new DefaultListCellRenderer();
746                @Override
747                public Component getListCellRendererComponent(JList<? extends ActionDefinition> list,
748                        ActionDefinition action, int index, boolean isSelected, boolean cellHasFocus) {
749                    String s;
750                    Icon i;
751                    if (!action.isSeparator()) {
752                        s = action.getDisplayName();
753                        i = action.getDisplayIcon();
754                    } else {
755                        i = ImageProvider.get("preferences/separator");
756                        s = tr("Separator");
757                    }
758                    JLabel l = (JLabel) def.getListCellRendererComponent(list, s, index, isSelected, cellHasFocus);
759                    l.setIcon(i);
760                    return l;
761                }
762            };
763            selectedList.setCellRenderer(renderer);
764            selectedList.addListSelectionListener(new ListSelectionListener() {
765                @Override
766                public void valueChanged(ListSelectionEvent e) {
767                    boolean sel = selectedList.getSelectedIndex() != -1;
768                    if (sel) {
769                        actionsTree.clearSelection();
770                        ActionDefinition action = selected.get(selectedList.getSelectedIndex());
771                        actionParametersModel.setCurrentAction(action);
772                        actionParametersPanel.setVisible(actionParametersModel.getRowCount() > 0);
773                    }
774                    updateEnabledState();
775                }
776            });
777
778            selectedList.setDragEnabled(true);
779            selectedList.setTransferHandler(new SelectedListTransferHandler());
780
781            actionsTree.setTransferHandler(new TransferHandler() {
782                private static final long serialVersionUID = 1L;
783
784                @Override
785                public int getSourceActions(JComponent c) {
786                    return TransferHandler.MOVE;
787                }
788
789                @Override
790                protected Transferable createTransferable(JComponent c) {
791                    TreePath[] paths = actionsTree.getSelectionPaths();
792                    List<ActionDefinition> dragActions = new ArrayList<>();
793                    for (TreePath path : paths) {
794                        DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
795                        Object obj = node.getUserObject();
796                        if (obj == null) {
797                            dragActions.add(ActionDefinition.getSeparator());
798                        } else if (obj instanceof Action) {
799                            dragActions.add(new ActionDefinition((Action) obj));
800                        }
801                    }
802                    return new ActionTransferable(dragActions);
803                }
804            });
805            actionsTree.setDragEnabled(true);
806            actionsTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
807                @Override public void valueChanged(TreeSelectionEvent e) {
808                    updateEnabledState();
809                }
810            });
811
812            final JPanel left = new JPanel(new GridBagLayout());
813            left.add(new JLabel(tr("Toolbar")), GBC.eol());
814            left.add(new JScrollPane(selectedList), GBC.std().fill(GBC.BOTH));
815
816            final JPanel right = new JPanel(new GridBagLayout());
817            right.add(new JLabel(tr("Available")), GBC.eol());
818            right.add(new JScrollPane(actionsTree), GBC.eol().fill(GBC.BOTH));
819
820            final JPanel buttons = new JPanel(new GridLayout(6, 1));
821            buttons.add(upButton);
822            buttons.add(addButton);
823            buttons.add(removeButton);
824            buttons.add(downButton);
825            updateEnabledState();
826
827            final JPanel p = new JPanel();
828            p.setLayout(new LayoutManager() {
829                @Override
830                public void addLayoutComponent(String name, Component comp) {
831                    // Do nothing
832                }
833
834                @Override
835                public void removeLayoutComponent(Component comp) {
836                    // Do nothing
837                }
838
839                @Override
840                public Dimension minimumLayoutSize(Container parent) {
841                    Dimension l = left.getMinimumSize();
842                    Dimension r = right.getMinimumSize();
843                    Dimension b = buttons.getMinimumSize();
844                    return new Dimension(l.width+b.width+10+r.width, l.height+b.height+10+r.height);
845                }
846
847                @Override
848                public Dimension preferredLayoutSize(Container parent) {
849                    Dimension l = new Dimension(200, 200);
850                    Dimension r = new Dimension(200, 200);
851                    return new Dimension(l.width+r.width+10+buttons.getPreferredSize().width, Math.max(l.height, r.height));
852                }
853
854                @Override
855                public void layoutContainer(Container parent) {
856                    Dimension d = p.getSize();
857                    Dimension b = buttons.getPreferredSize();
858                    int width = (d.width-10-b.width)/2;
859                    left.setBounds(new Rectangle(0, 0, width, d.height));
860                    right.setBounds(new Rectangle(width+10+b.width, 0, width, d.height));
861                    buttons.setBounds(new Rectangle(width+5, d.height/2-b.height/2, b.width, b.height));
862                }
863            });
864            p.add(left);
865            p.add(buttons);
866            p.add(right);
867
868            actionParametersPanel = new JPanel(new GridBagLayout());
869            actionParametersPanel.add(new JLabel(tr("Action parameters")), GBC.eol().insets(0, 10, 0, 20));
870            actionParametersTable.getColumnModel().getColumn(0).setHeaderValue(tr("Parameter name"));
871            actionParametersTable.getColumnModel().getColumn(1).setHeaderValue(tr("Parameter value"));
872            actionParametersPanel.add(actionParametersTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL));
873            actionParametersPanel.add(actionParametersTable, GBC.eol().fill(GBC.BOTH).insets(0, 0, 0, 10));
874            actionParametersPanel.setVisible(false);
875
876            JPanel panel = gui.createPreferenceTab(this);
877            panel.add(p, GBC.eol().fill(GBC.BOTH));
878            panel.add(actionParametersPanel, GBC.eol().fill(GBC.HORIZONTAL));
879            selected.removeAllElements();
880            for (ActionDefinition actionDefinition: getDefinedActions()) {
881                selected.addElement(actionDefinition);
882            }
883        }
884
885        @Override
886        public boolean ok() {
887            Collection<String> t = new LinkedList<>();
888            ActionParser parser = new ActionParser(null);
889            for (int i = 0; i < selected.size(); ++i) {
890                ActionDefinition action = selected.get(i);
891                if (action.isSeparator()) {
892                    t.add("|");
893                } else {
894                    String res = parser.saveAction(action);
895                    if (res != null) {
896                        t.add(res);
897                }
898            }
899            }
900            if (t.isEmpty()) {
901                t = Collections.singletonList(EMPTY_TOOLBAR_MARKER);
902            }
903            Main.pref.putCollection("toolbar", t);
904            Main.toolbar.refreshToolbarControl();
905            return false;
906        }
907
908    }
909
910    /**
911     * Constructs a new {@code ToolbarPreferences}.
912     */
913    public ToolbarPreferences() {
914        control.setFloatable(false);
915        control.setComponentPopupMenu(popupMenu);
916        Main.pref.addPreferenceChangeListener(new PreferenceChangedListener() {
917            @Override
918            public void preferenceChanged(PreferenceChangeEvent e) {
919                if ("toolbar.visible".equals(e.getKey())) {
920                    refreshToolbarControl();
921                }
922            }
923        });
924    }
925
926    private void loadAction(DefaultMutableTreeNode node, MenuElement menu) {
927        Object userObject = null;
928        MenuElement menuElement = menu;
929        if (menu.getSubElements().length > 0 &&
930                menu.getSubElements()[0] instanceof JPopupMenu) {
931            menuElement = menu.getSubElements()[0];
932        }
933        for (MenuElement item : menuElement.getSubElements()) {
934            if (item instanceof JMenuItem) {
935                JMenuItem menuItem = (JMenuItem) item;
936                if (menuItem.getAction() != null) {
937                    Action action = menuItem.getAction();
938                    userObject = action;
939                    Object tb = action.getValue("toolbar");
940                    if (tb == null) {
941                        Main.info(tr("Toolbar action without name: {0}",
942                        action.getClass().getName()));
943                        continue;
944                    } else if (!(tb instanceof String)) {
945                        if (!(tb instanceof Boolean) || (Boolean) tb) {
946                            Main.info(tr("Strange toolbar value: {0}",
947                            action.getClass().getName()));
948                        }
949                        continue;
950                    } else {
951                        String toolbar = (String) tb;
952                        Action r = actions.get(toolbar);
953                        if (r != null && r != action && !toolbar.startsWith("imagery_")) {
954                            Main.info(tr("Toolbar action {0} overwritten: {1} gets {2}",
955                            toolbar, r.getClass().getName(), action.getClass().getName()));
956                        }
957                        actions.put(toolbar, action);
958                    }
959                } else {
960                    userObject = menuItem.getText();
961                }
962            }
963            DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(userObject);
964            node.add(newNode);
965            loadAction(newNode, item);
966        }
967    }
968
969    private void loadActions() {
970        rootActionsNode.removeAllChildren();
971        loadAction(rootActionsNode, Main.main.menu);
972        for (Map.Entry<String, Action> a : regactions.entrySet()) {
973            if (actions.get(a.getKey()) == null) {
974                rootActionsNode.add(new DefaultMutableTreeNode(a.getValue()));
975            }
976        }
977        rootActionsNode.add(new DefaultMutableTreeNode(null));
978    }
979
980    private static final String[] deftoolbar = {"open", "save", "download", "upload", "|",
981    "undo", "redo", "|", "dialogs/search", "preference", "|", "splitway", "combineway",
982    "wayflip", "|", "imagery-offset", "|", "tagginggroup_Highways/Streets",
983    "tagginggroup_Highways/Ways", "tagginggroup_Highways/Waypoints",
984    "tagginggroup_Highways/Barriers", "|", "tagginggroup_Transport/Car",
985    "tagginggroup_Transport/Public Transport", "|", "tagginggroup_Facilities/Tourism",
986    "tagginggroup_Facilities/Food+Drinks", "|", "tagginggroup_Man Made/Historic Places", "|",
987    "tagginggroup_Man Made/Man Made"};
988
989    public static Collection<String> getToolString() {
990
991        Collection<String> toolStr = Main.pref.getCollection("toolbar", Arrays.asList(deftoolbar));
992        if (toolStr == null || toolStr.isEmpty()) {
993            toolStr = Arrays.asList(deftoolbar);
994        }
995        return toolStr;
996    }
997
998    private Collection<ActionDefinition> getDefinedActions() {
999        loadActions();
1000
1001        Map<String, Action> allActions = new ConcurrentHashMap<>(regactions);
1002        allActions.putAll(actions);
1003        ActionParser actionParser = new ActionParser(allActions);
1004
1005        Collection<ActionDefinition> result = new ArrayList<>();
1006
1007        for (String s : getToolString()) {
1008            if ("|".equals(s)) {
1009                result.add(ActionDefinition.getSeparator());
1010            } else {
1011                ActionDefinition a = actionParser.loadAction(s);
1012                if (a != null) {
1013                    result.add(a);
1014                } else {
1015                    Main.info("Could not load tool definition "+s);
1016                }
1017            }
1018        }
1019
1020        return result;
1021    }
1022
1023    /**
1024     * @param action Action to register
1025     * @return The parameter (for better chaining)
1026     */
1027    public Action register(Action action) {
1028        String toolbar = (String) action.getValue("toolbar");
1029        if (toolbar == null) {
1030            Main.info(tr("Registered toolbar action without name: {0}",
1031            action.getClass().getName()));
1032        } else {
1033            Action r = regactions.get(toolbar);
1034            if (r != null) {
1035                Main.info(tr("Registered toolbar action {0} overwritten: {1} gets {2}",
1036                toolbar, r.getClass().getName(), action.getClass().getName()));
1037            }
1038        }
1039        if (toolbar != null) {
1040            regactions.put(toolbar, action);
1041        }
1042        return action;
1043    }
1044
1045    /**
1046     * Parse the toolbar preference setting and construct the toolbar GUI control.
1047     *
1048     * Call this, if anything has changed in the toolbar settings and you want to refresh
1049     * the toolbar content (e.g. after registering actions in a plugin)
1050     */
1051    public void refreshToolbarControl() {
1052        control.removeAll();
1053        buttonActions.clear();
1054        boolean unregisterTab = Shortcut.findShortcut(KeyEvent.VK_TAB, 0) != null;
1055
1056        for (ActionDefinition action : getDefinedActions()) {
1057            if (action.isSeparator()) {
1058                control.addSeparator();
1059            } else {
1060                final JButton b = addButtonAndShortcut(action);
1061                buttonActions.put(b, action);
1062
1063                Icon i = action.getDisplayIcon();
1064                if (i != null) {
1065                    b.setIcon(i);
1066                    Dimension s = b.getPreferredSize();
1067                    /* make squared toolbar icons */
1068                    if (s.width < s.height) {
1069                        s.width = s.height;
1070                        b.setMinimumSize(s);
1071                        b.setMaximumSize(s);
1072                        //b.setSize(s);
1073                    } else if (s.height < s.width) {
1074                        s.height = s.width;
1075                        b.setMinimumSize(s);
1076                        b.setMaximumSize(s);
1077                    }
1078                } else {
1079                    // hide action text if an icon is set later (necessary for delayed/background image loading)
1080                    action.getParametrizedAction().addPropertyChangeListener(new PropertyChangeListener() {
1081
1082                        @Override
1083                        public void propertyChange(PropertyChangeEvent evt) {
1084                            if (Action.SMALL_ICON.equals(evt.getPropertyName())) {
1085                                b.setHideActionText(evt.getNewValue() != null);
1086                            }
1087                        }
1088                    });
1089                }
1090                b.setInheritsPopupMenu(true);
1091                b.setFocusTraversalKeysEnabled(!unregisterTab);
1092            }
1093        }
1094
1095        boolean visible = Main.pref.getBoolean("toolbar.visible", true);
1096
1097        control.setFocusTraversalKeysEnabled(!unregisterTab);
1098        control.setVisible(visible && control.getComponentCount() != 0);
1099        control.repaint();
1100    }
1101
1102    /**
1103     * The method to add custom button on toolbar like search or preset buttons
1104     * @param definitionText toolbar definition text to describe the new button,
1105     * must be carefully generated by using {@link ActionParser}
1106     * @param preferredIndex place to put the new button, give -1 for the end of toolbar
1107     * @param removeIfExists if true and the button already exists, remove it
1108     */
1109    public void addCustomButton(String definitionText, int preferredIndex, boolean removeIfExists) {
1110        List<String> t = new LinkedList<>(getToolString());
1111        if (t.contains(definitionText)) {
1112            if (!removeIfExists) return; // do nothing
1113            t.remove(definitionText);
1114        } else {
1115            if (preferredIndex >= 0 && preferredIndex < t.size()) {
1116                t.add(preferredIndex, definitionText); // add to specified place
1117            } else {
1118                t.add(definitionText); // add to the end
1119            }
1120        }
1121        Main.pref.putCollection("toolbar", t);
1122        Main.toolbar.refreshToolbarControl();
1123    }
1124
1125    private JButton addButtonAndShortcut(ActionDefinition action) {
1126        Action act = action.getParametrizedAction();
1127        JButton b = control.add(act);
1128
1129        Shortcut sc = null;
1130        if (action.getAction() instanceof JosmAction) {
1131            sc = ((JosmAction) action.getAction()).getShortcut();
1132            if (sc.getAssignedKey() == KeyEvent.CHAR_UNDEFINED) {
1133                sc = null;
1134        }
1135        }
1136
1137        long paramCode = 0;
1138        if (action.hasParameters()) {
1139            paramCode =  action.parameters.hashCode();
1140        }
1141
1142        String tt = action.getDisplayTooltip();
1143        if (tt == null) {
1144            tt = "";
1145        }
1146
1147        if (sc == null || paramCode != 0) {
1148            String name = (String) action.getAction().getValue("toolbar");
1149            if (name == null) {
1150                name = action.getDisplayName();
1151            }
1152            if (paramCode != 0) {
1153                name = name+paramCode;
1154            }
1155            String desc = action.getDisplayName() + ((paramCode == 0) ? "" : action.parameters.toString());
1156            sc = Shortcut.registerShortcut("toolbar:"+name, tr("Toolbar: {0}", desc),
1157                KeyEvent.CHAR_UNDEFINED, Shortcut.NONE);
1158            Main.unregisterShortcut(sc);
1159            Main.registerActionShortcut(act, sc);
1160
1161            // add shortcut info to the tooltip if needed
1162            if (sc.isAssignedUser()) {
1163                if (tt.startsWith("<html>") && tt.endsWith("</html>")) {
1164                    tt = tt.substring(6, tt.length()-6);
1165                }
1166                tt = Main.platform.makeTooltip(tt, sc);
1167            }
1168        }
1169
1170        if (!tt.isEmpty()) {
1171            b.setToolTipText(tt);
1172        }
1173        return b;
1174    }
1175
1176    private static final DataFlavor ACTION_FLAVOR = new DataFlavor(ActionDefinition.class, "ActionItem");
1177}