diff options
-rw-r--r-- | Filelist | 2 | ||||
-rw-r--r-- | src/Make_cyg_ming.mak | 1 | ||||
-rw-r--r-- | src/Make_morph.mak | 1 | ||||
-rw-r--r-- | src/Make_mvc.mak | 4 | ||||
-rw-r--r-- | src/Make_vms.mms | 6 | ||||
-rw-r--r-- | src/Makefile | 11 | ||||
-rw-r--r-- | src/gui_xim.c | 1777 | ||||
-rw-r--r-- | src/mbyte.c | 1764 | ||||
-rw-r--r-- | src/proto.h | 1 | ||||
-rw-r--r-- | src/proto/gui_xim.pro | 17 | ||||
-rw-r--r-- | src/proto/mbyte.pro | 15 | ||||
-rw-r--r-- | src/version.c | 2 |
12 files changed, 1822 insertions, 1779 deletions
@@ -419,6 +419,7 @@ SRC_UNIX = \ src/gui_gtk_x11.c \ src/gui_gtk_res.xml \ src/gui_motif.c \ + src/gui_xim.c \ src/gui_xmdlg.c \ src/gui_xmebw.c \ src/gui_xmebw.h \ @@ -442,6 +443,7 @@ SRC_UNIX = \ src/proto/gui_gtk_x11.pro \ src/proto/gui_gtk_gresources.pro \ src/proto/gui_motif.pro \ + src/proto/gui_xim.pro \ src/proto/gui_xmdlg.pro \ src/proto/gui_x11.pro \ src/proto/if_xcmdsrv.pro \ diff --git a/src/Make_cyg_ming.mak b/src/Make_cyg_ming.mak index d0b82f1b80..39f1e847d6 100644 --- a/src/Make_cyg_ming.mak +++ b/src/Make_cyg_ming.mak @@ -741,6 +741,7 @@ OBJ = \ $(OUTDIR)/findfile.o \ $(OUTDIR)/fold.o \ $(OUTDIR)/getchar.o \ + $(OUTDIR)/gui_xim.o \ $(OUTDIR)/hardcopy.o \ $(OUTDIR)/hashtab.o \ $(OUTDIR)/highlight.o \ diff --git a/src/Make_morph.mak b/src/Make_morph.mak index 4efc1d67ab..ce615b2e37 100644 --- a/src/Make_morph.mak +++ b/src/Make_morph.mak @@ -61,6 +61,7 @@ SRC = arabic.c \ findfile.c \ fold.c \ getchar.c \ + gui_xim.c \ hardcopy.c \ hashtab.c \ highlight.c \ diff --git a/src/Make_mvc.mak b/src/Make_mvc.mak index aec94a981d..8559c9dc29 100644 --- a/src/Make_mvc.mak +++ b/src/Make_mvc.mak @@ -761,6 +761,7 @@ OBJ = \ $(OUTDIR)\findfile.obj \ $(OUTDIR)\fold.obj \ $(OUTDIR)\getchar.obj \ + $(OUTDIR)\gui_xim.obj \ $(OUTDIR)\hardcopy.obj \ $(OUTDIR)\hashtab.obj \ $(OUTDIR)\highlight.obj \ @@ -1591,6 +1592,8 @@ $(OUTDIR)/fold.obj: $(OUTDIR) fold.c $(INCL) $(OUTDIR)/getchar.obj: $(OUTDIR) getchar.c $(INCL) +$(OUTDIR)/gui_xim.obj: $(OUTDIR) gui_xim.c $(INCL) + $(OUTDIR)/hardcopy.obj: $(OUTDIR) hardcopy.c $(INCL) version.h $(OUTDIR)/hashtab.obj: $(OUTDIR) hashtab.c $(INCL) @@ -1908,6 +1911,7 @@ proto.h: \ proto/filepath.pro \ proto/findfile.pro \ proto/getchar.pro \ + proto/gui_xim.pro \ proto/hardcopy.pro \ proto/hashtab.pro \ proto/highlight.pro \ diff --git a/src/Make_vms.mms b/src/Make_vms.mms index e5ff857518..1194acdd43 100644 --- a/src/Make_vms.mms +++ b/src/Make_vms.mms @@ -334,6 +334,7 @@ SRC = \ findfile.c \ fold.c \ getchar.c \ + gui_xim.c \ hardcopy.c \ hashtab.c \ highlight.c \ @@ -445,6 +446,7 @@ OBJ = \ findfile.obj \ fold.obj \ getchar.obj \ + gui_xim.obj \ hardcopy.obj \ hashtab.obj \ highlight.obj \ @@ -818,6 +820,10 @@ getchar.obj : getchar.c vim.h [.auto]config.h feature.h os_unix.h \ ascii.h keymap.h term.h macros.h structs.h regexp.h \ gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \ globals.h +gui_xim.obj : gui_xim.c vim.h [.auto]config.h feature.h os_unix.h \ + ascii.h keymap.h term.h macros.h structs.h regexp.h \ + gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \ + globals.h hardcopy.obj : hardcopy.c vim.h [.auto]config.h feature.h os_unix.h \ ascii.h keymap.h term.h macros.h structs.h regexp.h \ gui.h beval.h [.proto]gui_beval.pro option.h ex_cmds.h proto.h \ diff --git a/src/Makefile b/src/Makefile index 9a8e24f830..520ef0b933 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1636,6 +1636,7 @@ BASIC_SRC = \ findfile.c \ fold.c \ getchar.c \ + gui_xim.c \ hardcopy.c \ hashtab.c \ highlight.c \ @@ -1785,6 +1786,7 @@ OBJ_COMMON = \ objects/findfile.o \ objects/fold.o \ objects/getchar.o \ + objects/gui_xim.o \ objects/hardcopy.o \ objects/hashtab.o \ objects/highlight.o \ @@ -1950,6 +1952,7 @@ PRO_AUTO = \ findfile.pro \ fold.pro \ getchar.pro \ + gui_xim.pro \ gui_beval.pro \ hardcopy.pro \ hashtab.pro \ @@ -3301,6 +3304,9 @@ objects/gui_xmebw.o: gui_xmebw.c objects/gui_x11.o: gui_x11.c $(CCC) -o $@ gui_x11.c +objects/gui_xim.o: gui_xim.c + $(CCC) -o $@ gui_xim.c + objects/gui_photon.o: gui_photon.c $(CCC) -o $@ gui_photon.c @@ -4239,6 +4245,11 @@ objects/gui_x11.o: gui_x11.c vim.h protodef.h auto/config.h feature.h os_unix.h proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \ proto.h globals.h ../runtime/vim32x32.xpm ../runtime/vim16x16.xpm \ ../runtime/vim48x48.xpm +objects/gui_xim.o: gui_xim.c vim.h protodef.h auto/config.h feature.h os_unix.h \ + auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \ + proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \ + proto.h globals.h ../runtime/vim32x32.xpm ../runtime/vim16x16.xpm \ + ../runtime/vim48x48.xpm objects/gui_at_sb.o: gui_at_sb.c vim.h protodef.h auto/config.h feature.h \ os_unix.h auto/osdef.h ascii.h keymap.h term.h macros.h option.h beval.h \ proto/gui_beval.pro structs.h regexp.h gui.h alloc.h ex_cmds.h spell.h \ diff --git a/src/gui_xim.c b/src/gui_xim.c new file mode 100644 index 0000000000..e3a48b5cb3 --- /dev/null +++ b/src/gui_xim.c @@ -0,0 +1,1777 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +/* + * gui_xim.c: functions for the X Input Method + */ + +#include "vim.h" + +#if defined(FEAT_GUI_GTK) && defined(FEAT_XIM) +# if GTK_CHECK_VERSION(3,0,0) +# include <gdk/gdkkeysyms-compat.h> +# else +# include <gdk/gdkkeysyms.h> +# endif +# ifdef MSWIN +# include <gdk/gdkwin32.h> +# else +# include <gdk/gdkx.h> +# endif +#endif + +/* + * XIM often causes trouble. Define XIM_DEBUG to get a log of XIM callbacks + * in the "xim.log" file. + */ +// #define XIM_DEBUG +#ifdef XIM_DEBUG + static void +xim_log(char *s, ...) +{ + va_list arglist; + static FILE *fd = NULL; + + if (fd == (FILE *)-1) + return; + if (fd == NULL) + { + fd = mch_fopen("xim.log", "w"); + if (fd == NULL) + { + emsg("Cannot open xim.log"); + fd = (FILE *)-1; + return; + } + } + + va_start(arglist, s); + vfprintf(fd, s, arglist); + va_end(arglist); +} +#endif + +#ifdef FEAT_GUI +# define USE_IMACTIVATEFUNC (!gui.in_use && *p_imaf != NUL) +# define USE_IMSTATUSFUNC (!gui.in_use && *p_imsf != NUL) +#else +# define USE_IMACTIVATEFUNC (*p_imaf != NUL) +# define USE_IMSTATUSFUNC (*p_imsf != NUL) +#endif + +#if defined(FEAT_EVAL) && \ + (defined(FEAT_XIM) || defined(IME_WITHOUT_XIM) || defined(VIMDLL)) + static void +call_imactivatefunc(int active) +{ + typval_T argv[2]; + + argv[0].v_type = VAR_NUMBER; + argv[0].vval.v_number = active ? 1 : 0; + argv[1].v_type = VAR_UNKNOWN; + (void)call_func_retnr(p_imaf, 1, argv); +} + + static int +call_imstatusfunc(void) +{ + int is_active; + + // FIXME: Don't execute user function in unsafe situation. + if (exiting || is_autocmd_blocked()) + return FALSE; + // FIXME: :py print 'xxx' is shown duplicate result. + // Use silent to avoid it. + ++msg_silent; + is_active = call_func_retnr(p_imsf, 0, NULL); + --msg_silent; + return (is_active > 0); +} +#endif + +#if defined(FEAT_XIM) || defined(PROTO) + +# if defined(FEAT_GUI_GTK) || defined(PROTO) +static int xim_has_preediting INIT(= FALSE); // IM current status + +/* + * Set preedit_start_col to the current cursor position. + */ + static void +init_preedit_start_col(void) +{ + if (State & CMDLINE) + preedit_start_col = cmdline_getvcol_cursor(); + else if (curwin != NULL && curwin->w_buffer != NULL) + getvcol(curwin, &curwin->w_cursor, &preedit_start_col, NULL, NULL); + // Prevent that preediting marks the buffer as changed. + xim_changed_while_preediting = curbuf->b_changed; +} + +static int im_is_active = FALSE; // IM is enabled for current mode +static int preedit_is_active = FALSE; +static int im_preedit_cursor = 0; // cursor offset in characters +static int im_preedit_trailing = 0; // number of characters after cursor + +static unsigned long im_commit_handler_id = 0; +static unsigned int im_activatekey_keyval = GDK_VoidSymbol; +static unsigned int im_activatekey_state = 0; + +static GtkWidget *preedit_window = NULL; +static GtkWidget *preedit_label = NULL; + +static void im_preedit_window_set_position(void); + + void +im_set_active(int active) +{ + int was_active; + + was_active = !!im_get_status(); + im_is_active = (active && !p_imdisable); + + if (im_is_active != was_active) + xim_reset(); +} + + void +xim_set_focus(int focus) +{ + if (xic != NULL) + { + if (focus) + gtk_im_context_focus_in(xic); + else + gtk_im_context_focus_out(xic); + } +} + + void +im_set_position(int row, int col) +{ + if (xic != NULL) + { + GdkRectangle area; + + area.x = FILL_X(col); + area.y = FILL_Y(row); + area.width = gui.char_width * (mb_lefthalve(row, col) ? 2 : 1); + area.height = gui.char_height; + + gtk_im_context_set_cursor_location(xic, &area); + + if (p_imst == IM_OVER_THE_SPOT) + im_preedit_window_set_position(); + } +} + +# if 0 || defined(PROTO) // apparently only used in gui_x11.c + void +xim_set_preedit(void) +{ + im_set_position(gui.row, gui.col); +} +# endif + + static void +im_add_to_input(char_u *str, int len) +{ + // Convert from 'termencoding' (always "utf-8") to 'encoding' + if (input_conv.vc_type != CONV_NONE) + { + str = string_convert(&input_conv, str, &len); + g_return_if_fail(str != NULL); + } + + add_to_input_buf_csi(str, len); + + if (input_conv.vc_type != CONV_NONE) + vim_free(str); + + if (p_mh) // blank out the pointer if necessary + gui_mch_mousehide(TRUE); +} + + static void +im_preedit_window_set_position(void) +{ + int x, y, width, height; + int screen_x, screen_y, screen_width, screen_height; + + if (preedit_window == NULL) + return; + + gui_gtk_get_screen_geom_of_win(gui.drawarea, + &screen_x, &screen_y, &screen_width, &screen_height); + gdk_window_get_origin(gtk_widget_get_window(gui.drawarea), &x, &y); + gtk_window_get_size(GTK_WINDOW(preedit_window), &width, &height); + x = x + FILL_X(gui.col); + y = y + FILL_Y(gui.row); + if (x + width > screen_x + screen_width) + x = screen_x + screen_width - width; + if (y + height > screen_y + screen_height) + y = screen_y + screen_height - height; + gtk_window_move(GTK_WINDOW(preedit_window), x, y); +} + + static void +im_preedit_window_open() +{ + char *preedit_string; +#if !GTK_CHECK_VERSION(3,16,0) + char buf[8]; +#endif + PangoAttrList *attr_list; + PangoLayout *layout; +#if GTK_CHECK_VERSION(3,0,0) +# if !GTK_CHECK_VERSION(3,16,0) + GdkRGBA color; +# endif +#else + GdkColor color; +#endif + gint w, h; + + if (preedit_window == NULL) + { + preedit_window = gtk_window_new(GTK_WINDOW_POPUP); + gtk_window_set_transient_for(GTK_WINDOW(preedit_window), + GTK_WINDOW(gui.mainwin)); + preedit_label = gtk_label_new(""); + gtk_widget_set_name(preedit_label, "vim-gui-preedit-area"); + gtk_container_add(GTK_CONTAINER(preedit_window), preedit_label); + } + +#if GTK_CHECK_VERSION(3,16,0) + { + GtkStyleContext * const context + = gtk_widget_get_style_context(gui.drawarea); + GtkCssProvider * const provider = gtk_css_provider_new(); + gchar *css = NULL; + const char * const fontname + = pango_font_description_get_family(gui.norm_font); + gint fontsize + = pango_font_description_get_size(gui.norm_font) / PANGO_SCALE; + gchar *fontsize_propval = NULL; + + if (!pango_font_description_get_size_is_absolute(gui.norm_font)) + { + // fontsize was given in points. Convert it into that in pixels + // to use with CSS. + GdkScreen * const screen + = gdk_window_get_screen(gtk_widget_get_window(gui.mainwin)); + const gdouble dpi = gdk_screen_get_resolution(screen); + fontsize = dpi * fontsize / 72; + } + if (fontsize > 0) + fontsize_propval = g_strdup_printf("%dpx", fontsize); + else + fontsize_propval = g_strdup_printf("inherit"); + + css = g_strdup_printf( + "widget#vim-gui-preedit-area {\n" + " font-family: %s,monospace;\n" + " font-size: %s;\n" + " color: #%.2lx%.2lx%.2lx;\n" + " background-color: #%.2lx%.2lx%.2lx;\n" + "}\n", + fontname != NULL ? fontname : "inherit", + fontsize_propval, + (gui.norm_pixel >> 16) & 0xff, + (gui.norm_pixel >> 8) & 0xff, + gui.norm_pixel & 0xff, + (gui.back_pixel >> 16) & 0xff, + (gui.back_pixel >> 8) & 0xff, + gui.back_pixel & 0xff); + + gtk_css_provider_load_from_data(provider, css, -1, NULL); + gtk_style_context_add_provider(context, + GTK_STYLE_PROVIDER(provider), G_MAXUINT); + + g_free(css); + g_free(fontsize_propval); + g_object_unref(provider); + } +#elif GTK_CHECK_VERSION(3,0,0) + gtk_widget_override_font(preedit_label, gui.norm_font); + + vim_snprintf(buf, sizeof(buf), "#%06X", gui.norm_pixel); + gdk_rgba_parse(&color, buf); + gtk_widget_override_color(preedit_label, GTK_STATE_FLAG_NORMAL, &color); + + vim_snprintf(buf, sizeof(buf), "#%06X", gui.back_pixel); + gdk_rgba_parse(&color, buf); + gtk_widget_override_background_color(preedit_label, GTK_STATE_FLAG_NORMAL, + &color); +#else + gtk_widget_modify_font(preedit_label, gui.norm_font); + + vim_snprintf(buf, sizeof(buf), "#%06X", (unsigned)gui.norm_pixel); + gdk_color_parse(buf, &color); + gtk_widget_modify_fg(preedit_label, GTK_STATE_NORMAL, &color); + + vim_snprintf(buf, sizeof(buf), "#%06X", (unsigned)gui.back_pixel); + gdk_color_parse(buf, &color); + gtk_widget_modify_bg(preedit_window, GTK_STATE_NORMAL, &color); +#endif + + gtk_im_context_get_preedit_string(xic, &preedit_string, &attr_list, NULL); + + if (preedit_string[0] != NUL) + { + gtk_label_set_text(GTK_LABEL(preedit_label), preedit_string); + gtk_label_set_attributes(GTK_LABEL(preedit_label), attr_list); + + layout = gtk_label_get_layout(GTK_LABEL(preedit_label)); + pango_layout_get_pixel_size(layout, &w, &h); + h = MAX(h, gui.char_height); + gtk_window_resize(GTK_WINDOW(preedit_window), w, h); + + gtk_widget_show_all(preedit_window); + + im_preedit_window_set_position(); + } + + g_free(preedit_string); + pango_attr_list_unref(attr_list); +} + + static void +im_preedit_window_close() +{ + if (preedit_window != NULL) + gtk_widget_hide(preedit_window); +} + + static void +im_show_preedit() +{ + im_preedit_window_open(); + + if (p_mh) // blank out the pointer if necessary + gui_mch_mousehide(TRUE); +} + + static void +im_delete_preedit(void) +{ + char_u bskey[] = {CSI, 'k', 'b'}; + char_u delkey[] = {CSI, 'k', 'D'}; + + if (p_imst == IM_OVER_THE_SPOT) + { + im_preedit_window_close(); + return; + } + + if (State & NORMAL +#ifdef FEAT_TERMINAL + && !term_use_loop() +#endif + ) + { + im_preedit_cursor = 0; + return; + } + for (; im_preedit_cursor > 0; --im_preedit_cursor) + add_to_input_buf(bskey, (int)sizeof(bskey)); + + for (; im_preedit_trailing > 0; --im_preedit_trailing) + add_to_input_buf(delkey, (int)sizeof(delkey)); +} + +/* + * Move the cursor left by "num_move_back" characters. + * Note that ins_left() checks im_is_preediting() to avoid breaking undo for + * these K_LEFT keys. + */ + static void +im_correct_cursor(int num_move_back) +{ + char_u backkey[] = {CSI, 'k', 'l'}; + + if (State & NORMAL) + return; +# ifdef FEAT_RIGHTLEFT + if ((State & CMDLINE) == 0 && curwin != NULL && curwin->w_p_rl) + backkey[2] = 'r'; +# endif + for (; num_move_back > 0; --num_move_back) + add_to_input_buf(backkey, (int)sizeof(backkey)); +} + +static int xim_expected_char = NUL; +static int xim_ignored_char = FALSE; + +/* + * Update the mode and cursor while in an IM callback. + */ + static void +im_show_info(void) +{ + int old_vgetc_busy; + + old_vgetc_busy = vgetc_busy; + vgetc_busy = TRUE; + showmode(); + vgetc_busy = old_vgetc_busy; + if ((State & NORMAL) || (State & INSERT)) + setcursor(); + out_flush(); +} + +/* + * Callback invoked when the user finished preediting. + * Put the final string into the input buffer. + */ + static void +im_commit_cb(GtkIMContext *context UNUSED, + const gchar *str, + gpointer data UNUSED) +{ + int slen = (int)STRLEN(str); + int add_to_input = TRUE; + int clen; + int len = slen; + int commit_with_preedit = TRUE; + char_u *im_str; + +#ifdef XIM_DEBUG + xim_log("im_commit_cb(): %s\n", str); +#endif + + if (p_imst == IM_ON_THE_SPOT) + { + // The imhangul module doesn't reset the preedit string before + // committing. Call im_delete_preedit() to work around that. + im_delete_preedit(); + + // Indicate that preediting has finished. + if (preedit_start_col == MAXCOL) + { + init_preedit_start_col(); + commit_with_preedit = FALSE; + } + + // The thing which setting "preedit_start_col" to MAXCOL means that + // "preedit_start_col" will be set forcedly when calling + // preedit_changed_cb() next time. + // "preedit_start_col" should not reset with MAXCOL on this part. Vim + // is simulating the preediting by using add_to_input_str(). when + // preedit begin immediately before committed, the typebuf is not + // flushed to screen, then it can't get correct "preedit_start_col". + // Thus, it should calculate the cells by adding cells of the committed + // string. + if (input_conv.vc_type != CONV_NONE) + { + im_str = string_convert(&input_conv, (char_u *)str, &len); + g_return_if_fail(im_str != NULL); + } + else + im_str = (char_u *)str; + + clen = mb_string2cells(im_str, len); + + if (input_conv.vc_type != CONV_NONE) + vim_free(im_str); + preedit_start_col += clen; + } + + // Is this a single character that matches a keypad key that's just + // been pressed? If so, we don't want it to be entered as such - let + // us carry on processing the raw keycode so that it may be used in + // mappings as <kSomething>. + if (xim_expected_char != NUL) + { + // We're currently processing a keypad or other special key + if (slen == 1 && str[0] == xim_expected_char) + { + // It's a match - don't do it here + xim_ignored_char = TRUE; + add_to_input = FALSE; + } + else + { + // Not a match + xim_ignored_char = FALSE; + } + } + + if (add_to_input) + im_add_to_input((char_u *)str, slen); + + if (p_imst == IM_ON_THE_SPOT) + { + // Inserting chars while "im_is_active" is set does not cause a + // change of buffer. When the chars are committed the buffer must be + // marked as changed. + if (!commit_with_preedit) + preedit_start_col = MAXCOL; + + // This flag is used in changed() at next call. + xim_changed_while_preediting = TRUE; + } + + if (gtk_main_level() > 0) + gtk_main_quit(); +} + +/* + * Callback invoked after start to the preedit. + */ + static void +im_preedit_start_cb(GtkIMContext *context UNUSED, gpointer data UNUSED) +{ +#ifdef XIM_DEBUG + xim_log("im_preedit_start_cb()\n"); +#endif + + im_is_active = TRUE; + preedit_is_active = TRUE; + gui_update_cursor(TRUE, FALSE); + im_show_info(); +} + +/* + * Callback invoked after end to the preedit. + */ + static void +im_preedit_end_cb(GtkIMContext *context UNUSED, gpointer data UNUSED) +{ +#ifdef XIM_DEBUG + xim_log("im_preedit_end_cb()\n"); +#endif + im_delete_preedit(); + + // Indicate that preediting has finished + if (p_imst == IM_ON_THE_SPOT) + preedit_start_col = MAXCOL; + xim_has_preediting = FALSE; + +#if 0 + // Removal of this line suggested by Takuhiro Nishioka. Fixes that IM was + // switched off unintentionally. We now use preedit_is_active (added by + // SungHyun Nam). + im_is_active = FALSE; +#endif + preedit_is_active = FALSE; + gui_update_cursor(TRUE, FALSE); + im_show_info(); +} + +/* + * Callback invoked after changes to the preedit string. If the preedit + * string was empty before, remember the preedit start column so we know + * where to apply feedback attributes. Delete the previous preedit string + * if there was one, save the new preedit cursor offset, and put the new + * string into the input buffer. + * + * TODO: The pragmatic "put into input buffer" approach used here has + * several fundamental problems: + * + * - The characters in the preedit string are subject to remapping. + * That's broken, only the finally committed string should be remapped. + * + * - There is a race condition involved: The retrieved value for the + * current cursor position will be wrong if any unprocessed characters + * are still queued in the input buffer. + * + * - Due to the lack of synchronization between the file buffer in memory + * and any typed characters, it's practically impossible to implement the + * "retrieve_surrounding" and "delete_surrounding" signals reliably. IM + * modules for languages such as Thai are likely to rely on this feature + * for proper operation. + * + * Conclusions: I think support for preediting needs to be moved to the + * core parts of Vim. Ideally, until it has been committed, the preediting + * string should only be displayed and not affect the buffer content at all. + * The question how to deal with the synchronization issue still remains. + * Circumventing the input buffer is probably not desirable. Anyway, I think + * implementing "retrieve_surrounding" is the only hard problem. + * + * One way to solve all of this in a clean manner would be to queue all key + * press/release events "as is" in the input buffer, and apply the IM filtering + * at the receiving end of the queue. This, however, would have a rather large + * impact on the code base. If there is an easy way to force processing of all + * remaining input from within the "retrieve_surrounding" signal handler, this + * might not be necessary. Gotta ask on vim-dev for opinions. + */ + static void +im_preedit_changed_cb(GtkIMContext *context, gpointer data UNUSED) +{ + char *preedit_string = NULL; + int cursor_index = 0; + int num_move_back = 0; + char_u *str; + char_u *p; + int i; + + if (p_imst == IM_ON_THE_SPOT) + gtk_im_context_get_preedit_string(context, + &preedit_string, NULL, + &cursor_index); + else + gtk_im_context_get_preedit_string(context, + &preedit_string, NULL, + NULL); + +#ifdef XIM_DEBUG + xim_log("im_preedit_changed_cb(): %s\n", preedit_string); +#endif + + g_return_if_fail(preedit_string != NULL); // just in case + + if (p_imst == IM_OVER_THE_SPOT) + { + if (preedit_string[0] == NUL) + { + xim_has_preediting = FALSE; + im_delete_preedit(); + } + else + { + xim_has_preediting = TRUE; + im_show_preedit(); + } + } + else + { + // If preedit_start_col is MAXCOL set it to the current cursor position. + if (preedit_start_col == MAXCOL && preedit_string[0] != '\0') + { + xim_has_preediting = TRUE; + + // Urgh, this breaks if the input buffer isn't empty now + init_preedit_start_col(); + } + else if (cursor_index == 0 && preedit_string[0] == '\0') + { + xim_has_preediting = FALSE; + + // If at the start position (after typing backspace) + // preedit_start_col must be reset. + preedit_start_col = MAXCOL; + } + + im_delete_preedit(); + + /* + * Compute the end of the preediting area: "preedit_end_col". + * According to the documentation of gtk_im_context_get_preedit_string(), + * the cursor_pos output argument returns the offset in bytes. This is + * unfortunately not true -- real life shows the offset is in characters, + * and the GTK+ source code agrees with me. Will file a bug later. + */ + if (preedit_start_col != MAXCOL) + preedit_end_col = preedit_start_col; + str = (char_u *)preedit_string; + for (p = str, i = 0; *p != NUL; p += utf_byte2len(*p), ++i) + { + int is_composing; + + is_composing = ((*p & 0x80) != 0 && utf_iscomposing(utf_ptr2char(p))); + /* + * These offsets are used as counters when generating <BS> and <Del> + * to delete the preedit string. So don't count composing characters + * unless 'delcombine' is enabled. + */ + if (!is_composing || p_deco) + { + if (i < cursor_index) + ++im_preedit_cursor; + else + ++im_preedit_trailing; + } + if (!is_composing && i >= cursor_index) + { + // This is essentially the same as im_preedit_trailing, except + // composing characters are not counted even if p_deco is set. + ++num_move_back; + } + if (preedit_start_col != MAXCOL) + preedit_end_col += utf_ptr2cells(p); + } + + if (p > str) + { + im_add_to_input(str, (int)(p - str)); + im_correct_cursor(num_move_back); + } + } + + g_free(preedit_string); + + if (gtk_main_level() > 0) + gtk_main_quit(); +} + +/* + * Translate the Pango attributes at iter to Vim highlighting attributes. + * Ignore attributes not supported by Vim highlighting. This shouldn't have + * too much impact -- right now we handle even more attributes than necessary + * for the IM modules I tested with. + */ + static int +translate_pango_attributes(PangoAttrIterator *iter) +{ + PangoAttribute *attr; + int char_attr = HL_NORMAL; + + attr = pango_attr_iterator_get(iter, PANGO_ATTR_UNDERLINE); + if (attr != NULL && ((PangoAttrInt *)attr)->value + != (int)PANGO_UNDERLINE_NONE) + char_attr |= HL_UNDERLINE; + + attr = pango_attr_iterator_get(iter, PANGO_ATTR_WEIGHT); + if (attr != NULL && ((PangoAttrInt *)attr)->value >= (int)PANGO_WEIGHT_BOLD) + char_attr |= HL_BOLD; + + attr = pango_attr_iterator_get(iter, PANGO_ATTR_STYLE); + if (attr != NULL && ((PangoAttrInt *)attr)->value + != (int)PANGO_STYLE_NORMAL) + char_attr |= HL_ITALIC; + + attr = pango_attr_iterator_get(iter, PANGO_ATTR_BACKGROUND); + if (attr != NULL) + { + const PangoColor *color = &((PangoAttrColor *)attr)->color; + + // Assume inverse if black background is requested + if ((color->red | color->green | color->blue) == 0) + char_attr |= HL_INVERSE; + } + + return char_attr; +} + +/* + * Retrieve the highlighting attributes at column col in the preedit string. + * Return -1 if not in preediting mode or if col is out of range. + */ + int +im_get_feedback_attr(int col) +{ + char *preedit_string = NULL; + PangoAttrList *attr_list = NULL; + int char_attr = -1; + + if (xic == NULL) + return char_attr; + + gtk_im_context_get_preedit_string(xic, &preedit_string, &attr_list, NULL); + + if (preedit_string != NULL && attr_list != NULL) + { + int idx; + + // Get the byte index as used by PangoAttrIterator + for (idx = 0; col > 0 && preedit_string[idx] != '\0'; --col) + idx += utfc_ptr2len((char_u *)preedit_string + idx); + + if (preedit_string[idx] != '\0') + { + PangoAttrIterator *iter; + int start, end; + + char_attr = HL_NORMAL; + iter = pango_attr_list_get_iterator(attr_list); + + // Extract all relevant attributes from the list. + do + { + pango_attr_iterator_range(iter, &start, &end); + + if (idx >= start && idx < end) + char_attr |= translate_pango_attributes(iter); + } + while (pango_attr_iterator_next(iter)); + + pango_attr_iterator_destroy(iter); + } + } + + if (attr_list != NULL) + pango_attr_list_unref(attr_list); + g_free(preedit_string); + + return char_attr; +} + + void +xim_init(void) +{ +#ifdef XIM_DEBUG + xim_log("xim_init()\n"); +#endif + + g_return_if_fail(gui.drawarea != NULL); + g_return_if_fail(gtk_widget_get_window(gui.drawarea) != NULL); + + xic = gtk_im_multicontext_new(); + g_object_ref(xic); + + im_commit_handler_id = g_signal_connect(G_OBJECT(xic), "commit", + G_CALLBACK(&im_commit_cb), NULL); + g_signal_connect(G_OBJECT(xic), "preedit_changed", + G_CALLBACK(&im_preedit_changed_cb), NULL); + g_signal_connect(G_OBJECT(xic), "preedit_start", + G_CALLBACK(&im_preedit_start_cb), NULL); + g_signal_connect(G_OBJECT(xic), "preedit_end", + G_CALLBACK(&im_preedit_end_cb), NULL); + + gtk_im_context_set_client_window(xic, gtk_widget_get_window(gui.drawarea)); +} + + void +im_shutdown(void) +{ +#ifdef XIM_DEBUG + xim_log("im_shutdown()\n"); +#endif + + if (xic != NULL) + { + gtk_im_context_focus_out(xic); + g_object_unref(xic); + xic = NULL; + } + im_is_active = FALSE; + im_commit_handler_id = 0; + if (p_imst == IM_ON_THE_SPOT) + preedit_start_col = MAXCOL; + xim_has_preediting = FALSE; +} + +/* + * Convert the string argument to keyval and state for GdkEventKey. + * If str is valid return TRUE, otherwise FALSE. + * + * See 'imactivatekey' for documentation of the format. + */ + static int +im_string_to_keyval(const char *str, unsigned int *keyval, unsigned int *state) +{ + const char *mods_end; + unsigned tmp_keyval; + unsigned tmp_state = 0; + + mods_end = strrchr(str, '-'); + mods_end = (mods_end != NULL) ? mods_end + 1 : str; + + // Parse modifier keys + while (str < mods_end) + switch (*str++) + { + case '-': break; + case 'S': case 's': tmp_state |= (unsigned)GDK_SHIFT_MASK; break; + case 'L': case 'l': tmp_state |= (unsigned)GDK_LOCK_MASK; break; + case 'C': case 'c': tmp_state |= (unsigned)GDK_CONTROL_MASK;break; + case '1': tmp_state |= (unsigned)GDK_MOD1_MASK; break; + case '2': tmp_state |= (unsigned)GDK_MOD2_MASK; break; + case '3': tmp_state |= (unsigned)GDK_MOD3_MASK; break; + case '4': tmp_state |= (unsigned)GDK_MOD4_MASK; break; + case '5': tmp_state |= (unsigned)GDK_MOD5_MASK; break; + default: + return FALSE; + } + + tmp_keyval = gdk_keyval_from_name(str); + + if (tmp_keyval == 0 || tmp_keyval == GDK_VoidSymbol) + return FALSE; + + if (keyval != NULL) + *keyval = tmp_keyval; + if (state != NULL) + *state = tmp_state; + + return TRUE; +} + +/* + * Return TRUE if p_imak is valid, otherwise FALSE. As a special case, an + * empty string is also regarded as valid. + * + * Note: The numerical key value of p_imak is cached if it was valid; thus + * boldly assuming im_xim_isvalid_imactivate() will always be called whenever + * 'imak' changes. This is currently the case but not obvious -- should + * probably rename the function for clarity. + */ + int +im_xim_isvalid_imactivate(void) +{ + if (p_imak[0] == NUL) + { + im_activatekey_keyval = GDK_VoidSymbol; + im_activatekey_state = 0; + return TRUE; + } + + return im_string_to_keyval((const char *)p_imak, + &im_activatekey_keyval, + &im_activatekey_state); +} + + static void +im_synthesize_keypress(unsigned int keyval, unsigned int state) +{ + GdkEventKey *event; + + event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS); + g_object_ref(gtk_widget_get_window(gui.drawarea)); + // unreffed by gdk_event_free() + event->window = gtk_widget_get_window(gui.drawarea); + event->send_event = TRUE; + ev |