Haiku OS のインプットメソッド

インプットメソッド関連の環境は OS ごとに異なるため、一つでも対応するのは大変な作業です。大抵、詳しいドキュメントが見つからずに苦労します。

ここでは Haiku OS のインプットメソッドを実装するにあたって必要ことをすべて説明します。Haiku OS rev52698 頃の仕様にしたがっています。

クラスと配置

インプットメソッドはインプットサーバーが呼び出せるように BInputServerMethod クラスを継承したクラスとします。

#include <add-ons/input_server/InputServerMethod.h>

class CustomuInputMethod : public BInputServerMethod
{
public:
    CustomuInputMethod();
    virtual ~CustomuInputMethod();

    // BInputServerFilter
    // キーの押し下げインベントを受け取る
    virtual filter_result Filter(BMessage *msg, BList *_list);

    // 初期化
    virtual status_t InitCheck();

    // BInputServerMethod
    // メソッドが選択・非選択されると呼ばれる
    virtual status_t      MethodActivated(bool active);
};

extern "C" _EXPORT
BInputServerMethod *instantiate_input_method()
{
    return new CustomInputMethod();
}

インプットメソッドのインスタンスを取得するためのエントリーポイントが instantiate_input_method 関数です。

このコードをダイナミックリンクライブラリにコンパイルします。以下のライブラリにリンクしなければいけません。

-lbe -lroot /boot/system/servers/input_server

インプットメソッドを実装したライブラリを以下のどこかに配置すると、インプットサーバーが自動的に認識して、インプットメソッドが使用できるようになります。

# システム
/boot/system/add-ons/input_server/methods
# ユーザー
/boot/home/config/non-packaged/add-ons/input_server/methods

インプットメソッドのテスト時にはユーザー用の配置場所が便利です。パッケージとしてインストールするときはシステム用の配置場所に入れるようにしてください。

初期化

コンストラクタで親クラス BInputServerMethod にインプットメソッド名とアイコンのデータを渡します。これらは、デスクバーアイコンとして使用されます。

CustomuInputMethod::CustomuInputMethod()
    : BInputServerMethod("IM_NAME", kICON_DATA)
{
}

アイコンは一度設定すると、後で変更するために SetIcon メソッドを呼び出しても表示が変わりません (再描写されないバグ?)。

アイコンデータは 16x16 ピクセルの B_CMAP8 形式です。画像ファイルからアイコンデータを生成する方法は後述します。

インプットメソッドのクラスが最初に利用される前に InitCheck メソッドが呼ばれます。ここでインスタンス変数などを初期化できます。別スレッドの Looper などをインスタンス化するのが一般的です。

void CustomuInputMethod::InitCheck()
{
    // ...
}

キーの押し下げイベント

ユーザーがキーボードのキーを押し下げると、インプットサーバーからインプットメソッドへイベントが送られてきます。

filter_result CustomInputMethod::Filter(BMessage *msg, BList *_list)
{
    if (msg->what == B_KEY_DOWN) {
        fLooper.SendMessage(msg);
        return B_SKIP_MESSAGE;
    }
    return B_DISPATCH_MESSAGE;
}

BInputServerMethod の基底クラス BInputServerFilter のメソッドを実装してイベントを受け取ります。B_KEY_DOWN イベントを消費する場合、B_SKIP_MESSAGE を返します。 ここで消費しないイベントは B_DISPATCH_MESSAGE を返すことで、インプットサーバーが次のフィルタへ渡すように指示します。

このコードではイベントのメッセージをそのまま別の Looper (別スレッド) へ丸投げしています。キー入力から変換と表示までの処理に時間がかかるともたつくので、別スレッドで処理させます。

キー押し下げ B_KEY_DOWN メッセージは以下のデータを含みます。

int32 key = msg->GetInt32("key", 0); // raw keycode
uint8 byte = (uint8)msg->GetInt8("byte", 0); // ascii code
uint32 modifiers = (uint32)msg->GetInt32("modifiers", 0); // 修飾キー

キーコードは Preference -> Keymap で選択しているマッピングファイルを参照してください。コンパイル前のファイルがhaiku/src/data/keymapsにあります。

特殊キーや修飾キーは /boot/system/develop/headers/os/interface/InterfaceDefs.h に定義されています。

メソッドの選択・非選択

インプットサーバーがメソッドの切り替えを検出し、該当するメソッドの場合にイベントが送られてきます。

status_t CustomuInputMethod::MethodActivated(bool active)
{
    BMessage msg(active ? IM_METHOD_ACTIVATED : IM_METHOD_DEACTIVATED);
    fLooper.SendMessage(msg);
    return BInputServerMethod::MethodActivated(active);
}

インプットメソッドのエンジンの状態の切り替えなどに利用できます。また、不意にメソッドが非選択状態になった場合を検出できます。候補ウィンドウの非表示にするなどの処理を行いましょう。

