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

Last change on this file was 19106, checked in by taylor.smock, 2 years ago

Cleanup some new PMD warnings from PMD 7.x (followup of r19101)

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