1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 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#include <unistd.h>
24
25#include "SDL_fcitx.h"
26#include "../../video/SDL_sysvideo.h"
27#include "../../events/SDL_keyboard_c.h"
28#include "SDL_dbus.h"
29
30#ifdef SDL_VIDEO_DRIVER_X11
31#include "../../video/x11/SDL_x11video.h"
32#endif
33
34#define FCITX_DBUS_SERVICE "org.freedesktop.portal.Fcitx"
35
36#define FCITX_IM_DBUS_PATH "/org/freedesktop/portal/inputmethod"
37
38#define FCITX_IM_DBUS_INTERFACE "org.fcitx.Fcitx.InputMethod1"
39#define FCITX_IC_DBUS_INTERFACE "org.fcitx.Fcitx.InputContext1"
40
41#define DBUS_TIMEOUT 500
42
43typedef struct FcitxClient
44{
45 SDL_DBusContext *dbus;
46
47 char *ic_path;
48
49 int id;
50
51 SDL_Rect cursor_rect;
52} FcitxClient;
53
54static FcitxClient fcitx_client;
55
56static char *GetAppName(void)
57{
58#if defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_FREEBSD)
59 char *spot;
60 char procfile[1024];
61 char linkfile[1024];
62 int linksize;
63
64#ifdef SDL_PLATFORM_LINUX
65 (void)SDL_snprintf(procfile, sizeof(procfile), "/proc/%d/exe", getpid());
66#elif defined(SDL_PLATFORM_FREEBSD)
67 (void)SDL_snprintf(procfile, sizeof(procfile), "/proc/%d/file", getpid());
68#endif
69 linksize = readlink(procfile, linkfile, sizeof(linkfile) - 1);
70 if (linksize > 0) {
71 linkfile[linksize] = '\0';
72 spot = SDL_strrchr(linkfile, '/');
73 if (spot) {
74 return SDL_strdup(spot + 1);
75 } else {
76 return SDL_strdup(linkfile);
77 }
78 }
79#endif // SDL_PLATFORM_LINUX || SDL_PLATFORM_FREEBSD
80
81 return SDL_strdup("SDL_App");
82}
83
84static size_t Fcitx_GetPreeditString(SDL_DBusContext *dbus,
85 DBusMessage *msg,
86 char **ret,
87 Sint32 *start_pos,
88 Sint32 *end_pos)
89{
90 char *text = NULL, *subtext;
91 size_t text_bytes = 0;
92 DBusMessageIter iter, array, sub;
93 Sint32 p_start_pos = -1;
94 Sint32 p_end_pos = -1;
95
96 dbus->message_iter_init(msg, &iter);
97 // Message type is a(si)i, we only need string part
98 if (dbus->message_iter_get_arg_type(&iter) == DBUS_TYPE_ARRAY) {
99 size_t pos = 0;
100 // First pass: calculate string length
101 dbus->message_iter_recurse(&iter, &array);
102 while (dbus->message_iter_get_arg_type(&array) == DBUS_TYPE_STRUCT) {
103 dbus->message_iter_recurse(&array, &sub);
104 subtext = NULL;
105 if (dbus->message_iter_get_arg_type(&sub) == DBUS_TYPE_STRING) {
106 dbus->message_iter_get_basic(&sub, &subtext);
107 if (subtext && *subtext) {
108 text_bytes += SDL_strlen(subtext);
109 }
110 }
111 dbus->message_iter_next(&sub);
112 if (dbus->message_iter_get_arg_type(&sub) == DBUS_TYPE_INT32 && p_end_pos == -1) {
113 // Type is a bit field defined as follows:
114 // bit 3: Underline, bit 4: HighLight, bit 5: DontCommit,
115 // bit 6: Bold, bit 7: Strike, bit 8: Italic
116 Sint32 type;
117 dbus->message_iter_get_basic(&sub, &type);
118 // We only consider highlight
119 if (type & (1 << 4)) {
120 if (p_start_pos == -1) {
121 p_start_pos = pos;
122 }
123 } else if (p_start_pos != -1 && p_end_pos == -1) {
124 p_end_pos = pos;
125 }
126 }
127 dbus->message_iter_next(&array);
128 if (subtext && *subtext) {
129 pos += SDL_utf8strlen(subtext);
130 }
131 }
132 if (p_start_pos != -1 && p_end_pos == -1) {
133 p_end_pos = pos;
134 }
135 if (text_bytes) {
136 text = SDL_malloc(text_bytes + 1);
137 }
138
139 if (text) {
140 char *pivot = text;
141 // Second pass: join all the sub string
142 dbus->message_iter_recurse(&iter, &array);
143 while (dbus->message_iter_get_arg_type(&array) == DBUS_TYPE_STRUCT) {
144 dbus->message_iter_recurse(&array, &sub);
145 if (dbus->message_iter_get_arg_type(&sub) == DBUS_TYPE_STRING) {
146 dbus->message_iter_get_basic(&sub, &subtext);
147 if (subtext && *subtext) {
148 size_t length = SDL_strlen(subtext);
149 SDL_strlcpy(pivot, subtext, length + 1);
150 pivot += length;
151 }
152 }
153 dbus->message_iter_next(&array);
154 }
155 } else {
156 text_bytes = 0;
157 }
158 }
159
160 *ret = text;
161 *start_pos = p_start_pos;
162 *end_pos = p_end_pos;
163 return text_bytes;
164}
165
166static Sint32 Fcitx_GetPreeditCursorByte(SDL_DBusContext *dbus, DBusMessage *msg)
167{
168 Sint32 byte = -1;
169 DBusMessageIter iter;
170
171 dbus->message_iter_init(msg, &iter);
172
173 dbus->message_iter_next(&iter);
174
175 if (dbus->message_iter_get_arg_type(&iter) != DBUS_TYPE_INT32) {
176 return -1;
177 }
178
179 dbus->message_iter_get_basic(&iter, &byte);
180
181 return byte;
182}
183
184static DBusHandlerResult DBus_MessageFilter(DBusConnection *conn, DBusMessage *msg, void *data)
185{
186 SDL_DBusContext *dbus = (SDL_DBusContext *)data;
187
188 if (dbus->message_is_signal(msg, FCITX_IC_DBUS_INTERFACE, "CommitString")) {
189 DBusMessageIter iter;
190 const char *text = NULL;
191
192 dbus->message_iter_init(msg, &iter);
193 dbus->message_iter_get_basic(&iter, &text);
194
195 SDL_SendKeyboardText(text);
196
197 return DBUS_HANDLER_RESULT_HANDLED;
198 }
199
200 if (dbus->message_is_signal(msg, FCITX_IC_DBUS_INTERFACE, "UpdateFormattedPreedit")) {
201 char *text = NULL;
202 Sint32 start_pos, end_pos;
203 size_t text_bytes = Fcitx_GetPreeditString(dbus, msg, &text, &start_pos, &end_pos);
204 if (text_bytes) {
205 if (start_pos == -1) {
206 Sint32 byte_pos = Fcitx_GetPreeditCursorByte(dbus, msg);
207 start_pos = byte_pos >= 0 ? SDL_utf8strnlen(text, byte_pos) : -1;
208 }
209 SDL_SendEditingText(text, start_pos, end_pos >= 0 ? end_pos - start_pos : -1);
210 SDL_free(text);
211 } else {
212 SDL_SendEditingText("", 0, 0);
213 }
214
215 SDL_Fcitx_UpdateTextInputArea(SDL_GetKeyboardFocus());
216 return DBUS_HANDLER_RESULT_HANDLED;
217 }
218
219 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
220}
221
222static void FcitxClientICCallMethod(FcitxClient *client, const char *method)
223{
224 if (!client->ic_path) {
225 return;
226 }
227 SDL_DBus_CallVoidMethod(FCITX_DBUS_SERVICE, client->ic_path, FCITX_IC_DBUS_INTERFACE, method, DBUS_TYPE_INVALID);
228}
229
230static void SDLCALL Fcitx_SetCapabilities(void *data,
231 const char *name,
232 const char *old_val,
233 const char *hint)
234{
235 FcitxClient *client = (FcitxClient *)data;
236 Uint64 caps = 0;
237 if (!client->ic_path) {
238 return;
239 }
240
241 if (hint && SDL_strstr(hint, "composition")) {
242 caps |= (1 << 1); // Preedit Flag
243 caps |= (1 << 4); // Formatted Preedit Flag
244 }
245 if (hint && SDL_strstr(hint, "candidates")) {
246 // FIXME, turn off native candidate rendering
247 }
248
249 SDL_DBus_CallVoidMethod(FCITX_DBUS_SERVICE, client->ic_path, FCITX_IC_DBUS_INTERFACE, "SetCapability", DBUS_TYPE_UINT64, &caps, DBUS_TYPE_INVALID);
250}
251
252static bool FcitxCreateInputContext(SDL_DBusContext *dbus, const char *appname, char **ic_path)
253{
254 const char *program = "program";
255 bool result = false;
256
257 if (dbus && dbus->session_conn) {
258 DBusMessage *msg = dbus->message_new_method_call(FCITX_DBUS_SERVICE, FCITX_IM_DBUS_PATH, FCITX_IM_DBUS_INTERFACE, "CreateInputContext");
259 if (msg) {
260 DBusMessage *reply = NULL;
261 DBusMessageIter args, array, sub;
262 dbus->message_iter_init_append(msg, &args);
263 dbus->message_iter_open_container(&args, DBUS_TYPE_ARRAY, "(ss)", &array);
264 dbus->message_iter_open_container(&array, DBUS_TYPE_STRUCT, 0, &sub);
265 dbus->message_iter_append_basic(&sub, DBUS_TYPE_STRING, &program);
266 dbus->message_iter_append_basic(&sub, DBUS_TYPE_STRING, &appname);
267 dbus->message_iter_close_container(&array, &sub);
268 dbus->message_iter_close_container(&args, &array);
269 reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, 300, NULL);
270 if (reply) {
271 if (dbus->message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, ic_path, DBUS_TYPE_INVALID)) {
272 result = true;
273 }
274 dbus->message_unref(reply);
275 }
276 dbus->message_unref(msg);
277 }
278 }
279 return result;
280}
281
282static bool FcitxClientCreateIC(FcitxClient *client)
283{
284 char *appname = GetAppName();
285 char *ic_path = NULL;
286 SDL_DBusContext *dbus = client->dbus;
287
288 // SDL_DBus_CallMethod cannot handle a(ss) type, call dbus function directly
289 if (!FcitxCreateInputContext(dbus, appname, &ic_path)) {
290 ic_path = NULL; // just in case.
291 }
292
293 SDL_free(appname);
294
295 if (ic_path) {
296 SDL_free(client->ic_path);
297 client->ic_path = SDL_strdup(ic_path);
298
299 dbus->bus_add_match(dbus->session_conn,
300 "type='signal', interface='org.fcitx.Fcitx.InputContext1'",
301 NULL);
302 dbus->connection_add_filter(dbus->session_conn,
303 &DBus_MessageFilter, dbus,
304 NULL);
305 dbus->connection_flush(dbus->session_conn);
306
307 SDL_AddHintCallback(SDL_HINT_IME_IMPLEMENTED_UI, Fcitx_SetCapabilities, client);
308 return true;
309 }
310
311 return false;
312}
313
314static Uint32 Fcitx_ModState(void)
315{
316 Uint32 fcitx_mods = 0;
317 SDL_Keymod sdl_mods = SDL_GetModState();
318
319 if (sdl_mods & SDL_KMOD_SHIFT) {
320 fcitx_mods |= (1 << 0);
321 }
322 if (sdl_mods & SDL_KMOD_CAPS) {
323 fcitx_mods |= (1 << 1);
324 }
325 if (sdl_mods & SDL_KMOD_CTRL) {
326 fcitx_mods |= (1 << 2);
327 }
328 if (sdl_mods & SDL_KMOD_ALT) {
329 fcitx_mods |= (1 << 3);
330 }
331 if (sdl_mods & SDL_KMOD_NUM) {
332 fcitx_mods |= (1 << 4);
333 }
334 if (sdl_mods & SDL_KMOD_MODE) {
335 fcitx_mods |= (1 << 7);
336 }
337 if (sdl_mods & SDL_KMOD_LGUI) {
338 fcitx_mods |= (1 << 6);
339 }
340 if (sdl_mods & SDL_KMOD_RGUI) {
341 fcitx_mods |= (1 << 28);
342 }
343
344 return fcitx_mods;
345}
346
347bool SDL_Fcitx_Init(void)
348{
349 fcitx_client.dbus = SDL_DBus_GetContext();
350
351 fcitx_client.cursor_rect.x = -1;
352 fcitx_client.cursor_rect.y = -1;
353 fcitx_client.cursor_rect.w = 0;
354 fcitx_client.cursor_rect.h = 0;
355
356 return FcitxClientCreateIC(&fcitx_client);
357}
358
359void SDL_Fcitx_Quit(void)
360{
361 FcitxClientICCallMethod(&fcitx_client, "DestroyIC");
362 if (fcitx_client.ic_path) {
363 SDL_free(fcitx_client.ic_path);
364 fcitx_client.ic_path = NULL;
365 }
366}
367
368void SDL_Fcitx_SetFocus(bool focused)
369{
370 if (focused) {
371 FcitxClientICCallMethod(&fcitx_client, "FocusIn");
372 } else {
373 FcitxClientICCallMethod(&fcitx_client, "FocusOut");
374 }
375}
376
377void SDL_Fcitx_Reset(void)
378{
379 FcitxClientICCallMethod(&fcitx_client, "Reset");
380}
381
382bool SDL_Fcitx_ProcessKeyEvent(Uint32 keysym, Uint32 keycode, bool down)
383{
384 Uint32 mod_state = Fcitx_ModState();
385 Uint32 handled = false;
386 Uint32 is_release = !down;
387 Uint32 event_time = 0;
388
389 if (!fcitx_client.ic_path) {
390 return false;
391 }
392
393 if (SDL_DBus_CallMethod(FCITX_DBUS_SERVICE, fcitx_client.ic_path, FCITX_IC_DBUS_INTERFACE, "ProcessKeyEvent",
394 DBUS_TYPE_UINT32, &keysym, DBUS_TYPE_UINT32, &keycode, DBUS_TYPE_UINT32, &mod_state, DBUS_TYPE_BOOLEAN, &is_release, DBUS_TYPE_UINT32, &event_time, DBUS_TYPE_INVALID,
395 DBUS_TYPE_BOOLEAN, &handled, DBUS_TYPE_INVALID)) {
396 if (handled) {
397 SDL_Fcitx_UpdateTextInputArea(SDL_GetKeyboardFocus());
398 return true;
399 }
400 }
401
402 return false;
403}
404
405void SDL_Fcitx_UpdateTextInputArea(SDL_Window *window)
406{
407 int x = 0, y = 0;
408 SDL_Rect *cursor = &fcitx_client.cursor_rect;
409
410 if (!window) {
411 return;
412 }
413
414 // We'll use a square at the text input cursor location for the cursor_rect
415 cursor->x = window->text_input_rect.x + window->text_input_cursor;
416 cursor->y = window->text_input_rect.y;
417 cursor->w = window->text_input_rect.h;
418 cursor->h = window->text_input_rect.h;
419
420 SDL_GetWindowPosition(window, &x, &y);
421
422#ifdef SDL_VIDEO_DRIVER_X11
423 {
424 SDL_PropertiesID props = SDL_GetWindowProperties(window);
425 Display *x_disp = (Display *)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
426 int x_screen = SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_SCREEN_NUMBER, 0);
427 Window x_win = SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
428 Window unused;
429 if (x_disp && x_win) {
430 X11_XTranslateCoordinates(x_disp, x_win, RootWindow(x_disp, x_screen), 0, 0, &x, &y, &unused);
431 }
432 }
433#endif
434
435 if (cursor->x == -1 && cursor->y == -1 && cursor->w == 0 && cursor->h == 0) {
436 // move to bottom left
437 int w = 0, h = 0;
438 SDL_GetWindowSize(window, &w, &h);
439 cursor->x = 0;
440 cursor->y = h;
441 }
442
443 x += cursor->x;
444 y += cursor->y;
445
446 SDL_DBus_CallVoidMethod(FCITX_DBUS_SERVICE, fcitx_client.ic_path, FCITX_IC_DBUS_INTERFACE, "SetCursorRect",
447 DBUS_TYPE_INT32, &x, DBUS_TYPE_INT32, &y, DBUS_TYPE_INT32, &cursor->w, DBUS_TYPE_INT32, &cursor->h, DBUS_TYPE_INVALID);
448}
449
450void SDL_Fcitx_PumpEvents(void)
451{
452 SDL_DBusContext *dbus = fcitx_client.dbus;
453 DBusConnection *conn = dbus->session_conn;
454
455 dbus->connection_read_write(conn, 0);
456
457 while (dbus->connection_dispatch(conn) == DBUS_DISPATCH_DATA_REMAINS) {
458 // Do nothing, actual work happens in DBus_MessageFilter
459 }
460}
461