2015年8月1日 星期六

在 Linux 版的 Sublime Text 3 中,使用 gcin 輸入中文

 今天試用了 Sublime Text 3, 要支援中文輸入,得用 fcitx 輸入法,但是實在不習慣它的拆字界面,還是覺得使用 gcin 比較順手。但知道憑一己之力,不知那時才有成功的可能。很幸運的是,爬文幾個小時候,發現在網友的互相討論之下,已有了解決方式。
參考連結如下。

http://www.sublimetext.com/forum/viewtopic.php?f=3&t=7006&start=10#p41343
http://www.sublimetext.com/forum/viewtopic.php?f=3&t=7006&start=20#p61143
(以上為 Sublime Text forum)
http://whitequark.org/blog/2014/04/14/xcompose-support-in-sublime-text/

PS: 我使用的是 Gentoo Linux

原始碼
    /*
    sublime-imfix.c
    Use LD_PRELOAD to interpose some function to fix sublime input method support for linux.
    By Cjacker Huang <jianzhong.huang at i-soft.com.cn>
    By whitequark@whitequark.org

    How to compile:
    gcc -shared -o libsublime-imfix.so sublime_imfix.c  `pkg-config --libs --cflags gtk+-2.0` -fPIC
    How to use:
    LD_PRELOAD=./libsublime-imfix.so sublime_text

    Changes:
    2014 06-09
    1, Fix cursor position update for sublime text 3.
    2, Combine the codes from whitequark(fix for xim immodule) and add cursor update support for XIM immodule.
    */

    /*for RTLD_NEXT*/
    #define _GNU_SOURCE

    #include <gtk/gtk.h>
    #include <gdk/gdkx.h>
    #include <assert.h>
    #include <dlfcn.h>
    #include <stdio.h>
    #include <string.h>
    #include <gtk/gtk.h>
    #include <X11/Xlib.h>
    #include <X11/Xutil.h>

    #ifdef VERBOSE
    #define DEBUG(fmt, ...) do { \
        FILE* err = fopen("/tmp/libsublime-immethod-fix.log", "a"); \
        if (err) { \
          fprintf(err, fmt, __VA_ARGS__); \
          fclose(err); \
        } \
      } while(0)
    #else
    #define DEBUG(fmt, ...)
    #endif

    typedef GdkSegment GdkRegionBox;

    struct _GdkRegion
    {
      long size;
      long numRects;
      GdkRegionBox *rects;
      GdkRegionBox extents;
    };

    GtkIMContext *local_context;

    //this func is interposed to support cursor position update.
    void gdk_region_get_clipbox (const GdkRegion *region,
                GdkRectangle    *rectangle)
    {
      g_return_if_fail (region != NULL);
      g_return_if_fail (rectangle != NULL);

      rectangle->x = region->extents.x1;
      rectangle->y = region->extents.y1;
      rectangle->width = region->extents.x2 - region->extents.x1;
      rectangle->height = region->extents.y2 - region->extents.y1;
      GdkRectangle rect;
      rect.x = rectangle->x;
      rect.y = rectangle->y;
      rect.width = 0;
      rect.height = rectangle->height;
      //The caret width is 2 in sublime text 2
      //And is 1 in sublime text 3.
      //Maybe sometimes we will make a mistake, but for most of the time, it should be the caret.
      if((rectangle->width == 2 || rectangle->width == 1)  && GTK_IS_IM_CONTEXT(local_context)) {
            gtk_im_context_set_cursor_location(local_context, rectangle);
      }
    }

    //this is needed, for example, if you input something in file dialog and return back the edit area
    //context will lost, so here we set it again.
    static GdkFilterReturn event_filter (GdkXEvent *xevent, GdkEvent *event, gpointer im_context)
    {
        XEvent *xev = (XEvent *)xevent;
        if(xev->type == KeyRelease && GTK_IS_IM_CONTEXT(im_context)) {
           GdkWindow * win = g_object_get_data(G_OBJECT(im_context),"window");
           if(GDK_IS_WINDOW(win))
             gtk_im_context_set_client_window(im_context, win);
        }
        return GDK_FILTER_CONTINUE;
    }

    void gtk_im_context_set_client_window (GtkIMContext *context,
              GdkWindow    *window)
    {
        GtkIMContextClass *klass;
        g_return_if_fail (GTK_IS_IM_CONTEXT (context));
        klass = GTK_IM_CONTEXT_GET_CLASS (context);
        if (klass->set_client_window)
            klass->set_client_window (context, window);

        //below is our interposed codes to save the context to local_context.
        if(!GDK_IS_WINDOW (window))
            return;
        g_object_set_data(G_OBJECT(context),"window",window);
        int width = gdk_window_get_width(window);
        int height = gdk_window_get_height(window);
        if(width != 0 && height !=0) {
            gtk_im_context_focus_in(context);
            local_context = context;
        }
        //only add this event_filter when using 'fcitx' immodule.
        //for xim immodule, this function is as same as original from gtk2.
        const gchar * immodule = g_getenv("GTK_IM_MODULE");
        if(immodule && !strcmp(immodule, "fcitx")) {
            gdk_window_add_filter (window, event_filter, context);
        }
    }

    /*below codes is from whitequark, fix for xim immodule */

    /* See gtkimcontextxim.c */
    GType gtk_type_im_context_xim = 0;

    #define GTK_TYPE_IM_CONTEXT_XIM            (gtk_type_im_context_xim)
    #define GTK_IM_CONTEXT_XIM(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_IM_CONTEXT_XIM, GtkIMContextXIM))
    #define GTK_IM_CONTEXT_XIM_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_IM_CONTEXT_XIM, GtkIMContextXIMClass))
    #define GTK_IS_IM_CONTEXT_XIM(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_IM_CONTEXT_XIM))
    #define GTK_IS_IM_CONTEXT_XIM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_IM_CONTEXT_XIM))
    #define GTK_IM_CONTEXT_XIM_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_IM_CONTEXT_XIM, GtkIMContextXIMClass))

    typedef struct _GtkIMContextXIM       GtkIMContextXIM;
    typedef struct _GtkIMContextXIMClass  GtkIMContextXIMClass;

    struct _GtkIMContextXIMClass
    {
      GtkIMContextClass parent_class;
    };

    typedef struct _StatusWindow StatusWindow;
    typedef struct _GtkXIMInfo GtkXIMInfo;

    struct _GtkIMContextXIM
    {
      GtkIMContext object;

      GtkXIMInfo *im_info;

      gchar *locale;
      gchar *mb_charset;

      GdkWindow *client_window;
      GtkWidget *client_widget;

   /* The status window for this input context; we claim the
    * status window when we are focused and have created an XIC
    */
      StatusWindow *status_window;

      gint preedit_size;
      gint preedit_length;
      gunichar *preedit_chars;
      XIMFeedback *feedbacks;

      gint preedit_cursor;

      XIMCallback preedit_start_callback;
      XIMCallback preedit_done_callback;
      XIMCallback preedit_draw_callback;
      XIMCallback preedit_caret_callback;

      XIMCallback status_start_callback;
      XIMCallback status_done_callback;
      XIMCallback status_draw_callback;

      XIMCallback string_conversion_callback;

      XIC ic;

      guint filter_key_release : 1;
      guint use_preedit : 1;
      guint finalizing : 1;
      guint in_toplevel : 1;
      guint has_focus : 1;
    };

    static GClassInitFunc orig_gtk_im_context_xim_class_init;
    static GType (*orig_g_type_module_register_type)(
            GTypeModule *, GType, const gchar *, const GTypeInfo *,
            GTypeFlags);
    static gboolean (*orig_gtk_im_context_xim_filter_keypress)(
            GtkIMContext *context, GdkEventKey *event);

    static gboolean
    hook_gtk_im_context_xim_filter_keypress(GtkIMContext *context, GdkEventKey *event)
    {
      GtkIMContextXIM *im_context_xim = GTK_IM_CONTEXT_XIM(context);
      if (!im_context_xim->client_window) {
        DEBUG("im_context_xim == %p\n", im_context_xim);
        DEBUG("event->window == %p\n", event->window);

        gtk_im_context_set_client_window(context, event->window);
      }

      return orig_gtk_im_context_xim_filter_keypress(context, event);
    }

    static void hook_gtk_im_context_xim_class_init (
            GtkIMContextXIMClass *class)
    {
      orig_gtk_im_context_xim_class_init(class, NULL); /* wat? */

      GtkIMContextClass *im_context_class = GTK_IM_CONTEXT_CLASS (class);

      assert(!orig_gtk_im_context_xim_filter_keypress);
      orig_gtk_im_context_xim_filter_keypress = im_context_class->filter_keypress;
      im_context_class->filter_keypress = hook_gtk_im_context_xim_filter_keypress;
      DEBUG("orig_gtk_im_context_xim_filter_keypress: %p\n",
            orig_gtk_im_context_xim_filter_keypress);
    }

    GType
    g_type_module_register_type (GTypeModule *module,
                                 GType parent_type,
                                 const gchar *type_name,
                                 const GTypeInfo *type_info,
                                 GTypeFlags flags)
    {
      if (!orig_g_type_module_register_type) {
        orig_g_type_module_register_type = dlsym(RTLD_NEXT, "g_type_module_register_type");
        assert(orig_g_type_module_register_type);
      }

      if (type_name && !strcmp(type_name, "GtkIMContextXIM")) {
        assert(!orig_gtk_im_context_xim_class_init);
        orig_gtk_im_context_xim_class_init = type_info->class_init;

        assert(sizeof(GtkIMContextXIM) == type_info->instance_size);

        const GTypeInfo hook_im_context_xim_info =
        {
          type_info->class_size,
          type_info->base_init,
          type_info->base_finalize,
          (GClassInitFunc) hook_gtk_im_context_xim_class_init,
          type_info->class_finalize,
          type_info->class_data,
          type_info->instance_size,
          type_info->n_preallocs,
          type_info->instance_init,
        };

        DEBUG("orig_gtk_im_context_xim_class_init: %p\n", orig_gtk_im_context_xim_class_init);

        gtk_type_im_context_xim =
          orig_g_type_module_register_type(module, parent_type,
              type_name, &hook_im_context_xim_info, flags);

        return gtk_type_im_context_xim;
      }

      return orig_g_type_module_register_type(module, parent_type, type_name, type_info, flags);
    }



