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):
Field | Type code | Description |
"when" | B_INT64_TYPE | Event time, in microseconds since 01/01/70 |
"key" | B_INT32_TYPE | The 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_TYPE | The "iteration number" of this key down. |
"modifiers" | B_INT32_TYPE | The modifier keys that were in effect at the time of the event. |
"states" | B_UINT8_TYPE | The state of all keys at the time of the event. |
"byte"[3] | B_INT8_TYPE | The UTF-8 data that's generated, it contains three bytes. |
"bytes" | B_STRING_TYPE | The 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_TYPE | Modifier-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:
Value | Description |
B_INPUT_METHOD_STARTED | Indicates that a new input transaction has begun. |
B_INPUT_METHOD_STOPPED | Indicates that the transaction is over. |
B_INPUT_METHOD_CHANGED | Indicates that the state of transaction is changed. |
B_INPUT_METHOD_LOCATION_REQUEST | Indicates 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:
Field | Type code | Description |
"be:reply_to" | B_MESSENGER_TYPE | The 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:
Field | Type code | Description |
"be:string" | B_STRING_TYPE | The 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_TYPE | A 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_TYPE | Zero 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_TYPE | Zero 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_TYPE | True 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:
Field | Type code | Description |
"be:opcode" | B_INT32_TYPE | Must set to B_INPUT_METHOD_LOCATION_REQUEST. |
"be:location_reply" | B_POINT_TYPE | The 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_TYPE | The 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); }