Skip to content

002 imk introduction

Gamcheong Yuen edited this page May 25, 2022 · 2 revisions

Another Introduction to InputMethodKit

Go to this page for an overview of the InputMethodKit framework by Apple. Here we will see what Apple did not tell.

Also see this page (in Chinese) for comparison between TSM and IMK.

TL;DR: IMK allows an input method application run as a single server to serve each other application (client) for text input.

Input Sources

First read this header file (or you can find it on GitHub):

<SDKFolder>/MacOSX<version>.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/TextInputSources.h

We know that there are three kinds of keyboard input sources: keyboard layouts, keyboard input methods, and keyboard input modes. All above are automatically register to the system with their self-defined, unique InputSourceID.

An single input method may act as a standalone input source, or may contain a set of input modes. For the latter case, the input method itself will be hidden from the user, and the input modes will be shown to be selectable. Once the user enables one of the input modes, the input method will be also enabled.

About Input Method Registering

A lot of tutorials on the Internet will tell you to call TISRegisterInputSource to register an input method. I found it is not necessary to call this function. When you put the .app package into /Library/Input Methods or ~/Library/Input Methods, macOS will refresh the list of input sources.

If it really does not work, try this code:

import InputMethodKit

TISRegisterInputSource(Bundle.main.bundleURL as CFURL)

Or, specify the path of the .app package if you run these code from external installers.

No need to register more than once, even if you update the package later. To refresh the update, perform a logout and login again.

Enable and Select Input Sources

As Apple mentioned, after registering an input source, the system rebuilds the cache and we can immediately use it.

If it is intended, you can enable and select the input source with these codes:

import InputMethodKit

func getInputSourceID(_ inputSource: TISInputSource) -> String {
    // kTIPropertyInputSourceID is a pre-defined property key constant.
    // Refer TextInputSources.h for more property key constants.
    let ptr = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)

    // Note: InputSourceID is always available for any input sources.
    //       If you are acquiring other properties like ModeID,
    //       carefully check if ptr is nil.
    return Unmanaged<CFString>
        .fromOpaque(ptr!)
        .takeUnretainedValue() as String
}

func enableInputSource() {
    let inputSourceList = TISCreateInputSourceList(nil, false)
        .takeUnretainedValue()

    for index in 0...CFArrayGetCount(inputSourceList) - 1 {
        let inputSource = Unmanaged<TISInputSource>
            .fromOpaque(CFArrayGetValueAtIndex(inputSourceList, index))
            .takeUnretainedValue()
        let inputSourceID = getInputSourceID(inputSource)

        if inputSourceID == "com.example.inputmethod.MyInputMethod" {
            // This will prompt the user to enable the input source.
            TISEnableInputSource(inputSource)

            // Select the input source immediately if you want.
            let isInputSourceSelectable = Unmanaged<CFBoolean>
                .fromOpaque(TISGetInputSourceProperty(inputSource,
                    kTISPropertyInputSourceIsSelectCapable))
                .takeUnretainedValue()
            if CFBoolGetValue(isInputSourceSelectable) {
                TISSelectInputSource(inputSource)
            }

            // If you are checking more than one input source,
            // such as your list of input modes,
            // perform an iteration to check every InputSourceID,
            // and omit this return statement.
            return
        }
    }
}

Again, no necessary to perform these codes unless you are making some user-friendly (as you think) installers.

IMKServer

Initializing a IMKServer object will instantiate an IMKServer for your input method. Putting the initialization in your main function (Objective-C) is just good. For Swift, add a property to AppDelegate class and initialize it in applicationDidFinishLaunching. (Another way is write just one line declaring and initializing the IMKServer.)

Two ways to initialize a IMKServer with different init functions:

let server = IMKServer(name: "ConnectionString",
    bundleIdentifier: Bundle.main.bundleIdentifier)

// OR

let server = IMKServer(name: "ConnectionString",
    controllerClass: MyInputController.self,
    delegateClass: MyInputController.self)

For the first way, some key-value pairs are required to be defined on the top-most level in the Info.plist file. We will discuss them later.

I have once tried to use the second way, but it failed to initialize with the delegateClass, as I use the controller class as the delegate class.

No documentation on delegateClass is available, and we will meet it again later in this series of tutorials.