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

Last change on this file was 19050, checked in by taylor.smock, 43 hours 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.