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