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