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