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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 4 weeks ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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