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