Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / platform-api / src / com / intellij / openapi / MnemonicWrapper.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi;
17
18 import com.intellij.ide.ui.UISettings;
19 import com.intellij.openapi.util.SystemInfo;
20 import com.intellij.openapi.util.registry.Registry;
21 import com.intellij.util.ui.UIUtil;
22
23 import javax.swing.*;
24 import java.awt.*;
25 import java.awt.event.InputEvent;
26 import java.awt.event.KeyEvent;
27 import java.beans.PropertyChangeEvent;
28 import java.beans.PropertyChangeListener;
29
30 /**
31  * @author Sergey.Malenkov
32  */
33 abstract class MnemonicWrapper<T extends Component> implements Runnable, PropertyChangeListener {
34   public static MnemonicWrapper getWrapper(Component component) {
35     if (component == null || component.getClass().getName().equals("com.intellij.openapi.wm.impl.StripeButton")) {
36       return null;
37     }
38     for (PropertyChangeListener listener : component.getPropertyChangeListeners()) {
39       if (listener instanceof MnemonicWrapper) {
40         MnemonicWrapper wrapper = (MnemonicWrapper)listener;
41         wrapper.run(); // update mnemonics immediately
42         return wrapper;
43       }
44     }
45     if (component instanceof JMenuItem) {
46       return null; // TODO: new MenuWrapper((JMenuItem)component);
47     }
48     if (component instanceof AbstractButton) {
49       return new ButtonWrapper((AbstractButton)component);
50     }
51     if (component instanceof JLabel) {
52       return new LabelWrapper((JLabel)component);
53     }
54     return null;
55   }
56
57   final T myComponent; // direct access from inner classes
58   private final String myTextProperty;
59   private final String myCodeProperty;
60   private final String myIndexProperty;
61   private int myCode;
62   private int myIndex;
63   private boolean myFocusable;
64   private boolean myEvent;
65   private Runnable myRunnable;
66
67   private MnemonicWrapper(T component, String text, String code, String index) {
68     myComponent = component;
69     myTextProperty = text;
70     myCodeProperty = code;
71     myIndexProperty = index;
72     if (!updateText()) {
73       // assume that it is already set
74       myCode = getMnemonicCode();
75       myIndex = getMnemonicIndex();
76     }
77     myFocusable = isFocusable();
78     myComponent.addPropertyChangeListener(this);
79     run(); // update mnemonics immediately
80   }
81
82   @Override
83   public final void run() {
84     boolean disabled = isDisabled();
85     try {
86       myEvent = true;
87       setMnemonicCode(disabled ? KeyEvent.VK_UNDEFINED : myCode);
88       setMnemonicIndex(disabled ? -1 : myIndex);
89       Component component = getFocusableComponent();
90       if (component != null) {
91         component.setFocusable(disabled || myFocusable);
92       }
93     }
94     finally {
95       myEvent = false;
96       myRunnable = null;
97     }
98   }
99
100   @Override
101   public final void propertyChange(PropertyChangeEvent event) {
102     if (!myEvent) {
103       String property = event.getPropertyName();
104       if (myTextProperty.equals(property)) {
105         if (updateText()) {
106           updateRequest();
107         }
108       }
109       else if (myCodeProperty.equals(property)) {
110         myCode = getMnemonicCode();
111         updateRequest();
112       }
113       else if (myIndexProperty.equals(property)) {
114         myIndex = getMnemonicIndex();
115         updateRequest();
116       }
117       else if ("focusable".equals(property) || "labelFor".equals(property)) {
118         myFocusable = isFocusable();
119         updateRequest();
120       }
121     }
122   }
123
124   private boolean updateText() {
125     String text = getText();
126     if (text != null) {
127       int code = KeyEvent.VK_UNDEFINED;
128       int index = -1;
129       int length = text.length();
130       StringBuilder sb = new StringBuilder(length);
131       for (int i = 0; i < length; i++) {
132         char ch = text.charAt(i);
133         if (ch != UIUtil.MNEMONIC) {
134           sb.append(ch);
135         }
136         else if (i + 1 < length) {
137           code = KeyEvent.getExtendedKeyCodeForChar((int)text.charAt(i + 1));
138           index = sb.length();
139         }
140       }
141       if (code != KeyEvent.VK_UNDEFINED) {
142         try {
143           myEvent = true;
144           setText(sb.toString());
145         }
146         finally {
147           myEvent = false;
148         }
149         myCode = code;
150         myIndex = index;
151         return true;
152       }
153     }
154     return false;
155   }
156
157   private void updateRequest() {
158     if (myRunnable == null) {
159       myRunnable = this; // run once
160       SwingUtilities.invokeLater(this);
161     }
162   }
163
164   private boolean isFocusable() {
165     Component component = getFocusableComponent();
166     return component == null || component.isFocusable();
167   }
168
169   Component getFocusableComponent() {
170     return myComponent;
171   }
172
173   boolean isDisabled() {
174     return UISettings.getShadowInstance().DISABLE_MNEMONICS_IN_CONTROLS;
175   }
176
177   abstract String getText();
178
179   abstract void setText(String text);
180
181   abstract int getMnemonicCode();
182
183   abstract void setMnemonicCode(int code);
184
185   abstract int getMnemonicIndex();
186
187   abstract void setMnemonicIndex(int index);
188
189   static KeyStroke fixMacKeyStroke(KeyStroke stroke, InputMap map, int code, boolean onKeyRelease, String action) {
190     if (stroke != null && code != stroke.getKeyCode()) {
191       map.remove(stroke);
192       stroke = null;
193     }
194     if (stroke == null && code != KeyEvent.VK_UNDEFINED) {
195       stroke = KeyStroke.getKeyStroke(code, InputEvent.ALT_MASK | InputEvent.ALT_DOWN_MASK, onKeyRelease);
196       map.put(stroke, action);
197     }
198     return stroke;
199   }
200
201   private static class MenuWrapper extends ButtonWrapper {
202     private MenuWrapper(AbstractButton component) {
203       super(component);
204     }
205
206     @Override
207     boolean isDisabled() {
208       return UISettings.getShadowInstance().DISABLE_MNEMONICS;
209     }
210   }
211
212   private static class ButtonWrapper extends MnemonicWrapper<AbstractButton> {
213     private KeyStroke myStrokePressed;
214     private KeyStroke myStrokeReleased;
215
216     private ButtonWrapper(AbstractButton component) {
217       super(component, "text", "mnemonic", "displayedMnemonicIndex");
218     }
219
220     @Override
221     String getText() {
222       return myComponent.getText();
223     }
224
225     @Override
226     void setText(String text) {
227       myComponent.setText(text);
228     }
229
230     @Override
231     int getMnemonicCode() {
232       return myComponent.getMnemonic();
233     }
234
235     @Override
236     void setMnemonicCode(int code) {
237       myComponent.setMnemonic(code);
238       if (SystemInfo.isMac && Registry.is("ide.mac.alt.mnemonic.without.ctrl")) {
239         InputMap map = myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
240         if (map != null) {
241           myStrokePressed = fixMacKeyStroke(myStrokePressed, map, code, false, "pressed");
242           myStrokeReleased = fixMacKeyStroke(myStrokeReleased, map, code, true, "released");
243         }
244       }
245     }
246
247     @Override
248     int getMnemonicIndex() {
249       return myComponent.getDisplayedMnemonicIndex();
250     }
251
252     @Override
253     void setMnemonicIndex(int index) {
254       myComponent.setDisplayedMnemonicIndex(index);
255     }
256   }
257
258   private static class LabelWrapper extends MnemonicWrapper<JLabel> {
259     private KeyStroke myStrokeRelease;
260
261     private LabelWrapper(JLabel component) {
262       super(component, "text", "displayedMnemonic", "displayedMnemonicIndex");
263     }
264
265     @Override
266     String getText() {
267       return myComponent.getText();
268     }
269
270     @Override
271     void setText(String text) {
272       myComponent.setText(text);
273     }
274
275     @Override
276     int getMnemonicCode() {
277       return myComponent.getDisplayedMnemonic();
278     }
279
280     @Override
281     void setMnemonicCode(int code) {
282       myComponent.setDisplayedMnemonic(code);
283       if (SystemInfo.isMac && Registry.is("ide.mac.alt.mnemonic.without.ctrl")) {
284         InputMap map = myComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
285         if (map != null) {
286           myStrokeRelease = fixMacKeyStroke(myStrokeRelease, map, code, true, "release");
287         }
288       }
289     }
290
291     @Override
292     int getMnemonicIndex() {
293       return myComponent.getDisplayedMnemonicIndex();
294     }
295
296     @Override
297     void setMnemonicIndex(int index) {
298       myComponent.setDisplayedMnemonicIndex(index);
299     }
300
301     @Override
302     Component getFocusableComponent() {
303       return myComponent.getLabelFor();
304     }
305   }
306 }