みずぴー日記

人間の再起動ボタンはハワイのビーチにある

☕️ JavaScriptと入力メソッド

AquaSKK 4.5.0ではEscキーの扱いを改善した。 この修正のためにWebKitにおけるキーイベント配信の仕組みを追ったので、まとめる。

💡入力メソッドごとの差異

上記Tweetにあるように、テキストを入力中にEscを押した際にJavaScriptで発火するイベントが入力メソッドごとに違う。

macOS標準の日本語入力やGoogle日本語入力ではキーコードが229のイベントが発生するが、AquaSKKではキーコードが27のイベントが発生していた。 このイベントが違うと入力メソッドを扱うJavaScriptコードが複雑化するので修正した。

🔑キーコード 229

キーコード229はUI Eventsで次のように定義されている。

If an Input Method Editor is processing key input and the event is keydown, return 229.

(訳: 入力メソッドがキー入力を処理しておりイベントが keydown の場合は、キーコードは229となる)

🌐WebKit

W3Cが定めているキーコードなのでブラウザ内部の処理が関係するのだろうと想定して、WebKitのコードを調べた。

WebKitはアプリケーションのUIを実現するUIプロセスとタブごとに作られるWebプロセスが協調して動作している。 キー入力の処理もこの2つのプロセスが関連している。

f:id:mzp:20180514223203p:plain

UIプロセス

// Source/WebKit/UIProcess/Cocoa/WebViewImpl.mm

void WebViewImpl::keyDown(NSEvent *event)
{
    // (snip)

    // 入力メソッドに処理を転送する
    interpretKeyEvent(event, [weakThis = createWeakPtr(), capturedEvent = retainPtr(event)](BOOL handledByInputMethod, const Vector<WebCore::KeypressCommand>& commands) {
        ASSERT(!handledByInputMethod || commands.isEmpty());

        // キーイベントに入力メソッドが処理したかどうか(handledByInputMethod)を付与して、Webプロセスに転送する
        if (weakThis)
            weakThis->m_page->handleKeyboardEvent(NativeWebKeyboardEvent(capturedEvent.get(), handledByInputMethod, weakThis->m_isTextInsertionReplacingSoftSpace, commands));
    });
}

void WebViewImpl::interpretKeyEvent(NSEvent *event, void(^completionHandler)(BOOL handled, const Vector<WebCore::KeypressCommand>& commands))
{
    // (snip)

    // Cocoaが提供するAPIを使ってイベントを入力メソッドに転送する
    [inputContext() handleEventByInputMethod:event completionHandler:[weakThis = createWeakPtr(), capturedEvent = retainPtr(event), capturedBlock = makeBlockPtr(completionHandler)](BOOL handled) {
        if (!weakThis) {
            capturedBlock(NO, { });
            return;
        }

        LOG(TextInput, "... handleEventByInputMethod%s handled", handled ? "" : " not");
        if (handled) {
            capturedBlock(YES, { });
            return;
        }

        auto commands = weakThis->collectKeyboardLayoutCommandsForEvent(capturedEvent.get());
        capturedBlock(NO, commands);
    }];
}

入力メソッドにキー入力を転送するのに非公開のAPIを利用している。愉快。公開されているAPIと違い、処理結果をコールバックで受けとれるようになっている。

// FIXME: Move to an SPI header.
@interface NSTextInputContext (WKNSTextInputContextDetails)
- (void)handleEvent:(NSEvent *)event completionHandler:(void(^)(BOOL handled))completionHandler;
- (void)handleEventByInputMethod:(NSEvent *)event completionHandler:(void(^)(BOOL handled))completionHandler;
- (BOOL)handleEventByKeyboardLayout:(NSEvent *)event;
@end

Webプロセス

WebプロセスではUIプロセスで付けられたマークをもとに、元のキーイベントを配信するかキーコード229のイベントを配信するかを決めている。

// Soruce/WebCore/page/EventHandler.cpp

// Match key code of composition keydown event on windows.
// IE sends VK_PROCESSKEY which has value 229;
const int CompositionEventKeyCode = 229;


bool EventHandler::internalKeyEvent(const PlatformKeyboardEvent& initialKeyEvent)
{
    // (snip)
    
    // キー入力が入力メソッドで処理ずみかどうかを判定する
    bool handledByInputMethod = keydown->defaultHandled();
    
    if (handledByInputMethod) {     
        // 入力メソッドで処理済みだったらキーコードを229に入れ替える
        keyDownEvent.setWindowsVirtualKeyCode(CompositionEventKeyCode);
        keydown = KeyboardEvent::create(keyDownEvent, &m_frame.windowProxy());
        keydown->setTarget(element);
        keydown->setDefaultHandled();
    }

    // (snip)
    // キーイベントを配信する
    element->dispatchEvent(keydown);

    if (handledByInputMethod)
        return true;

    // 入力メソッドで処理されていないなら処理を継続する
    // (snip)
}

🙊余談

  • JSに配信されるイベントを見るにはKeyboard Event Viewerが便利。
  • WebKitを処理を追うときはXcodeでブレイクポイントを設定して追った。 Debugging WebKitが参考になる。
  • WebKitのビルドに時間がかかってつらかったので、途中で会社のiMacProに接続してビルドしはじめた。 あとからビルドを始めたのに、先にビルドが終わったのでびびった。