source: josm/trunk/src/org/openstreetmap/josm/gui/preferences/ToolbarPreferences.java @ 12841

Last change on this file since 12841 was 12841, checked in by bastiK, 5 weeks ago

see #15229 - fix deprecations caused by [12840]

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