Swing's mnemonic system is based around two properties:mnemonic (or displayedMnemonic) anddisplayedMnemonicIndex. They're powerful enough to do everything you need, but then again, so is machine code. There are a number of problems and limitations with the skeletal mnemonic support built in to Swing.
- They're annoying to define - One common approach is to use a properties file to define actions. You generally end up with a property for the action's name, a property for its mnemonic, and an (optional) property for its mnemonic index -- three properties for the name of each action.
- It's easy to accidentally duplicate mnemonics - if you add new actions, or build a new menu from existing actions, it's incredibly easy to forget that (say) both "Lock" and "Layout" are using 'L' as their mnemonic character
- It's difficult to spot mistakes - the only visual cue to a mnemonic is an underline. Using Windows XP with default settings, the underline doesn't even show up unless you hold the Alt key. Mistakes are easily overlooked, and there is no error reporting.
- The same action may need different mnemonics in different contexts - If an action appears (say) in both a menubar menu and a right-click context menu, a mnemonic character which works in one menu may conflict in the second menu. I once ran into a menu item with a short name which, in a particular context menu, conflicted with other menu items on every single character in its name. With the action defined by static properties, though, you have to use the same mnemonic everywhere... so what do you do? Not have a mnemonic anywhere just because it conflicts in one rarely accessed location?
- It poses serious problems for internationalization - A quick survey of the bug database turned up a number of bugs relating to incorrect translations of mnemonics and mnemonic indices: 6411189,4983399,4664265,4735183. There are two general problems: first, all of these properties are interrelated, and it's easy to miss one of them during translation -- leading to incorrect mnemonics or (worse yet) a mismatch between the actual mnemonic and the underlined character. Second, translation is quite complicated because the translator has to check that the mnemonic doesn't conflict with any other mnemonics in every menu in which the action appears. If the folks at Sun are making these kinds of mistakes, what hope is there for the rest of us?
The SolutionThe solution I propose is twofold. First, combine all three properties into a single value. Second, eliminate the need to specify mnemonics at all in most cases. Combining all three properties is quite easy. You just need to embed the mnemonic and mnemonicIndex information into the label itself. Before:
action.saveAs.name = Save As... action.saveAs.mnemonic = A action.saveAs.mnemonicIndex = 5After:
action.saveAs.name = Save _A_s...I chose to use a pair of underscores surrounding the mnemonic character to identify it, as opposed to most toolkits which use a single escape character. Windows, for example, would have specified the label as "Save &As...". The advantage of the underscore encoding is that there is essentially no chance of ever "accidentally" specifying a mnemonic character, whereas you might legitimately try to use an ampersand in a control, which in Windows would lead to an incorrect mnemonic. Being able to specify the mnemonic character in this fashion carries a lot of benefits. Because it's a single value, specifying it in properties files is much easier, internationalization is much easier, and it's impossible to accidentally have a mismatch between the index and the mnemonic character. While this is much easier to deal with than three separate properties, it doesn't solve some of the other thorny problems, like trying to prevent duplicate mnemonics. The rest of the solution is automatically assigning mnemonics. This is easier than it sounds, because we tend to follow certain heuristics when assigning mnemonics to actions and labels, and we can just turn those heuristics into a program. The algorithm I use is as follows:
- Try the first character in the string. Check to see if it is available (among other menu items in the menu or components in the window, as appropriate).
- If that isn't available, try capitalized letters in the order in which they appear.
- If no capitalized letters are available, try lowercase consonants.
- If no lowercase consonants are available, try lowercase vowels.
- If no lowercase vowels are available, give up.