No issues found
1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 /*
3 * st-im-text.c: Text widget with input method support
4 *
5 * Copyright 2009 Red Hat, Inc.
6 *
7 * This started as a copy of ClutterIMText converted to use
8 * GtkIMContext rather than ClutterIMContext. Original code:
9 *
10 * Author: raymond liu <raymond.liu@intel.com>
11 *
12 * Copyright (C) 2009, Intel Corporation.
13 *
14 * This library is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU Lesser General Public License
16 * version 2.1 as published by the Free Software Foundation.
17 *
18 * This library is distributed in the hope that it will be useful, but
19 * WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * Lesser General Public License for more details.
22 *
23 * You should have received a copy of the GNU Lesser General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 /**
28 * SECTION:st-im-text
29 * @short_description: Text widget with input method support
30 * @stability: Unstable
31 * @see_also: #ClutterText
32 * @include: st-imtext/st-imtext.h
33 *
34 * #StIMText derives from ClutterText and hooks up better text input
35 * via #GtkIMContext. It is meant to be a drop-in replacement for
36 * ClutterIMText but using GtkIMContext rather than ClutterIMContext.
37 */
38
39 #ifdef HAVE_CONFIG_H
40 #include "config.h"
41 #endif
42
43 #include <clutter/clutter.h>
44 #include <clutter/x11/clutter-x11.h>
45 #include <glib.h>
46 #include <gtk/gtk.h>
47 #include <gdk/gdkkeysyms.h>
48 #include <gdk/gdkx.h>
49 #include <X11/extensions/XKB.h>
50
51 #include "st-im-text.h"
52
53 #define ST_IM_TEXT_GET_PRIVATE(obj) \
54 (G_TYPE_INSTANCE_GET_PRIVATE ((obj), ST_TYPE_IM_TEXT, StIMTextPrivate))
55
56 struct _StIMTextPrivate
57 {
58 GtkIMContext *im_context;
59 GdkWindow *window;
60
61 guint need_im_reset : 1;
62 };
63
64 G_DEFINE_TYPE (StIMText, st_im_text, CLUTTER_TYPE_TEXT)
65
66 static void
67 st_im_text_dispose (GObject *object)
68 {
69 StIMTextPrivate *priv = ST_IM_TEXT (object)->priv;
70
71 G_OBJECT_CLASS (st_im_text_parent_class)->dispose (object);
72
73 g_clear_object (&priv->im_context);
74 }
75
76 static void
77 update_im_cursor_location (StIMText *self)
78 {
79 StIMTextPrivate *priv = self->priv;
80 ClutterText *clutter_text = CLUTTER_TEXT (self);
81 gint position;
82 gfloat cursor_x, cursor_y, cursor_height;
83 gfloat actor_x, actor_y;
84 GdkRectangle area;
85
86 position = clutter_text_get_cursor_position (clutter_text);
87 clutter_text_position_to_coords (clutter_text, position,
88 &cursor_x, &cursor_y, &cursor_height);
89
90 clutter_actor_get_transformed_position (CLUTTER_ACTOR (self), &actor_x, &actor_y);
91
92 area.x = (int)(0.5 + cursor_x + actor_x);
93 area.y = (int)(0.5 + cursor_y + actor_y);
94 area.width = 0;
95 area.height = (int)(0.5 + cursor_height);
96
97 gtk_im_context_set_cursor_location (priv->im_context, &area);
98 }
99
100 static void
101 st_im_text_commit_cb (GtkIMContext *context,
102 const gchar *str,
103 StIMText *imtext)
104 {
105 ClutterText *clutter_text = CLUTTER_TEXT (imtext);
106
107 if (clutter_text_get_editable (clutter_text))
108 {
109 clutter_text_delete_selection (clutter_text);
110 clutter_text_insert_text (clutter_text, str,
111 clutter_text_get_cursor_position (clutter_text));
112 }
113 }
114
115 static void
116 st_im_text_preedit_changed_cb (GtkIMContext *context,
117 StIMText *imtext)
118 {
119 ClutterText *clutter_text = CLUTTER_TEXT (imtext);
120 gchar *preedit_str = NULL;
121 PangoAttrList *preedit_attrs = NULL;
122 gint cursor_pos = 0;
123
124 gtk_im_context_get_preedit_string (context,
125 &preedit_str,
126 &preedit_attrs,
127 &cursor_pos);
128
129 clutter_text_set_preedit_string (clutter_text,
130 preedit_str,
131 preedit_attrs,
132 cursor_pos);
133
134 g_free (preedit_str);
135 pango_attr_list_unref (preedit_attrs);
136 }
137
138 static gboolean
139 st_im_text_retrieve_surrounding_cb (GtkIMContext *context,
140 StIMText *imtext)
141 {
142 ClutterText *clutter_text = CLUTTER_TEXT (imtext);
143 ClutterTextBuffer *buffer;
144 const gchar *text;
145 gint cursor_pos;
146
147 buffer = clutter_text_get_buffer (clutter_text);
148 text = clutter_text_buffer_get_text (buffer);
149
150 cursor_pos = clutter_text_get_cursor_position (clutter_text);
151 if (cursor_pos < 0)
152 cursor_pos = clutter_text_buffer_get_length (buffer);
153
154 gtk_im_context_set_surrounding (context, text,
155 /* length and cursor_index are in bytes */
156 clutter_text_buffer_get_bytes (buffer),
157 g_utf8_offset_to_pointer (text, cursor_pos) - text);
158
159 return TRUE;
160 }
161
162 static gboolean
163 st_im_text_delete_surrounding_cb (GtkIMContext *context,
164 gint offset,
165 gint n_chars,
166 StIMText *imtext)
167 {
168 ClutterText *clutter_text = CLUTTER_TEXT (imtext);
169
170 if (clutter_text_get_editable (clutter_text))
171 {
172 gint cursor_pos = clutter_text_get_cursor_position (clutter_text);
173 clutter_text_delete_text (clutter_text,
174 cursor_pos + offset,
175 cursor_pos + offset + n_chars);
176 }
177
178 return TRUE;
179 }
180
181 static void
182 reset_im_context (StIMText *self)
183 {
184 StIMTextPrivate *priv = self->priv;
185
186 if (priv->need_im_reset)
187 {
188 gtk_im_context_reset (priv->im_context);
189 priv->need_im_reset = FALSE;
190 }
191 }
192
193 static void
194 st_im_text_paint (ClutterActor *actor)
195 {
196 StIMText *self = ST_IM_TEXT (actor);
197 ClutterText *clutter_text = CLUTTER_TEXT (actor);
198
199 /* This updates the cursor position as a side-effect */
200 if (CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->paint)
201 CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->paint (actor);
202
203 if (clutter_text_get_editable (clutter_text))
204 update_im_cursor_location (self);
205 }
206
207 static gboolean
208 st_im_text_get_paint_volume (ClutterActor *self,
209 ClutterPaintVolume *volume)
210 {
211 return clutter_paint_volume_set_from_allocation (volume, self);
212 }
213
214 /* Returns a new reference to window */
215 static GdkWindow *
216 window_for_actor (ClutterActor *actor)
217 {
218 GdkDisplay *display = gdk_display_get_default ();
219 ClutterActor *stage;
220 Window xwindow;
221 GdkWindow *window;
222
223 stage = clutter_actor_get_stage (actor);
224 xwindow = clutter_x11_get_stage_window ((ClutterStage *)stage);
225
226 window = gdk_x11_window_lookup_for_display (display, xwindow);
227 if (window)
228 g_object_ref (window);
229 else
230 window = gdk_x11_window_foreign_new_for_display (display, xwindow);
231
232 return window;
233 }
234
235 static void
236 st_im_text_realize (ClutterActor *actor)
237 {
238 StIMTextPrivate *priv = ST_IM_TEXT (actor)->priv;
239
240 priv->window = window_for_actor (actor);
241 gtk_im_context_set_client_window (priv->im_context, priv->window);
242 }
243
244 static void
245 st_im_text_unrealize (ClutterActor *actor)
246 {
247 StIMText *self = ST_IM_TEXT (actor);
248 StIMTextPrivate *priv = self->priv;
249
250 reset_im_context (self);
251 gtk_im_context_set_client_window (priv->im_context, NULL);
252 g_object_unref (priv->window);
253 priv->window = NULL;
254 }
255
256 static gboolean
257 key_is_modifier (guint16 keyval)
258 {
259 /* See gdkkeys-x11.c:_gdk_keymap_key_is_modifier() for how this
260 * really should be implemented */
261
262 switch (keyval)
263 {
264 case GDK_KEY_Shift_L:
265 case GDK_KEY_Shift_R:
266 case GDK_KEY_Control_L:
267 case GDK_KEY_Control_R:
268 case GDK_KEY_Caps_Lock:
269 case GDK_KEY_Shift_Lock:
270 case GDK_KEY_Meta_L:
271 case GDK_KEY_Meta_R:
272 case GDK_KEY_Alt_L:
273 case GDK_KEY_Alt_R:
274 case GDK_KEY_Super_L:
275 case GDK_KEY_Super_R:
276 case GDK_KEY_Hyper_L:
277 case GDK_KEY_Hyper_R:
278 case GDK_KEY_ISO_Lock:
279 case GDK_KEY_ISO_Level2_Latch:
280 case GDK_KEY_ISO_Level3_Shift:
281 case GDK_KEY_ISO_Level3_Latch:
282 case GDK_KEY_ISO_Level3_Lock:
283 case GDK_KEY_ISO_Level5_Shift:
284 case GDK_KEY_ISO_Level5_Latch:
285 case GDK_KEY_ISO_Level5_Lock:
286 case GDK_KEY_ISO_Group_Shift:
287 case GDK_KEY_ISO_Group_Latch:
288 case GDK_KEY_ISO_Group_Lock:
289 return TRUE;
290 default:
291 return FALSE;
292 }
293 }
294
295 static GdkEventKey *
296 key_event_to_gdk (ClutterKeyEvent *event_clutter)
297 {
298 GdkDisplay *display = gdk_display_get_default ();
299 GdkKeymap *keymap = gdk_keymap_get_for_display (display);
300 GdkEventKey *event_gdk;
301 event_gdk = (GdkEventKey *)gdk_event_new ((event_clutter->type == CLUTTER_KEY_PRESS) ?
302 GDK_KEY_PRESS : GDK_KEY_RELEASE);
303
304 event_gdk->window = window_for_actor ((ClutterActor *)event_clutter->stage);
305 event_gdk->send_event = FALSE;
306 event_gdk->time = event_clutter->time;
307 /* This depends on ClutterModifierType and GdkModifierType being
308 * identical, which they are currently. (They both match the X
309 * modifier state in the low 16-bits and have the same extensions.) */
310 event_gdk->state = event_clutter->modifier_state;
311 event_gdk->keyval = event_clutter->keyval;
312 event_gdk->hardware_keycode = event_clutter->hardware_keycode;
313 /* For non-proper non-XKB support, we'd need a huge cut-and-paste
314 * from gdkkeys-x11.c; this is a macro that just shifts a few bits
315 * out of state, so won't make the situation worse if the server
316 * doesn't support XKB; we'll just end up with group == 0 */
317 event_gdk->group = XkbGroupForCoreState (event_gdk->state);
318
319 gdk_keymap_translate_keyboard_state (keymap, event_gdk->hardware_keycode,
320 event_gdk->state, event_gdk->group,
321 &event_gdk->keyval, NULL, NULL, NULL);
322
323 if (event_clutter->unicode_value)
324 {
325 /* This is not particularly close to what GDK does - event_gdk->string
326 * is supposed to be in the locale encoding, and have control keys
327 * as control characters, etc. See gdkevents-x11.c:translate_key_event().
328 * Hopefully no input method is using event.string.
329 */
330 char buf[6];
331
332 event_gdk->length = g_unichar_to_utf8 (event_clutter->unicode_value, buf);
333 event_gdk->string = g_strndup (buf, event_gdk->length);
334 }
335
336 event_gdk->is_modifier = key_is_modifier (event_gdk->keyval);
337
338 return event_gdk;
339 }
340
341 static gboolean
342 st_im_text_button_press_event (ClutterActor *actor,
343 ClutterButtonEvent *event)
344 {
345 /* The button press indicates the user moving the cursor, or selecting
346 * etc, so we should abort any current preedit operation. ClutterText
347 * treats all buttons identically, so so do we.
348 */
349 reset_im_context (ST_IM_TEXT (actor));
350
351 if (CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->button_press_event)
352 return CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->button_press_event (actor, event);
353 else
354 return FALSE;
355 }
356
357 static gboolean
358 st_im_text_captured_event (ClutterActor *actor,
359 ClutterEvent *event)
360 {
361 StIMText *self = ST_IM_TEXT (actor);
362 StIMTextPrivate *priv = self->priv;
363 ClutterText *clutter_text = CLUTTER_TEXT (actor);
364 ClutterEventType type = clutter_event_type (event);
365 gboolean result = FALSE;
366 int old_position;
367
368 if (type != CLUTTER_KEY_PRESS && type != CLUTTER_KEY_RELEASE)
369 return FALSE;
370
371 if (clutter_text_get_editable (clutter_text))
372 {
373 GdkEventKey *event_gdk = key_event_to_gdk ((ClutterKeyEvent *)event);
374
375 if (gtk_im_context_filter_keypress (priv->im_context, event_gdk))
376 {
377 priv->need_im_reset = TRUE;
378 result = TRUE;
379 }
380
381 gdk_event_free ((GdkEvent *)event_gdk);
382 }
383
384 old_position = clutter_text_get_cursor_position (clutter_text);
385
386 if (!result &&
387 CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->captured_event)
388 result = CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->captured_event (actor, event);
389
390 if (type == CLUTTER_KEY_PRESS &&
391 clutter_text_get_cursor_position (clutter_text) != old_position)
392 reset_im_context (self);
393
394 return result;
395 }
396
397 static void
398 st_im_text_key_focus_in (ClutterActor *actor)
399 {
400 StIMTextPrivate *priv = ST_IM_TEXT (actor)->priv;
401 ClutterText *clutter_text = CLUTTER_TEXT (actor);
402
403 if (clutter_text_get_editable (clutter_text))
404 {
405 priv->need_im_reset = TRUE;
406 gtk_im_context_focus_in (priv->im_context);
407 }
408
409 if (CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->key_focus_in)
410 CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->key_focus_in (actor);
411 }
412
413 static void
414 st_im_text_key_focus_out (ClutterActor *actor)
415 {
416 StIMTextPrivate *priv = ST_IM_TEXT (actor)->priv;
417 ClutterText *clutter_text = CLUTTER_TEXT (actor);
418
419 if (clutter_text_get_editable (clutter_text))
420 {
421 priv->need_im_reset = TRUE;
422 gtk_im_context_focus_out (priv->im_context);
423 }
424
425 if (CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->key_focus_out)
426 CLUTTER_ACTOR_CLASS (st_im_text_parent_class)->key_focus_out (actor);
427 }
428
429 static void
430 st_im_text_class_init (StIMTextClass *klass)
431 {
432 GObjectClass *object_class = G_OBJECT_CLASS (klass);
433 ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
434
435 g_type_class_add_private (klass, sizeof (StIMTextPrivate));
436
437 object_class->dispose = st_im_text_dispose;
438
439 actor_class->paint = st_im_text_paint;
440 actor_class->get_paint_volume = st_im_text_get_paint_volume;
441 actor_class->realize = st_im_text_realize;
442 actor_class->unrealize = st_im_text_unrealize;
443
444 actor_class->button_press_event = st_im_text_button_press_event;
445 actor_class->captured_event = st_im_text_captured_event;
446 actor_class->key_focus_in = st_im_text_key_focus_in;
447 actor_class->key_focus_out = st_im_text_key_focus_out;
448 }
449
450 static void
451 st_im_text_init (StIMText *self)
452 {
453 StIMTextPrivate *priv;
454
455 self->priv = priv = ST_IM_TEXT_GET_PRIVATE (self);
456
457 priv->im_context = gtk_im_multicontext_new ();
458 g_signal_connect (priv->im_context, "commit",
459 G_CALLBACK (st_im_text_commit_cb), self);
460 g_signal_connect (priv->im_context, "preedit-changed",
461 G_CALLBACK (st_im_text_preedit_changed_cb), self);
462 g_signal_connect (priv->im_context, "retrieve-surrounding",
463 G_CALLBACK (st_im_text_retrieve_surrounding_cb), self);
464 g_signal_connect (priv->im_context, "delete-surrounding",
465 G_CALLBACK (st_im_text_delete_surrounding_cb), self);
466 }
467
468 /**
469 * st_im_text_new:
470 * @text: text to set to
471 *
472 * Create a new #StIMText with the specified text
473 *
474 * Returns: a new #ClutterActor
475 */
476 ClutterActor *
477 st_im_text_new (const gchar *text)
478 {
479 return g_object_new (ST_TYPE_IM_TEXT,
480 "text", text,
481 NULL);
482 }