source: josm/trunk/src/org/openstreetmap/josm/gui/preferences/ToolbarPreferences.java@ 12841

Last change on this file since 12841 was 12841, checked in by bastiK, 7 years ago

see #15229 - fix deprecations caused by [12840]

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