-
Notifications
You must be signed in to change notification settings - Fork 0
002 imk introduction
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.
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.
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.
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.
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.