Developing IME Aware Applications

To support Haiku and do something useful for it, I am pleased to show you how to communicate with the input server in BeOS.

In BeOS, almost everything uses the UTF-8 character encoding for the processing of characters. Thus, if you want to write or display the characters of other languages than English, all you need to do is to find a font able to display the characters properly and an input method add-on to help you at outputting characters to the running application. The input method handling of BeOS was originally found in the Japanese support. It works this way: The input server loads the add-ons located at B_SYSTEM_ADDONS_DIRECTORY /input_server/methods or B_USER_ADDONS_DIRECTORY/input_server/methods, then filter all events generated by the keyboard or mouse with the Filter() method of BInputServerFilter to change them. There are only a few input methods written since the time BeOS R4 was released. Maybe it's because only ERGOSOFT Crop. and just a few developers know how to have method working. In my memory, existing methods are BeCJK, HanBe, Canna, ChineseTool and more recently "Anthy for Zeta".

Maybe you don't care about the development of an input method, but you probably need to know a bit when implementing BView-derived classes. Your own class needs to deal with internationalized text, such as a word editor or a web page etc. Although the BTextView class would do most of the work for you, BTextView is a huge class, sometime you need to write a custom and flexible class for other purposes.

We'll explain that in two parts. Part 1 will show you how to write an input aware application, then we'll see how to write an input method at part 2.

Part 1: Input method aware derived class from BView

The tutorial is based on a class I wrote a long time ago, the contents of its header file is shown below:

#ifndef __SINPUT_AWARE_STRING_VIEW_H__
#define __SINPUT_AWARE_STRING_VIEW_H__

#include <SupportDefs.h>
#include <Messenger.h>
#include <Input.h>

#include <besavager/StringView.h>


class SInputAwareStringView : public SStringView {
public:
        SInputAwareStringView(BRect frame, const char *name,
                              uint32 resizeMask = B_FOLLOW_LEFT | B_FOLLOW_TOP,
                              uint32 flags = B_WILL_DRAW | B_FRAME_EVENTS);

        virtual void MessageReceived(BMessage *message);

private:
        BMessenger msgr_input;
        bool im_started;

        void IMStarted(BMessage *message);
        void IMStopped();
        void IMChanged(BMessage *message);
        void IMLocationRequest(BMessage *message);
};

#endif /* __SINPUT_AWARE_STRING_VIEW_H__ */

Section 1.1 The B_INPUT_METHOD_AWARE flag

If BView's flags contains B_INPUT_METHOD_AWARE, it means the B_INPUT_METHOD_EVENT message generated by the active input method will be processed by the MessageReceived() method, otherwise the input server will popup a window to deal with the message then send a message (B_KEY_DOWN) to the application later (This can cause strange issues in newer versions of BeOS, like Dano/Zeta etc.).

So, the derived class should do something like below in its constructor:

SInputAwareStringView::SInputAwareStringView(BRect frame, const char *name, uint32 resizeMask, uint32 flags)
        : SStringView(frame, name, NULL, NULL, 0, NULL, resizeMask, flags | B_INPUT_METHOD_AWARE), im_started(false)
{
}

Section 1.2 The B_KEY_DOWN message

The B_KEY_DOWN message is the most important keyboard event. The input server sends this message to the active view of the running application when its flags don't contain B_INPUT_METHOD_AWARE.

The message contains the fields shown below (from the Be Book):

FieldType codeDescription
"when"B_INT64_TYPEEvent time, in microseconds since 01/01/70
"key"B_INT32_TYPEThe code for the physical key that was pressed, you can find out how to get the key by the functions by "key" shown below
"be:key_repeat"B_INT32_TYPEThe "iteration number" of this key down.
"modifiers"B_INT32_TYPEThe modifier keys that were in effect at the time of the event.
"states"B_UINT8_TYPEThe state of all keys at the time of the event.
"byte"[3]B_INT8_TYPEThe UTF-8 data that's generated, it contains three bytes.
"bytes"B_STRING_TYPEThe UTF-8 string that's generated. (The string usually contains a single character, and usually contains words when it was sent by input server as the B_INPUT_METHOD_EVENT handled.)
"raw_char"B_INT32_TYPEModifier-independent ASCII code for the character.

A perfect derived class should pay attention to the contents of the field named "bytes", then the field named "byte"[3] if "bytes" doesn't exist, then lastly "raw_char".

Section 1.3 The B_INPUT_METHOD_EVENT message