編譯:
$ gcc -shared -o libsublime-imfix.so sublime-imfix.c  `pkg-config --libs --cflags gtk+-2.0` -fPIC

設定環境變數:
$ export GTK_IM_MODULE=xim

執行:
$ LD_PRELOAD=./libsublime-imfix.so subl3

如上,Sublime Text 3 中文切換,必需將 GTK_IM_MODULE 設成 xim
但是會讓所有的程式,gcin 的輸入視窗都黏在視窗左上角,不會跟著程式的游標 。
只好要用 Sublime Text 時,才設成 xim。終究能夠直接輸入中文,比起以往必需用 copy and paste 來輸入中文,要好得多了。

另外,有下面一些小小的不便。
  • 在輸入中文時,若想用 Backspace 來修正時,無法刪除輸入的字根,反而會刪除編輯的內容。必須按 Escape 鍵,取消所有的輸入,重新輸一次。
  • 無法用 ctrl+, 或 ctrl+. 來輸入中文的豆號和句號。
修改 sublime-text-3.desktop 中的執行指令
Exec=bash -c 'GTK_IM_MODULE=xim LD_PRELOAD=/usr/local/lib/libsublime-imfix.so subl3' %F



關於中文輸入法的環境變數的設定,可以參考
http://moto.debian.tw/viewtopic.php?t=9525

如果是 GTK(gnome) 的程式,可能你沒有設定 GTK_IM_MODULE=gcin, QT 的程式必須在在 qtconfig 設定 XIM over-the-spot。如果你的 QT 支援 QT_IM_MODULE,只要設定 QT_IM_MODULE=gcin就可以了。

1 則留言:

Jim T. Tang 提到...

謝謝您阿吉!

雖然不能用enter輸入字串,還有其他問題,不過已經比複製貼上好多了!
謝謝您!