001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.widgets;
003
004import java.awt.event.FocusEvent;
005import java.awt.event.KeyEvent;
006import java.util.ArrayList;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Set;
010
011import javax.swing.Action;
012import javax.swing.JMenu;
013import javax.swing.JMenuItem;
014import javax.swing.KeyStroke;
015import javax.swing.text.Document;
016
017import org.openstreetmap.josm.actions.JosmAction;
018import org.openstreetmap.josm.gui.MainApplication;
019import org.openstreetmap.josm.tools.Pair;
020import org.openstreetmap.josm.tools.Shortcut;
021
022/**
023 * A JTextField that disabled all JOSM shortcuts composed of a single key without modifier (except F1 to F12),
024 * in order to avoid them to be triggered while typing.
025 * This allows to include text fields in toggle dialogs (needed for relation filter).
026 * @since 5696
027 */
028public class DisableShortcutsOnFocusGainedTextField extends JosmTextField {
029
030    /**
031     * Constructs a new <code>TextField</code>. A default model is created,
032     * the initial string is <code>null</code>, and the number of columns is set to 0.
033     */
034    public DisableShortcutsOnFocusGainedTextField() {
035        // Contents can be set with parent methods
036    }
037
038    /**
039     * Constructs a new <code>TextField</code> initialized with the
040     * specified text. A default model is created and the number of columns is 0.
041     *
042     * @param text the text to be displayed, or <code>null</code>
043     */
044    public DisableShortcutsOnFocusGainedTextField(String text) {
045        super(text);
046    }
047
048    /**
049     * Constructs a new empty <code>TextField</code> with the specified number of columns.
050     * A default model is created and the initial string is set to <code>null</code>.
051     *
052     * @param columns  the number of columns to use to calculate
053     *   the preferred width; if columns is set to zero, the
054     *   preferred width will be whatever naturally results from the component implementation
055     */
056    public DisableShortcutsOnFocusGainedTextField(int columns) {
057        super(columns);
058    }
059
060    /**
061     * Constructs a new <code>TextField</code> initialized with the
062     * specified text and columns.  A default model is created.
063     *
064     * @param text the text to be displayed, or <code>null</code>
065     * @param columns  the number of columns to use to calculate
066     *   the preferred width; if columns is set to zero, the
067     *   preferred width will be whatever naturally results from the component implementation
068     */
069    public DisableShortcutsOnFocusGainedTextField(String text, int columns) {
070        super(text, columns);
071    }
072
073    /**
074     * Constructs a new <code>JTextField</code> that uses the given text
075     * storage model and the given number of columns.
076     * This is the constructor through which the other constructors feed.
077     * If the document is <code>null</code>, a default model is created.
078     *
079     * @param doc  the text storage to use; if this is <code>null</code>,
080     *      a default will be provided by calling the
081     *      <code>createDefaultModel</code> method
082     * @param text  the initial string to display, or <code>null</code>
083     * @param columns  the number of columns to use to calculate
084     *   the preferred width &gt;= 0; if <code>columns</code>
085     *   is set to zero, the preferred width will be whatever
086     *   naturally results from the component implementation
087     * @throws IllegalArgumentException if <code>columns</code> &lt; 0
088     */
089    public DisableShortcutsOnFocusGainedTextField(Document doc, String text, int columns) {
090        super(doc, text, columns);
091    }
092
093    private final transient List<Pair<Action, Shortcut>> unregisteredActionShortcuts = new ArrayList<>();
094    private final Set<JosmAction> disabledMenuActions = new HashSet<>();
095
096    @Override
097    public void focusGained(FocusEvent e) {
098        super.focusGained(e);
099        disableMenuActions();
100        unregisterActionShortcuts();
101    }
102
103    @Override
104    public void focusLost(FocusEvent e) {
105        super.focusLost(e);
106        restoreActionShortcuts();
107        restoreMenuActions();
108    }
109
110    /**
111     * Disables all relevant menu actions.
112     * @see #hasToBeDisabled
113     */
114    protected void disableMenuActions() {
115        disabledMenuActions.clear();
116        for (int i = 0; i < MainApplication.getMenu().getMenuCount(); i++) {
117            JMenu menu = MainApplication.getMenu().getMenu(i);
118            if (menu != null) {
119                for (int j = 0; j < menu.getItemCount(); j++) {
120                    JMenuItem item = menu.getItem(j);
121                    if (item != null) {
122                        Action action = item.getAction();
123                        if (action instanceof JosmAction && action.isEnabled()) {
124                            Shortcut shortcut = ((JosmAction) action).getShortcut();
125                            if (shortcut != null) {
126                                KeyStroke ks = shortcut.getKeyStroke();
127                                if (hasToBeDisabled(ks)) {
128                                    action.setEnabled(false);
129                                    disabledMenuActions.add((JosmAction) action);
130                                }
131                            }
132                        }
133                    }
134                }
135            }
136        }
137    }
138
139    /**
140     * Unregisters all relevant action shortcuts.
141     * @see #hasToBeDisabled
142     */
143    protected void unregisterActionShortcuts() {
144        unregisteredActionShortcuts.clear();
145        // Unregister all actions without modifiers to avoid them to be triggered by typing in this text field
146        for (Shortcut shortcut : Shortcut.listAll()) {
147            KeyStroke ks = shortcut.getKeyStroke();
148            if (hasToBeDisabled(ks)) {
149                Action action = MainApplication.getRegisteredActionShortcut(shortcut);
150                if (action != null) {
151                    MainApplication.unregisterActionShortcut(action, shortcut);
152                    unregisteredActionShortcuts.add(new Pair<>(action, shortcut));
153                }
154            }
155        }
156    }
157
158    /**
159     * Returns true if the given shortcut has no modifier and is not an actions key.
160     * @param ks key stroke
161     * @return {@code true} if the given shortcut has to be disabled
162     * @see KeyEvent#isActionKey()
163     */
164    protected boolean hasToBeDisabled(KeyStroke ks) {
165        return ks != null && ks.getModifiers() == 0 && !new KeyEvent(
166                this, KeyEvent.KEY_PRESSED, 0, ks.getModifiers(), ks.getKeyCode(), ks.getKeyChar()).isActionKey();
167    }
168
169    /**
170     * Restore all actions previously disabled
171     */
172    protected void restoreMenuActions() {
173        for (JosmAction a : disabledMenuActions) {
174            a.setEnabled(true);
175        }
176        disabledMenuActions.clear();
177    }
178
179    /**
180     * Restore all action shortcuts previously unregistered
181     */
182    protected void restoreActionShortcuts() {
183        for (Pair<Action, Shortcut> p : unregisteredActionShortcuts) {
184            MainApplication.registerActionShortcut(p.a, p.b);
185        }
186        unregisteredActionShortcuts.clear();
187    }
188}