ウィンドウサイズ

ウィンドウのサイズを取得するメソッド GetScreenRegion が基底クラスの BInputServerFilter に用意されています。

BRegion region;
if (GetScreenRegion(&region) == B_OK) {
    BRect rect = region.Frame();
}

候補ウィンドウの配置調整に利用できます。

変換イベントの種類

変換の開始と終了を送信するためのイベントがあります。

開始イベントは以下のものです。

要素 説明
be:reply_to BMessenger 返信受取相手

返信を受け取るのは大抵、メソッドや内部の実装で、BHandler や BLooper を BMessenger でラップして指定します。

BMessage* msg = new BMessage(B_INPUT_METHOD_EVENT);
msg->AddInt32("be:opcode", B_INPUT_METHOD_STARTED);
msg->AddMessenger("be:reply_to", BMessenger(NULL, this));
EnqueueMessage(msg);

変換の終了送信には他のオプションはありません。

BMessage* msg = new BMessage(B_INPUT_METHOD_EVENT);
msg->AddInt32("be:opcode", B_INPUT_METHOD_STOPPED);
EnqueueMessage(msg);

B_INPUT_METHOD_STOPPED が外部からメソッドへ送られてくることがあります。これは、以下のような場合に送られます。 このとき、メソッドは適当に変換を終了させてください。

  • デスクバーなどで入力メソッドが変更されたとき
  • 変換中のウィンドウがフォーカスを失った時
  • その他

変換中テキストの送信

変換中で確定前のプレエディットとして表示する文字列を送信します。

要素 説明
be:clause_start int32 効果を付ける範囲開始位置
be:clause_end int32 効果を付ける範囲終了位置
be:selection int32 選択効果を付ける位置、二つでそれぞれ開始位置と終了位置を指定
be:string const char* 文字列

be:clause_xxxx の指定で背景色などが設定されます。テキスト全体を含めてしまってかまいません。 be:selection は一つ目で開始位置を、二つ目で終了位置を指定します。

be:confirmed が含まれていても構いませんが、false を指定してください。be:confirmed が true のときは、入力確定とみなされます。

BMessage* msg = new BMessage(B_INPUT_METHOD_EVENT);
msg->AddInt32("be:opcode", B_INPUT_METHOD_CHANGED);
msg->AddInt32("be:clause_start", 0);
msg->AddInt32("be:clause_end", str.length()-1);
msg->AddInt32("be:selection", 0);
msg->AddInt32("be:selection", str.length()-1);
msg->AddString("be:string", str.c_str());
EnqueueMessage(msg);

入力テキストの送信

入力テキストが確定されたときに、文字列の挿入を要求します。

要素 説明
be:confirmed bool 確定状態
be:string const char* 文字列
BMessage* msg = new BMessage(B_INPUT_METHOD_EVENT);
msg->AddInt32("be:opcode", B_INPUT_METHOD_CHANGED);
msg->AddBool("be:confirmed", true);
msg->AddString("be:string", str.c_str());
EnqueueMessage(msg);

入力位置の取得

テキスト入力中のカーソル位置を取得するには、B_INPUT_METHOD_EVENT のうち B_INPUT_METHOD_LOCATION_REQUEST を送ります。

要素 説明
be:location_reply BPoint キャレット位置
be:height_reply float キャレット高さ
BMessage* msg = new BMessage(B_INPUT_METHOD_EVENT);
msg->AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST);
EnqueueMessage(msg);

返答は以下のように be:location_reply および be:height_reply です。

case B_INPUT_METHOD_EVENT:
{
    uint32 opcode = msg->GetInt32("be:opcode", 0);
    if (opcode == B_INPUT_METHOD_LOCATION_REQUEST) {
        // move window position to the start of selected char
        // be:height_reply: float, be:location_reply: BPoint
        BPoint point;
        float height;
        if (msg->FindPoint("be:location_reply", fCursorIndex, &point) == B_OK &&
            msg->FindFloat("be:height_reply", fCursorIndex, &height) == B_OK) {
            point.y += height + 1.;
            fLastLocation = point;
        }
        float x = fLastLocation.x - fCandidateView->GetValueLeft();
        MoveTo(x >= 0 ? x : 0,
               fLastLocation.y);
    }
    break;
}

現在の実装ではプレエディットに文字が含まれていない場合、返答の要素数はゼロのため、位置が取得できません。

Looper

BInputServerMethod クラスを継承してメソッドクラスを作成した場合、変換などを担当する Looper (別スレッド) を利用します。

このとき、Looper の初期化を忘れないようにしましょう。

#include <Application.h>

// in constructor
if (be_app) {
    if (be_app->Lock()) {
        be_app->AddHandler(this);
        be_app->Unlock();
    }
}

Run();

