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

Last change on this file since 11657 was 11657, checked in by Don-vip, 7 years ago

fix #14451 - Local imagery toolbar buttons disappear on restart

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