1 /*
2   Simple DirectMedia Layer
3   Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org>
4 
5   This software is provided 'as-is', without any express or implied
6   warranty.  In no event will the authors be held liable for any damages
7   arising from the use of this software.
8 
9   Permission is granted to anyone to use this software for any purpose,
10   including commercial applications, and to alter it and redistribute it
11   freely, subject to the following restrictions:
12 
13   1. The origin of this software must not be misrepresented; you must not
14      claim that you wrote the original software. If you use this software
15      in a product, an acknowledgment in the product documentation would be
16      appreciated but is not required.
17   2. Altered source versions must be plainly marked as such, and must not be
18      misrepresented as being the original software.
19   3. This notice may not be removed or altered from any source distribution.
20 */
21 #include "../../SDL_internal.h"
22 
23 #ifdef HAVE_IBUS_IBUS_H
24 #include "SDL.h"
25 #include "SDL_syswm.h"
26 #include "SDL_ibus.h"
27 #include "SDL_dbus.h"
28 #include "../../video/SDL_sysvideo.h"
29 #include "../../events/SDL_keyboard_c.h"
30 
31 #if SDL_VIDEO_DRIVER_X11
32     #include "../../video/x11/SDL_x11video.h"
33 #endif
34 
35 #include <sys/inotify.h>
36 #include <unistd.h>
37 #include <fcntl.h>
38 
39 static const char IBUS_SERVICE[]         = "org.freedesktop.IBus";
40 static const char IBUS_PATH[]            = "/org/freedesktop/IBus";
41 static const char IBUS_INTERFACE[]       = "org.freedesktop.IBus";
42 static const char IBUS_INPUT_INTERFACE[] = "org.freedesktop.IBus.InputContext";
43 
44 static char *input_ctx_path = NULL;
45 static SDL_Rect ibus_cursor_rect = { 0, 0, 0, 0 };
46 static DBusConnection *ibus_conn = NULL;
47 static char *ibus_addr_file = NULL;
48 static int inotify_fd = -1, inotify_wd = -1;
49 
50 static Uint32
IBus_ModState(void)51 IBus_ModState(void)
52 {
53     Uint32 ibus_mods = 0;
54     SDL_Keymod sdl_mods = SDL_GetModState();
55 
56     /* Not sure about MOD3, MOD4 and HYPER mappings */
57     if (sdl_mods & KMOD_LSHIFT) ibus_mods |= IBUS_SHIFT_MASK;
58     if (sdl_mods & KMOD_CAPS)   ibus_mods |= IBUS_LOCK_MASK;
59     if (sdl_mods & KMOD_LCTRL)  ibus_mods |= IBUS_CONTROL_MASK;
60     if (sdl_mods & KMOD_LALT)   ibus_mods |= IBUS_MOD1_MASK;
61     if (sdl_mods & KMOD_NUM)    ibus_mods |= IBUS_MOD2_MASK;
62     if (sdl_mods & KMOD_MODE)   ibus_mods |= IBUS_MOD5_MASK;
63     if (sdl_mods & KMOD_LGUI)   ibus_mods |= IBUS_SUPER_MASK;
64     if (sdl_mods & KMOD_RGUI)   ibus_mods |= IBUS_META_MASK;
65 
66     return ibus_mods;
67 }
68 
69 static const char *
IBus_GetVariantText(DBusConnection * conn,DBusMessageIter * iter,SDL_DBusContext * dbus)70 IBus_GetVariantText(DBusConnection *conn, DBusMessageIter *iter, SDL_DBusContext *dbus)
71 {
72     /* The text we need is nested weirdly, use dbus-monitor to see the structure better */
73     const char *text = NULL;
74     const char *struct_id = NULL;
75     DBusMessageIter sub1, sub2;
76 
77     if (dbus->message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) {
78         return NULL;
79     }
80 
81     dbus->message_iter_recurse(iter, &sub1);
82 
83     if (dbus->message_iter_get_arg_type(&sub1) != DBUS_TYPE_STRUCT) {
84         return NULL;
85     }
86 
87     dbus->message_iter_recurse(&sub1, &sub2);
88 
89     if (dbus->message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) {
90         return NULL;
91     }
92 
93     dbus->message_iter_get_basic(&sub2, &struct_id);
94     if (!struct_id || SDL_strncmp(struct_id, "IBusText", sizeof("IBusText")) != 0) {
95         return NULL;
96     }
97 
98     dbus->message_iter_next(&sub2);
99     dbus->message_iter_next(&sub2);
100 
101     if (dbus->message_iter_get_arg_type(&sub2) != DBUS_TYPE_STRING) {
102         return NULL;
103     }
104 
105     dbus->message_iter_get_basic(&sub2, &text);
106 
107     return text;
108 }
109 
110 static DBusHandlerResult
IBus_MessageHandler(DBusConnection * conn,DBusMessage * msg,void * user_data)111 IBus_MessageHandler(DBusConnection *conn, DBusMessage *msg, void *user_data)
112 {
113     SDL_DBusContext *dbus = (SDL_DBusContext *)user_data;
114 
115     if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "CommitText")) {
116         DBusMessageIter iter;
117         const char *text;
118 
119         dbus->message_iter_init(msg, &iter);
120 
121         text = IBus_GetVariantText(conn, &iter, dbus);
122         if (text && *text) {
123             char buf[SDL_TEXTINPUTEVENT_TEXT_SIZE];
124             size_t text_bytes = SDL_strlen(text), i = 0;
125 
126             while (i < text_bytes) {
127                 size_t sz = SDL_utf8strlcpy(buf, text+i, sizeof(buf));
128                 SDL_SendKeyboardText(buf);
129 
130                 i += sz;
131             }
132         }
133 
134         return DBUS_HANDLER_RESULT_HANDLED;
135     }
136 
137     if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "UpdatePreeditText")) {
138         DBusMessageIter iter;
139         const char *text;
140 
141         dbus->message_iter_init(msg, &iter);
142         text = IBus_GetVariantText(conn, &iter, dbus);
143 
144         if (text) {
145             char buf[SDL_TEXTEDITINGEVENT_TEXT_SIZE];
146             size_t text_bytes = SDL_strlen(text), i = 0;
147             size_t cursor = 0;
148 
149             do {
150                 const size_t sz = SDL_utf8strlcpy(buf, text+i, sizeof(buf));
151                 const size_t chars = SDL_utf8strlen(buf);
152 
153                 SDL_SendEditingText(buf, cursor, chars);
154 
155                 i += sz;
156                 cursor += chars;
157             } while (i < text_bytes);
158         }
159 
160         SDL_IBus_UpdateTextRect(NULL);
161 
162         return DBUS_HANDLER_RESULT_HANDLED;
163     }
164 
165     if (dbus->message_is_signal(msg, IBUS_INPUT_INTERFACE, "HidePreeditText")) {
166         SDL_SendEditingText("", 0, 0);
167         return DBUS_HANDLER_RESULT_HANDLED;
168     }
169 
170     return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
171 }
172 
173 static char *
IBus_ReadAddressFromFile(const char * file_path)174 IBus_ReadAddressFromFile(const char *file_path)
175 {
176     char addr_buf[1024];
177     SDL_bool success = SDL_FALSE;
178     FILE *addr_file;
179 
180     addr_file = fopen(file_path, "r");
181     if (!addr_file) {
182         return NULL;
183     }
184 
185     while (fgets(addr_buf, sizeof(addr_buf), addr_file)) {
186         if (SDL_strncmp(addr_buf, "IBUS_ADDRESS=", sizeof("IBUS_ADDRESS=")-1) == 0) {
187             size_t sz = SDL_strlen(addr_buf);
188             if (addr_buf[sz-1] == '\n') addr_buf[sz-1] = 0;
189             if (addr_buf[sz-2] == '\r') addr_buf[sz-2] = 0;
190             success = SDL_TRUE;
191             break;
192         }
193     }
194 
195     fclose(addr_file);
196 
197     if (success) {
198         return SDL_strdup(addr_buf + (sizeof("IBUS_ADDRESS=") - 1));
199     } else {
200         return NULL;
201     }
202 }
203 
204 static char *
IBus_GetDBusAddressFilename(void)205 IBus_GetDBusAddressFilename(void)
206 {
207     SDL_DBusContext *dbus;
208     const char *disp_env;
209     char config_dir[PATH_MAX];
210     char *display = NULL;
211     const char *addr;
212     const char *conf_env;
213     char *key;
214     char file_path[PATH_MAX];
215     const char *host;
216     char *disp_num, *screen_num;
217 
218     if (ibus_addr_file) {
219         return SDL_strdup(ibus_addr_file);
220     }
221 
222     dbus = SDL_DBus_GetContext();
223     if (!dbus) {
224         return NULL;
225     }
226 
227     /* Use this environment variable if it exists. */
228     addr = SDL_getenv("IBUS_ADDRESS");
229     if (addr && *addr) {
230         return SDL_strdup(addr);
231     }
232 
233     /* Otherwise, we have to get the hostname, display, machine id, config dir
234        and look up the address from a filepath using all those bits, eek. */
235     disp_env = SDL_getenv("DISPLAY");
236 
237     if (!disp_env || !*disp_env) {
238         display = SDL_strdup(":0.0");
239     } else {
240         display = SDL_strdup(disp_env);
241     }
242 
243     host = display;
244     disp_num   = SDL_strrchr(display, ':');
245     screen_num = SDL_strrchr(display, '.');
246 
247     if (!disp_num) {
248         SDL_free(display);
249         return NULL;
250     }
251 
252     *disp_num = 0;
253     disp_num++;
254 
255     if (screen_num) {
256         *screen_num = 0;
257     }
258 
259     if (!*host) {
260         host = "unix";
261     }
262 
263     SDL_memset(config_dir, 0, sizeof(config_dir));
264 
265     conf_env = SDL_getenv("XDG_CONFIG_HOME");
266     if (conf_env && *conf_env) {
267         SDL_strlcpy(config_dir, conf_env, sizeof(config_dir));
268     } else {
269         const char *home_env = SDL_getenv("HOME");
270         if (!home_env || !*home_env) {
271             SDL_free(display);
272             return NULL;
273         }
274         SDL_snprintf(config_dir, sizeof(config_dir), "%s/.config", home_env);
275     }
276 
277     key = dbus->get_local_machine_id();
278 
279     SDL_memset(file_path, 0, sizeof(file_path));
280     SDL_snprintf(file_path, sizeof(file_path), "%s/ibus/bus/%s-%s-%s",
281                                                config_dir, key, host, disp_num);
282     dbus->free(key);
283     SDL_free(display);
284 
285     return SDL_strdup(file_path);
286 }
287 
288 static SDL_bool IBus_CheckConnection(SDL_DBusContext *dbus);
289 
290 static void SDLCALL
IBus_SetCapabilities(void * data,const char * name,const char * old_val,const char * internal_editing)291 IBus_SetCapabilities(void *data, const char *name, const char *old_val,
292                                                    const char *internal_editing)
293 {
294     SDL_DBusContext *dbus = SDL_DBus_GetContext();
295 
296     if (IBus_CheckConnection(dbus)) {
297         Uint32 caps = IBUS_CAP_FOCUS;
298         if (!(internal_editing && *internal_editing == '1')) {
299             caps |= IBUS_CAP_PREEDIT_TEXT;
300         }
301 
302         SDL_DBus_CallVoidMethodOnConnection(ibus_conn, IBUS_SERVICE, input_ctx_path, IBUS_INPUT_INTERFACE, "SetCapabilities",
303                                 DBUS_TYPE_UINT32, &caps, DBUS_TYPE_INVALID);
304     }
305 }
306 
307 
308 static SDL_bool
IBus_SetupConnection(SDL_DBusContext * dbus,const char * addr)309 IBus_SetupConnection(SDL_DBusContext *dbus, const char* addr)
310 {
311     const char *client_name = "SDL2_Application";
312     const char *path = NULL;
313     SDL_bool result = SDL_FALSE;
314     DBusObjectPathVTable ibus_vtable;
315 
316     SDL_zero(ibus_vtable);
317     ibus_vtable.message_function = &IBus_MessageHandler;
318 
319     ibus_conn = dbus->connection_open_private(addr, NULL);
320 
321     if (!ibus_conn) {
322         return SDL_FALSE;
323     }
324 
325     dbus->connection_flush(ibus_conn);
326 
327     if (!dbus->bus_register(ibus_conn, NULL)) {
328         ibus_conn = NULL;
329         return SDL_FALSE;
330     }
331 
332     dbus->connection_flush(ibus_conn);
333 
334     if (SDL_DBus_CallMethodOnConnection(ibus_conn, IBUS_SERVICE, IBUS_PATH, IBUS_INTERFACE, "CreateInputContext",
335             DBUS_TYPE_STRING, &client_name, DBUS_TYPE_INVALID,
336             DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID)) {
337         SDL_free(input_ctx_path);
338         input_ctx_path = SDL_strdup(path);
339         SDL_AddHintCallback(SDL_HINT_IME_INTERNAL_EDITING, IBus_SetCapabilities, NULL);
340 
341         dbus->bus_add_match(ibus_conn, "type='signal',interface='org.freedesktop.IBus.InputContext'", NULL);
342         dbus->connection_try_register_object_path(ibus_conn, input_ctx_path, &ibus_vtable, dbus, NULL);
343         dbus->connection_flush(ibus_conn);
344         result = SDL_TRUE;
345     }
346 
347     SDL_IBus_SetFocus(SDL_GetKeyboardFocus() != NULL);
348     SDL_IBus_UpdateTextRect(NULL);
349 
350     return result;
351 }
352 
353 static SDL_bool
IBus_CheckConnection(SDL_DBusContext * dbus)354 IBus_CheckConnection(SDL_DBusContext *dbus)
355 {
356     if (!dbus) return SDL_FALSE;
357 
358     if (ibus_conn && dbus->connection_get_is_connected(ibus_conn)) {
359         return SDL_TRUE;
360     }
361 
362     if (inotify_fd > 0 && inotify_wd > 0) {
363         char buf[1024];
364         ssize_t readsize = read(inotify_fd, buf, sizeof(buf));
365         if (readsize > 0) {
366 
367             char *p;
368             SDL_bool file_updated = SDL_FALSE;
369 
370             for (p = buf; p < buf + readsize; /**/) {
371                 struct inotify_event *event = (struct inotify_event*) p;
372                 if (event->len > 0) {
373                     char *addr_file_no_path = SDL_strrchr(ibus_addr_file, '/');
374                     if (!addr_file_no_path) return SDL_FALSE;
375 
376                     if (SDL_strcmp(addr_file_no_path + 1, event->name) == 0) {
377                         file_updated = SDL_TRUE;
378                         break;
379                     }
380                 }
381 
382                 p += sizeof(struct inotify_event) + event->len;
383             }
384 
385             if (file_updated) {
386                 char *addr = IBus_ReadAddressFromFile(ibus_addr_file);
387                 if (addr) {
388                     SDL_bool result = IBus_SetupConnection(dbus, addr);
389                     SDL_free(addr);
390                     return result;
391                 }
392             }
393         }
394     }
395 
396     return SDL_FALSE;
397 }
398 
399 SDL_bool
SDL_IBus_Init(void)400 SDL_IBus_Init(void)
401 {
402     SDL_bool result = SDL_FALSE;
403     SDL_DBusContext *dbus = SDL_DBus_GetContext();
404 
405     if (dbus) {
406         char *addr_file = IBus_GetDBusAddressFilename();
407         char *addr;
408         char *addr_file_dir;
409 
410         if (!addr_file) {
411             return SDL_FALSE;
412         }
413 
414         /* !!! FIXME: if ibus_addr_file != NULL, this will overwrite it and leak (twice!) */
415         ibus_addr_file = SDL_strdup(addr_file);
416 
417         addr = IBus_ReadAddressFromFile(addr_file);
418         if (!addr) {
419             SDL_free(addr_file);
420             return SDL_FALSE;
421         }
422 
423         if (inotify_fd < 0) {
424             inotify_fd = inotify_init();
425             fcntl(inotify_fd, F_SETFL, O_NONBLOCK);
426         }
427 
428         addr_file_dir = SDL_strrchr(addr_file, '/');
429         if (addr_file_dir) {
430             *addr_file_dir = 0;
431         }
432 
433         inotify_wd = inotify_add_watch(inotify_fd, addr_file, IN_CREATE | IN_MODIFY);
434         SDL_free(addr_file);
435 
436         if (addr) {
437             result = IBus_SetupConnection(dbus, addr);
438             SDL_free(addr);
439         }
440     }
441 
442     return result;
443 }
444 
445 void
SDL_IBus_Quit(void)446 SDL_IBus_Quit(void)
447 {
448     SDL_DBusContext *dbus;
449 
450     if (input_ctx_path) {
451         SDL_free(input_ctx_path);
452         input_ctx_path = NULL;
453     }
454 
455     if (ibus_addr_file) {
456         SDL_free(ibus_addr_file);
457         ibus_addr_file = NULL;
458     }
459 
460     dbus = SDL_DBus_GetContext();
461 
462     if (dbus && ibus_conn) {
463         dbus->connection_close(ibus_conn);
464         dbus->connection_unref(ibus_conn);
465     }
466 
467     if (inotify_fd > 0 && inotify_wd > 0) {
468         inotify_rm_watch(inotify_fd, inotify_wd);
469         inotify_wd = -1;
470     }
471 
472     SDL_DelHintCallback(SDL_HINT_IME_INTERNAL_EDITING, IBus_SetCapabilities, NULL);
473 
474     SDL_memset(&ibus_cursor_rect, 0, sizeof(ibus_cursor_rect));
475 }
476 
477 static void
IBus_SimpleMessage(const char * method)478 IBus_SimpleMessage(const char *method)
479 {
480     SDL_DBusContext *dbus = SDL_DBus_GetContext();
481 
482     if (IBus_CheckConnection(dbus)) {
483         SDL_DBus_CallVoidMethodOnConnection(ibus_conn, IBUS_SERVICE, input_ctx_path, IBUS_INPUT_INTERFACE, method, DBUS_TYPE_INVALID);
484     }
485 }
486 
487 void
SDL_IBus_SetFocus(SDL_bool focused)488 SDL_IBus_SetFocus(SDL_bool focused)
489 {
490     const char *method = focused ? "FocusIn" : "FocusOut";
491     IBus_SimpleMessage(method);
492 }
493 
494 void
SDL_IBus_Reset(void)495 SDL_IBus_Reset(void)
496 {
497     IBus_SimpleMessage("Reset");
498 }
499 
500 SDL_bool
SDL_IBus_ProcessKeyEvent(Uint32 keysym,Uint32 keycode)501 SDL_IBus_ProcessKeyEvent(Uint32 keysym, Uint32 keycode)
502 {
503     Uint32 result = 0;
504     SDL_DBusContext *dbus = SDL_DBus_GetContext();
505 
506     if (IBus_CheckConnection(dbus)) {
507         Uint32 mods = IBus_ModState();
508         if (!SDL_DBus_CallMethodOnConnection(ibus_conn, IBUS_SERVICE, input_ctx_path, IBUS_INPUT_INTERFACE, "ProcessKeyEvent",
509                 DBUS_TYPE_UINT32, &keysym, DBUS_TYPE_UINT32, &keycode, DBUS_TYPE_UINT32, &mods, DBUS_TYPE_INVALID,
510                 DBUS_TYPE_BOOLEAN, &result, DBUS_TYPE_INVALID)) {
511             result = 0;
512         }
513     }
514 
515     SDL_IBus_UpdateTextRect(NULL);
516 
517     return result ? SDL_TRUE : SDL_FALSE;
518 }
519 
520 void
SDL_IBus_UpdateTextRect(SDL_Rect * rect)521 SDL_IBus_UpdateTextRect(SDL_Rect *rect)
522 {
523     SDL_Window *focused_win;
524     SDL_SysWMinfo info;
525     int x = 0, y = 0;
526     SDL_DBusContext *dbus;
527 
528     if (rect) {
529         SDL_memcpy(&ibus_cursor_rect, rect, sizeof(ibus_cursor_rect));
530     }
531 
532     focused_win = SDL_GetKeyboardFocus();
533     if (!focused_win) {
534         return;
535     }
536 
537     SDL_VERSION(&info.version);
538     if (!SDL_GetWindowWMInfo(focused_win, &info)) {
539         return;
540     }
541 
542     SDL_GetWindowPosition(focused_win, &x, &y);
543 
544 #if SDL_VIDEO_DRIVER_X11
545     if (info.subsystem == SDL_SYSWM_X11) {
546         SDL_DisplayData *displaydata = (SDL_DisplayData *) SDL_GetDisplayForWindow(focused_win)->driverdata;
547 
548         Display *x_disp = info.info.x11.display;
549         Window x_win = info.info.x11.window;
550         int x_screen = displaydata->screen;
551         Window unused;
552 
553         X11_XTranslateCoordinates(x_disp, x_win, RootWindow(x_disp, x_screen), 0, 0, &x, &y, &unused);
554     }
555 #endif
556 
557     x += ibus_cursor_rect.x;
558     y += ibus_cursor_rect.y;
559 
560     dbus = SDL_DBus_GetContext();
561 
562     if (IBus_CheckConnection(dbus)) {
563         SDL_DBus_CallVoidMethodOnConnection(ibus_conn, IBUS_SERVICE, input_ctx_path, IBUS_INPUT_INTERFACE, "SetCursorLocation",
564                 DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y, DBUS_TYPE_INT32, &ibus_cursor_rect.w, DBUS_TYPE_INT32, &ibus_cursor_rect.h, DBUS_TYPE_INVALID);
565     }
566 }
567 
568 void
SDL_IBus_PumpEvents(void)569 SDL_IBus_PumpEvents(void)
570 {
571     SDL_DBusContext *dbus = SDL_DBus_GetContext();
572 
573     if (IBus_CheckConnection(dbus)) {
574         dbus->connection_read_write(ibus_conn, 0);
575 
576         while (dbus->connection_dispatch(ibus_conn) == DBUS_DISPATCH_DATA_REMAINS) {
577             /* Do nothing, actual work happens in IBus_MessageHandler */
578         }
579     }
580 }
581 
582 #endif
583 
584 /* vi: set ts=4 sw=4 expandtab: */
585