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

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

see #15229 - use Config.getPref() wherever possible

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