メソッドから Looper へイベントが送信されてこない場合、Looper の初期化を忘れています。

トレイ

インプットメソッドが一つでも追加されるとトレイに板状のアイコンが表示されます。ここからマウスでインプットメソッドの切り替えやメニューが利用できます。

このメニューに表示されるメソッド名やアイコン、サブメニューを設定できます。 名前とアイコンはインスタンス化時に設定できます。

status_t  SetName(const char* name);
status_t  SetIcon(const uchar* icon);
status_t  SetMenu(const BMenu* menu,
            const BMessenger target);
// target はメニューの項目が選択されたときにイベントを受け取る相手

後からアイコンを変更しようとすると動作しません (hrev52698)。バグかどうか不明です。

メニューはシリアライズされて渡されるため、チェックマークの状態などを更新するには、メニューをまるごと設定しなおしてください。

メニューは一度開くと、二度目にクリックしても反応しません。バグ(#14860)のようです。

候補ウィンドウ

変換候補を表示する候補ウィンドウは以下のような属性を指定して作成します。

CandidateWindow::CandidateWindow()
    : BWindow(
        BRect(100, 100, 200, 250),
        "CandidateWindow",
        B_NO_BORDER_WINDOW_LOOK,
        B_FLOATING_ALL_WINDOW_FEEL,
        B_NOT_RESIZABLE | B_NOT_CLOSABLE | B_NOT_ZOOMABLE |
        B_NOT_MINIMIZABLE | B_NOT_MOVABLE |
        B_AVOID_FOCUS |
        B_NOT_ANCHORED_ON_ACTIVATE)
{

}
  • 枠線なし
  • サイズ変更なし、閉じる不可、ズーム不可、最小化不可、移動不可、フォーカスなし

アイコンデータ

2019/1 頃に新しい API が導入されてベクターアイコンがトレイアイコンとして利用できるようになりました。インプットメソッド関連は API の変更が必要なため、後回しのようですが、じきにベクターアイコンが利用できるようになるはずです。

トレイアイコンのデータは B_CMAP8 形式です。16 x 16 ピクセルPNG ファイルを用意しておき、以下のプログラムで変換できます。

// tocmap8.cc

#include <Application.h>
#include <Bitmap.h>
#include <BitmapStream.h>
#include <IconUtils.h>
#include <TranslationUtils.h>

#include <stdio.h>

// g++ -o tocmap8 tocmap8.cc -lroot -ltranslation -lbe

class App : public BApplication
{
public:
    App(const char* path);
};

App::App(const char* path)
    : BApplication("application/x-vnd.icon.conv")
{
    const char *var_name = "kIcon";
    BBitmap *b = BTranslationUtils::GetBitmapFile(path);
    if (b == NULL) {
        return;
    }
    //printf("len: %d, row_len: %d, type: 0x%04x\n", b->BitsLength(), b->BytesPerRow(), (int)b->ColorSpace());
    BBitmap *r = new BBitmap(BRect(0, 0, 15, 15), B_CMAP8);
    if (BIconUtils::ConvertToCMAP8((const uint8 *)b->Bits(), 16, 16, 64, r) == B_OK) {
        uchar *d = (uchar *)r->Bits();
        //printf("len: %d, row_len: %d\n", r->BitsLength(), r->BytesPerRow());
        int len = r->BitsLength();
        printf("const uchar %s[] = {\n", var_name);
        int i = 0;
        while (i < len) {
            for (int j = 0; j < 16; ++j) {
                printf("0x%02x,", d[i++]);
            }
            printf("\n");
        }
        printf("};\n");
    }
    
    delete r;
    delete b;
}

int main(int argc, char *argv[])
{
    if (argc == 2) {
        App *app = new App(argv[1]);
        delete app;
    } else {
        printf("tocmap8 path_to_16x16.png\n");
    }
    return 0;
}

以下のようにコンパイルしてください。

g++ -o tocmap8 tocmap8.cc -lroot -ltranslation -lbe

ターミナルから次のように変換するファイルを指定して実行します。

tocmap8 hoge.png

標準出力に変換結果が表示されるので、コピーして利用してください。

ディレクトリパス取得

インプットメソッドの設定ファイルは /boot/home/config/settings/method_name 以下に入れるのが一般的です。このパスを取得する API が提供されています。

#include <FindDirectory.h>
#include <Path.h>
#include <stdio.h>

BPath path;
if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) == B_OK) {
    path.Append("method_name");
    printf("%s\n", path.Path());
}

インストール済みのインプットメソッドのデータファイルは /boot/system/data/method_name など、パッケージ化されていないものは /boot/home/config/non-packaged/data/method_name 辺りに配置しますが、これらのパスも B_USER_NONPACKAGED_DATA_DIRECTORY や B_SYSTEM_DATA_DIRECTORY を指定すると取得できます。