blob: 7703bb789f67b7a0c555f6ff2de04c6b422219a9 [file] [log] [blame]
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001/* vi:set ts=8 sts=4 sw=4 noet:
2 *
3 * VIM - Vi IMproved by Bram Moolenaar
4 *
5 * Do ":help uganda" in Vim to read copying and usage conditions.
6 * Do ":help credits" in Vim to see a list of people who contributed.
7 * See README.txt for an overview of the Vim source code.
8 */
9
10/*
11 * gui_xim.c: functions for the X Input Method
12 */
13
14#include "vim.h"
15
Yegappan Lakshmananaf936912023-02-20 12:16:39 +000016#if !defined(GTK_CHECK_VERSION)
17# define GTK_CHECK_VERSION(a, b, c) 0
18#endif
19#if !defined(FEAT_GUI_GTK) && defined(PROTO)
20typedef int GtkWidget;
21typedef int GtkIMContext;
22typedef int gchar;
23typedef int gpointer;
24typedef int PangoAttrIterator;
25typedef int GdkEventKey;
26#endif
27
Bram Moolenaarf15c8b62020-06-01 14:34:43 +020028#if defined(FEAT_GUI_GTK) && defined(FEAT_XIM)
29# if GTK_CHECK_VERSION(3,0,0)
30# include <gdk/gdkkeysyms-compat.h>
31# else
32# include <gdk/gdkkeysyms.h>
33# endif
34# ifdef MSWIN
35# include <gdk/gdkwin32.h>
36# else
37# include <gdk/gdkx.h>
38# endif
39#endif
40
41/*
42 * XIM often causes trouble. Define XIM_DEBUG to get a log of XIM callbacks
43 * in the "xim.log" file.
44 */
45// #define XIM_DEBUG
Bram Moolenaar952d9d82021-08-02 18:07:18 +020046#if defined(XIM_DEBUG) && defined(FEAT_GUI_GTK)
47static void xim_log(char *s, ...) ATTRIBUTE_FORMAT_PRINTF(1, 2);
48
Bram Moolenaarf15c8b62020-06-01 14:34:43 +020049 static void
50xim_log(char *s, ...)
51{
52 va_list arglist;
53 static FILE *fd = NULL;
54
55 if (fd == (FILE *)-1)
56 return;
57 if (fd == NULL)
58 {
59 fd = mch_fopen("xim.log", "w");
60 if (fd == NULL)
61 {
62 emsg("Cannot open xim.log");
63 fd = (FILE *)-1;
64 return;
65 }
66 }
67
68 va_start(arglist, s);
69 vfprintf(fd, s, arglist);
70 va_end(arglist);
71}
72#endif
73
Bram Moolenaaref8c6172020-07-01 15:12:44 +020074#if defined(FEAT_GUI_MSWIN)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +020075# define USE_IMACTIVATEFUNC (!gui.in_use && *p_imaf != NUL)
76# define USE_IMSTATUSFUNC (!gui.in_use && *p_imsf != NUL)
77#else
78# define USE_IMACTIVATEFUNC (*p_imaf != NUL)
79# define USE_IMSTATUSFUNC (*p_imsf != NUL)
80#endif
81
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +000082#if (defined(FEAT_EVAL) && \
83 (defined(FEAT_XIM) || defined(IME_WITHOUT_XIM) || defined(VIMDLL))) || \
84 defined(PROTO)
85static callback_T imaf_cb; // 'imactivatefunc' callback function
86static callback_T imsf_cb; // 'imstatusfunc' callback function
87
Yegappan Lakshmananf2e30d02023-01-30 13:04:42 +000088 char *
Yegappan Lakshmananaf936912023-02-20 12:16:39 +000089did_set_imactivatefunc(optset_T *args UNUSED)
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +000090{
Yegappan Lakshmananf2e30d02023-01-30 13:04:42 +000091 if (option_set_callback_func(p_imaf, &imaf_cb) == FAIL)
92 return e_invalid_argument;
93
94 return NULL;
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +000095}
96
Yegappan Lakshmananf2e30d02023-01-30 13:04:42 +000097 char *
Yegappan Lakshmananaf936912023-02-20 12:16:39 +000098did_set_imstatusfunc(optset_T *args UNUSED)
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +000099{
Yegappan Lakshmananf2e30d02023-01-30 13:04:42 +0000100 if (option_set_callback_func(p_imsf, &imsf_cb) == FAIL)
101 return e_invalid_argument;
102
103 return NULL;
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +0000104}
105
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200106 static void
107call_imactivatefunc(int active)
108{
109 typval_T argv[2];
Millyc7e54ef2022-05-26 13:16:25 +0100110 int save_KeyTyped = KeyTyped;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200111
112 argv[0].v_type = VAR_NUMBER;
113 argv[0].vval.v_number = active ? 1 : 0;
114 argv[1].v_type = VAR_UNKNOWN;
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +0000115 (void)call_callback_retnr(&imaf_cb, 1, argv);
Millyc7e54ef2022-05-26 13:16:25 +0100116
117 KeyTyped = save_KeyTyped;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200118}
119
120 static int
121call_imstatusfunc(void)
122{
123 int is_active;
Millyc7e54ef2022-05-26 13:16:25 +0100124 int save_KeyTyped = KeyTyped;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200125
126 // FIXME: Don't execute user function in unsafe situation.
127 if (exiting || is_autocmd_blocked())
128 return FALSE;
129 // FIXME: :py print 'xxx' is shown duplicate result.
130 // Use silent to avoid it.
131 ++msg_silent;
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +0000132 is_active = call_callback_retnr(&imsf_cb, 0, NULL);
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200133 --msg_silent;
Millyc7e54ef2022-05-26 13:16:25 +0100134
135 KeyTyped = save_KeyTyped;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200136 return (is_active > 0);
137}
138#endif
139
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +0000140#if defined(EXITFREE) || defined(PROTO)
141 void
142free_xim_stuff(void)
143{
Yegappan Lakshmanan6ae8fae2021-12-12 16:26:44 +0000144# if defined(FEAT_EVAL) && \
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +0000145 (defined(FEAT_XIM) || defined(IME_WITHOUT_XIM) || defined(VIMDLL))
146 free_callback(&imaf_cb);
147 free_callback(&imsf_cb);
148# endif
149}
150#endif
151
Dominique Pelle748b3082022-01-08 12:41:16 +0000152#if defined(FEAT_EVAL) || defined(PROTO)
Yegappan Lakshmanan6ae8fae2021-12-12 16:26:44 +0000153/*
Bram Moolenaar24fe33a2022-11-24 00:09:02 +0000154 * Mark the global 'imactivatefunc' and 'imstatusfunc' callbacks with "copyID"
Yegappan Lakshmanan6ae8fae2021-12-12 16:26:44 +0000155 * so that they are not garbage collected.
156 */
157 int
158set_ref_in_im_funcs(int copyID UNUSED)
159{
160 int abort = FALSE;
161
Dominique Pelle748b3082022-01-08 12:41:16 +0000162# if defined(FEAT_XIM) || defined(IME_WITHOUT_XIM) || defined(VIMDLL)
Yegappan Lakshmanan6ae8fae2021-12-12 16:26:44 +0000163 abort = set_ref_in_callback(&imaf_cb, copyID);
164 abort = abort || set_ref_in_callback(&imsf_cb, copyID);
Dominique Pelle748b3082022-01-08 12:41:16 +0000165# endif
Yegappan Lakshmanan6ae8fae2021-12-12 16:26:44 +0000166
167 return abort;
168}
Dominique Pelle748b3082022-01-08 12:41:16 +0000169#endif
Yegappan Lakshmanan6ae8fae2021-12-12 16:26:44 +0000170
Yegappan Lakshmanan7645da52021-12-04 14:02:30 +0000171
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200172#if defined(FEAT_XIM) || defined(PROTO)
173
174# if defined(FEAT_GUI_GTK) || defined(PROTO)
zeertzjq9b7d2a92022-08-26 10:33:54 +0100175static int xim_has_preediting = FALSE; // IM current status
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200176
177/*
178 * Set preedit_start_col to the current cursor position.
179 */
180 static void
181init_preedit_start_col(void)
182{
Bram Moolenaar24959102022-05-07 20:01:16 +0100183 if (State & MODE_CMDLINE)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200184 preedit_start_col = cmdline_getvcol_cursor();
185 else if (curwin != NULL && curwin->w_buffer != NULL)
186 getvcol(curwin, &curwin->w_cursor, &preedit_start_col, NULL, NULL);
187 // Prevent that preediting marks the buffer as changed.
188 xim_changed_while_preediting = curbuf->b_changed;
189}
190
191static int im_is_active = FALSE; // IM is enabled for current mode
192static int preedit_is_active = FALSE;
193static int im_preedit_cursor = 0; // cursor offset in characters
194static int im_preedit_trailing = 0; // number of characters after cursor
195
196static unsigned long im_commit_handler_id = 0;
197static unsigned int im_activatekey_keyval = GDK_VoidSymbol;
198static unsigned int im_activatekey_state = 0;
199
200static GtkWidget *preedit_window = NULL;
201static GtkWidget *preedit_label = NULL;
202
203static void im_preedit_window_set_position(void);
204
205 void
206im_set_active(int active)
207{
208 int was_active;
209
210 was_active = !!im_get_status();
211 im_is_active = (active && !p_imdisable);
212
213 if (im_is_active != was_active)
214 xim_reset();
215}
216
217 void
218xim_set_focus(int focus)
219{
Yegappan Lakshmanan7f8b2552023-01-08 13:44:24 +0000220 if (xic == NULL)
221 return;
222
223 if (focus)
224 gtk_im_context_focus_in(xic);
225 else
226 gtk_im_context_focus_out(xic);
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200227}
228
229 void
230im_set_position(int row, int col)
231{
Yegappan Lakshmanan7f8b2552023-01-08 13:44:24 +0000232 if (xic == NULL)
233 return;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200234
Yegappan Lakshmanan7f8b2552023-01-08 13:44:24 +0000235 GdkRectangle area;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200236
Yegappan Lakshmanan7f8b2552023-01-08 13:44:24 +0000237 area.x = FILL_X(col);
238 area.y = FILL_Y(row);
239 area.width = gui.char_width * (mb_lefthalve(row, col) ? 2 : 1);
240 area.height = gui.char_height;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200241
Yegappan Lakshmanan7f8b2552023-01-08 13:44:24 +0000242 gtk_im_context_set_cursor_location(xic, &area);
243
244 if (p_imst == IM_OVER_THE_SPOT)
245 im_preedit_window_set_position();
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200246}
247
248# if 0 || defined(PROTO) // apparently only used in gui_x11.c
249 void
250xim_set_preedit(void)
251{
252 im_set_position(gui.row, gui.col);
253}
254# endif
255
256 static void
257im_add_to_input(char_u *str, int len)
258{
259 // Convert from 'termencoding' (always "utf-8") to 'encoding'
260 if (input_conv.vc_type != CONV_NONE)
261 {
262 str = string_convert(&input_conv, str, &len);
263 g_return_if_fail(str != NULL);
264 }
265
266 add_to_input_buf_csi(str, len);
267
268 if (input_conv.vc_type != CONV_NONE)
269 vim_free(str);
270
271 if (p_mh) // blank out the pointer if necessary
272 gui_mch_mousehide(TRUE);
273}
274
Yegappan Lakshmanane89aef32025-05-14 20:31:55 +0200275 static void
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200276im_preedit_window_set_position(void)
277{
278 int x, y, width, height;
279 int screen_x, screen_y, screen_width, screen_height;
280
281 if (preedit_window == NULL)
282 return;
283
Bram Moolenaara555e6f2021-03-18 22:28:57 +0100284 gui_gtk_get_screen_geom_of_win(gui.drawarea, 0, 0,
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200285 &screen_x, &screen_y, &screen_width, &screen_height);
286 gdk_window_get_origin(gtk_widget_get_window(gui.drawarea), &x, &y);
287 gtk_window_get_size(GTK_WINDOW(preedit_window), &width, &height);
288 x = x + FILL_X(gui.col);
289 y = y + FILL_Y(gui.row);
290 if (x + width > screen_x + screen_width)
291 x = screen_x + screen_width - width;
292 if (y + height > screen_y + screen_height)
293 y = screen_y + screen_height - height;
294 gtk_window_move(GTK_WINDOW(preedit_window), x, y);
295}
296
297 static void
Yegappan Lakshmanana23a11b2023-02-21 14:27:41 +0000298im_preedit_window_open(void)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200299{
300 char *preedit_string;
301#if !GTK_CHECK_VERSION(3,16,0)
302 char buf[8];
303#endif
304 PangoAttrList *attr_list;
305 PangoLayout *layout;
306#if GTK_CHECK_VERSION(3,0,0)
307# if !GTK_CHECK_VERSION(3,16,0)
308 GdkRGBA color;
309# endif
310#else
311 GdkColor color;
312#endif
313 gint w, h;
314
315 if (preedit_window == NULL)
316 {
317 preedit_window = gtk_window_new(GTK_WINDOW_POPUP);
318 gtk_window_set_transient_for(GTK_WINDOW(preedit_window),
319 GTK_WINDOW(gui.mainwin));
320 preedit_label = gtk_label_new("");
321 gtk_widget_set_name(preedit_label, "vim-gui-preedit-area");
322 gtk_container_add(GTK_CONTAINER(preedit_window), preedit_label);
323 }
324
325#if GTK_CHECK_VERSION(3,16,0)
326 {
327 GtkStyleContext * const context
lilydjwgb1457d42024-02-06 10:49:14 +0100328 = gtk_widget_get_style_context(preedit_label);
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200329 GtkCssProvider * const provider = gtk_css_provider_new();
330 gchar *css = NULL;
331 const char * const fontname
332 = pango_font_description_get_family(gui.norm_font);
333 gint fontsize
334 = pango_font_description_get_size(gui.norm_font) / PANGO_SCALE;
335 gchar *fontsize_propval = NULL;
336
337 if (!pango_font_description_get_size_is_absolute(gui.norm_font))
338 {
339 // fontsize was given in points. Convert it into that in pixels
340 // to use with CSS.
341 GdkScreen * const screen
342 = gdk_window_get_screen(gtk_widget_get_window(gui.mainwin));
343 const gdouble dpi = gdk_screen_get_resolution(screen);
344 fontsize = dpi * fontsize / 72;
345 }
346 if (fontsize > 0)
347 fontsize_propval = g_strdup_printf("%dpx", fontsize);
348 else
349 fontsize_propval = g_strdup_printf("inherit");
350
351 css = g_strdup_printf(
lilydjwgb1457d42024-02-06 10:49:14 +0100352 "#vim-gui-preedit-area {\n"
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200353 " font-family: %s,monospace;\n"
354 " font-size: %s;\n"
355 " color: #%.2lx%.2lx%.2lx;\n"
356 " background-color: #%.2lx%.2lx%.2lx;\n"
357 "}\n",
358 fontname != NULL ? fontname : "inherit",
359 fontsize_propval,
360 (gui.norm_pixel >> 16) & 0xff,
361 (gui.norm_pixel >> 8) & 0xff,
362 gui.norm_pixel & 0xff,
363 (gui.back_pixel >> 16) & 0xff,
364 (gui.back_pixel >> 8) & 0xff,
365 gui.back_pixel & 0xff);
366
367 gtk_css_provider_load_from_data(provider, css, -1, NULL);
368 gtk_style_context_add_provider(context,
369 GTK_STYLE_PROVIDER(provider), G_MAXUINT);
370
371 g_free(css);
372 g_free(fontsize_propval);
373 g_object_unref(provider);
374 }
375#elif GTK_CHECK_VERSION(3,0,0)
376 gtk_widget_override_font(preedit_label, gui.norm_font);
377
378 vim_snprintf(buf, sizeof(buf), "#%06X", gui.norm_pixel);
379 gdk_rgba_parse(&color, buf);
380 gtk_widget_override_color(preedit_label, GTK_STATE_FLAG_NORMAL, &color);
381
382 vim_snprintf(buf, sizeof(buf), "#%06X", gui.back_pixel);
383 gdk_rgba_parse(&color, buf);
384 gtk_widget_override_background_color(preedit_label, GTK_STATE_FLAG_NORMAL,
385 &color);
386#else
387 gtk_widget_modify_font(preedit_label, gui.norm_font);
388
389 vim_snprintf(buf, sizeof(buf), "#%06X", (unsigned)gui.norm_pixel);
390 gdk_color_parse(buf, &color);
391 gtk_widget_modify_fg(preedit_label, GTK_STATE_NORMAL, &color);
392
393 vim_snprintf(buf, sizeof(buf), "#%06X", (unsigned)gui.back_pixel);
394 gdk_color_parse(buf, &color);
395 gtk_widget_modify_bg(preedit_window, GTK_STATE_NORMAL, &color);
396#endif
397
398 gtk_im_context_get_preedit_string(xic, &preedit_string, &attr_list, NULL);
399
400 if (preedit_string[0] != NUL)
401 {
402 gtk_label_set_text(GTK_LABEL(preedit_label), preedit_string);
403 gtk_label_set_attributes(GTK_LABEL(preedit_label), attr_list);
404
405 layout = gtk_label_get_layout(GTK_LABEL(preedit_label));
406 pango_layout_get_pixel_size(layout, &w, &h);
407 h = MAX(h, gui.char_height);
408 gtk_window_resize(GTK_WINDOW(preedit_window), w, h);
409
410 gtk_widget_show_all(preedit_window);
411
412 im_preedit_window_set_position();
413 }
414
415 g_free(preedit_string);
416 pango_attr_list_unref(attr_list);
417}
418
419 static void
Yegappan Lakshmanana23a11b2023-02-21 14:27:41 +0000420im_preedit_window_close(void)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200421{
422 if (preedit_window != NULL)
423 gtk_widget_hide(preedit_window);
424}
425
426 static void
Yegappan Lakshmanana23a11b2023-02-21 14:27:41 +0000427im_show_preedit(void)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200428{
429 im_preedit_window_open();
430
431 if (p_mh) // blank out the pointer if necessary
432 gui_mch_mousehide(TRUE);
433}
434
435 static void
436im_delete_preedit(void)
437{
438 char_u bskey[] = {CSI, 'k', 'b'};
439 char_u delkey[] = {CSI, 'k', 'D'};
440
441 if (p_imst == IM_OVER_THE_SPOT)
442 {
443 im_preedit_window_close();
444 return;
445 }
446
Bram Moolenaar24959102022-05-07 20:01:16 +0100447 if (State & MODE_NORMAL
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200448#ifdef FEAT_TERMINAL
449 && !term_use_loop()
450#endif
451 )
452 {
453 im_preedit_cursor = 0;
454 return;
455 }
456 for (; im_preedit_cursor > 0; --im_preedit_cursor)
457 add_to_input_buf(bskey, (int)sizeof(bskey));
458
459 for (; im_preedit_trailing > 0; --im_preedit_trailing)
460 add_to_input_buf(delkey, (int)sizeof(delkey));
461}
462
463/*
464 * Move the cursor left by "num_move_back" characters.
465 * Note that ins_left() checks im_is_preediting() to avoid breaking undo for
466 * these K_LEFT keys.
467 */
468 static void
469im_correct_cursor(int num_move_back)
470{
471 char_u backkey[] = {CSI, 'k', 'l'};
472
Bram Moolenaar24959102022-05-07 20:01:16 +0100473 if (State & MODE_NORMAL)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200474 return;
475# ifdef FEAT_RIGHTLEFT
Bram Moolenaar24959102022-05-07 20:01:16 +0100476 if ((State & MODE_CMDLINE) == 0 && curwin != NULL && curwin->w_p_rl)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200477 backkey[2] = 'r';
478# endif
479 for (; num_move_back > 0; --num_move_back)
480 add_to_input_buf(backkey, (int)sizeof(backkey));
481}
482
483static int xim_expected_char = NUL;
484static int xim_ignored_char = FALSE;
485
486/*
487 * Update the mode and cursor while in an IM callback.
488 */
489 static void
490im_show_info(void)
491{
492 int old_vgetc_busy;
493
494 old_vgetc_busy = vgetc_busy;
495 vgetc_busy = TRUE;
496 showmode();
497 vgetc_busy = old_vgetc_busy;
Bram Moolenaar24959102022-05-07 20:01:16 +0100498 if ((State & MODE_NORMAL) || (State & MODE_INSERT))
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200499 setcursor();
500 out_flush();
501}
502
503/*
504 * Callback invoked when the user finished preediting.
505 * Put the final string into the input buffer.
506 */
507 static void
508im_commit_cb(GtkIMContext *context UNUSED,
509 const gchar *str,
510 gpointer data UNUSED)
511{
512 int slen = (int)STRLEN(str);
513 int add_to_input = TRUE;
514 int clen;
515 int len = slen;
516 int commit_with_preedit = TRUE;
517 char_u *im_str;
518
519#ifdef XIM_DEBUG
520 xim_log("im_commit_cb(): %s\n", str);
521#endif
522
523 if (p_imst == IM_ON_THE_SPOT)
524 {
525 // The imhangul module doesn't reset the preedit string before
526 // committing. Call im_delete_preedit() to work around that.
527 im_delete_preedit();
528
529 // Indicate that preediting has finished.
530 if (preedit_start_col == MAXCOL)
531 {
532 init_preedit_start_col();
533 commit_with_preedit = FALSE;
534 }
535
536 // The thing which setting "preedit_start_col" to MAXCOL means that
537 // "preedit_start_col" will be set forcedly when calling
538 // preedit_changed_cb() next time.
539 // "preedit_start_col" should not reset with MAXCOL on this part. Vim
540 // is simulating the preediting by using add_to_input_str(). when
541 // preedit begin immediately before committed, the typebuf is not
542 // flushed to screen, then it can't get correct "preedit_start_col".
543 // Thus, it should calculate the cells by adding cells of the committed
544 // string.
545 if (input_conv.vc_type != CONV_NONE)
546 {
547 im_str = string_convert(&input_conv, (char_u *)str, &len);
548 g_return_if_fail(im_str != NULL);
549 }
550 else
551 im_str = (char_u *)str;
552
553 clen = mb_string2cells(im_str, len);
554
555 if (input_conv.vc_type != CONV_NONE)
556 vim_free(im_str);
557 preedit_start_col += clen;
558 }
559
560 // Is this a single character that matches a keypad key that's just
561 // been pressed? If so, we don't want it to be entered as such - let
562 // us carry on processing the raw keycode so that it may be used in
563 // mappings as <kSomething>.
564 if (xim_expected_char != NUL)
565 {
566 // We're currently processing a keypad or other special key
567 if (slen == 1 && str[0] == xim_expected_char)
568 {
569 // It's a match - don't do it here
570 xim_ignored_char = TRUE;
571 add_to_input = FALSE;
572 }
573 else
574 {
575 // Not a match
576 xim_ignored_char = FALSE;
577 }
578 }
579
580 if (add_to_input)
581 im_add_to_input((char_u *)str, slen);
582
583 if (p_imst == IM_ON_THE_SPOT)
584 {
585 // Inserting chars while "im_is_active" is set does not cause a
586 // change of buffer. When the chars are committed the buffer must be
587 // marked as changed.
588 if (!commit_with_preedit)
589 preedit_start_col = MAXCOL;
590
591 // This flag is used in changed() at next call.
592 xim_changed_while_preediting = TRUE;
593 }
594
595 if (gtk_main_level() > 0)
596 gtk_main_quit();
597}
598
599/*
600 * Callback invoked after start to the preedit.
601 */
602 static void
603im_preedit_start_cb(GtkIMContext *context UNUSED, gpointer data UNUSED)
604{
605#ifdef XIM_DEBUG
606 xim_log("im_preedit_start_cb()\n");
607#endif
608
609 im_is_active = TRUE;
610 preedit_is_active = TRUE;
611 gui_update_cursor(TRUE, FALSE);
612 im_show_info();
613}
614
615/*
616 * Callback invoked after end to the preedit.
617 */
618 static void
619im_preedit_end_cb(GtkIMContext *context UNUSED, gpointer data UNUSED)
620{
621#ifdef XIM_DEBUG
622 xim_log("im_preedit_end_cb()\n");
623#endif
624 im_delete_preedit();
625
626 // Indicate that preediting has finished
627 if (p_imst == IM_ON_THE_SPOT)
628 preedit_start_col = MAXCOL;
629 xim_has_preediting = FALSE;
630
631#if 0
632 // Removal of this line suggested by Takuhiro Nishioka. Fixes that IM was
633 // switched off unintentionally. We now use preedit_is_active (added by
634 // SungHyun Nam).
635 im_is_active = FALSE;
636#endif
637 preedit_is_active = FALSE;
638 gui_update_cursor(TRUE, FALSE);
639 im_show_info();
640}
641
642/*
643 * Callback invoked after changes to the preedit string. If the preedit
644 * string was empty before, remember the preedit start column so we know
645 * where to apply feedback attributes. Delete the previous preedit string
646 * if there was one, save the new preedit cursor offset, and put the new
647 * string into the input buffer.
648 *
649 * TODO: The pragmatic "put into input buffer" approach used here has
650 * several fundamental problems:
651 *
652 * - The characters in the preedit string are subject to remapping.
653 * That's broken, only the finally committed string should be remapped.
654 *
655 * - There is a race condition involved: The retrieved value for the
656 * current cursor position will be wrong if any unprocessed characters
657 * are still queued in the input buffer.
658 *
659 * - Due to the lack of synchronization between the file buffer in memory
660 * and any typed characters, it's practically impossible to implement the
661 * "retrieve_surrounding" and "delete_surrounding" signals reliably. IM
662 * modules for languages such as Thai are likely to rely on this feature
663 * for proper operation.
664 *
665 * Conclusions: I think support for preediting needs to be moved to the
666 * core parts of Vim. Ideally, until it has been committed, the preediting
667 * string should only be displayed and not affect the buffer content at all.
668 * The question how to deal with the synchronization issue still remains.
669 * Circumventing the input buffer is probably not desirable. Anyway, I think
670 * implementing "retrieve_surrounding" is the only hard problem.
671 *
672 * One way to solve all of this in a clean manner would be to queue all key
673 * press/release events "as is" in the input buffer, and apply the IM filtering
674 * at the receiving end of the queue. This, however, would have a rather large
675 * impact on the code base. If there is an easy way to force processing of all
676 * remaining input from within the "retrieve_surrounding" signal handler, this
677 * might not be necessary. Gotta ask on vim-dev for opinions.
678 */
679 static void
680im_preedit_changed_cb(GtkIMContext *context, gpointer data UNUSED)
681{
682 char *preedit_string = NULL;
683 int cursor_index = 0;
684 int num_move_back = 0;
685 char_u *str;
686 char_u *p;
687 int i;
688
689 if (p_imst == IM_ON_THE_SPOT)
690 gtk_im_context_get_preedit_string(context,
691 &preedit_string, NULL,
692 &cursor_index);
693 else
694 gtk_im_context_get_preedit_string(context,
695 &preedit_string, NULL,
696 NULL);
697
698#ifdef XIM_DEBUG
699 xim_log("im_preedit_changed_cb(): %s\n", preedit_string);
700#endif
701
702 g_return_if_fail(preedit_string != NULL); // just in case
703
704 if (p_imst == IM_OVER_THE_SPOT)
705 {
706 if (preedit_string[0] == NUL)
707 {
708 xim_has_preediting = FALSE;
709 im_delete_preedit();
710 }
711 else
712 {
713 xim_has_preediting = TRUE;
714 im_show_preedit();
715 }
716 }
717 else
718 {
719 // If preedit_start_col is MAXCOL set it to the current cursor position.
720 if (preedit_start_col == MAXCOL && preedit_string[0] != '\0')
721 {
722 xim_has_preediting = TRUE;
723
724 // Urgh, this breaks if the input buffer isn't empty now
725 init_preedit_start_col();
726 }
727 else if (cursor_index == 0 && preedit_string[0] == '\0')
728 {
729 xim_has_preediting = FALSE;
730
731 // If at the start position (after typing backspace)
732 // preedit_start_col must be reset.
733 preedit_start_col = MAXCOL;
734 }
735
736 im_delete_preedit();
737
Bram Moolenaar791fb1b2020-06-02 22:24:36 +0200738 // Compute the end of the preediting area: "preedit_end_col".
739 // According to the documentation of
740 // gtk_im_context_get_preedit_string(), the cursor_pos output argument
741 // returns the offset in bytes. This is unfortunately not true -- real
742 // life shows the offset is in characters, and the GTK+ source code
743 // agrees with me. Will file a bug later.
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200744 if (preedit_start_col != MAXCOL)
745 preedit_end_col = preedit_start_col;
746 str = (char_u *)preedit_string;
747 for (p = str, i = 0; *p != NUL; p += utf_byte2len(*p), ++i)
748 {
749 int is_composing;
750
Bram Moolenaar791fb1b2020-06-02 22:24:36 +0200751 is_composing = ((*p & 0x80) != 0
752 && utf_iscomposing(utf_ptr2char(p)));
753 // These offsets are used as counters when generating <BS> and
754 // <Del> to delete the preedit string. So don't count composing
755 // characters unless 'delcombine' is enabled.
Bram Moolenaarf15c8b62020-06-01 14:34:43 +0200756 if (!is_composing || p_deco)
757 {
758 if (i < cursor_index)
759 ++im_preedit_cursor;
760 else
761 ++im_preedit_trailing;
762 }
763 if (!is_composing && i >= cursor_index)
764 {
765 // This is essentially the same as im_preedit_trailing, except
766 // composing characters are not counted even if p_deco is set.
767 ++num_move_back;
768 }
769 if (preedit_start_col != MAXCOL)
770 preedit_end_col += utf_ptr2cells(p);
771 }
772
773 if (p > str)
774 {
775 im_add_to_input(str, (int)(p - str));
776 im_correct_cursor(num_move_back);
777 }
778 }
779
780 g_free(preedit_string);
781
782 if (gtk_main_level() > 0)
783 gtk_main_quit();
784}
785
786/*
787 * Translate the Pango attributes at iter to Vim highlighting attributes.
788 * Ignore attributes not supported by Vim highlighting. This shouldn't have
789 * too much impact -- right now we handle even more attributes than necessary
790 * for the IM modules I tested with.
791 */
792 static int
793translate_pango_attributes(PangoAttrIterator *iter)
794{
795 PangoAttribute *attr;
796 int char_attr = HL_NORMAL;
797
798 attr = pango_attr_iterator_get(iter, PANGO_ATTR_UNDERLINE);
799 if (attr != NULL && ((PangoAttrInt *)attr)->value
800 != (int)PANGO_UNDERLINE_NONE)
801 char_attr |= HL_UNDERLINE;
802
803 attr = pango_attr_iterator_get(iter, PANGO_ATTR_WEIGHT);
804 if (attr != NULL && ((PangoAttrInt *)attr)->value >= (int)PANGO_WEIGHT_BOLD)
805 char_attr |= HL_BOLD;
806
807 attr = pango_attr_iterator_get(iter, PANGO_ATTR_STYLE);
808 if (attr != NULL && ((PangoAttrInt *)attr)->value
809 != (int)PANGO_STYLE_NORMAL)
810 char_attr |= HL_ITALIC;
811
812 attr = pango_attr_iterator_get(iter, PANGO_ATTR_BACKGROUND);
813 if (attr != NULL)
814 {
815 const PangoColor *color = &((PangoAttrColor *)attr)->color;
816
817 // Assume inverse if black background is requested
818 if ((color->red | color->green | color->blue) == 0)
819 char_attr |= HL_INVERSE;
820 }
821
822 return char_attr;
823}
824
825/*
826 * Retrieve the highlighting attributes at column col in the preedit string.
827 * Return -1 if not in preediting mode or if col is out of range.
828 */
829 int
830im_get_feedback_attr(int col)
831{
832 char *preedit_string = NULL;
833 PangoAttrList *attr_list = NULL;
834 int char_attr = -1;
835
836 if (xic == NULL)
837 return char_attr;
838
839 gtk_im_context_get_preedit_string(xic, &preedit_string, &attr_list, NULL);
840
841 if (preedit_string != NULL && attr_list != NULL)
842 {
843 int idx;
844
845 // Get the byte index as used by PangoAttrIterator
846 for (idx = 0; col > 0 && preedit_string[idx] != '\0'; --col)
847 idx += utfc_ptr2len((char_u *)preedit_string + idx);
848
849 if (preedit_string[idx] != '\0')
850 {
851 PangoAttrIterator *iter;
852 int start, end;
853
854 char_attr = HL_NORMAL;
855 iter = pango_attr_list_get_iterator(attr_list);
856
857 // Extract all relevant attributes from the list.
858 do
859 {
860 pango_attr_iterator_range(iter, &start, &end);
861
862 if (idx >= start && idx < end)
863 char_attr |= translate_pango_attributes(iter);
864 }
865 while (pango_attr_iterator_next(iter));
866
867 pango_attr_iterator_destroy(iter);
868 }
869 }
870
871 if (attr_list != NULL)
872 pango_attr_list_unref(attr_list);
873 g_free(preedit_string);
874
875 return char_attr;
876}
877
878 void
879xim_init(void)
880{
881#ifdef XIM_DEBUG
882 xim_log("xim_init()\n");
883#endif
884
885 g_return_if_fail(gui.drawarea != NULL);
886 g_return_if_fail(gtk_widget_get_window(gui.drawarea) != NULL);
887
888 xic = gtk_im_multicontext_new();
889 g_object_ref(xic);
890
891 im_commit_handler_id = g_signal_connect(G_OBJECT(xic), "commit",
892 G_CALLBACK(&im_commit_cb), NULL);
893 g_signal_connect(G_OBJECT(xic), "preedit_changed",
894 G_CALLBACK(&im_preedit_changed_cb), NULL);
895 g_signal_connect(G_OBJECT(xic), "preedit_start",
896 G_CALLBACK(&im_preedit_start_cb), NULL);
897 g_signal_connect(G_OBJECT(xic), "preedit_end",
898 G_CALLBACK(&im_preedit_end_cb), NULL);
899
900 gtk_im_context_set_client_window(xic, gtk_widget_get_window(gui.drawarea));
901}
902
903 void
904im_shutdown(void)
905{
906#ifdef XIM_DEBUG
907 xim_log("im_shutdown()\n");
908#endif
909
910 if (xic != NULL)
911 {
912 gtk_im_context_focus_out(xic);
913 g_object_unref(xic);
914 xic = NULL;
915 }
916 im_is_active = FALSE;
917 im_commit_handler_id = 0;
918 if (p_imst == IM_ON_THE_SPOT)
919 preedit_start_col = MAXCOL;
920 xim_has_preediting = FALSE;
921}
922
923/*
924 * Convert the string argument to keyval and state for GdkEventKey.
925 * If str is valid return TRUE, otherwise FALSE.
926 *
927 * See 'imactivatekey' for documentation of the format.
928 */
929 static int
930im_string_to_keyval(const char *str, unsigned int *keyval, unsigned int *state)
931{
932 const char *mods_end;
933 unsigned tmp_keyval;
934 unsigned tmp_state = 0;
935
936 mods_end = strrchr(str, '-');
937 mods_end = (mods_end != NULL) ? mods_end + 1 : str;
938
939 // Parse modifier keys
940 while (str < mods_end)
941 switch (*str++)
942 {
943 case '-': break;
944 case 'S': case 's': tmp_state |= (unsigned)GDK_SHIFT_MASK; break;
945 case 'L': case 'l': tmp_state |= (unsigned)GDK_LOCK_MASK; break;
946 case 'C': case 'c': tmp_state |= (unsigned)GDK_CONTROL_MASK;break;
947 case '1': tmp_state |= (unsigned)GDK_MOD1_MASK; break;
948 case '2': tmp_state |= (unsigned)GDK_MOD2_MASK; break;
949 case '3': tmp_state |= (unsigned)GDK_MOD3_MASK; break;
950 case '4': tmp_state |= (unsigned)GDK_MOD4_MASK; break;
951 case '5': tmp_state |= (unsigned)GDK_MOD5_MASK; break;
952 default:
953 return FALSE;
954 }
955
956 tmp_keyval = gdk_keyval_from_name(str);
957
958 if (tmp_keyval == 0 || tmp_keyval == GDK_VoidSymbol)
959 return FALSE;
960
961 if (keyval != NULL)
962 *keyval = tmp_keyval;
963 if (state != NULL)
964 *state = tmp_state;
965
966 return TRUE;
967}
968
969/*
970 * Return TRUE if p_imak is valid, otherwise FALSE. As a special case, an
971 * empty string is also regarded as valid.
972 *
973 * Note: The numerical key value of p_imak is cached if it was valid; thus
974 * boldly assuming im_xim_isvalid_imactivate() will always be called whenever
975 * 'imak' changes. This is currently the case but not obvious -- should
976 * probably rename the function for clarity.
977 */
978 int
979im_xim_isvalid_imactivate(void)
980{
981 if (p_imak[0] == NUL)
982 {
983 im_activatekey_keyval = GDK_VoidSymbol;
984 im_activatekey_state = 0;
985 return TRUE;
986 }
987
988 return im_string_to_keyval((const char *)p_imak,
989 &im_activatekey_keyval,
990 &im_activatekey_state);
991}
992
993 static void
994im_synthesize_keypress(unsigned int keyval, unsigned int state)
995{
996 GdkEventKey *event;
997
998 event = (GdkEventKey *)gdk_event_new(GDK_KEY_PRESS);
999 g_object_ref(gtk_widget_get_window(gui.drawarea));
1000 // unreffed by gdk_event_free()
1001 event->window = gtk_widget_get_window(gui.drawarea);
1002 event->send_event = TRUE;
1003 event->time = GDK_CURRENT_TIME;
1004 event->state = state;
1005 event->keyval = keyval;
1006 event->hardware_keycode = // needed for XIM
1007 XKeysymToKeycode(GDK_WINDOW_XDISPLAY(event->window), (KeySym)keyval);
1008 event->length = 0;
1009 event->string = NULL;
1010
1011 gtk_im_context_filter_keypress(xic, event);
1012
1013 // For consistency, also send the corresponding release event.
1014 event->type = GDK_KEY_RELEASE;
1015 event->send_event = FALSE;
1016 gtk_im_context_filter_keypress(xic, event);
1017
1018 gdk_event_free((GdkEvent *)event);
1019}
1020
1021 void
1022xim_reset(void)
1023{
1024# ifdef FEAT_EVAL
1025 if (USE_IMACTIVATEFUNC)
1026 call_imactivatefunc(im_is_active);
1027 else
1028# endif
1029 if (xic != NULL)
1030 {
1031 gtk_im_context_reset(xic);
1032
1033 if (p_imdisable)
1034 im_shutdown();
1035 else
1036 {
1037 xim_set_focus(gui.in_focus);
1038
1039 if (im_activatekey_keyval != GDK_VoidSymbol)
1040 {
1041 if (im_is_active)
1042 {
1043 g_signal_handler_block(xic, im_commit_handler_id);
1044 im_synthesize_keypress(im_activatekey_keyval,
1045 im_activatekey_state);
1046 g_signal_handler_unblock(xic, im_commit_handler_id);
1047 }
1048 }
1049 else
1050 {
1051 im_shutdown();
1052 xim_init();
1053 xim_set_focus(gui.in_focus);
1054 }
1055 }
1056 }
1057
1058 if (p_imst == IM_ON_THE_SPOT)
1059 preedit_start_col = MAXCOL;
1060 xim_has_preediting = FALSE;
1061}
1062
1063 int
1064xim_queue_key_press_event(GdkEventKey *event, int down)
1065{
Casey Tucker92e90a12024-01-25 22:44:00 +01001066#ifdef FEAT_GUI_GTK
1067 if (event->state & GDK_SUPER_MASK) return FALSE;
1068#endif
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001069 if (down)
1070 {
Bram Moolenaar791fb1b2020-06-02 22:24:36 +02001071 // Workaround GTK2 XIM 'feature' that always converts keypad keys to
1072 // chars., even when not part of an IM sequence (ref. feature of
1073 // gdk/gdkkeyuni.c).
1074 // Flag any keypad keys that might represent a single char.
1075 // If this (on its own - i.e., not part of an IM sequence) is
1076 // committed while we're processing one of these keys, we can ignore
1077 // that commit and go ahead & process it ourselves. That way we can
1078 // still distinguish keypad keys for use in mappings.
1079 // Also add GDK_space to make <S-Space> work.
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001080 switch (event->keyval)
1081 {
1082 case GDK_KP_Add: xim_expected_char = '+'; break;
1083 case GDK_KP_Subtract: xim_expected_char = '-'; break;
1084 case GDK_KP_Divide: xim_expected_char = '/'; break;
1085 case GDK_KP_Multiply: xim_expected_char = '*'; break;
1086 case GDK_KP_Decimal: xim_expected_char = '.'; break;
1087 case GDK_KP_Equal: xim_expected_char = '='; break;
1088 case GDK_KP_0: xim_expected_char = '0'; break;
1089 case GDK_KP_1: xim_expected_char = '1'; break;
1090 case GDK_KP_2: xim_expected_char = '2'; break;
1091 case GDK_KP_3: xim_expected_char = '3'; break;
1092 case GDK_KP_4: xim_expected_char = '4'; break;
1093 case GDK_KP_5: xim_expected_char = '5'; break;
1094 case GDK_KP_6: xim_expected_char = '6'; break;
1095 case GDK_KP_7: xim_expected_char = '7'; break;
1096 case GDK_KP_8: xim_expected_char = '8'; break;
1097 case GDK_KP_9: xim_expected_char = '9'; break;
1098 case GDK_space: xim_expected_char = ' '; break;
1099 default: xim_expected_char = NUL;
1100 }
1101 xim_ignored_char = FALSE;
1102 }
1103
Bram Moolenaar791fb1b2020-06-02 22:24:36 +02001104 // When typing fFtT, XIM may be activated. Thus it must pass
1105 // gtk_im_context_filter_keypress() in Normal mode.
1106 // And while doing :sh too.
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001107 if (xic != NULL && !p_imdisable
Bram Moolenaar24959102022-05-07 20:01:16 +01001108 && (State & (MODE_INSERT | MODE_CMDLINE
1109 | MODE_NORMAL | MODE_EXTERNCMD)))
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001110 {
Bram Moolenaar791fb1b2020-06-02 22:24:36 +02001111 // Filter 'imactivatekey' and map it to CTRL-^. This way, Vim is
1112 // always aware of the current status of IM, and can even emulate
1113 // the activation key for modules that don't support one.
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001114 if (event->keyval == im_activatekey_keyval
1115 && (event->state & im_activatekey_state) == im_activatekey_state)
1116 {
1117 unsigned int state_mask;
1118
1119 // Require the state of the 3 most used modifiers to match exactly.
1120 // Otherwise e.g. <S-C-space> would be unusable for other purposes
1121 // if the IM activate key is <S-space>.
1122 state_mask = im_activatekey_state;
1123 state_mask |= ((int)GDK_SHIFT_MASK | (int)GDK_CONTROL_MASK
1124 | (int)GDK_MOD1_MASK);
1125
1126 if ((event->state & state_mask) != im_activatekey_state)
1127 return FALSE;
1128
1129 // Don't send it a second time on GDK_KEY_RELEASE.
1130 if (event->type != GDK_KEY_PRESS)
1131 return TRUE;
1132
Bram Moolenaar24959102022-05-07 20:01:16 +01001133 if (map_to_exists_mode((char_u *)"", MODE_LANGMAP, FALSE))
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001134 {
1135 im_set_active(FALSE);
1136
1137 // ":lmap" mappings exists, toggle use of mappings.
Bram Moolenaar24959102022-05-07 20:01:16 +01001138 State ^= MODE_LANGMAP;
1139 if (State & MODE_LANGMAP)
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001140 {
1141 curbuf->b_p_iminsert = B_IMODE_NONE;
Bram Moolenaar24959102022-05-07 20:01:16 +01001142 State &= ~MODE_LANGMAP;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001143 }
1144 else
1145 {
1146 curbuf->b_p_iminsert = B_IMODE_LMAP;
Bram Moolenaar24959102022-05-07 20:01:16 +01001147 State |= MODE_LANGMAP;
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001148 }
1149 return TRUE;
1150 }
1151
1152 return gtk_im_context_filter_keypress(xic, event);
1153 }
1154
1155 // Don't filter events through the IM context if IM isn't active
1156 // right now. Unlike with GTK+ 1.2 we cannot rely on the IM module
1157 // not doing anything before the activation key was sent.
1158 if (im_activatekey_keyval == GDK_VoidSymbol || im_is_active)
1159 {
1160 int imresult = gtk_im_context_filter_keypress(xic, event);
1161
1162 if (p_imst == IM_ON_THE_SPOT)
1163 {
1164 // Some XIM send following sequence:
1165 // 1. preedited string.
1166 // 2. committed string.
1167 // 3. line changed key.
1168 // 4. preedited string.
1169 // 5. remove preedited string.
1170 // if 3, Vim can't move back the above line for 5.
1171 // thus, this part should not parse the key.
1172 if (!imresult && preedit_start_col != MAXCOL
1173 && event->keyval == GDK_Return)
1174 {
1175 im_synthesize_keypress(GDK_Return, 0U);
1176 return FALSE;
1177 }
1178 }
1179
1180 // If XIM tried to commit a keypad key as a single char.,
1181 // ignore it so we can use the keypad key 'raw', for mappings.
1182 if (xim_expected_char != NUL && xim_ignored_char)
1183 // We had a keypad key, and XIM tried to thieve it
1184 return FALSE;
1185
1186 // This is supposed to fix a problem with iBus, that space
1187 // characters don't work in input mode.
1188 xim_expected_char = NUL;
1189
1190 // Normal processing
1191 return imresult;
1192 }
1193 }
1194
1195 return FALSE;
1196}
1197
1198 int
1199im_get_status(void)
1200{
1201# ifdef FEAT_EVAL
1202 if (USE_IMSTATUSFUNC)
1203 return call_imstatusfunc();
1204# endif
1205 return im_is_active;
1206}
1207
1208 int
1209preedit_get_status(void)
1210{
1211 return preedit_is_active;
1212}
1213
1214 int
1215im_is_preediting(void)
1216{
1217 return xim_has_preediting;
1218}
1219
1220# else // !FEAT_GUI_GTK
1221
1222static int xim_is_active = FALSE; // XIM should be active in the current
1223 // mode
1224static int xim_has_focus = FALSE; // XIM is really being used for Vim
1225# ifdef FEAT_GUI_X11
1226static XIMStyle input_style;
1227static int status_area_enabled = TRUE;
1228# endif
1229
1230/*
1231 * Switch using XIM on/off. This is used by the code that changes "State".
1232 * When 'imactivatefunc' is defined use that function instead.
1233 */
1234 void
1235im_set_active(int active_arg)
1236{
1237 int active = active_arg;
1238
1239 // If 'imdisable' is set, XIM is never active.
1240 if (p_imdisable)
1241 active = FALSE;
1242 else if (input_style & XIMPreeditPosition)
1243 // There is a problem in switching XIM off when preediting is used,
1244 // and it is not clear how this can be solved. For now, keep XIM on
1245 // all the time, like it was done in Vim 5.8.
1246 active = TRUE;
1247
1248# if defined(FEAT_EVAL)
1249 if (USE_IMACTIVATEFUNC)
1250 {
1251 if (active != im_get_status())
1252 {
1253 call_imactivatefunc(active);
1254 xim_has_focus = active;
1255 }
1256 return;
1257 }
1258# endif
1259
1260 if (xic == NULL)
1261 return;
1262
1263 // Remember the active state, it is needed when Vim gets keyboard focus.
1264 xim_is_active = active;
1265 xim_set_preedit();
1266}
1267
1268/*
1269 * Adjust using XIM for gaining or losing keyboard focus. Also called when
1270 * "xim_is_active" changes.
1271 */
1272 void
1273xim_set_focus(int focus)
1274{
1275 if (xic == NULL)
1276 return;
1277
Bram Moolenaar791fb1b2020-06-02 22:24:36 +02001278 // XIM only gets focus when the Vim window has keyboard focus and XIM has
1279 // been set active for the current mode.
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001280 if (focus && xim_is_active)
1281 {
1282 if (!xim_has_focus)
1283 {
1284 xim_has_focus = TRUE;
1285 XSetICFocus(xic);
1286 }
1287 }
1288 else
1289 {
1290 if (xim_has_focus)
1291 {
1292 xim_has_focus = FALSE;
1293 XUnsetICFocus(xic);
1294 }
1295 }
1296}
1297
1298 void
1299im_set_position(int row UNUSED, int col UNUSED)
1300{
1301 xim_set_preedit();
1302}
1303
1304/*
1305 * Set the XIM to the current cursor position.
1306 */
1307 void
1308xim_set_preedit(void)
1309{
1310 XVaNestedList attr_list;
1311 XRectangle spot_area;
1312 XPoint over_spot;
1313 int line_space;
1314
1315 if (xic == NULL)
1316 return;
1317
1318 xim_set_focus(TRUE);
1319
1320 if (!xim_has_focus)
1321 {
1322 // hide XIM cursor
1323 over_spot.x = 0;
1324 over_spot.y = -100; // arbitrary invisible position
1325 attr_list = (XVaNestedList) XVaCreateNestedList(0,
1326 XNSpotLocation,
1327 &over_spot,
1328 NULL);
1329 XSetICValues(xic, XNPreeditAttributes, attr_list, NULL);
1330 XFree(attr_list);
1331 return;
1332 }
1333
1334 if (input_style & XIMPreeditPosition)
1335 {
1336 if (xim_fg_color == INVALCOLOR)
1337 {
1338 xim_fg_color = gui.def_norm_pixel;
1339 xim_bg_color = gui.def_back_pixel;
1340 }
1341 over_spot.x = TEXT_X(gui.col);
1342 over_spot.y = TEXT_Y(gui.row);
1343 spot_area.x = 0;
1344 spot_area.y = 0;
1345 spot_area.height = gui.char_height * Rows;
1346 spot_area.width = gui.char_width * Columns;
1347 line_space = gui.char_height;
1348 attr_list = (XVaNestedList) XVaCreateNestedList(0,
1349 XNSpotLocation, &over_spot,
1350 XNForeground, (Pixel) xim_fg_color,
1351 XNBackground, (Pixel) xim_bg_color,
1352 XNArea, &spot_area,
1353 XNLineSpace, line_space,
1354 NULL);
1355 if (XSetICValues(xic, XNPreeditAttributes, attr_list, NULL))
Bram Moolenaar9a846fb2022-01-01 21:59:18 +00001356 emsg(_(e_cannot_set_ic_values));
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001357 XFree(attr_list);
1358 }
1359}
1360
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001361# if defined(FEAT_GUI_X11) || defined(PROTO)
1362# if defined(XtSpecificationRelease) && XtSpecificationRelease >= 6 && !defined(SUN_SYSTEM)
1363# define USE_X11R6_XIM
1364# endif
1365
1366static int xim_real_init(Window x11_window, Display *x11_display);
1367
1368
1369# ifdef USE_X11R6_XIM
1370 static void
1371xim_instantiate_cb(
1372 Display *display,
1373 XPointer client_data UNUSED,
1374 XPointer call_data UNUSED)
1375{
1376 Window x11_window;
1377 Display *x11_display;
1378
1379# ifdef XIM_DEBUG
1380 xim_log("xim_instantiate_cb()\n");
1381# endif
1382
1383 gui_get_x11_windis(&x11_window, &x11_display);
1384 if (display != x11_display)
1385 return;
1386
1387 xim_real_init(x11_window, x11_display);
1388 gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH);
1389 if (xic != NULL)
1390 XUnregisterIMInstantiateCallback(x11_display, NULL, NULL, NULL,
1391 xim_instantiate_cb, NULL);
1392}
1393
1394 static void
1395xim_destroy_cb(
1396 XIM im UNUSED,
1397 XPointer client_data UNUSED,
1398 XPointer call_data UNUSED)
1399{
1400 Window x11_window;
1401 Display *x11_display;
1402
1403# ifdef XIM_DEBUG
1404 xim_log("xim_destroy_cb()\n");
1405 #endif
1406 gui_get_x11_windis(&x11_window, &x11_display);
1407
1408 xic = NULL;
1409 status_area_enabled = FALSE;
1410
1411 gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH);
1412
1413 XRegisterIMInstantiateCallback(x11_display, NULL, NULL, NULL,
1414 xim_instantiate_cb, NULL);
1415}
1416# endif
1417
1418 void
1419xim_init(void)
1420{
1421 Window x11_window;
1422 Display *x11_display;
1423
1424# ifdef XIM_DEBUG
1425 xim_log("xim_init()\n");
1426# endif
1427
1428 gui_get_x11_windis(&x11_window, &x11_display);
1429
1430 xic = NULL;
1431
1432 if (xim_real_init(x11_window, x11_display))
1433 return;
1434
1435 gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH);
1436
1437# ifdef USE_X11R6_XIM
1438 XRegisterIMInstantiateCallback(x11_display, NULL, NULL, NULL,
1439 xim_instantiate_cb, NULL);
1440# endif
1441}
1442
1443 static int
1444xim_real_init(Window x11_window, Display *x11_display)
1445{
1446 int i;
1447 char *p,
1448 *s,
1449 *ns,
1450 *end,
1451 tmp[1024];
1452# define IMLEN_MAX 40
1453 char buf[IMLEN_MAX + 7];
1454 XIM xim = NULL;
1455 XIMStyles *xim_styles;
1456 XIMStyle this_input_style = 0;
1457 Boolean found;
1458 XPoint over_spot;
1459 XVaNestedList preedit_list, status_list;
1460
1461 input_style = 0;
1462 status_area_enabled = FALSE;
1463
1464 if (xic != NULL)
1465 return FALSE;
1466
1467 if (gui.rsrc_input_method != NULL && *gui.rsrc_input_method != NUL)
1468 {
1469 strcpy(tmp, gui.rsrc_input_method);
1470 for (ns = s = tmp; ns != NULL && *s != NUL;)
1471 {
1472 s = (char *)skipwhite((char_u *)s);
1473 if (*s == NUL)
1474 break;
1475 if ((ns = end = strchr(s, ',')) == NULL)
1476 end = s + strlen(s);
Keith Thompson184f71c2024-01-04 21:19:04 +01001477 while (SAFE_isspace(end[-1]))
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001478 end--;
1479 *end = NUL;
1480
1481 if (strlen(s) <= IMLEN_MAX)
1482 {
1483 strcpy(buf, "@im=");
1484 strcat(buf, s);
1485 if ((p = XSetLocaleModifiers(buf)) != NULL && *p != NUL
1486 && (xim = XOpenIM(x11_display, NULL, NULL, NULL))
1487 != NULL)
1488 break;
1489 }
1490
1491 s = ns + 1;
1492 }
1493 }
1494
1495 if (xim == NULL && (p = XSetLocaleModifiers("")) != NULL && *p != NUL)
1496 xim = XOpenIM(x11_display, NULL, NULL, NULL);
1497
1498 // This is supposed to be useful to obtain characters through
1499 // XmbLookupString() without really using a XIM.
1500 if (xim == NULL && (p = XSetLocaleModifiers("@im=none")) != NULL
1501 && *p != NUL)
1502 xim = XOpenIM(x11_display, NULL, NULL, NULL);
1503
1504 if (xim == NULL)
1505 {
1506 // Only give this message when verbose is set, because too many people
1507 // got this message when they didn't want to use a XIM.
1508 if (p_verbose > 0)
1509 {
1510 verbose_enter();
Bram Moolenaar9a846fb2022-01-01 21:59:18 +00001511 emsg(_(e_failed_to_open_input_method));
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001512 verbose_leave();
1513 }
1514 return FALSE;
1515 }
1516
1517# ifdef USE_X11R6_XIM
1518 {
1519 XIMCallback destroy_cb;
1520
1521 destroy_cb.callback = xim_destroy_cb;
1522 destroy_cb.client_data = NULL;
1523 if (XSetIMValues(xim, XNDestroyCallback, &destroy_cb, NULL))
Bram Moolenaar9a846fb2022-01-01 21:59:18 +00001524 emsg(_(e_warning_could_not_set_destroy_callback_to_im));
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001525 }
1526# endif
1527
1528 if (XGetIMValues(xim, XNQueryInputStyle, &xim_styles, NULL) || !xim_styles)
1529 {
Bram Moolenaar9a846fb2022-01-01 21:59:18 +00001530 emsg(_(e_input_method_doesnt_support_any_style));
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001531 XCloseIM(xim);
1532 return FALSE;
1533 }
1534
1535 found = False;
1536 strcpy(tmp, gui.rsrc_preedit_type_name);
1537 for (s = tmp; s && !found; )
1538 {
Keith Thompson184f71c2024-01-04 21:19:04 +01001539 while (*s && SAFE_isspace(*s))
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001540 s++;
1541 if (!*s)
1542 break;
1543 if ((ns = end = strchr(s, ',')) != 0)
1544 ns++;
1545 else
1546 end = s + strlen(s);
Keith Thompson184f71c2024-01-04 21:19:04 +01001547 while (SAFE_isspace(*end))
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001548 end--;
1549 *end = '\0';
1550
1551 if (!strcmp(s, "OverTheSpot"))
1552 this_input_style = (XIMPreeditPosition | XIMStatusArea);
1553 else if (!strcmp(s, "OffTheSpot"))
1554 this_input_style = (XIMPreeditArea | XIMStatusArea);
1555 else if (!strcmp(s, "Root"))
1556 this_input_style = (XIMPreeditNothing | XIMStatusNothing);
1557
1558 for (i = 0; (unsigned short)i < xim_styles->count_styles; i++)
1559 {
1560 if (this_input_style == xim_styles->supported_styles[i])
1561 {
1562 found = True;
1563 break;
1564 }
1565 }
1566 if (!found)
1567 for (i = 0; (unsigned short)i < xim_styles->count_styles; i++)
1568 {
1569 if ((xim_styles->supported_styles[i] & this_input_style)
1570 == (this_input_style & ~XIMStatusArea))
1571 {
1572 this_input_style &= ~XIMStatusArea;
1573 found = True;
1574 break;
1575 }
1576 }
1577
1578 s = ns;
1579 }
1580 XFree(xim_styles);
1581
1582 if (!found)
1583 {
1584 // Only give this message when verbose is set, because too many people
1585 // got this message when they didn't want to use a XIM.
1586 if (p_verbose > 0)
1587 {
1588 verbose_enter();
Bram Moolenaar9a846fb2022-01-01 21:59:18 +00001589 emsg(_(e_input_method_doesnt_support_my_preedit_type));
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001590 verbose_leave();
1591 }
1592 XCloseIM(xim);
1593 return FALSE;
1594 }
1595
1596 over_spot.x = TEXT_X(gui.col);
1597 over_spot.y = TEXT_Y(gui.row);
1598 input_style = this_input_style;
1599
1600 // A crash was reported when trying to pass gui.norm_font as XNFontSet,
1601 // thus that has been removed. Hopefully the default works...
1602# ifdef FEAT_XFONTSET
1603 if (gui.fontset != NOFONTSET)
1604 {
1605 preedit_list = XVaCreateNestedList(0,
1606 XNSpotLocation, &over_spot,
1607 XNForeground, (Pixel)gui.def_norm_pixel,
1608 XNBackground, (Pixel)gui.def_back_pixel,
1609 XNFontSet, (XFontSet)gui.fontset,
1610 NULL);
1611 status_list = XVaCreateNestedList(0,
1612 XNForeground, (Pixel)gui.def_norm_pixel,
1613 XNBackground, (Pixel)gui.def_back_pixel,
1614 XNFontSet, (XFontSet)gui.fontset,
1615 NULL);
1616 }
1617 else
1618# endif
1619 {
1620 preedit_list = XVaCreateNestedList(0,
1621 XNSpotLocation, &over_spot,
1622 XNForeground, (Pixel)gui.def_norm_pixel,
1623 XNBackground, (Pixel)gui.def_back_pixel,
1624 NULL);
1625 status_list = XVaCreateNestedList(0,
1626 XNForeground, (Pixel)gui.def_norm_pixel,
1627 XNBackground, (Pixel)gui.def_back_pixel,
1628 NULL);
1629 }
1630
1631 xic = XCreateIC(xim,
1632 XNInputStyle, input_style,
1633 XNClientWindow, x11_window,
1634 XNFocusWindow, gui.wid,
1635 XNPreeditAttributes, preedit_list,
1636 XNStatusAttributes, status_list,
1637 NULL);
1638 XFree(status_list);
1639 XFree(preedit_list);
1640 if (xic != NULL)
1641 {
1642 if (input_style & XIMStatusArea)
1643 {
1644 xim_set_status_area();
1645 status_area_enabled = TRUE;
1646 }
1647 else
1648 gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH);
1649 }
1650 else
1651 {
1652 if (!is_not_a_term())
Bram Moolenaar9a846fb2022-01-01 21:59:18 +00001653 emsg(_(e_failed_to_create_input_context));
Bram Moolenaarf15c8b62020-06-01 14:34:43 +02001654 XCloseIM(xim);
1655 return FALSE;
1656 }
1657
1658 return TRUE;
1659}
1660
1661# endif // FEAT_GUI_X11
1662
1663/*
1664 * Get IM status. When IM is on, return TRUE. Else return FALSE.
1665 * FIXME: This doesn't work correctly: Having focus doesn't always mean XIM is
1666 * active, when not having focus XIM may still be active (e.g., when using a
1667 * tear-off menu item).
1668 */
1669 int
1670im_get_status(void)
1671{
1672# ifdef FEAT_EVAL
1673 if (USE_IMSTATUSFUNC)
1674 return call_imstatusfunc();
1675# endif
1676 return xim_has_focus;
1677}
1678
1679# endif // !FEAT_GUI_GTK
1680
1681# if !defined(FEAT_GUI_GTK) || defined(PROTO)
1682/*
1683 * Set up the status area.
1684 *
1685 * This should use a separate Widget, but that seems not possible, because
1686 * preedit_area and status_area should be set to the same window as for the
1687 * text input. Unfortunately this means the status area pollutes the text
1688 * window...
1689 */
1690 void
1691xim_set_status_area(void)
1692{
1693 XVaNestedList preedit_list = 0, status_list = 0, list = 0;
1694 XRectangle pre_area, status_area;
1695
1696 if (xic == NULL)
1697 return;
1698
1699 if (input_style & XIMStatusArea)
1700 {
1701 if (input_style & XIMPreeditArea)
1702 {
1703 XRectangle *needed_rect;
1704
1705 // to get status_area width
1706 status_list = XVaCreateNestedList(0, XNAreaNeeded,
1707 &needed_rect, NULL);
1708 XGetICValues(xic, XNStatusAttributes, status_list, NULL);
1709 XFree(status_list);
1710
1711 status_area.width = needed_rect->width;
1712 }
1713 else
1714 status_area.width = gui.char_width * Columns;
1715
1716 status_area.x = 0;
1717 status_area.y = gui.char_height * Rows + gui.border_offset;
1718 if (gui.which_scrollbars[SBAR_BOTTOM])
1719 status_area.y += gui.scrollbar_height;
1720#ifdef FEAT_MENU
1721 if (gui.menu_is_active)
1722 status_area.y += gui.menu_height;
1723#endif
1724 status_area.height = gui.char_height;
1725 status_list = XVaCreateNestedList(0, XNArea, &status_area, NULL);
1726 }
1727 else
1728 {
1729 status_area.x = 0;
1730 status_area.y = gui.char_height * Rows + gui.border_offset;
1731 if (gui.which_scrollbars[SBAR_BOTTOM])
1732 status_area.y += gui.scrollbar_height;
1733#ifdef FEAT_MENU
1734 if (gui.menu_is_active)
1735 status_area.y += gui.menu_height;
1736#endif
1737 status_area.width = 0;
1738 status_area.height = gui.char_height;
1739 }
1740
1741 if (input_style & XIMPreeditArea) // off-the-spot
1742 {
1743 pre_area.x = status_area.x + status_area.width;
1744 pre_area.y = gui.char_height * Rows + gui.border_offset;
1745 pre_area.width = gui.char_width * Columns - pre_area.x;
1746 if (gui.which_scrollbars[SBAR_BOTTOM])
1747 pre_area.y += gui.scrollbar_height;
1748#ifdef FEAT_MENU
1749 if (gui.menu_is_active)
1750 pre_area.y += gui.menu_height;
1751#endif
1752 pre_area.height = gui.char_height;
1753 preedit_list = XVaCreateNestedList(0, XNArea, &pre_area, NULL);
1754 }
1755 else if (input_style & XIMPreeditPosition) // over-the-spot
1756 {
1757 pre_area.x = 0;
1758 pre_area.y = 0;
1759 pre_area.height = gui.char_height * Rows;
1760 pre_area.width = gui.char_width * Columns;
1761 preedit_list = XVaCreateNestedList(0, XNArea, &pre_area, NULL);
1762 }
1763
1764 if (preedit_list && status_list)
1765 list = XVaCreateNestedList(0, XNPreeditAttributes, preedit_list,
1766 XNStatusAttributes, status_list, NULL);
1767 else if (preedit_list)
1768 list = XVaCreateNestedList(0, XNPreeditAttributes, preedit_list,
1769 NULL);
1770 else if (status_list)
1771 list = XVaCreateNestedList(0, XNStatusAttributes, status_list,
1772 NULL);
1773 else
1774 list = NULL;
1775
1776 if (list)
1777 {
1778 XSetICValues(xic, XNVaNestedList, list, NULL);
1779 XFree(list);
1780 }
1781 if (status_list)
1782 XFree(status_list);
1783 if (preedit_list)
1784 XFree(preedit_list);
1785}
1786
1787 int
1788xim_get_status_area_height(void)
1789{
1790 if (status_area_enabled)
1791 return gui.char_height;
1792 return 0;
1793}
1794# endif
1795
1796#else // !defined(FEAT_XIM)
1797
1798# if defined(IME_WITHOUT_XIM) || defined(VIMDLL) || defined(PROTO)
1799static int im_was_set_active = FALSE;
1800
1801 int
1802# ifdef VIMDLL
1803mbyte_im_get_status(void)
1804# else
1805im_get_status(void)
1806# endif
1807{
1808# if defined(FEAT_EVAL)
1809 if (USE_IMSTATUSFUNC)
1810 return call_imstatusfunc();
1811# endif
1812 return im_was_set_active;
1813}
1814
1815 void
1816# ifdef VIMDLL
1817mbyte_im_set_active(int active_arg)
1818# else
1819im_set_active(int active_arg)
1820# endif
1821{
1822# if defined(FEAT_EVAL)
1823 int active = !p_imdisable && active_arg;
1824
1825 if (USE_IMACTIVATEFUNC && active != im_get_status())
1826 {
1827 call_imactivatefunc(active);
1828 im_was_set_active = active;
1829 }
1830# endif
1831}
1832
1833# if defined(FEAT_GUI) && !defined(FEAT_GUI_HAIKU) && !defined(VIMDLL)
1834 void
1835im_set_position(int row UNUSED, int col UNUSED)
1836{
1837}
1838# endif
1839# endif
1840
1841#endif // FEAT_XIM