💻

[InputMethodKit] azooKey on macOSの開発知見

2024/04/06に公開

普段はiOS向けにazooKeyという日本語入力キーボードアプリを作っています。少し前からazooKey on macOSの開発を始めました。

https://github.com/ensan-hcl/azooKey-Desktop

といっても、独自実装した変換エンジンのAzooKeyKanaKanjiConverter自体は既にmacOSで動作確認していたので、macOSの入力メソッドフレームワークであるInputMethodKitとAzooKeyKanaKanjiConverterを繋ぐのがメインの開発内容です。以前にも一度チャレンジしてInputMethodKitで挫折していたのですが、この度ついにまともに動くものができました。

出来たもの

https://x.com/miwa_ensan/status/1770447420788199558?s=20

作り方

大人しく先人の知恵に縋りました。1000円で全てが分かります。

https://mzp.booth.pm/items/809262

何年も前のものですが、InputMethodKitの側の変更が皆無であることもあり、ほとんど書いてあるとおりに実装できました。参考実装のEmojiIMは当時との環境差が大きいため動かせませんでしたが、とても有用でありがたい本です。

この記事は、この本の内容を前提としつつ、最低限使えるレベルのものを作るまでに+αで持っておきたかった知識をいくつかまとめておきます。

デバッグ

デバッグ出来なければ開発が出来ません。

プリントデバッグ

printが効かないのでNSLogを使う必要があります。「コンソール.app」でログを見ることができるようです。

アプリのリロード

ビルドしたアプリケーションは/Library/Input\ Methodsに配置し、ログアウト&再ログインすることで認識されます。しかし、ビルドするたびにこれをやるのはダルすぎます。

そこで、アプリケーションを新しいバージョンで上書きしてから古いプロセスをキルして再起動することでこれを反映することができます。私は以下のようなシェルスクリプトを用意してビルド〜上書き〜キルまでを行っています。

set -e
# ビルド
xcodebuild -workspace azooKeyMac.xcodeproj/project.xcworkspace -scheme azooKeyMac clean archive -archivePath build/archive.xcarchive | xcpretty
# 上書き
sudo rm -rf /Library/Input\ Methods/azooKeyMac.app
sudo cp -r build/archive.xcarchive/Products/Applications/azooKeyMac.app /Library/Input\ Methods/
# 再起動
pkill "azooKeyMac"

GUIで同等の操作をするには、まずkillを行うには「アクティビティモニタ.app」上でアプリケーション名で検索し、ツールバーから「停止」→「終了」します。この次にアプリケーションが/Library/Input\ Methods/にあるので、これを新しいバージョンで上書きし、メニューバーなどでアプリケーションを再選択することで再起動が完了します。
この際、アプリケーションを終了したあとから上書きまでの間に再起動が発生しないよう注意する必要があります。再起動された場合、アプリケーションの起動中は上書きが許可されないため、再度終了してから上書きを行う必要があります。

なお、アイコンの変更などはこの方法では反映できないので、macOS自体のログアウト&再ログインが必要です。また、これを繰り返しているとたまに動かなくなるので、その際も大人しくログアウト&再ログインすると解決します。

標準の日本語入力を参考にする

開発中何度かInfo.plistをいじる機会があるのですが、よくわからない設定項目が多いので戸惑います。

そんなときは、標準の日本語入力システムが参考になります。macOSで標準で付属するローマ字日本語入力システムは/System/Library/Input Methods/JapaneseIM-RomajiTyping.appとして配置されていて、/System/Library/Input Methods/JapaneseIM-RomajiTyping.app/Contents/PlugIns/JapaneseIM-RomajiTyping.appex/Contents/Info.plistが標準の日本語入力のInfo.plistにあたります。困ったらこれを参考に設定するのが良さそうです。

また、Google日本語入力のInfo.plistもとても参考になります。

https://github.com/google/mozc/blob/master/src/mac/Info.plist

英数入力周り

当初入力モードとして日本語のみを持っていたところ、英数入力との切り替えがうまくいかず困りました。結論から言うと、日本語入力モードとは別に英語入力モードを持っておくことでうまく行きます。

Option+←のような動作がうまく動かない問題がありましたが、これはキーボードレイアウトをオーバーライドすることで解決しました。

IMKCandidates周り

