-
Notifications
You must be signed in to change notification settings - Fork 9
Technical Notes: Input Method
Apple's input method API is pretty robust and gives you fine-grained control over text entry behavior. You can customize exactly what the user sees, what gets underlined, and what happens to the transliteration when an action happens.
One major advantage of using an input method is that it adapts to your current keyboard layout! This blew my mind when I first discovered it. If you add and select, for example, the Dvorak keyboard and then switch to the transliterator input method, your keyboard will still behave like Dvorak! Remove Dvorak and the transliterator will revert to standard QWERTY behavior. I suppose OSX remembers the previously-used "standard" keyboard layout.
Developing an input method is fraught with peril. First, you can't actually debug an input method in Xcode. You instead have to rely on NSLog statements and the Console app to debug. Unfortunately, this is not easy either: whenever you add a new input method to your input sources, you can no longer delete the app from your Input Sources directory until you log out and log back in again. (An error message pops up that says, "The item 'Cyrillic Transliterator Input Method' can't be moved to the Trash because it's open.") This means closing all your windows, all your browsers, all your text editors, etc., every time you make even the smallest change.
My solution was to create a new Mavericks VM through Parallels and do all my testing there. (Fortunately, Parallels can do this for you by using your recovery partition.) After each build, I did the logout/login cycle in the VM and copied the input method app to a shared folder. (This could have easily been automated with a script if I wasn't feeling lazy.) It wasn't pretty, but it was much faster than doing the same thing in my main OS, which had about a billion tabs open in Safari, among many other things.
There's probably a way to avoid this hassle by resetting some process, but I couldn't find any answers on Google.
The Input Method APIs have very little documentation. I relied mostly on Apple's code sample (which features several undocumented techniques, such as the custom attributes in their Info.plist) and corroborated my findings with a few tactical Google searches for people who have gone down the same path. (There's a number of open source projects that implement input methods, but they're mostly related to Asian input.)
A few important points to start with:
- Your bundle ID needs to have the 'inputmethod' subdomain in it.
- The 'tsInputModeScriptKey' plist key determines the language your input method will appear under.
- In order to be able to set the input method language (as well as a number of other advanced options), you need to reference your input method by an reverse domain name formatted alias, which is not the same thing as your bundle ID. This alias is mapped to an actual name in your InfoPlist.strings file.
- Keyboard icons are 16x16. (32x32 for Retina — simply add the @2x to the icon file and it'll show up automatically.)
There are three ways to capture input events. The one I'm using sends an inputText:client:
message whenever some text is about to be entered in a text field, and a didCommandBySelector:client:
message whenever a so-called "action method" is requested. (An "action method", according to Apple, is "an action specified in the input method dictionary of keys and actions... or one of the NSResponder action methods". Mostly, this means things like delete, backspace, and cursor movement. I'm not entirely sure how you specify custom actions.) When you return YES from inputText:client:
, you take responsibility for the text that the user is entering. If you do nothing, no text will show up as the user types. You have to call setMarkedText:selectionRange:replacementRange:
on the sender to show any text after the point at which you claimed it (which will show up as underlined), and insertText:replacementRange:
to commit the text. (This should be called from commitComposition:
, since I believe this method is called whenever the user does something that changes text focus, like switching to another app.)
Fun fact! You can actually create an input method by simply creating a text-based .inputplugin file. You can find instructions here. It's not enough for my use case (no real-time transliteration support), but still, a pretty interesting feature that I'm sure few people are aware of. (Now that I think of it, this might be a great way to create a simple TextExpander clone.)
Input Method Kit Framework Reference
Chinese(?) slides on Input Method Kit programming and debugging