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