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