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

Last change on this file since 17667 was 17667, checked in by simon04, 3 years ago

fix #15397 - Toolbar: make ToggleAction buttons reflect toggle state

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