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