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

Last change on this file since 18361 was 18361, checked in by GerdP, 2 years ago

fix #6725: Plugins need to be loaded before tool definitions for toolbar buttons

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