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

Last change on this file since 14206 was 14153, checked in by Don-vip, 6 years ago

see #15229 - deprecate Main.parent and Main itself

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