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