The input server sends this message to the view when the input method did some work. The message will be ignored if the view's flags doesn't contain B_INPUT_METHOD_AWARE. The paragraphs shown below are quoted from the Be Book.

Each B_INPUT_METHOD_EVENT message contains a be:opcode field (an int32 value) indicating the kind of event:

ValueDescription
B_INPUT_METHOD_STARTEDIndicates that a new input transaction has begun.
B_INPUT_METHOD_STOPPEDIndicates that the transaction is over.
B_INPUT_METHOD_CHANGEDIndicates that the state of transaction is changed.
B_INPUT_METHOD_LOCATION_REQUESTIndicates that the input method asking for the on-screen location of each character.

In addition, except B_INPUT_METHOD_STOPPED, the special fields contained in each other kind.

The B_INPUT_METHOD_STARTED contains fields shown below:

FieldType codeDescription
"be:reply_to"B_MESSENGER_TYPEThe messenger to communicate with you during the transaction

The messenger pointed by "be:reply_to" usually for replying the location of characters to input method, you can also use it to send a B_INPUT_METHOD_STOPPED kind message or others to stop the transaction.

The B_INPUT_METHOD_CHANGED contains fields shown below:

FieldType codeDescription
"be:string"B_STRING_TYPEThe text the user is currently entering; the receiver will display it at the current insertion point. BTextView also highlights the text in blue to show that it's part of a transitory transaction.
"be:selection"B_INT32_TYPEA pair of B_INT32_TYPE offsets in bytes into the be:string if part of be:string is current selected. BTextView highlights this selection in red instead of drawing it in blue.
"be:clause_start"B_INT32_TYPEZero or more offsets into the be:string for handling languages (such as Japanese) that separate a sentence or phrase into numerous clauses. An equal number of be:clause_start and be:clause_end pairs delimit these clauses; BTextView separates the blue/red highlighting wherever there is a clause boundary.
"be:clause_end"B_INT32_TYPEZero or more offsets into be:string; there must be as many be:clause_end entries as there are be:clause_start.
"be:confirmed"B_BOOL_TYPETrue when the user has entered and "confirmed" the current string and wishes to end the transaction. BTextView unhighlights the blue/red text and waits for a B_INPUT_METHOD_STOPPED (to close the transaction) or another B_INPUT_METHOD_CHANGED (to start a new transaction immediately).

When the kind of B_INPUT_METHOD_EVENT is B_INPUT_METHOD_LOCATION_REQUEST, the derived class should reply the message contains the fields shown below to the messenger that pointed by "be:reply_to" in the B_INPUT_METHOD_STARTED kind:

FieldType codeDescription
"be:opcode"B_INT32_TYPEMust set to B_INPUT_METHOD_LOCATION_REQUEST.
"be:location_reply"B_POINT_TYPEThe coordinates of each UTF-8 character (there should be one be:location_reply for every character in be:string, and the character maybe contains more than one byte) relative to the display (not your view or your window).
"be:height_reply"B_FLOAT_TYPEThe height of each character(maybe contains more than on byte) in be:string.

Section 1.4 Example

Now, we will start our practice if you are ready for coding.

void
SInputAwareStringView::MessageReceived(BMessage *message)
{
        switch(message->what)
        {
                case B_INPUT_METHOD_EVENT: // input method event received
                        {
                                int32 op_code; // the kind of message
                                if(message->FindInt32("be:opcode", &op_code) != B_OK) break;

                                switch(op_code)
                                {
                                        case B_INPUT_METHOD_STARTED: // prepare for input transaction
                                                IMStarted(message);
                                                break;

                                        case B_INPUT_METHOD_STOPPED: // stop the transaction and clear something
                                                IMStopped();
                                                break;

                                        case B_INPUT_METHOD_CHANGED: // displaying characters when the string entering changed
                                                IMChanged(message);
                                                break;

                                        case B_INPUT_METHOD_LOCATION_REQUEST: // reply the lcoation of characters
                                                IMLocationRequest(message);
                                                break;

                                        default: // call member function of base class
                                                SStringView::MessageReceived(message);
                                }
                        }
                        break;

                default: // call member function of base class
                        SStringView::MessageReceived(message);
        }
}


void
SInputAwareStringView::IMStarted(BMessage *message)
{
        // here we store the messenger that we would communicate with the input method during the transaction
        im_started = (message->FindMessenger("be:reply_to", &msgr_input) == B_OK);
}


