本文討論輸入法模組的 GUI Request: Menu Selection window 的 API 介面。
閱讀本文時，建議您可以同時參考:

http://xcin.linux.org.tw/xcin-2.5/xcin_menu.png

可以幫助您理解 :-))


1. 簡介:

自 xcin-2.5.2 起輸入法模組開始支援 GUI Request 功能，它是提供輸入法模組
開啟其他視窗以顯示資料的方式。由於 xcin 主視窗的空間有限，這對於需要顯
示複雜資訊的輸入法模組而言可能不夠用，因而有這樣的設計。

GUI Request 是與 winlist 架構整合在一起的，它可以是各種型態或功能的視窗，
凡是可供輸入法模組用來顯示其資訊的，我都稱為 GUI Request 視窗。其在 winlist
的定義中， wid 是設成 WID_OVERSPOT 的 (見 include/gui.h)。

而在目前，我只設計了一個 GUI Request 的視窗供輸入法模組使用，我稱它為
Menu Selection window (簡稱 menusel), 它的功能就是顯示出一個選單，其
source 就在 gui_menusel.c 中。未來若還有需要，可以再加入其他的 GUI Request
視窗。

輸入法模組可以透過 inpinfo 結構 (見 include/module.h) 來啟動、操作 GUI 
Request 視窗。由於 inpinfo 結構屬於 IMC 的一部分 (有關 IMC 介紹，請見我
上一篇 post: xcin-2.5 & OverTheSpot), 因此，若在 XCIN_SINGLE_IMC OFF 的
狀態下，不同的 IMC 可以分別有它自己的一套 GUI Request window 群。這意思
是說，您可以做到在 XIM client A 開啟一個 GUI Request window, 而在 client
B 開啟另一個 GUI Request window, 二者互相獨立。當然，如果在 XCIN_SINGLE_IMC
ON 的狀態下，所有的 clients (或 IC) 分享同一個 IMC, 則它底下的 GUI Request
window 也同樣被所有的 clients 分享。

在同一個 IMC (或 inpinfo) 下，輸入法模組可以開啟多個 GUI Request window, 
而且開啟的種類並沒有限制。當然，目前只有一種，即 menusel, 因此若您高興的
話，也可以同時開啟兩三個選單來用。目前我是設定每個 IMC 最多只能開出五個
GUI Request window, 我想這樣應該是很夠用了吧。

PS. 寫到這裏，我又發現 xcin-2.5.2-pre1 有 bug, 在 gui.c, update_gui_request()
    中沒有檢查同一個 IMC 中開啟的 GUI Request window 數目是否已達到上限，
    這部分我會馬上進行修正。


2. 啟動 GUI Request:

在 inpinfo 結構中，有一個 greq_t *qui_request 的指標，它就是用來操作 GUI
Request 用的，其定義如下:

typedef struct greq_s {
    short reqid;			/* request id */
    short type;				/* request type */
    ubyte_t deleted_return;		/* request window is deleted */
    void *data;				/* request data */
    struct greq_s *next;
} greq_t;

其中 type 就是用來指定您要啟動那一種 GUI Request window, 對於 menusel 而
言，您要在這裏設為 GREQ_MENUSEL。

之前說過，輸入法模組可以在同一個 IMC 中開啟多個 GUI Request window, 在這
裏每一個這樣的 window 就是一個 greq_t，然後再一個個用 link list 串起來，
最後的那一個的 next 必須設為 NULL。而不同的 window 就以 reqid 來區分。這
個 reqid 可以是大於等於 1 的任何數字，由輸入法模組自己去設，只要每個 greq_t
的 reqid 都不一樣即可。

各 GUI Request window 真正的資料結構是存放在 data 所指的區域。由於不同的
GUI Request 其所需的資料結構不見得相同，像 menusel 的需的資料結構就稱為
greq_menusel_t (見後述), 因此這裏我是以一個 void * 指標來代表，因此您要
使用那一個 GUI Request, 就必須將 data 指向正確的資料結構，不可弄錯了。
有人曾建議我說這裏最好是弄成 union 指標, 但老實說我有點笨，不曉得該怎麼
弄 :-)) 所以如有人知道的話請教我一下 :-))

