/* 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(GTK_CHECK_VERSION) # define GTK_CHECK_VERSION(a, b, c) 0 #endif #if !defined(FEAT_GUI_GTK) && defined(PROTO) typedef int GtkWidget; typedef int GtkIMContext; typedef int gchar; typedef int gpointer; typedef int PangoAttrIterator; typedef int GdkEventKey; #endif #if defined(FEAT_GUI_GTK) && defined(FEAT_XIM) # if GTK_CHECK_VERSION(3,0,0) # include # else # include # endif # ifdef MSWIN # include # else # include # endif #endif /* * XIM often causes trouble. Define XIM_DEBUG to get a log of XIM callbacks * in the "xim.log" file. */ // #define XIM_DEBUG #if defined(XIM_DEBUG) && defined(FEAT_GUI_GTK) static void xim_log(char *s, ...) ATTRIBUTE_FORMAT_PRINTF(1, 2); 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 #if defined(FEAT_GUI_MSWIN) # 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))) || \ defined(PROTO) static callback_T imaf_cb; // 'imactivatefunc' callback function static callback_T imsf_cb; // 'imstatusfunc' callback function char * did_set_imactivatefunc(optset_T *args UNUSED) { if (option_set_callback_func(p_imaf, &imaf_cb) == FAIL) return e_invalid_argument; return NULL; } char * did_set_imstatusfunc(optset_T *args UNUSED) { if (option_set_callback_func(p_imsf, &imsf_cb) == FAIL) return e_invalid_argument; return NULL; } static void call_imactivatefunc(int active) { typval_T argv[2]; int save_KeyTyped = KeyTyped; argv[0].v_type = VAR_NUMBER; argv[0].vval.v_number = active ? 1 : 0; argv[1].v_type = VAR_UNKNOWN; (void)call_callback_retnr(&imaf_cb, 1, argv); KeyTyped = save_KeyTyped; } static int call_imstatusfunc(void) { int is_active; int save_KeyTyped = KeyTyped; // 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_callback_retnr(&imsf_cb, 0, NULL); --msg_silent; KeyTyped = save_KeyTyped; return (is_active > 0); } #endif #if defined(EXITFREE) || defined(PROTO) void free_xim_stuff(void) { # if defined(FEAT_EVAL) && \ (defined(FEAT_XIM) || defined(IME_WITHOUT_XIM) || defined(VIMDLL)) free_callback(&imaf_cb); free_callback(&imsf_cb); # endif } #endif #if defined(FEAT_EVAL) || defined(PROTO) /* * Mark the global 'imactivatefunc' and 'imstatusfunc' callbacks with "copyID" * so that they are not garbage collected. */ int set_ref_in_im_funcs(int copyID UNUSED) { int abort = FALSE; # if defined(FEAT_XIM) || defined(IME_WITHOUT_XIM) || defined(VIMDLL) abort = set_ref_in_callback(&imaf_cb, copyID); abort = abort || set_ref_in_callback(&imsf_cb, copyID); # endif return abort; } #endif #if defined(FEAT_XIM) || defined(PROTO) # if defined(FEAT_GUI_GTK) || defined(PROTO) static int xim_has_preediting = FALSE; // IM current status /* * Set preedit_start_col to the current cursor position. */ static void init_preedit_start_col(void) { if (State & MODE_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) return; 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) return; 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, 0, 0, &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(void) { 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(void) { if (preedit_window != NULL) gtk_widget_hide(preedit_window); } static void im_show_preedit(void) { 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 & MODE_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 & MODE_NORMAL) return; # ifdef FEAT_RIGHTLEFT if ((State & MODE_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 & MODE_NORMAL) || (State & MODE_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 . 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 and // 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; event->time = GDK_CURRENT_TIME; event->state = state; event->keyval = keyval; event->hardware_keycode = // needed for XIM XKeysymToKeycode(GDK_WINDOW_XDISPLAY(event->window), (KeySym)keyval); event->length = 0; event->string = NULL; gtk_im_context_filter_keypress(xic, event); // For consistency, also send the corresponding release event. event->type = GDK_KEY_RELEASE; event->send_event = FALSE; gtk_im_context_filter_keypress(xic, event); gdk_event_free((GdkEvent *)event); } void xim_reset(void) { # ifdef FEAT_EVAL if (USE_IMACTIVATEFUNC) call_imactivatefunc(im_is_active); else # endif if (xic != NULL) { gtk_im_context_reset(xic); if (p_imdisable) im_shutdown(); else { xim_set_focus(gui.in_focus); if (im_activatekey_keyval != GDK_VoidSymbol) { if (im_is_active) { g_signal_handler_block(xic, im_commit_handler_id); im_synthesize_keypress(im_activatekey_keyval, im_activatekey_state); g_signal_handler_unblock(xic, im_commit_handler_id); } } else { im_shutdown(); xim_init(); xim_set_focus(gui.in_focus); } } } if (p_imst == IM_ON_THE_SPOT) preedit_start_col = MAXCOL; xim_has_preediting = FALSE; } int xim_queue_key_press_event(GdkEventKey *event, int down) { if (down) { // Workaround GTK2 XIM 'feature' that always converts keypad keys to // chars., even when not part of an IM sequence (ref. feature of // gdk/gdkkeyuni.c). // Flag any keypad keys that might represent a single char. // If this (on its own - i.e., not part of an IM sequence) is // committed while we're processing one of these keys, we can ignore // that commit and go ahead & process it ourselves. That way we can // still distinguish keypad keys for use in mappings. // Also add GDK_space to make work. switch (event->keyval) { case GDK_KP_Add: xim_expected_char = '+'; break; case GDK_KP_Subtract: xim_expected_char = '-'; break; case GDK_KP_Divide: xim_expected_char = '/'; break; case GDK_KP_Multiply: xim_expected_char = '*'; break; case GDK_KP_Decimal: xim_expected_char = '.'; break; case GDK_KP_Equal: xim_expected_char = '='; break; case GDK_KP_0: xim_expected_char = '0'; break; case GDK_KP_1: xim_expected_char = '1'; break; case GDK_KP_2: xim_expected_char = '2'; break; case GDK_KP_3: xim_expected_char = '3'; break; case GDK_KP_4: xim_expected_char = '4'; break; case GDK_KP_5: xim_expected_char = '5'; break; case GDK_KP_6: xim_expected_char = '6'; break; case GDK_KP_7: xim_expected_char = '7'; break; case GDK_KP_8: xim_expected_char = '8'; break; case GDK_KP_9: xim_expected_char = '9'; break; case GDK_space: xim_expected_char = ' '; break; default: xim_expected_char = NUL; } xim_ignored_char = FALSE; } // When typing fFtT, XIM may be activated. Thus it must pass // gtk_im_context_filter_keypress() in Normal mode. // And while doing :sh too. if (xic != NULL && !p_imdisable && (State & (MODE_INSERT | MODE_CMDLINE | MODE_NORMAL | MODE_EXTERNCMD))) { // Filter 'imactivatekey' and map it to CTRL-^. This way, Vim is // always aware of the current status of IM, and can even emulate // the activation key for modules that don't support one. if (event->keyval == im_activatekey_keyval && (event->state & im_activatekey_state) == im_activatekey_state) { unsigned int state_mask; // Require the state of the 3 most used modifiers to match exactly. // Otherwise e.g. would be unusable for other purposes // if the IM activate key is . state_mask = im_activatekey_state; state_mask |= ((int)GDK_SHIFT_MASK | (int)GDK_CONTROL_MASK | (int)GDK_MOD1_MASK); if ((event->state & state_mask) != im_activatekey_state) return FALSE; // Don't send it a second time on GDK_KEY_RELEASE. if (event->type != GDK_KEY_PRESS) return TRUE; if (map_to_exists_mode((char_u *)"", MODE_LANGMAP, FALSE)) { im_set_active(FALSE); // ":lmap" mappings exists, toggle use of mappings. State ^= MODE_LANGMAP; if (State & MODE_LANGMAP) { curbuf->b_p_iminsert = B_IMODE_NONE; State &= ~MODE_LANGMAP; } else { curbuf->b_p_iminsert = B_IMODE_LMAP; State |= MODE_LANGMAP; } return TRUE; } return gtk_im_context_filter_keypress(xic, event); } // Don't filter events through the IM context if IM isn't active // right now. Unlike with GTK+ 1.2 we cannot rely on the IM module // not doing anything before the activation key was sent. if (im_activatekey_keyval == GDK_VoidSymbol || im_is_active) { int imresult = gtk_im_context_filter_keypress(xic, event); if (p_imst == IM_ON_THE_SPOT) { // Some XIM send following sequence: // 1. preedited string. // 2. committed string. // 3. line changed key. // 4. preedited string. // 5. remove preedited string. // if 3, Vim can't move back the above line for 5. // thus, this part should not parse the key. if (!imresult && preedit_start_col != MAXCOL && event->keyval == GDK_Return) { im_synthesize_keypress(GDK_Return, 0U); return FALSE; } } // If XIM tried to commit a keypad key as a single char., // ignore it so we can use the keypad key 'raw', for mappings. if (xim_expected_char != NUL && xim_ignored_char) // We had a keypad key, and XIM tried to thieve it return FALSE; // This is supposed to fix a problem with iBus, that space // characters don't work in input mode. xim_expected_char = NUL; // Normal processing return imresult; } } return FALSE; } int im_get_status(void) { # ifdef FEAT_EVAL if (USE_IMSTATUSFUNC) return call_imstatusfunc(); # endif return im_is_active; } int preedit_get_status(void) { return preedit_is_active; } int im_is_preediting(void) { return xim_has_preediting; } # else // !FEAT_GUI_GTK static int xim_is_active = FALSE; // XIM should be active in the current // mode static int xim_has_focus = FALSE; // XIM is really being used for Vim # ifdef FEAT_GUI_X11 static XIMStyle input_style; static int status_area_enabled = TRUE; # endif /* * Switch using XIM on/off. This is used by the code that changes "State". * When 'imactivatefunc' is defined use that function instead. */ void im_set_active(int active_arg) { int active = active_arg; // If 'imdisable' is set, XIM is never active. if (p_imdisable) active = FALSE; else if (input_style & XIMPreeditPosition) // There is a problem in switching XIM off when preediting is used, // and it is not clear how this can be solved. For now, keep XIM on // all the time, like it was done in Vim 5.8. active = TRUE; # if defined(FEAT_EVAL) if (USE_IMACTIVATEFUNC) { if (active != im_get_status()) { call_imactivatefunc(active); xim_has_focus = active; } return; } # endif if (xic == NULL) return; // Remember the active state, it is needed when Vim gets keyboard focus. xim_is_active = active; xim_set_preedit(); } /* * Adjust using XIM for gaining or losing keyboard focus. Also called when * "xim_is_active" changes. */ void xim_set_focus(int focus) { if (xic == NULL) return; // XIM only gets focus when the Vim window has keyboard focus and XIM has // been set active for the current mode. if (focus && xim_is_active) { if (!xim_has_focus) { xim_has_focus = TRUE; XSetICFocus(xic); } } else { if (xim_has_focus) { xim_has_focus = FALSE; XUnsetICFocus(xic); } } } void im_set_position(int row UNUSED, int col UNUSED) { xim_set_preedit(); } /* * Set the XIM to the current cursor position. */ void xim_set_preedit(void) { XVaNestedList attr_list; XRectangle spot_area; XPoint over_spot; int line_space; if (xic == NULL) return; xim_set_focus(TRUE); if (!xim_has_focus) { // hide XIM cursor over_spot.x = 0; over_spot.y = -100; // arbitrary invisible position attr_list = (XVaNestedList) XVaCreateNestedList(0, XNSpotLocation, &over_spot, NULL); XSetICValues(xic, XNPreeditAttributes, attr_list, NULL); XFree(attr_list); return; } if (input_style & XIMPreeditPosition) { if (xim_fg_color == INVALCOLOR) { xim_fg_color = gui.def_norm_pixel; xim_bg_color = gui.def_back_pixel; } over_spot.x = TEXT_X(gui.col); over_spot.y = TEXT_Y(gui.row); spot_area.x = 0; spot_area.y = 0; spot_area.height = gui.char_height * Rows; spot_area.width = gui.char_width * Columns; line_space = gui.char_height; attr_list = (XVaNestedList) XVaCreateNestedList(0, XNSpotLocation, &over_spot, XNForeground, (Pixel) xim_fg_color, XNBackground, (Pixel) xim_bg_color, XNArea, &spot_area, XNLineSpace, line_space, NULL); if (XSetICValues(xic, XNPreeditAttributes, attr_list, NULL)) emsg(_(e_cannot_set_ic_values)); XFree(attr_list); } } # if defined(FEAT_GUI_X11) || defined(PROTO) # if defined(XtSpecificationRelease) && XtSpecificationRelease >= 6 && !defined(SUN_SYSTEM) # define USE_X11R6_XIM # endif static int xim_real_init(Window x11_window, Display *x11_display); # ifdef USE_X11R6_XIM static void xim_instantiate_cb( Display *display, XPointer client_data UNUSED, XPointer call_data UNUSED) { Window x11_window; Display *x11_display; # ifdef XIM_DEBUG xim_log("xim_instantiate_cb()\n"); # endif gui_get_x11_windis(&x11_window, &x11_display); if (display != x11_display) return; xim_real_init(x11_window, x11_display); gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH); if (xic != NULL) XUnregisterIMInstantiateCallback(x11_display, NULL, NULL, NULL, xim_instantiate_cb, NULL); } static void xim_destroy_cb( XIM im UNUSED, XPointer client_data UNUSED, XPointer call_data UNUSED) { Window x11_window; Display *x11_display; # ifdef XIM_DEBUG xim_log("xim_destroy_cb()\n"); #endif gui_get_x11_windis(&x11_window, &x11_display); xic = NULL; status_area_enabled = FALSE; gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH); XRegisterIMInstantiateCallback(x11_display, NULL, NULL, NULL, xim_instantiate_cb, NULL); } # endif void xim_init(void) { Window x11_window; Display *x11_display; # ifdef XIM_DEBUG xim_log("xim_init()\n"); # endif gui_get_x11_windis(&x11_window, &x11_display); xic = NULL; if (xim_real_init(x11_window, x11_display)) return; gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH); # ifdef USE_X11R6_XIM XRegisterIMInstantiateCallback(x11_display, NULL, NULL, NULL, xim_instantiate_cb, NULL); # endif } static int xim_real_init(Window x11_window, Display *x11_display) { int i; char *p, *s, *ns, *end, tmp[1024]; # define IMLEN_MAX 40 char buf[IMLEN_MAX + 7]; XIM xim = NULL; XIMStyles *xim_styles; XIMStyle this_input_style = 0; Boolean found; XPoint over_spot; XVaNestedList preedit_list, status_list; input_style = 0; status_area_enabled = FALSE; if (xic != NULL) return FALSE; if (gui.rsrc_input_method != NULL && *gui.rsrc_input_method != NUL) { strcpy(tmp, gui.rsrc_input_method); for (ns = s = tmp; ns != NULL && *s != NUL;) { s = (char *)skipwhite((char_u *)s); if (*s == NUL) break; if ((ns = end = strchr(s, ',')) == NULL) end = s + strlen(s); while (isspace(((char_u *)end)[-1])) end--; *end = NUL; if (strlen(s) <= IMLEN_MAX) { strcpy(buf, "@im="); strcat(buf, s); if ((p = XSetLocaleModifiers(buf)) != NULL && *p != NUL && (xim = XOpenIM(x11_display, NULL, NULL, NULL)) != NULL) break; } s = ns + 1; } } if (xim == NULL && (p = XSetLocaleModifiers("")) != NULL && *p != NUL) xim = XOpenIM(x11_display, NULL, NULL, NULL); // This is supposed to be useful to obtain characters through // XmbLookupString() without really using a XIM. if (xim == NULL && (p = XSetLocaleModifiers("@im=none")) != NULL && *p != NUL) xim = XOpenIM(x11_display, NULL, NULL, NULL); if (xim == NULL) { // Only give this message when verbose is set, because too many people // got this message when they didn't want to use a XIM. if (p_verbose > 0) { verbose_enter(); emsg(_(e_failed_to_open_input_method)); verbose_leave(); } return FALSE; } # ifdef USE_X11R6_XIM { XIMCallback destroy_cb; destroy_cb.callback = xim_destroy_cb; destroy_cb.client_data = NULL; if (XSetIMValues(xim, XNDestroyCallback, &destroy_cb, NULL)) emsg(_(e_warning_could_not_set_destroy_callback_to_im)); } # endif if (XGetIMValues(xim, XNQueryInputStyle, &xim_styles, NULL) || !xim_styles) { emsg(_(e_input_method_doesnt_support_any_style)); XCloseIM(xim); return FALSE; } found = False; strcpy(tmp, gui.rsrc_preedit_type_name); for (s = tmp; s && !found; ) { while (*s && isspace((unsigned char)*s)) s++; if (!*s) break; if ((ns = end = strchr(s, ',')) != 0) ns++; else end = s + strlen(s); while (isspace((unsigned char)*end)) end--; *end = '\0'; if (!strcmp(s, "OverTheSpot")) this_input_style = (XIMPreeditPosition | XIMStatusArea); else if (!strcmp(s, "OffTheSpot")) this_input_style = (XIMPreeditArea | XIMStatusArea); else if (!strcmp(s, "Root")) this_input_style = (XIMPreeditNothing | XIMStatusNothing); for (i = 0; (unsigned short)i < xim_styles->count_styles; i++) { if (this_input_style == xim_styles->supported_styles[i]) { found = True; break; } } if (!found) for (i = 0; (unsigned short)i < xim_styles->count_styles; i++) { if ((xim_styles->supported_styles[i] & this_input_style) == (this_input_style & ~XIMStatusArea)) { this_input_style &= ~XIMStatusArea; found = True; break; } } s = ns; } XFree(xim_styles); if (!found) { // Only give this message when verbose is set, because too many people // got this message when they didn't want to use a XIM. if (p_verbose > 0) { verbose_enter(); emsg(_(e_input_method_doesnt_support_my_preedit_type)); verbose_leave(); } XCloseIM(xim); return FALSE; } over_spot.x = TEXT_X(gui.col); over_spot.y = TEXT_Y(gui.row); input_style = this_input_style; // A crash was reported when trying to pass gui.norm_font as XNFontSet, // thus that has been removed. Hopefully the default works... # ifdef FEAT_XFONTSET if (gui.fontset != NOFONTSET) { preedit_list = XVaCreateNestedList(0, XNSpotLocation, &over_spot, XNForeground, (Pixel)gui.def_norm_pixel, XNBackground, (Pixel)gui.def_back_pixel, XNFontSet, (XFontSet)gui.fontset, NULL); status_list = XVaCreateNestedList(0, XNForeground, (Pixel)gui.def_norm_pixel, XNBackground, (Pixel)gui.def_back_pixel, XNFontSet, (XFontSet)gui.fontset, NULL); } else # endif { preedit_list = XVaCreateNestedList(0, XNSpotLocation, &over_spot, XNForeground, (Pixel)gui.def_norm_pixel, XNBackground, (Pixel)gui.def_back_pixel, NULL); status_list = XVaCreateNestedList(0, XNForeground, (Pixel)gui.def_norm_pixel, XNBackground, (Pixel)gui.def_back_pixel, NULL); } xic = XCreateIC(xim, XNInputStyle, input_style, XNClientWindow, x11_window, XNFocusWindow, gui.wid, XNPreeditAttributes, preedit_list, XNStatusAttributes, status_list, NULL); XFree(status_list); XFree(preedit_list); if (xic != NULL) { if (input_style & XIMStatusArea) { xim_set_status_area(); status_area_enabled = TRUE; } else gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH); } else { if (!is_not_a_term()) emsg(_(e_failed_to_create_input_context)); XCloseIM(xim); return FALSE; } return TRUE; } # endif // FEAT_GUI_X11 /* * Get IM status. When IM is on, return TRUE. Else return FALSE. * FIXME: This doesn't work correctly: Having focus doesn't always mean XIM is * active, when not having focus XIM may still be active (e.g., when using a * tear-off menu item). */ int im_get_status(void) { # ifdef FEAT_EVAL if (USE_IMSTATUSFUNC) return call_imstatusfunc(); # endif return xim_has_focus; } # endif // !FEAT_GUI_GTK # if !defined(FEAT_GUI_GTK) || defined(PROTO) /* * Set up the status area. * * This should use a separate Widget, but that seems not possible, because * preedit_area and status_area should be set to the same window as for the * text input. Unfortunately this means the status area pollutes the text * window... */ void xim_set_status_area(void) { XVaNestedList preedit_list = 0, status_list = 0, list = 0; XRectangle pre_area, status_area; if (xic == NULL) return; if (input_style & XIMStatusArea) { if (input_style & XIMPreeditArea) { XRectangle *needed_rect; // to get status_area width status_list = XVaCreateNestedList(0, XNAreaNeeded, &needed_rect, NULL); XGetICValues(xic, XNStatusAttributes, status_list, NULL); XFree(status_list); status_area.width = needed_rect->width; } else status_area.width = gui.char_width * Columns; status_area.x = 0; status_area.y = gui.char_height * Rows + gui.border_offset; if (gui.which_scrollbars[SBAR_BOTTOM]) status_area.y += gui.scrollbar_height; #ifdef FEAT_MENU if (gui.menu_is_active) status_area.y += gui.menu_height; #endif status_area.height = gui.char_height; status_list = XVaCreateNestedList(0, XNArea, &status_area, NULL); } else { status_area.x = 0; status_area.y = gui.char_height * Rows + gui.border_offset; if (gui.which_scrollbars[SBAR_BOTTOM]) status_area.y += gui.scrollbar_height; #ifdef FEAT_MENU if (gui.menu_is_active) status_area.y += gui.menu_height; #endif status_area.width = 0; status_area.height = gui.char_height; } if (input_style & XIMPreeditArea) // off-the-spot { pre_area.x = status_area.x + status_area.width; pre_area.y = gui.char_height * Rows + gui.border_offset; pre_area.width = gui.char_width * Columns - pre_area.x; if (gui.which_scrollbars[SBAR_BOTTOM]) pre_area.y += gui.scrollbar_height; #ifdef FEAT_MENU if (gui.menu_is_active) pre_area.y += gui.menu_height; #endif pre_area.height = gui.char_height; preedit_list = XVaCreateNestedList(0, XNArea, &pre_area, NULL); } else if (input_style & XIMPreeditPosition) // over-the-spot { pre_area.x = 0; pre_area.y = 0; pre_area.height = gui.char_height * Rows; pre_area.width = gui.char_width * Columns; preedit_list = XVaCreateNestedList(0, XNArea, &pre_area, NULL); } if (preedit_list && status_list) list = XVaCreateNestedList(0, XNPreeditAttributes, preedit_list, XNStatusAttributes, status_list, NULL); else if (preedit_list) list = XVaCreateNestedList(0, XNPreeditAttributes, preedit_list, NULL); else if (status_list) list = XVaCreateNestedList(0, XNStatusAttributes, status_list, NULL); else list = NULL; if (list) { XSetICValues(xic, XNVaNestedList, list, NULL); XFree(list); } if (status_list) XFree(status_list); if (preedit_list) XFree(preedit_list); } int xim_get_status_area_height(void) { if (status_area_enabled) return gui.char_height; return 0; } # endif #else // !defined(FEAT_XIM) # if defined(IME_WITHOUT_XIM) || defined(VIMDLL) || defined(PROTO) static int im_was_set_active = FALSE; int # ifdef VIMDLL mbyte_im_get_status(void) # else im_get_status(void) # endif { # if defined(FEAT_EVAL) if (USE_IMSTATUSFUNC) return call_imstatusfunc(); # endif return im_was_set_active; } void # ifdef VIMDLL mbyte_im_set_active(int active_arg) # else im_set_active(int active_arg) # endif { # if defined(FEAT_EVAL) int active = !p_imdisable && active_arg; if (USE_IMACTIVATEFUNC && active != im_get_status()) { call_imactivatefunc(active); im_was_set_active = active; } # endif } # if defined(FEAT_GUI) && !defined(FEAT_GUI_HAIKU) && !defined(VIMDLL) void im_set_position(int row UNUSED, int col UNUSED) { } # endif # endif #endif // FEAT_XIM