void
SInputAwareStringView::IMStopped()
{
        msgr_input = BMessenger();
        SetText(NULL); // clear the text had shown
        im_started = false;
}


void
SInputAwareStringView::IMChanged(BMessage *message)
{
        if(!im_started) return;

        const char *im_string = NULL;
        message->FindString("be:string", &im_string);
        if(im_string == NULL) im_string = "";

        int32 start = 0, end = 0;

        BList color_list;

        for(int32 i = 0;
                message->FindInt32("be:clause_start", i, &start) == B_OK &&
                message->FindInt32("be:clause_end", i, &end) == B_OK; i++)
        {
                // handle clauses
                if(end > start) // visible
                {
                        // set the background of clauses to be blue, the offsets are in bytes.
                        // for example, the "[/]" means the start/end offset of that.
                        // string: T h i s i s a [w o r d] .
                        // offset: 0 1 2 3 4 5 6 7  8 9 10 11
                        // "be:clause_start" = 7
                        // "be:clause_end" = 11
                        s_string_view_color *color = new s_string_view_color;
                        s_rgb_color_setto(&color->color, 0, 0, 0);
                        s_rgb_color_setto(&color->background, 152, 203, 255);
                        color->draw_background = true;
                        color->start_offset = start;
                        color->end_offset = end - 1;
                        color_list.AddItem((void*)color);
                }
        }

        for(int32 i = 0;
                message->FindInt32("be:selection", i * 2, &start) == B_OK &&
                message->FindInt32("be:selection", i * 2 + 1, &end) == B_OK; i++)
        {
                // handle selection
                if(end > start) // visible
                {
                        // set the background of clauses to be red, the offsets are in bytes.
                        // for example, the "[/]" means the start/end offset of that.
                        // string: T h i s i s a [w o r d] .
                        // offset: 0 1 2 3 4 5 6 7  8 9 10 11
                        // "be:selection"[0] = 7
                        // "be:selection"[1] = 11
                        s_string_view_color *color = new s_string_view_color;
                        s_rgb_color_setto(&color->color, 0, 0, 0);
                        s_rgb_color_setto(&color->background, 255, 152, 152);
                        color->draw_background = true;
                        color->start_offset = start;
                        color->end_offset = end - 1;
                        color_list.AddItem((void*)color);
                }
        }

        if(!color_list.IsEmpty()) // you don't need to know what it is below
        {
                int32 n = color_list.CountItems();
                s_string_view_color *colors = new s_string_view_color[n];

                for(int32 i = 0; i < n; i++)
                {
                        s_string_view_color *color = (s_string_view_color*)color_list.ItemAt(i);
                        if(color)
                        {
                                if(colors)
                                {
                                        s_rgb_color_setto(&colors[i].color, color->color);
                                        s_rgb_color_setto(&colors[i].background, color->background);
                                        colors[i].draw_background = color->draw_background;
                                        colors[i].start_offset = color->start_offset;
                                        colors[i].end_offset = color->end_offset;
                                }

                                delete color;
                        }
                }

                color_list.MakeEmpty();

                SetText(im_string, colors, n);
                if(colors) delete[] colors;
        }
        else
        {
                SetText(im_string);
        }
}


void
SInputAwareStringView::IMLocationRequest(BMessage *message)
{
        if(!im_started) return;

        const char *im_string = Text();
        if(im_string == NULL || *im_string == 0) return;

        BMessage reply(B_INPUT_METHOD_EVENT); // the message for reply
        reply.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST); // must set this

        BPoint left_top = ConvertToScreen(BPoint(0, 0)); // first we convert the (0, 0) to the coordinate relative to the display

        int32 offset = 0;
        uint32 index = 0;

        while(true)
        {
                if(__utf8_char_at(im_string, index, &offset)) // the (index + 1)th UTF-8 character
                {
                        BRect rect = TextRegion(offset, offset + 1).Frame(); // get the region of text relative to the view
                        if(!rect.IsValid()) break;

                        // convert to the coordinate relative to the display
                        BPoint pt = left_top;
                        pt += rect.LeftTop();

                        reply.AddPoint("be:location_reply", pt); // the left-top point of each character
                        reply.AddFloat("be:height_reply", rect.Height()); // the height of each character
                }
                else
                {
                        break;
                }

                index++;
        }

        // send the message to the input method within input server
        msgr_input.SendMessage(&reply);
}