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

Last change on this file since 18871 was 18871, checked in by taylor.smock, 8 months ago

See #23218: Use newer error_prone versions when compiling on Java 11+

error_prone 2.11 dropped support for compiling with Java 8, although it still
supports compiling for Java 8. The "major" new check for us is NotJavadoc since
we used /** in quite a few places which were not javadoc.

Other "new" checks that are of interest:

  • AlreadyChecked: if (foo) { doFoo(); } else if (!foo) { doBar(); }
  • UnnecessaryStringBuilder: Avoid StringBuilder (Java converts + to StringBuilder behind-the-scenes, but may also do something else if it performs better)
  • NonApiType: Avoid specific interface types in function definitions
  • NamedLikeContextualKeyword: Avoid using restricted names for classes and methods
  • UnusedMethod: Unused private methods should be removed

This fixes most of the new error_prone issues and some SonarLint issues.

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