// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.gui.preferences; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.LayoutManager; import java.awt.Rectangle; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.Action; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.ListCellRenderer; import javax.swing.MenuElement; import javax.swing.TransferHandler; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.ActionParameter; import org.openstreetmap.josm.actions.ParameterizedAction; import org.openstreetmap.josm.actions.ParameterizedActionDecorator; import org.openstreetmap.josm.gui.tagging.TaggingPreset; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.ImageProvider; public class ToolbarPreferences implements PreferenceSettingFactory { public static class ActionDefinition { public static ActionDefinition SEPARATOR = new ActionDefinition(null); private final Action action; private final Map<String, Object> parameters = new HashMap<String, Object>(); public ActionDefinition(Action action) { this.action = action; } public Map<String, Object> getParameters() { return parameters; } public Action getParametrizedAction() { if (getAction() instanceof ParameterizedAction) return new ParameterizedActionDecorator((ParameterizedAction) getAction(), parameters); else return getAction(); } public Action getAction() { return action; } } public static class ActionParser { private final Map<String, Action> actions; private final StringBuilder result = new StringBuilder(); private int index; private char[] s; public ActionParser(Map<String, Action> actions) { this.actions = actions; } private String readTillChar(char ch1, char ch2) { result.setLength(0); while (index < s.length && s[index] != ch1 && s[index] != ch2) { if (s[index] == '\\') { index++; if (index >= s.length) { break; } } result.append(s[index]); index++; } return result.toString(); } private void skip(char ch) { if (index < s.length && s[index] == ch) { index++; } } public ActionDefinition loadAction(String actionName) { index = 0; this.s = actionName.toCharArray(); String name = readTillChar('(', '('); Action action = actions.get(name); if (action == null) return null; ActionDefinition result = new ActionDefinition(action); if (action instanceof ParameterizedAction) { skip('('); ParameterizedAction parametrizedAction = (ParameterizedAction)action; Map<String, ActionParameter<?>> actionParams = new HashMap<String, ActionParameter<?>>(); for (ActionParameter<?> param: parametrizedAction.getActionParameters()) { actionParams.put(param.getName(), param); } do { String paramName = readTillChar('=', '='); skip('='); String paramValue = readTillChar(',',')'); if (paramName.length() > 0) { ActionParameter<?> actionParam = actionParams.get(paramName); if (actionParam != null) { result.getParameters().put(paramName, actionParam.readFromString(paramValue)); } } skip(','); } while (index < s.length && s[index] != ')'); } return result; } private void escape(String s) { for (int i=0; i<s.length(); i++) { char ch = s.charAt(i); if (ch == '\\' || ch == '(' || ch == ',' || ch == ')' || ch == '=') { result.append('\\'); result.append(ch); } else { result.append(ch); } } } @SuppressWarnings("unchecked") public String saveAction(ActionDefinition action) { result.setLength(0); escape((String) action.getAction().getValue("toolbar")); if (action.getAction() instanceof ParameterizedAction) { result.append('('); List<ActionParameter<?>> params = ((ParameterizedAction)action.getAction()).getActionParameters(); for (int i=0; i<params.size(); i++) { ActionParameter<Object> param = (ActionParameter<Object>)params.get(i); escape(param.getName()); result.append('='); Object value = action.getParameters().get(param.getName()); if (value != null) { escape(param.writeToString(value)); } if (i < params.size() - 1) { result.append(','); } else { result.append(')'); } } } return result.toString(); } } private static class ActionParametersTableModel extends AbstractTableModel { private ActionDefinition currentAction = ActionDefinition.SEPARATOR; public int getColumnCount() { return 2; } public int getRowCount() { if (currentAction == ActionDefinition.SEPARATOR || !(currentAction.getAction() instanceof ParameterizedAction)) return 0; ParameterizedAction pa = (ParameterizedAction)currentAction.getAction(); return pa.getActionParameters().size(); } @SuppressWarnings("unchecked") private ActionParameter<Object> getParam(int index) { ParameterizedAction pa = (ParameterizedAction)currentAction.getAction(); return (ActionParameter<Object>) pa.getActionParameters().get(index); } public Object getValueAt(int rowIndex, int columnIndex) { ActionParameter<Object> param = getParam(rowIndex); switch (columnIndex) { case 0: return param.getName(); case 1: return param.writeToString(currentAction.getParameters().get(param.getName())); default: return null; } } @Override public boolean isCellEditable(int row, int column) { return column == 1; } @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) { ActionParameter<Object> param = getParam(rowIndex); currentAction.getParameters().put(param.getName(), param.readFromString((String)aValue)); } public void setCurrentAction(ActionDefinition currentAction) { this.currentAction = currentAction; fireTableDataChanged(); } } /** * Key: Registered name (property "toolbar" of action). * Value: The action to execute. */ private Map<String, Action> actions = new HashMap<String, Action>(); private Map<String, Action> regactions = new HashMap<String, Action>(); private DefaultMutableTreeNode rootActionsNode = new DefaultMutableTreeNode(tr("Actions")); public JToolBar control = new JToolBar(); public PreferenceSetting createPreferenceSetting() { return new Settings(rootActionsNode); } public class Settings implements PreferenceSetting { private final class Move implements ActionListener { public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("<") && actionsTree.getSelectionCount() > 0) { int leadItem = selected.getSize(); if (selectedList.getSelectedIndex() != -1) { int[] indices = selectedList.getSelectedIndices(); leadItem = indices[indices.length - 1]; } for (TreePath selectedAction : actionsTree.getSelectionPaths()) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectedAction.getLastPathComponent(); if (node.getUserObject() == null) { selected.add(leadItem++, ActionDefinition.SEPARATOR); } else if (node.getUserObject() instanceof Action) { selected.add(leadItem++, new ActionDefinition((Action)node.getUserObject())); } } } else if (e.getActionCommand().equals(">") && selectedList.getSelectedIndex() != -1) { while (selectedList.getSelectedIndex() != -1) { selected.remove(selectedList.getSelectedIndex()); } } else if (e.getActionCommand().equals("up")) { int i = selectedList.getSelectedIndex(); Object o = selected.get(i); if (i != 0) { selected.remove(i); selected.add(i-1, o); selectedList.setSelectedIndex(i-1); } } else if (e.getActionCommand().equals("down")) { int i = selectedList.getSelectedIndex(); Object o = selected.get(i); if (i != selected.size()-1) { selected.remove(i); selected.add(i+1, o); selectedList.setSelectedIndex(i+1); } } } } private class ActionTransferable implements Transferable { private DataFlavor[] flavors = new DataFlavor[] { ACTION_FLAVOR }; private final List<ActionDefinition> actions; public ActionTransferable(List<ActionDefinition> actions) { this.actions = actions; } public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { return actions; } public DataFlavor[] getTransferDataFlavors() { return flavors; } public boolean isDataFlavorSupported(DataFlavor flavor) { return flavors[0] == flavor; } } private final Move moveAction = new Move(); private final DefaultListModel selected = new DefaultListModel(); private final JList selectedList = new JList(selected); private final DefaultTreeModel actionsTreeModel; private final JTree actionsTree; private final ActionParametersTableModel actionParametersModel = new ActionParametersTableModel(); private final JTable actionParametersTable = new JTable(actionParametersModel); private JPanel actionParametersPanel; private JButton upButton; private JButton downButton; private String movingComponent; public Settings(DefaultMutableTreeNode rootActionsNode) { actionsTreeModel = new DefaultTreeModel(rootActionsNode); actionsTree = new JTree(actionsTreeModel); } private JButton createButton(String name) { JButton b = new JButton(); if (name.equals("up")) { b.setIcon(ImageProvider.get("dialogs", "up")); } else if (name.equals("down")) { b.setIcon(ImageProvider.get("dialogs", "down")); } else { b.setText(name); } b.addActionListener(moveAction); b.setActionCommand(name); return b; } public void addGui(PreferenceTabbedPane gui) { actionsTree.setCellRenderer(new DefaultTreeCellRenderer() { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; JLabel comp = (JLabel) super.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus); if (node.getUserObject() == null) { comp.setText(tr("Separator")); comp.setIcon(ImageProvider.get("preferences/separator")); } else if (node.getUserObject() instanceof Action) { Action action = (Action) node.getUserObject(); comp.setText((String) action.getValue(Action.NAME)); comp.setIcon((Icon) action.getValue(Action.SMALL_ICON)); } return comp; } }); ListCellRenderer renderer = new DefaultListCellRenderer(){ @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { String s; Icon i; if (value != ActionDefinition.SEPARATOR) { ActionDefinition action = (ActionDefinition)value; s = (String) action.getAction().getValue(Action.NAME); i = (Icon) action.getAction().getValue(Action.SMALL_ICON); } else { i = ImageProvider.get("preferences/separator"); s = tr("Separator"); } JLabel l = (JLabel)super.getListCellRendererComponent(list, s, index, isSelected, cellHasFocus); l.setIcon(i); return l; } }; selectedList.setCellRenderer(renderer); selectedList.addListSelectionListener(new ListSelectionListener(){ public void valueChanged(ListSelectionEvent e) { boolean sel = selectedList.getSelectedIndex() != -1; if (sel) { actionsTree.clearSelection(); ActionDefinition action = (ActionDefinition) selected.get(selectedList.getSelectedIndex()); actionParametersModel.setCurrentAction(action); actionParametersPanel.setVisible(actionParametersModel.getRowCount() > 0); } upButton.setEnabled(sel); downButton.setEnabled(sel); } }); selectedList.setDragEnabled(true); selectedList.setTransferHandler(new TransferHandler() { @Override protected Transferable createTransferable(JComponent c) { List<ActionDefinition> actions = new ArrayList<ActionDefinition>(); for (Object o: ((JList)c).getSelectedValues()) { actions.add((ActionDefinition)o); } return new ActionTransferable(actions); } @Override public int getSourceActions(JComponent c) { return TransferHandler.MOVE; } @Override public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { for (DataFlavor f : transferFlavors) { if (ACTION_FLAVOR.equals(f)) return true; } return false; } @Override public void exportAsDrag(JComponent comp, InputEvent e, int action) { super.exportAsDrag(comp, e, action); movingComponent = "list"; } @Override public boolean importData(JComponent comp, Transferable t) { try { int dropIndex = selectedList.locationToIndex(selectedList.getMousePosition(true)); List<?> draggedData = (List<?>) t.getTransferData(ACTION_FLAVOR); Object leadItem = dropIndex >= 0 ? selected.elementAt(dropIndex) : null; int dataLength = draggedData.size(); if (leadItem != null) { for (Object o: draggedData) { if (leadItem.equals(o)) return false; } } int dragLeadIndex = -1; boolean localDrop = "list".equals(movingComponent); if (localDrop) { dragLeadIndex = selected.indexOf(draggedData.get(0)); for (Object o: draggedData) { selected.removeElement(o); } } int[] indices = new int[dataLength]; if (localDrop) { int adjustedLeadIndex = selected.indexOf(leadItem); int insertionAdjustment = dragLeadIndex <= adjustedLeadIndex ? 1 : 0; for (int i = 0; i < dataLength; i++) { selected.insertElementAt(draggedData.get(i), adjustedLeadIndex + insertionAdjustment + i); indices[i] = adjustedLeadIndex + insertionAdjustment + i; } } else { for (int i = 0; i < dataLength; i++) { selected.add(dropIndex, draggedData.get(i)); indices[i] = dropIndex + i; } } selectedList.clearSelection(); selectedList.setSelectedIndices(indices); movingComponent = ""; return true; } catch (Exception e) { e.printStackTrace(); } return false; } @Override protected void exportDone(JComponent source, Transferable data, int action) { if (movingComponent.equals("list")) { try { List<?> draggedData = (List<?>) data.getTransferData(ACTION_FLAVOR); boolean localDrop = selected.contains(draggedData.get(0)); if (localDrop) { int[] indices = selectedList.getSelectedIndices(); Arrays.sort(indices); for (int i = indices.length - 1; i >= 0; i--) { selected.remove(indices[i]); } } } catch (Exception e) { e.printStackTrace(); } movingComponent = ""; } } }); actionsTree.setTransferHandler(new TransferHandler() { private static final long serialVersionUID = 1L; @Override public int getSourceActions( JComponent c ){ return TransferHandler.MOVE; } @Override protected void exportDone(JComponent source, Transferable data, int action) { } @Override protected Transferable createTransferable(JComponent c) { TreePath[] paths = actionsTree.getSelectionPaths(); List<ActionDefinition> dragActions = new ArrayList<ActionDefinition>(); for (TreePath path : paths) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); Object obj = node.getUserObject(); if (obj == null) { dragActions.add(ActionDefinition.SEPARATOR); } else if (obj instanceof Action) { dragActions.add(new ActionDefinition((Action) obj)); } } return new ActionTransferable(dragActions); } }); actionsTree.setDragEnabled(true); final JPanel left = new JPanel(new GridBagLayout()); left.add(new JLabel(tr("Toolbar")), GBC.eol()); left.add(new JScrollPane(selectedList), GBC.std().fill(GBC.BOTH)); final JPanel right = new JPanel(new GridBagLayout()); right.add(new JLabel(tr("Available")), GBC.eol()); right.add(new JScrollPane(actionsTree), GBC.eol().fill(GBC.BOTH)); final JPanel buttons = new JPanel(new GridLayout(6,1)); buttons.add(upButton = createButton("up")); buttons.add(createButton("<")); buttons.add(createButton(">")); buttons.add(downButton = createButton("down")); upButton.setEnabled(false); downButton.setEnabled(false); final JPanel p = new JPanel(); p.setLayout(new LayoutManager(){ public void addLayoutComponent(String name, Component comp) {} public void removeLayoutComponent(Component comp) {} public Dimension minimumLayoutSize(Container parent) { Dimension l = left.getMinimumSize(); Dimension r = right.getMinimumSize(); Dimension b = buttons.getMinimumSize(); return new Dimension(l.width+b.width+10+r.width,l.height+b.height+10+r.height); } public Dimension preferredLayoutSize(Container parent) { Dimension l = new Dimension(200, 200); //left.getPreferredSize(); Dimension r = new Dimension(200, 200); //right.getPreferredSize(); return new Dimension(l.width+r.width+10+buttons.getPreferredSize().width,Math.max(l.height, r.height)); } public void layoutContainer(Container parent) { Dimension d = p.getSize(); Dimension b = buttons.getPreferredSize(); int width = (d.width-10-b.width)/2; left.setBounds(new Rectangle(0,0,width,d.height)); right.setBounds(new Rectangle(width+10+b.width,0,width,d.height)); buttons.setBounds(new Rectangle(width+5, d.height/2-b.height/2, b.width, b.height)); } }); p.add(left); p.add(buttons); p.add(right); actionParametersPanel = new JPanel(new GridBagLayout()); actionParametersPanel.add(new JLabel(tr("Action parameters")), GBC.eol().insets(0, 10, 0, 20)); actionParametersTable.getColumnModel().getColumn(0).setHeaderValue(tr("Parameter name")); actionParametersTable.getColumnModel().getColumn(1).setHeaderValue(tr("Parameter value")); actionParametersPanel.add(actionParametersTable.getTableHeader(), GBC.eol().fill(GBC.HORIZONTAL)); actionParametersPanel.add(actionParametersTable, GBC.eol().fill(GBC.BOTH).insets(0, 0, 0, 10)); actionParametersPanel.setVisible(false); JPanel panel = gui.createPreferenceTab("toolbar", tr("Toolbar customization"), tr("Customize the elements on the toolbar."), false); panel.add(p, GBC.eol().fill(GBC.BOTH)); panel.add(actionParametersPanel, GBC.eol().fill(GBC.HORIZONTAL)); selected.removeAllElements(); for (ActionDefinition actionDefinition: getDefinedActions()) { selected.addElement(actionDefinition); } } public boolean ok() { Collection<String> t = new LinkedList<String>(); ActionParser parser = new ActionParser(null); for (int i = 0; i < selected.size(); ++i) { if (selected.get(i) == ActionDefinition.SEPARATOR) { t.add("|"); } else { t.add(parser.saveAction((ActionDefinition)(selected.get(i)))); } } Main.pref.putCollection("toolbar", t); Main.toolbar.refreshToolbarControl(); return false; } } public ToolbarPreferences() { control.setFloatable(false); } private void loadAction(DefaultMutableTreeNode node, MenuElement menu) { Object userObject = null; MenuElement menuElement = menu; if (menu.getSubElements().length > 0 && menu.getSubElements()[0] instanceof JPopupMenu) { menuElement = menu.getSubElements()[0]; } for (MenuElement item : menuElement.getSubElements()) { if (item instanceof JMenuItem) { JMenuItem menuItem = ((JMenuItem)item); if (menuItem.getAction() != null) { Action action = menuItem.getAction(); userObject = action; actions.put((String) action.getValue("toolbar"), action); } else { userObject = menuItem.getText(); } } DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(userObject); node.add(newNode); loadAction(newNode, item); } } public Action getAction(String s) { Action e = actions.get(s); if(e == null) { e = regactions.get(s); } return e; } private void loadActions() { rootActionsNode.removeAllChildren(); loadAction(rootActionsNode, Main.main.menu); for(Map.Entry<String, Action> a : regactions.entrySet()) { if(actions.get(a.getKey()) == null) { rootActionsNode.add(new DefaultMutableTreeNode(a.getValue())); } } rootActionsNode.add(new DefaultMutableTreeNode(null)); } private static final String[] deftoolbar = {"open", "save", "download", "upload", "|", "undo", "redo", "|", "dialogs/search", "preference", "|", "splitway", "combineway", "wayflip", "|", "tagginggroup_Highways/Streets", "tagginggroup_Highways/Ways", "tagginggroup_Highways/Waypoints", "tagginggroup_Highways/Barriers", "|", "tagginggroup_Transport/Car", "tagginggroup_Transport/Public Transport", "|", "tagginggroup_Travel/Tourism", "tagginggroup_Travel/Food+Drinks", "|", "tagginggroup_Travel/Historic Places", "|", "tagginggroup_Man-Made/Man Made"}; private static Collection<String> getToolString() { Collection<String> toolStr = Main.pref.getCollection("toolbar", Arrays.asList(deftoolbar)); if (toolStr == null || toolStr.size() == 0) { toolStr = Arrays.asList(deftoolbar); } return toolStr; } private Collection<ActionDefinition> getDefinedActions() { loadActions(); Map<String, Action> allActions = new HashMap<String, Action>(regactions); allActions.putAll(actions); ActionParser actionParser = new ActionParser(allActions); Collection<ActionDefinition> result = new ArrayList<ActionDefinition>(); for (String s : getToolString()) { if (s.equals("|")) { result.add(ActionDefinition.SEPARATOR); } else { ActionDefinition a = actionParser.loadAction(s); if(a != null) { result.add(a); } } } return result; } /** * @return The parameter (for better chaining) */ public Action register(Action action) { regactions.put((String) action.getValue("toolbar"), action); return action; } /** * Parse the toolbar preference setting and construct the toolbar GUI control. * * Call this, if anything has changed in the toolbar settings and you want to refresh * the toolbar content (e.g. after registering actions in a plugin) */ public void refreshToolbarControl() { control.removeAll(); for (ActionDefinition action : getDefinedActions()) { if (action == ActionDefinition.SEPARATOR) { control.addSeparator(); } else { Action a = action.getParametrizedAction(); JButton b = control.add(a); Object tt = a.getValue(TaggingPreset.OPTIONAL_TOOLTIP_TEXT); if (tt != null) { b.setToolTipText((String)tt); } } } control.setVisible(control.getComponentCount() != 0); } private static DataFlavor ACTION_FLAVOR = new DataFlavor( ActionDefinition.class, "ActionItem"); }