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

Last change on this file since 17227 was 17227, checked in by simon04, 4 years ago

see #7548 - Re-organize the preference dialog (remove "settings" from tab names)

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