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