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