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