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