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