1 /*
2 * Copyright (C) 2020 The HIME team, Taiwan
3 * GTK - The GIMP Toolkit
4 * Copyright (C) 2000 Red Hat, Inc.
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20
21 #include <ctype.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25
26 #include <X11/keysym.h>
27
28 #include "gtkimcontexthime.h"
29 #include "hime-im-client.h"
30
31 #define DBG 0
32
33 struct _GtkIMContextHIME {
34 GtkIMContext object;
35
36 GdkWindow *client_window;
37
38 HIME_client_handle *hime_ch;
39
40 // preedit
41 char *pe_str;
42 HIME_PREEDIT_ATTR *pe_attr;
43 int pe_attrN;
44 int pe_cursor;
45 gboolean pe_started;
46 };
47
48 static const int BUFFER_SIZE = 256;
49
50 // GObject functions
51 static void gtk_im_context_hime_class_init (GtkIMContextHIMEClass *class);
52 static void gtk_im_context_hime_init (GtkIMContextHIME *im_context_hime);
53 static void gtk_im_context_hime_finalize (GObject *obj);
54
55 // GtkIMContext functions
56 static void gtk_im_context_hime_set_client_window (GtkIMContext *context,
57 GdkWindow *client_window);
58 static void gtk_im_context_hime_get_preedit_string (GtkIMContext *context,
59 gchar **str,
60 PangoAttrList **attrs,
61 gint *cursor_pos);
62 static gboolean gtk_im_context_hime_filter_keypress (GtkIMContext *context,
63 GdkEventKey *event);
64 static void gtk_im_context_hime_focus_in (GtkIMContext *context);
65 static void gtk_im_context_hime_focus_out (GtkIMContext *context);
66 static void gtk_im_context_hime_reset (GtkIMContext *context);
67 static void gtk_im_context_hime_set_cursor_location (GtkIMContext *context,
68 GdkRectangle *area);
69 static void gtk_im_context_hime_set_use_preedit (GtkIMContext *context,
70 gboolean use_preedit);
71
72 static void add_preedit_attr (PangoAttrList *attrs,
73 const gchar *str,
74 HIME_PREEDIT_ATTR *hime_attr);
75
76 GType gtk_type_im_context_hime = 0;
77
gtk_im_context_hime_register_type(GTypeModule * type_module)78 void gtk_im_context_hime_register_type (GTypeModule *type_module) {
79 static const GTypeInfo im_context_hime_info = {
80 sizeof (GtkIMContextHIMEClass),
81 (GBaseInitFunc) NULL,
82 (GBaseFinalizeFunc) NULL,
83 (GClassInitFunc) gtk_im_context_hime_class_init,
84 NULL, /* class_finalize */
85 NULL, /* class_data */
86 sizeof (GtkIMContextHIME),
87 0,
88 (GInstanceInitFunc) gtk_im_context_hime_init,
89 };
90
91 gtk_type_im_context_hime =
92 g_type_module_register_type (type_module,
93 GTK_TYPE_IM_CONTEXT,
94 "GtkIMContextHIME",
95 &im_context_hime_info, 0);
96 }
97
gtk_im_context_hime_new(void)98 GtkIMContext *gtk_im_context_hime_new (void) {
99 GtkIMContextHIME *result = GTK_IM_CONTEXT_HIME (
100 g_object_new (GTK_TYPE_IM_CONTEXT_HIME, NULL));
101
102 return GTK_IM_CONTEXT (result);
103 }
104
105 /**
106 * gtk_im_context_hime_shutdown:
107 *
108 * Destroys all the status windows that are kept by the HIME contexts. This
109 * function should only be called by the HIME module exit routine.
110 **/
gtk_im_context_hime_shutdown(void)111 void gtk_im_context_hime_shutdown (void) {
112 }
113
gtk_im_context_hime_class_init(GtkIMContextHIMEClass * class)114 static void gtk_im_context_hime_class_init (GtkIMContextHIMEClass *class) {
115 GtkIMContextClass *im_context_class = GTK_IM_CONTEXT_CLASS (class);
116 GObjectClass *gobject_class = G_OBJECT_CLASS (class);
117
118 im_context_class->set_client_window = gtk_im_context_hime_set_client_window;
119 im_context_class->get_preedit_string = gtk_im_context_hime_get_preedit_string;
120 im_context_class->filter_keypress = gtk_im_context_hime_filter_keypress;
121 im_context_class->focus_in = gtk_im_context_hime_focus_in;
122 im_context_class->focus_out = gtk_im_context_hime_focus_out;
123 im_context_class->reset = gtk_im_context_hime_reset;
124 im_context_class->set_cursor_location = gtk_im_context_hime_set_cursor_location;
125 im_context_class->set_use_preedit = gtk_im_context_hime_set_use_preedit;
126
127 gobject_class->finalize = gtk_im_context_hime_finalize;
128 }
129
130 static void
init_preedit(GtkIMContextHIME * im_context_hime)131 init_preedit (GtkIMContextHIME *im_context_hime) {
132 if (!im_context_hime) {
133 return;
134 }
135
136 im_context_hime->pe_str = NULL;
137 im_context_hime->pe_attr = NULL;
138 im_context_hime->pe_attrN = 0;
139 im_context_hime->pe_cursor = 0;
140 im_context_hime->pe_started = FALSE;
141 }
142
143 static void
gtk_im_context_hime_init(GtkIMContextHIME * im_context_hime)144 gtk_im_context_hime_init (GtkIMContextHIME *im_context_hime) {
145 im_context_hime->client_window = NULL;
146 im_context_hime->hime_ch = NULL;
147 init_preedit (im_context_hime);
148 }
149
clear_preedit(GtkIMContextHIME * context_hime)150 void clear_preedit (GtkIMContextHIME *context_hime) {
151 if (!context_hime) {
152 return;
153 }
154
155 if (context_hime->pe_str) {
156 free (context_hime->pe_str);
157 context_hime->pe_str = NULL;
158 }
159
160 if (context_hime->pe_attr) {
161 free (context_hime->pe_attr);
162 context_hime->pe_attr = NULL;
163 context_hime->pe_attrN = 0;
164 }
165
166 context_hime->pe_cursor = 0;
167 context_hime->pe_started = FALSE;
168 }
169
gtk_im_context_hime_finalize(GObject * obj)170 static void gtk_im_context_hime_finalize (GObject *obj) {
171 GtkIMContextHIME *context_xim = GTK_IM_CONTEXT_HIME (obj);
172
173 clear_preedit (context_xim);
174
175 if (context_xim->hime_ch) {
176 hime_im_client_close (context_xim->hime_ch);
177 context_xim->hime_ch = NULL;
178 }
179
180 context_xim->client_window = NULL;
181 }
182
get_hime_im_client(GtkIMContextHIME * context_xim)183 static void get_hime_im_client (GtkIMContextHIME *context_xim) {
184
185 if (!context_xim->client_window) {
186 return;
187 }
188
189 GdkDisplay *display = gdk_display_get_default ();
190 if (!display) {
191 return;
192 }
193
194 if (!context_xim->hime_ch) {
195 context_xim->hime_ch = hime_im_client_open (
196 GDK_DISPLAY_XDISPLAY (display));
197 if (!context_xim->hime_ch) {
198 perror ("cannot open hime_ch");
199 }
200
201 init_preedit (context_xim);
202 }
203 }
204
gtk_im_context_hime_set_client_window(GtkIMContext * context,GdkWindow * client_window)205 static void gtk_im_context_hime_set_client_window (GtkIMContext *context,
206 GdkWindow *client_window) {
207 GtkIMContextHIME *context_xim = GTK_IM_CONTEXT_HIME (context);
208
209 if (!client_window) {
210 return;
211 }
212
213 context_xim->client_window = client_window;
214
215 get_hime_im_client (context_xim);
216 if (context_xim->hime_ch) {
217 hime_im_client_set_client_window (context_xim->hime_ch,
218 GDK_WINDOW_XID (client_window));
219 }
220 }
221
gtk_im_context_hime_get_preedit_string(GtkIMContext * context,gchar ** str,PangoAttrList ** attrs,gint * cursor_pos)222 static void gtk_im_context_hime_get_preedit_string (
223 GtkIMContext *context,
224 gchar **str,
225 PangoAttrList **attrs,
226 gint *cursor_pos) {
227 GtkIMContextHIME *context_hime = GTK_IM_CONTEXT_HIME (context);
228
229 if (context_hime->hime_ch && cursor_pos) {
230 int ret = 0;
231 hime_im_client_set_flags (context_hime->hime_ch,
232 FLAG_HIME_client_handle_use_preedit, &ret);
233 }
234
235 if (str) {
236 // hime client handle not present, return an empty string
237 if (!context_hime->hime_ch) {
238 *str = g_strdup ("");
239 } else {
240 // return preedit buffer if any,
241 // otherwise, return an empty string
242 if (context_hime->pe_str) {
243 *str = g_strdup (context_hime->pe_str);
244 } else {
245 *str = g_strdup ("");
246 }
247 }
248 }
249
250 if (cursor_pos) {
251 *cursor_pos = context_hime->pe_cursor;
252 }
253
254 if (attrs) {
255 *attrs = pango_attr_list_new ();
256 for (int i = 0; i < context_hime->pe_attrN; i++) {
257 add_preedit_attr (*attrs, *str, &(context_hime->pe_attr[i]));
258 }
259 }
260 }
261
262 // returns 0 if failed
construct_xevent(const GdkEventKey * event,XKeyPressedEvent * xevent)263 static int construct_xevent (const GdkEventKey *event,
264 XKeyPressedEvent *xevent) {
265
266 GdkScreen *screen = gdk_window_get_screen (event->window);
267 if (!screen) {
268 return 0;
269 }
270
271 GdkWindow *root_window = gdk_screen_get_root_window (screen);
272
273 xevent->type = (event->type == GDK_KEY_PRESS) ? KeyPress : KeyRelease;
274 xevent->serial = 0;
275 xevent->send_event = (unsigned char) event->send_event;
276 xevent->display = GDK_WINDOW_XDISPLAY (event->window);
277 xevent->window = GDK_WINDOW_XID (event->window);
278 xevent->root = GDK_WINDOW_XID (root_window);
279 xevent->subwindow = xevent->window;
280 xevent->time = event->time;
281 xevent->x = 0;
282 xevent->y = 0;
283 xevent->x_root = 0;
284 xevent->y_root = 0;
285 xevent->state = event->state;
286 xevent->keycode = event->hardware_keycode;
287 xevent->same_screen = True;
288
289 return 1;
290 }
291
gtk_im_context_hime_filter_keypress(GtkIMContext * context,GdkEventKey * event)292 static gboolean gtk_im_context_hime_filter_keypress (GtkIMContext *context,
293 GdkEventKey *event) {
294 GtkIMContextHIME *context_xim = GTK_IM_CONTEXT_HIME (context);
295
296 // buffer between X and hime
297 gchar static_buffer[BUFFER_SIZE];
298 char *buffer = static_buffer;
299 gint buffer_size = sizeof (static_buffer) - 1;
300
301 // TRUE if the input method handled the key event.
302 // No further processing should be done for this key event for Gtk.
303 gboolean result = FALSE;
304
305 // the final result of preediting to be commited
306 char *result_str = NULL;
307
308 // construct key event
309 XKeyPressedEvent xevent;
310 int ok = construct_xevent (event, &xevent);
311 if (!ok) {
312 // can't get root window, skip processing
313 return result;
314 }
315
316 // XLookupString translates a key event to a KeySym and a string,
317 // returns the number of characters that are stored in the buffer.
318 KeySym keysym = 0;
319 gsize num_bytes = XLookupString (&xevent, buffer, buffer_size, &keysym, NULL);
320
321 #if (!FREEBSD)
322 // Convert from a GDK key symbol to the corresponding ISO10646 (Unicode) character.
323 // returns 0 if there is no corresponding character.
324 const guint32 unicode = gdk_keyval_to_unicode (event->keyval);
325 if (unicode) {
326 num_bytes = g_unichar_to_utf8 (unicode, buffer);
327 buffer[num_bytes] = '\0';
328 }
329 #endif
330
331 // tell hime-im-client to process key event
332 // result_str would hold the result
333 gboolean context_has_str = context_xim->pe_str && context_xim->pe_str[0];
334 if (event->type == GDK_KEY_PRESS) {
335 result = hime_im_client_forward_key_press (context_xim->hime_ch,
336 keysym, event->state, &result_str);
337 } else {
338 result = hime_im_client_forward_key_release (context_xim->hime_ch,
339 keysym, event->state, &result_str);
340 }
341 gboolean preedit_changed = result;
342
343 char *preedit_str = NULL;
344 HIME_PREEDIT_ATTR attr[HIME_PREEDIT_ATTR_MAX_N];
345 int cursor_pos = 0;
346 int sub_comp_len = 0;
347 int attrN = hime_im_client_get_preedit (context_xim->hime_ch,
348 &preedit_str, attr, &cursor_pos, &sub_comp_len);
349 gboolean has_preedit_str = preedit_str && preedit_str[0];
350 if (sub_comp_len) {
351 has_preedit_str = TRUE;
352 }
353
354 if (!context_xim->pe_started && has_preedit_str) {
355 g_signal_emit_by_name (context, "preedit-start");
356 context_xim->pe_started = TRUE;
357 }
358
359 // preedit_str and pe_str hold different strings
360 const gboolean different_str = preedit_str &&
361 context_xim->pe_str &&
362 (strcmp (preedit_str, context_xim->pe_str) != 0);
363
364 // update preedit string
365 if (context_has_str != has_preedit_str || different_str) {
366 if (context_xim->pe_str) {
367 free (context_xim->pe_str);
368 }
369 context_xim->pe_str = preedit_str;
370 }
371
372 size_t attrsz = sizeof (HIME_PREEDIT_ATTR) * attrN;
373
374 // pe_attr and attr hold different data
375 const gboolean different_attr = context_xim->pe_attr &&
376 (memcmp (context_xim->pe_attr, attr, attrsz) != 0);
377
378 // update pe_attr
379 if (context_xim->pe_attrN != attrN || different_attr) {
380 context_xim->pe_attrN = attrN;
381
382 if (context_xim->pe_attr) {
383 free (context_xim->pe_attr);
384 }
385 context_xim->pe_attr = NULL;
386
387 if (attrsz) {
388 context_xim->pe_attr = malloc (attrsz);
389
390 if (context_xim->pe_attr) {
391 memcpy (context_xim->pe_attr, attr, attrsz);
392 }
393 }
394 }
395
396 // update pe_cursor
397 if (context_xim->pe_cursor != cursor_pos) {
398 context_xim->pe_cursor = cursor_pos;
399 }
400
401 const gboolean alt_or_control_pressed = event->state & (Mod1Mask | ControlMask);
402 // GDK_KEY_PRESS event
403 // hime_im_client_forward_key_press returns False
404 // result_str is empty
405 // buffer[0] is printable
406 // not alt_or_control_pressed
407 if (event->type == GDK_KEY_PRESS &&
408 !result &&
409 !result_str &&
410 num_bytes &&
411 isprint (buffer[0]) &&
412 !alt_or_control_pressed) {
413
414 // copy buffer into result_str
415 result_str = (char *) malloc (num_bytes + 1);
416 memcpy (result_str, buffer, num_bytes);
417 result_str[num_bytes] = 0;
418 result = TRUE;
419 }
420
421 if (preedit_changed) {
422 g_signal_emit_by_name (context_xim, "preedit-changed");
423 }
424
425 if (result_str) {
426 g_signal_emit_by_name (context, "commit", result_str);
427 free (result_str);
428 }
429
430 if (!has_preedit_str && context_xim->pe_started) {
431 clear_preedit (context_xim);
432 context_xim->pe_started = FALSE;
433 g_signal_emit_by_name (context, "preedit-end");
434 }
435
436 return result;
437 }
438
gtk_im_context_hime_focus_in(GtkIMContext * context)439 static void gtk_im_context_hime_focus_in (GtkIMContext *context) {
440 GtkIMContextHIME *context_xim = GTK_IM_CONTEXT_HIME (context);
441
442 if (context_xim->hime_ch) {
443 hime_im_client_focus_in (context_xim->hime_ch);
444 }
445 }
446
gtk_im_context_hime_focus_out(GtkIMContext * context)447 static void gtk_im_context_hime_focus_out (GtkIMContext *context) {
448 GtkIMContextHIME *context_xim = GTK_IM_CONTEXT_HIME (context);
449
450 if (context_xim->hime_ch) {
451 char *result_str = NULL;
452 hime_im_client_focus_out2 (context_xim->hime_ch, &result_str);
453
454 if (result_str) {
455 g_signal_emit_by_name (context, "commit", result_str);
456 clear_preedit (context_xim);
457 g_signal_emit_by_name (context, "preedit-changed");
458 free (result_str);
459 }
460 }
461 }
462
gtk_im_context_hime_set_cursor_location(GtkIMContext * context,GdkRectangle * area)463 static void gtk_im_context_hime_set_cursor_location (GtkIMContext *context,
464 GdkRectangle *area) {
465 if (!area) {
466 return;
467 }
468
469 GtkIMContextHIME *context_xim = GTK_IM_CONTEXT_HIME (context);
470
471 if (!context_xim->hime_ch) {
472 get_hime_im_client (context_xim);
473 }
474
475 if (context_xim->hime_ch) {
476 hime_im_client_set_cursor_location (
477 context_xim->hime_ch,
478 area->x,
479 area->y + area->height);
480 }
481 }
482
gtk_im_context_hime_set_use_preedit(GtkIMContext * context,gboolean use_preedit)483 static void gtk_im_context_hime_set_use_preedit (GtkIMContext *context,
484 gboolean use_preedit) {
485 GtkIMContextHIME *context_hime = GTK_IM_CONTEXT_HIME (context);
486
487 if (!context_hime->hime_ch) {
488 return;
489 }
490
491 int ret = 0;
492
493 if (use_preedit) {
494 hime_im_client_set_flags (context_hime->hime_ch,
495 FLAG_HIME_client_handle_use_preedit, &ret);
496 } else {
497 hime_im_client_clear_flags (context_hime->hime_ch,
498 FLAG_HIME_client_handle_use_preedit, &ret);
499 }
500 }
501
gtk_im_context_hime_reset(GtkIMContext * context)502 static void gtk_im_context_hime_reset (GtkIMContext *context) {
503 GtkIMContextHIME *context_hime = GTK_IM_CONTEXT_HIME (context);
504
505 if (context_hime->hime_ch) {
506 hime_im_client_reset (context_hime->hime_ch);
507 clear_preedit (context_hime);
508 g_signal_emit_by_name (context, "preedit-changed");
509 }
510 }
511
512 /*
513 * Mask of feedback bits that we render
514 */
add_preedit_attr(PangoAttrList * attrs,const gchar * str,HIME_PREEDIT_ATTR * hime_attr)515 static void add_preedit_attr (PangoAttrList *attrs,
516 const gchar *str,
517 HIME_PREEDIT_ATTR *hime_attr) {
518 PangoAttribute *attr = NULL;
519 gint start_index = g_utf8_offset_to_pointer (str, hime_attr->ofs0) - str;
520 gint end_index = g_utf8_offset_to_pointer (str, hime_attr->ofs1) - str;
521
522 if (hime_attr->flag & HIME_PREEDIT_ATTR_FLAG_UNDERLINE) {
523 attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
524 attr->start_index = start_index;
525 attr->end_index = end_index;
526 pango_attr_list_change (attrs, attr);
527 }
528
529 if (hime_attr->flag & HIME_PREEDIT_ATTR_FLAG_REVERSE) {
530 const guint16 rgb_min = 0x0000;
531 const guint16 rgb_max = 0xffff;
532
533 // set foreground = white
534 attr = pango_attr_foreground_new (rgb_max, rgb_max, rgb_max);
535 attr->start_index = start_index;
536 attr->end_index = end_index;
537 pango_attr_list_change (attrs, attr);
538
539 // set background = black
540 attr = pango_attr_background_new (rgb_min, rgb_min, rgb_min);
541 attr->start_index = start_index;
542 attr->end_index = end_index;
543 pango_attr_list_change (attrs, attr);
544 }
545 }
546