輸入法可以自由控制 greq_t 的 window 的產生與關閉 (destroy), 若要產生它，
就多串上一個 greq_t 即可，若要關閉它，就將相對應的 greq_t 從 list 拿掉即
可。 xcin 完全不會替您維護此 list 的內容。因此，若此 inpinfo 要轉讓給其
他輸入法模組使用 (也就是本輸入法模組的 xim_end 函式被呼叫時)，則必須自己
將已開啟的所有 greq_t 拿掉 (或 free 掉)，否則的話將會有 memory leakage 
的問題。

但是，在有些情況下 xcin 會主動關掉目前正運作中的 GUI Request window, 例如:

	1. 使用者用滑鼠點了視窗上的按鈕將它關掉時。
	2. 使用者突然按 ctrl+alt+... 切換到別的輸入法時。

這時候， xcin 只會將 GUI Request window 關掉，但仍然不會去動 inpinfo 裏頭
的 greq_t, 它只會在已視窗已關掉的 greq_t 裏頭的 deleted_return 設為 1, 藉
此通知輸入法模組說該視窗已關掉了，您必須做後續處理。通常的情況是，輸入法
模組必須馬上將此 greq_t 自 list 中拿掉。若不去理會，那多半會跑出一些很好
玩的結果，然後我們就會聽到使用者的抱怨了 :-)) 因此，每次當輸入法模組的
keystroke 函式被執行時，它必須檢查每個 greq_t 中的 deleted_return 是不是
有異動。

至於是否可以暫時隱藏、顯示 GUI Request window (即 Map, Unmap), 則視該 window
的定義而定，並非所有的 window 都需要此功能。例如 menusel 就沒有。

綜上所述，如果輸入法模組決定要開啟兩個 GUI Request 視窗 (假設都是 menusel),
則程式這樣寫即可:

