2012年6月18日 星期一

實作 X11 底下的 Popup Menu

Standard
既然要投入 JavaScript 的發展,一個很重要的目標就是讓 JavaScript 能被用來開發 Native 桌面程式。而為了達成這樣的願景,我們的團隊沒日沒夜的發展各類基礎技術和擴充底階 API,甚至是將以 JavaScript 開發整個桌面環境(Desktop Environment, 如:GNOME、KDE、LXDE 或 XFCE 這類專案)為終極目標。

當然,跳出瀏覽器之後的 JavaScript,缺少了繪圖引擎,所以要拿來做圖型化使用者介面,會更為困難。慶幸的是,前些日子發展出的 jsdx-toolkit 已經解決了大多數的問題,除了有 3D、動畫等支援,也已經有了許多現代桌面有的UI元件(如:Entry、Label、Button... 等),以致我們完全可以放心的開發屬於自己的圖型化應用程式。

不過,目前的情況,在手機、平板或是特定用途的嵌入式系統中完全夠用,一旦回到更為複雜的桌面環境下,就會遭遇到許多的問題。像是回到 XWindow 底下後,會遇到許多 X11 的視窗管理機制,其視窗之間錯縱複雜的交互關係,就是我們要處理的。尤其是當我們在開發桌面環境時,就會發現在一般的桌面環境下,使用者可能會同時開無數個視窗和程式,並且隨機又大量的切換使用,這迫使我們必須去修改 jsdx-toolkit 以處理 X11 底下的更多狀況,符合並更完整支援一般桌面環境下的操作習慣。

你可能用過 GTK+/Qt 這類常見的 Toolkit,也知道在 X11 之下有很多種類型的視窗(Window),像是 Dialog、Splash、Menu、Popup Menu 等等,但你可能不知道這些現代 Toolkit 內部做了多少事。事實上,X11 本身雖定義了各類視窗的類型,但實際上 XWindow 和 Window Manager 並不會完全去照定義去處理你的視窗行為,更準確的說法是,X11 HWMH 中的基本定義和我們實際的認知是有出入的,該定義只是說希望達成的行為,但沒說是由誰(XWindow、Window Manage 還是 GUI Toolkit)去處理。

舉例來說,我們現在寫一支程式,該程式不使用任何現代的 GUI Toolkit,然後單純使用 X11 API 把一個 Window 的類型設成 Popup Menu。但是你會發現,它的實際行為並不像我們所想的,失去焦點(Focus)後就會消失,無論你是點擊其他視窗讓他失去焦點,還是採用快速鍵讓他失去焦點。此外,他也仍然會保留 Title bar 裝飾(Decorator),還有會在你的 Panel Taskbar 上出現一個新的 Task。就是這些種種視窗行為,和我們心中所認知的 Popup Menu 有很大的差異。

仔細研究 X11 EWMH Spec 就會發現,對於一個 Popup Menu 的視窗,除了設定類型外,你應該要做更多屬性設定,才會讓它合乎我們預想的行為,而這些東西被分散於 Spec 文件內的各處描述,想要一次性找出來困難重重。此外,失去焦點後要關閉 Popup Menu 視窗的功能,就不僅僅是設定屬性這麼簡單,而是要使用 XGrabPointer 攔截整個畫面的輸入事件(Input event),採用特殊的做法,才能達成。

說了這麼多,既然我們是自己開發一套新的 Toolkit,當然就要來實作一個合乎我們預期行為的 Popup Menu Window,首先在建立視窗並設定為 Popup Menu 類型後,需要做一些設定並欄截事件:
...

    /* Grab pointer */
    int i;
    Window grabWin = -1;
    XGetInputFocus(disp, &grabWin, &i);
    XGrabPointer(disp, grabWin, TRUE,
        ButtonPressMask | ButtonReleaseMask,
        GrabModeAsync, GrabModeAsync,
        None, None, CurrentTime);


    /* Skip Taskbar */
    Atom wm_state = XInternAtom(disp, "WM_STATE", False);
    Atom atom = XInternAtom(disp, "_NET_WM_STATE_SKIP_TASKBAR", False);

    XChangeProperty(disp, win, wm_state,
        XA_ATOM, 32, PropModeAppend,
        (unsigned char *)&atom, 1);


    /* Override redirect */
    XSetWindowAttributes attr;

    attr.override_redirect = True;
    XChangeWindowAttributes(disp, win, CWOverrideRedirect, &attr);


...

接著在監聽 X Event 迴圈處,去檢查是否為 grabWin 傳來的 ButtonRelease Event(意即使用者用點擊了 Popup Menu Window 之外的區域),如果是就停止 Event 的欄截並關閉視窗。
...
    if (xev->type == ButtonRelease && grabWin == xev->xbutton.window) {
        XUngrabPointer(disp, CurrentTime);
        grabWin = -1;

        XUnmapWindow(disp, win);

        continue;
    }
...

後記

經過許多努力,完善各項功能,完全用 JavaScript 打造的桌面環境,指日可待。:-P