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