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