Kivyの日本語入力中の文字を表示するようにした話

2018年7月、Kivyの最新のバージョン（1.10.1）がリリースされました。こちらではIME表示の
内容のプルリクが採用されており、IMEが動作して入力中の日本語が表示されるようになりました。

# 内容
PythonのGUIライブラリー「Kivy」ですが、windows/MacOS環境のtextinputでは日本語入力中に日本語（IME）が表示されないという問題があります。
今回はなぜ表示されないという理由をしらべて、表示できるように対策をおこないました。なお検証OSはwindowsです。
※今回は、私と[@sowan](https://twitter.com/sow_an)さんと共同で行っています。


# 日本語が表示されない理由

まずKivyですが以下のように、低レイヤーはOpenGLなどのライブリによって各種機能を実現しています。

![architecture.png](https://qiita-image-store.s3.amazonaws.com/0/11002/b2769bc9-a7ef-85fc-6f5c-07558993c695.png)

その中で文字入力はSDLを使って実現しています。

## SDL内でのテキスト入力の実現について
SDLのテキストインプットの[チュートリアル](http://sdl2referencejp.osdn.jp/TextInput.html)からソースを抜粋します。

```c

    SDL_StartTextInput();
    while (!done) {
        SDL_Event event;

        if (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_QUIT:
                    /* 終了 */
                    done = SDL_TRUE;
                    break;
                case SDL_TEXTINPUT:
                    /* テキストの末尾に新しいテキストを追加する */
                    strcat(text, event.text.text);
                    break;
                case SDL_TEXTEDITING:
                    /*
                    未変換テキストを更新する.
                    カーソル位置を更新する.
                    選択の長さを変換する(可能ならば).
                    */
                    composition = event.edit.text;
                    cursor = event.edit.start;
                    selection_len = event.edit.length;
                    break;
            }
        }
        Redraw();
    }

}

```

これを見ると、文字入力時には「SDL_TEXTINPUT」が、IMEを動作する際には「SDL_TEXTEDITING」のイベントが発行されていることがわかります。

## Kivy内部でのSDLからのイベントの受信について

これを踏まえてkivyの[githubのソースコード](https://github.com/kivy/kivy)を見てみます。SDLからのイベントを受信して処理を行っているのは、[core/window/_window_sdl2.pyx](https://github.com/kivy/kivy/blob/master/kivy/core/window/_window_sdl2.pyx)の「poll」関数です。poll関数はSDLからのイベントを受けて、window_sdl2.pyの_event_loop関数でイベントを発行しています。
「poll」関数のコードをみると以下のようになっています。


```ｐｙ
    def poll(self):
        cdef SDL_Event event
        cdef int rv

        with nogil:
            rv = SDL_PollEvent(&event)
        if rv == 0:
            return False

        action = None
        if event.type == SDL_QUIT:
            return ('quit', )
        elif event.type == SDL_DROPFILE:
            return ('dropfile', event.drop.file)
        elif event.type == SDL_MOUSEMOTION:
            x = event.motion.x
            y = event.motion.y
            return ('mousemotion', x, y)
            
　　～省略～

        elif event.type == SDL_KEYDOWN or event.type == SDL_KEYUP:
            action = 'keydown' if event.type == SDL_KEYDOWN else 'keyup'
            mod = event.key.keysym.mod
            scancode = event.key.keysym.scancode
            key = event.key.keysym.sym
            return (action, mod, key, scancode, None)
        elif event.type == SDL_TEXTINPUT:　★
            s = event.text.text.decode('utf-8')
            return ('textinput', s)
        else:
            #    print('receive unknown sdl event', event.type)
            pass
```

見るとわかるのですが、**文字入力が確定した際のイベント「SDL_TEXTINPUT」を受けて入力された文字と「textinput」イベントを返していますが、入力中（IME）の際にSDLから発行される「SDL_TEXTEDIT」イベントを受けて行う処理がありません。** 
これが日本語入力中に文字が表示されない理由です。


# 日本入力中の文字を表示できるようにする

理由がわかったので、今度はどうすればいいか考えます。処理的には以下の手順をまず追加すべきと考えます。

+ SDLから「SDL_TEXTEDIT」イベントを受けて文字列を返すようにする。

+ 新たに「textedit」イベントを登録して処理を行うようにする。

## 実際のソースについて

@souanさんがkivy本体に[プルリク](https://github.com/kivy/kivy/pull/5109)しました。
また、サンプルプログラムとして、githubに置いてあります。

texteditイベントの発行例：https://github.com/Adachinski/kivy/tree/textedit_patch_smple
使用例：https://github.com/Adachinski/kivy_textedit_sample

以下は、実際のソースについてです。

### 「SDL_TEXTEDIT」イベントの追加

ソース：https://github.com/kivy/kivy/pull/5109

kivy/core/window/_window_sdl2.pyx　ファイルのpoll関数にSDL_TEXTEDITトを受信した際の処理を追加しました。

```ｐｙ
    def poll(self):
        cdef SDL_Event event
        cdef int rv

        with nogil:
            rv = SDL_PollEvent(&event)
        if rv == 0:
            return False


　　～省略～

        elif event.type == SDL_TEXTINPUT:　
            s = event.text.text.decode('utf-8')
            return ('textinput', s)
            
        elif event.type == SDL_TEXTEDITING:　★イベントを追加
            s = event.edit.text.decode('utf-8')　★IMEの文字を入れる
            return ('textedit', s)
        else:
            #    print('receive unknown sdl event', event.type)
            pass
```


ちなみにこのファイルはpyxファイルなのでcythonでビルドしてライブラリを作り直す必要があります。
kivyはOpenGL+SDLなので環境構築が大変でビルド環境をwindowsで構築していましたが2～3日かかりました。環境構築については後日、また詳細をアップします。

追記：日本語入力の環境構築方法を記事にしました。

+ [Kivyで日本語入力を行う方法について（実行編）]
(https://qiita.com/dario_okazaki/items/815cdebe91211f2b3826)

### 「textedit」イベントの追加

ソース：https://github.com/Adachinski/kivy/tree/textedit_patch_smple


そのために、window_sdl2.py を修正します。
https://github.com/Adachinski/kivy/commits/textedit_patch_smple/kivy/core/window/window_sdl2.py

修正点は以下の通りです。

```
class WindowSDL(WindowBase):

    __events__ = ('on_textedit',)　★追加
 
 ～省略～  
        def _mainloop(self):
        EventLoop.idle()

        # for android/iOS, we don't want to have any event nor executing our
        # main loop while the pause is going on. This loop wait any event (not
        # handled by the event filter), and remove them from the queue.
        # Nothing happen during the pause on iOS, except gyroscope value sent
        # over joystick. So it's safe.
        while self._pause_loop:
            self._win.wait_event()
            if not self._pause_loop:
                break
            self._win.poll()

        while True:
            event = self._win.poll()
            if event is False:
                break
            if event is None:
                continue

            action, args = event[0], event[1:]
            if action == 'quit':
                if self.dispatch('on_request_close'):
                    continue
                EventLoop.quit = True
                self.close()
                break
～省略～


            elif action == 'textinput':
                text = args[0]
                self.dispatch('on_textinput', text)
            elif action == 'textedit':  ★texteditを追加
                text = args[0]
                self.dispatch('on_textedit', text)

            # unhandled event !
            else:
                Logger.trace('WindowSDL: Unhandled event %s' % str(event))
    

～省略～
    def on_textedit(self, text): ★追加
        pass

```




### 「textedit」イベントを使った実際の使用例

ソース：https://github.com/Adachinski/kivy_textedit_sample

TextInputIME.pyで「TextInput」クラスを継承して新たに
「TextInputIME」クラスを作成しました。また、変数testtextをStringPropertyで宣言しています。texteditイベントの処理が来た際に、入力中の文字をtesttextに格納しています。後は、kvファイル内で、Labelにtesttextの内容を表示しています。

実際の動作画面は以下になります。

#### 入力中

![editing.png](https://qiita-image-store.s3.amazonaws.com/0/11002/7352251f-87be-c6bc-670c-75f3d6945b42.png)

入力中は、IMEラベル（グレイの部分）に入力の文字が表示されます。

#### 入力確定後

![complete.png](https://qiita-image-store.s3.amazonaws.com/0/11002/8ca76884-19f5-39b0-f4ff-e6bac88beb71.png)
エンターキーを押して、入力すると入力後の文字がtextinputに表示されます。

※本来は入力中にのみIMEラベルが表示されるようにしたかったのですが、Kivy上で上手い方法が思いつきませんでした。



# 現状できていないこと
入力中の日本語はどうにか表示できるようになりましたが、SDL側の問題（仕様？）で以下の事ができていません。

+ スペースで変換時に変換候補の一覧が表示されない。
+ SDL側のテキストを保存する変数の容量が32バイトのために10文字程度しか表示できていない。

ただし、この内容自体は、個人的にはさほどクリティカルな要因ではないので現状は対応は未定です。