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