『日本語入力を作るときに必要だった本』で述べられておらず、少し悩んだのがIMKCandidatesの挙動のカスタマイズでした。IMKCandidatesは変換候補ウィンドウを操作するクラスで、標準IMのようなカッコいい変換候補ウィンドウが簡単に(※)表示できます。

変換候補ウィンドウを最前面に表示する

デフォルト状態では、Spotlight上で変換候補ウィンドウを表示するとSpotlightの後ろに隠れてしまいます。これを解決するには、Swiftからは直接呼び出せないsetWindowLevelというAPIを使うことが出来ます。

self.candidatesWindow.perform(
    Selector(("setWindowLevel:")),
    with: Int(max(
        CGShieldingWindowLevel(),
        kCGPopUpMenuWindowLevel
    ))
)

ただし、Info.plistでLSBackgroundOnly=YESが指定されている場合、変換候補ウィンドウが文字通りBackgroundに描画されてしまうため、注意が必要です。さらにLSUIElement=YESを設定することで正しく変換候補ウィンドウが最前面に表示されました。

スペースキーで変換候補を選択する

変換候補ウィンドウにキーイベントを転送することで矢印キーで変換候補を選択できるようになりますが、スペースキーはデフォルトではハンドルしてもらえません。

そこで、スペースキーが押された場合にはキーイベントを偽造して送信することでスペースキーでの変換を実現しました。

let newEvent: NSEvent = .keyEvent(
    with: .keyDown,
    location: event.locationInWindow,
    modifierFlags: event.modifierFlags,
    timestamp: event.timestamp,
    windowNumber: event.windowNumber,
    context: nil,
    characters: "\u{F701}",
    charactersIgnoringModifiers: "\u{F701}",
    isARepeat: event.isARepeat,
    keyCode: 125
) ?? event

setMarkedText周り

『日本語入力を作るときに必要だった本』ではsetMarkedTextStringを与えるよう指示されています。これで正しく動くのですが、WebKit系のエンジンで入力中のテキストが全選択されているように描画される問題がありました。

そこで、setMarkedTextの引数としてNSAttributedStringを与えると安定して描画されるようになりました。

このNSAttributedStringの部分もちょっとしたトリックがあります。標準の日本語入力やGoogle日本語入力では、文節ごとの変換の際に変換対象の文節とそれ以外でハイライトの色が変わります。これを実現するにはIMKInputControllermarkという関数を経由して得られるNSAttributedStringの属性を利用します。

let highlight = self.mark(
    forStyle: kTSMHiliteSelectedConvertedText,
    at: NSMakeRange(NSNotFound, 0)
) as? [NSAttributedString.Key: Any]
let underline = self.mark(
    forStyle: kTSMHiliteConvertedText,
    at: NSMakeRange(NSNotFound, 0)
) as? [NSAttributedString.Key: Any]
let text = NSMutableAttributedString(string: "")
text.append(NSAttributedString(string: targetString, attributes: highlight))
text.append(NSAttributedString(string: otherString, attributes: underline))
self.client()?.setMarkedText(
    text,
    selectionRange: NSRange(location: targetString.count, length: 0),
    replacementRange: NSRange(location: NSNotFound, length: 0)
)

将来的に必要そうなこと

  • 独自変換候補ウィンドウ
    IMKCandidatesは便利なのですが、挙動のカスタマイズが相当厳しい感じがしています。Google日本語入力、ATOK、かわせみなど、各種サードパーティの日本語IMEはどれも独自の変換候補ウィンドウを作っていますし、移行の必要はありそうです。
  • アップデータ
    macOSのApp Storeで直接配布することができないため、自動更新に対応する必要があります。難しそうです。

まとめ・感想

先人の知恵のおかげで、最低限使える日本語入力IMEを作るところまでは案外スッと来られました。ただ、とにかく情報が少ないため、探索要素がかなり大きい開発ではあります。引き続き開発の知見が得られれば共有したいです。

azooKey on macOSはアルファ版を既に公開していて、この記事も全部azooKeyで書いています。細かな不具合などはありつつも、非常に快適です。「ライブ変換を搭載したOSSのmacOS向け日本語入力」というニッチ領域では唯一無二ではないかと思います。興味があればぜひお試しください。
https://github.com/ensan-hcl/azooKey-Desktop/releases

azooKey blogs

Discussion