greq_t *new_gui_request(int type)
{
    static int reqid=0
    greq_t *greq;

    greq = malloc(sizeof(greq_t));
    greq->reqid = (++reqid);
    greq->type  = type
    greq->deleted_return = (ubyte_t)0;
    greq->next = NULL;
    switch (type) {
    case GREQ_MENUSEL:
	greq->data = (void *)malloc(sizeof(greq_menusel_t);
	.......
	break;
    }
    return greq;
}

inpinfo->gui_request = new_gui_request(GREQ_MENUSEL);
inpinfo->gui_request->next = new_gui_request(GREQ_MENUSEL);
/* 這裏就是開兩個 GUI Request, 用 link list 串起來。若要關掉任何一個，將它
   從 list 裏拿掉即可 */


3. Menu Selection Window:

menusel 所需的資料結構如下:

typedef struct {
    unsigned short n_item;              /* number of item lists. */
    unsigned short head_item;           /* head index of the item lists. */
    unsigned short n_sel;               /* number of selection keys. */
    unsigned short n_sel_return;        /* num of selection returned by xcin. */
    char *selkeys;                      /* the selection keys. */
    unsigned short focus_item;          /* index of the focused item. */
    unsigned short focus_elem;          /* index of the focused element. */
    ubyte_t enable_focus_elem;          /* use focus element facility or not. */
    menu_item_t *item;                  /* the item lists. */
} greq_menusel_t;

Menusel 視窗分兩部分，左半邊稱為 item head, 右半邊稱為 item list, 如下圖:

	head1  |  item1-1  item1-2  item1-3 ....
	head2  |  item2-1  item2-2  item2-3 ....
	.....  |  .......  .......  .......

該視窗的大小是固定的，長度等於 xcin 主視窗的長度，高度最多是五列，如果
n_item 的數目小於五的話，則列數就會等於 n_item 的值。 n_item 可以超過 5,
而視窗顯示時是從 head_item 開始作第一列顯示。例如，您有一筆資料總共有 8
個 item list, 但 head_item 是設成 2, 則顯示如下:

   (item head)   (item elements)
	head2  |  item2-1  item2-2  item2-3 ....
	head3  |  item3-1  item3-2  item3-3 ....
	head4  |  item4-1  item4-2  item4-3 ....
	head5  |  item5-1  item5-2  item5-3 ....
	head6  |  item6-1  item6-2  item6-3 ....

視窗本身不會去攔捷處理任何輸入字鍵，因此，如果您要實作 menu 上下捲頁的功
能，您可以在 module function keystroke() 接到上鍵時，將 head_item 減 1,
接到下鍵時將它加 1 即可。

n_sel 是指在一個 item list 中最多有幾個選擇項，但它只有在 selkeys 有設定
時才有用， selkeys 是設定顯示出的選擇鍵，而 n_sel 就是 selkeys 的數目。
例如，我可以設定:

	greq_menusel->selkeys = "asdfghjkl;";
	greq_menusel->n_sel = strlen(selkeys);

如此顯示出來的 item list 就是:

	headN  |  a itemN-a  s itemN-s  d itemN-d  ....

注意每個 item 前面都會伴隨一個 selkey, 就是您在 greq_menusel->selkeys 中
所設的內容。當然您在這裏可以設成 NULL, 則預設就直接以 "1234567890" 來使用。
然而， menusel 的寬度不見得足以容納所有的 item element, 如視窗畫不下時，xcin
會透過 n_sel_return 回報說實際上只畫了幾個 element，故輸入法模組可以藉此做
出必要的調整。

focus_item 是指那一個 item 要特別以反白標示，就好像游標停在那一個項目上一
樣。在這裏反白會標示在該 item list 的 head 上，圖示如下 (標示在第二個 item):

	head1  |  item1-1  item1-2  item1-3  ....
       <head2> |  item2-1  item2-2  item2-3  ....
	.....  |  .......  .......  .......

如果將 enable_focus_elem 設為 1 時，則反白標示也可以標示在一個 item list 中
的某個 element 上 (由 focus_elem 來指定)，例如 (標示在第二個 item 的第三個 
element):

	head1  |  item1-1  item1-2  item1-3  ....
       <head2> |  item2-1  item2-2 <item2-3> ....
	.....  |  .......  .......  .......

若將 enable_focus_elem 設為 0, 則 item list 中不會有 element 的反白標示，且
focus_elem 也不會有作用。

最後的 menu_item_t *item 是指向每一個 item list 的陣列，如:

	item[0].title     ==> head1
	       .elements  ==> item1-1 item1-2 item1-3 ....
	       .........
	item[1].title     ==> head2
	       .elements  ==> item2-1 item2-2 item2-3 ....
	       .........


4. Elements in each items:

每個 item list 的細部定義如下:

typedef struct {
    wch_t *title;                       /* title of the item list. */
    wch_t *elements;                    /* elements of the item list. */
    ubyte_t *elem_group;                /* grouping info. of the elements. */
    unsigned short n_elem;              /* size of "elements" array. */
    unsigned short head_idx;            /* head index of the item list. */
} menu_item_t;

其中 title 就是上面那幾個圖中某一 item 的 head, 而它的 element list 則是
設在 wch_t *elements 中， n_elem 是 elements 陣列的個數。如果 elem_group
是 NULL, 則每個 elements 的陣列原素就是一個選項，如果 elem_group 不是 NULL,
則可以用它來指定多個 elements 陣列原素合成一個選項，例如:

	elem_group = NULL		/* 每個 element 原素都是各別選項 */
	title = head1
	elements = {A}, {B}, {C}, {D}
	-------------------------------------------------------------------
	head1  |  1.{A}  2.{B}  3.{C}  4.{D}

	elem_group = ....		/* 可以指定若干 element 原素合成一 */
	title = head2			/* 個選項			   */
	elements = {A}, {B}, {C}, {D}
	-------------------------------------------------------------------
	head2  |  1.{AB}  2.{C}  3.{D}

至於 elem_group 的用法，與 inpinfo 中 lcch_grouping 與 mcch_grouping 的用
法一模一樣，您可以參考 xcin-2.5/doc/internal/module 有關這兩個東東的使用
說明，即可了解。

最後一個 head_idx 它的作用與先前提的 gui_menusel_t->head_item 一樣，也是
告訴 xcin 說此 item list 要從那一個 element 開始畫起。

PS. 寫到這裏，我突然想到， greq_menusel_t 裏頭的 n_sel_return 其實應該要
    放在 menu_item_t 裏頭，這樣才合理，因為每一個 item lists 的長度可能不
    見得一樣 (就算它們的 element 數都相同)，有可能 item A 畫到第十個 element
    都還沒超過長度，但 item B 畫到第八個就超過了，因此必須每個 item 都回
    報一個 n_sel_return。這部分我再來改。

最後提一點: 上述所提的所有的 index, 如 head_item, focus_item, focus_elem,
head_idx 等，全部都是由 1 開始計數，而不是從 0 開始，請不要弄錯了。


5. Example:

底下這段 example code, 就是我在那個 screen shot 中的 menusel window 。這段
code 寫得很醜，並沒有放在 xcin-2.5.2-pre1 裏頭，只是純測試用的，各位可以參
考。

static greq_menusel_t *data;
static menu_item_t mitem[3];
static wch_t mtitle1[10], element1[20];
static wch_t mtitle2[10], element2[20];
static wch_t mtitle3[10], element3[20];
static ubyte_t elem_group[10];

/* initialize gui_request */
inpinfo->gui_request = malloc(sizeof(greq_t));
inpinfo->gui_request->reqid = 1;
inpinfo->gui_request->type = GREQ_MENUSEL;
inpinfo->gui_request->deleted_return = (ubyte_t)0;
inpinfo->gui_request->next = NULL;
data = malloc(sizeof(greq_menusel_t));
inpinfo->gui_request->data = (void *)data;

/* initialize menusel */
data->n_item = 3;
data->head_item = 1;
data->n_sel = 10;
data->n_sel_return = 10;
data->selkeys = NULL;
data->focus_item = 1;
data->focus_elem = 1;
data->enable_focus_elem = (ubyte_t)1;
data->item = mitem;

/* initialize item 1 */
mitem[0].title = mtitle1;
	big5str_to_wch(mtitle1, "台灣大學");
        mitem[0].elements = element1;
        mitem[0].elem_group = NULL;
        mitem[0].n_elem = 4;
        mitem[0].head_idx = 1;
	big5str_to_wch(element1, "現在時間");

/* initialize item 2 */
mitem[1].title = mtitle2;
	big5str_to_wch(mtitle2, "物理系");
        mitem[1].elements = element2;
        mitem[1].elem_group = NULL;
        mitem[1].n_elem = 8;
        mitem[1].head_idx = 1;
	big5str_to_wch(element2, "我是居士正在測試");

/* initialize item 3 */
mitem[2].title = mtitle3;
	big5str_to_wch(mtitle3, "謝東翰");
        mitem[2].elements = element2;
        mitem[2].elem_group = elem_group;
        mitem[2].n_elem = 8;
        mitem[2].head_idx = 1;
        mitem[2].elem_group[0] = (ubyte_t)4;
        mitem[2].elem_group[1] = (ubyte_t)2;
        mitem[2].elem_group[2] = (ubyte_t)2;
        mitem[2].elem_group[3] = (ubyte_t)2;
        mitem[2].elem_group[4] = (ubyte_t)4;


6. 結語:

看到了這，您是不是已頭昏腦脹了呢？我是已經寫得頭昏腦脹了 :-))
我想我大概可以很自豪地表示， xcin 可以算是自由軟體界中寫得最 ugly 的 XIM
server 了 ....

我在上述提到的若干有待改進的問題，以及上一篇 xcin & OverTheSpot 中提到
的問題，我會盡快做出 patch release 出來。



T.H.Hsieh
