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