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