001: package abbot.editor.widgets;
002:
003: import java.awt.*;
004: import java.awt.event.KeyEvent;
005: import java.lang.reflect.Method;
006: import javax.swing.*;
007:
008: import abbot.Log;
009: import abbot.Platform;
010: import abbot.tester.KeyStrokeMap;
011: import abbot.util.AWT;
012: import abbot.i18n.Strings;
013:
014: /** Provide access to mnemonics appropriate for the current platform and
015: * locale. Encapsulates displayed text, a KeyEvent.VK_ constant, and a
016: * displayed mnemonic index. All instances are obtained through the factory
017: * method, {@link #getMnemonic(String)}.
018: * @see java.awt.event.KeyEvent
019: * @see javax.swing.AbstractButton#setMnemonic(int)
020: * @see javax.swing.AbstractButton#setDisplayedMnemonicIndex(int)
021: * @see javax.swing.JLabel#setDisplayedMnemonic(int)
022: * @see javax.swing.JLabel#setDisplayedMnemonicIndex(int)
023: * @see javax.swing.Action#MNEMONIC_KEY
024: */
025: public class Mnemonic {
026:
027: /** The unencoded text. For example "&File" results in "File". */
028: public String text;
029: /** The keycode to use as an argument to
030: {@link AbstractButton#setMnemonic(int)}. Returns
031: KeyEvent.VK_UNDEFINED if no mnemonic was found.
032: */
033: public int keycode;
034: /** The index to use as an argument to
035: {@link AbstractButton#setDisplayedMnemonicIndex(int)}. Returns
036: -1 if the default value should be used.
037: */
038: public int index;
039:
040: private Mnemonic(String text, int keycode, int index) {
041: this .text = text;
042: this .keycode = keycode;
043: this .index = index;
044: }
045:
046: /** Set the displayed mnemonic index, if doing so is supported. */
047: public static void setDisplayedMnemonicIndex(Component c, int index) {
048: if (index == -1)
049: return;
050: try {
051: Method m = c.getClass().getMethod(
052: "setDisplayedMnemonicIndex",
053: new Class[] { int.class });
054: m.invoke(c, new Object[] { new Integer(index) });
055: } catch (Exception e) {
056: // ignore errors
057: }
058: }
059:
060: public String toString() {
061: return "Mnemonic text=" + text + ", keycode="
062: + AWT.getKeyCode(keycode)
063: + (index != -1 ? ("displayed index=" + index) : "");
064: }
065:
066: /** Apply this mnemonic to the given AbstractButton. */
067: public void setMnemonic(AbstractButton button) {
068: button.setText(text);
069: button.setMnemonic(keycode);
070: setDisplayedMnemonicIndex(button, index);
071: }
072:
073: /** Apply this mnemonic to the given JLabel. */
074: public void setMnemonic(JLabel label) {
075: label.setText(text);
076: label.setDisplayedMnemonic(keycode);
077: setDisplayedMnemonicIndex(label, index);
078: }
079:
080: /** Apply this mnemonic to the given JLabel. */
081: public void setMnemonic(JTabbedPane tabbedPane, int tabIndex) {
082: tabbedPane.setTitleAt(tabIndex, text);
083: // NOTE: 1.4-only
084: try {
085: Method m = JTabbedPane.class.getMethod("setMnemonicAt",
086: new Class[] { int.class, int.class, });
087: m.invoke(tabbedPane, new Object[] { new Integer(tabIndex),
088: new Integer(keycode) });
089: m = JTabbedPane.class.getMethod(
090: "setDisplayedMnemonicIndexAt", new Class[] {
091: int.class, int.class, });
092: if (index != -1)
093: m.invoke(tabbedPane, new Object[] {
094: new Integer(tabIndex), new Integer(index) });
095: } catch (Exception e) {
096: // ignore errors
097: }
098: }
099:
100: /** Apply this mnemonic to the given Action. */
101: public void setMnemonic(Action action) {
102: action.putValue(Action.NAME, text);
103: if (keycode != KeyEvent.VK_UNDEFINED)
104: action.putValue(Action.MNEMONIC_KEY, new Integer(keycode));
105: // Don't think buttons listen for mnemonic index changes anyway...
106: //if (index != -1)
107: //action.putValue(Action.MNEMONIC_INDEX, new Integer(index));
108: }
109:
110: /** Return whether the character is disallowed as a mnemonic. */
111: private static boolean isDisallowed(char ch) {
112: return Character.isWhitespace(ch) || ch == '\'' || ch == '"';
113: }
114:
115: /** Return the appropriate mnemonic for the given character. */
116: private static int getMnemonicMapping(char ch) {
117: if (isDisallowed(ch))
118: return KeyEvent.VK_UNDEFINED;
119:
120: if (ch >= 'A' && ch <= 'Z')
121: return KeyEvent.VK_A + ch - 'A';
122: if (ch >= 'a' && ch <= 'z')
123: return KeyEvent.VK_A + ch - 'a';
124: if (ch >= '0' && ch <= '9')
125: return KeyEvent.VK_0 + ch - '0';
126:
127: // See if there's been a mapping defined; usage is similar to NetBeans
128: // handling, except that raw integers are not allowed (use the VK_
129: // constant name instead).
130: String str = Strings.get("MNEMONIC_" + ch, true);
131: if (str != null) {
132: try {
133: return AWT.getKeyCode("VK_" + str.toUpperCase());
134: } catch (IllegalArgumentException e) {
135: Log.warn("'" + str + "' is not a valid mnemonic "
136: + "(use a VK_ constant from KeyEvent)");
137: }
138: }
139:
140: // Make a guess based on keymaps
141: KeyStroke keystroke = KeyStrokeMap.getKeyStroke(ch);
142: if (keystroke != null) {
143: return keystroke.getKeyCode();
144: }
145:
146: return KeyEvent.VK_UNDEFINED;
147: }
148:
149: /** Create a Mnemonic instance with the mnemonic information from the
150: given encoded String. Unencoded text, the mnemonic keycode, and
151: the display index are encapsulated in the returned Mnemonic object.
152: Encoding consists of placing an ampersand (&) prior to the character
153: designated as the mnemonic.
154: <p>
155: Mnemonics may be encoded as follows:
156: <table border=1>
157: <tr><td><i>Original Text</i></td><td><i>Visible Text</i></td><td><i>Mnemonic</i></td></tr>
158: <tr><td>&File</td><td><u>F</u>ile</td><td><b>VK_F</b></td></tr>
159: <tr><td>Save &As...</td><td>Save <u>A</u>s...</td><td><b>VK_A</b> second instance</td></tr>
160: <tr><td>Me&&&You</td><td>Me&<u>Y</u>ou</td><td><b>VK_Y</b> ambiguous ampersands
161: must be escaped</td></tr>
162: <tr><td>Sugar & Spice</td><td>Sugar & Spice</td><td><b>None</b> ampersand is unambiguous,
163: whitespace is not allowed as a mnemonic</td></tr>
164: </table>
165: <p>
166: Swing restricts mnemonics to available KeyEvent VK_ constants, so
167: there must exist a mapping between text characters and said
168: constants. If the obvious mappings (A-Z, a-z, 0-9) don't hold, lookup
169: falls back to other methods. Whitespace, quotes, and ampersand are
170: disallowed as mnemonics.
171: <p>
172: Mappings from arbitrary characters to mnemonic keys may be defined
173: by providing a property
174: MNEMONIC_{unicode char}={KeyEvent.VK_ constant name} within a bundle
175: accessible by <code>abbot.i18n.Strings</code>. If no such
176: mapping is defined, a VK_ code is guessed by checking if there can be
177: found a keystroke mapping for the original character.
178: <p>
179: @see abbot.tester.KeyStrokeMap
180: @see abbot.i18n.Strings
181: */
182: public static Mnemonic getMnemonic(String input) {
183: String text = input;
184: int mnemonicIndex = -1;
185: int keycode = KeyEvent.VK_UNDEFINED;
186: int amp = text.indexOf("&");
187: int displayIndex = -1;
188: while (amp != -1 && amp < text.length() - 1) {
189: char ch = text.charAt(amp + 1);
190: if (ch == '&') {
191: text = text.substring(0, amp) + text.substring(amp + 1);
192: amp = text.indexOf("&", amp + 1);
193: } else {
194: int code = getMnemonicMapping(ch);
195: if (code == KeyEvent.VK_UNDEFINED) {
196: amp = text.indexOf("&", amp + 2);
197: } else {
198: // Only use the first mapping
199: if (mnemonicIndex == -1) {
200: text = text.substring(0, amp)
201: + text.substring(amp + 1);
202: displayIndex = mnemonicIndex = amp;
203: keycode = code;
204: }
205: amp = text.indexOf("&", amp + 1);
206: }
207: }
208: }
209: // Mnemonics are not used on OSX
210: if (!useMnemonics()) {
211: keycode = KeyEvent.VK_UNDEFINED;
212: displayIndex = -1;
213: }
214: Mnemonic m = new Mnemonic(text, keycode, displayIndex);
215: Log.debug(input + "->" + m);
216: return m;
217: }
218:
219: public static boolean useMnemonics() {
220: return !Platform.isOSX();
221: }
222: }
|