Developing IME Aware Applications

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);
}