1 /*
2 * Copyright (c) 2006-2009 Openismus GmbH
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17 * Boston, MA 02111-1307, USA.
18 */
19
20 #include "gtkimcontextmultipress.h"
21 #include <string.h>
22 #include <gtk/gtk.h>
23 #include <gdk/gdkkeysyms.h>
24 #include <gtk/gtkimmodule.h>
25 #include <config.h>
26
27 #define AUTOMATIC_COMPOSE_TIMEOUT 1 /* seconds */
28 #define CONFIGURATION_FILENAME MULTIPRESS_CONFDIR G_DIR_SEPARATOR_S "im-multipress.conf"
29
30 /* This contains rows of characters that can be entered by pressing
31 * a particular key repeatedly. Each row has one key (such as GDK_a),
32 * and an array of character strings, such as "a".
33 */
34 typedef struct
35 {
36 gchar **characters; /* array of strings */
37 gsize n_characters; /* number of strings in the array */
38 }
39 KeySequence;
40
41 static GObjectClass *im_context_multipress_parent_class = NULL;
42 static GType im_context_multipress_type = 0;
43
44 static void im_context_multipress_class_init (GtkImContextMultipressClass *klass);
45 static void im_context_multipress_init (GtkImContextMultipress *self);
46 static void im_context_multipress_finalize (GObject *obj);
47
48 static void load_config (GtkImContextMultipress *self);
49
50 static gboolean vfunc_filter_keypress (GtkIMContext *context,
51 GdkEventKey *event);
52 static void vfunc_reset (GtkIMContext *context);
53 static void vfunc_get_preedit_string (GtkIMContext *context,
54 gchar **str,
55 PangoAttrList **attrs,
56 gint *cursor_pos);
57
58 /* Notice that we have a *_register_type(GTypeModule*) function instead of a
59 * *_get_type() function, because we must use g_type_module_register_type(),
60 * providing the GTypeModule* that was provided to im_context_init(). That
61 * is also why we are not using G_DEFINE_TYPE().
62 */
63 void
gtk_im_context_multipress_register_type(GTypeModule * type_module)64 gtk_im_context_multipress_register_type (GTypeModule* type_module)
65 {
66 const GTypeInfo im_context_multipress_info =
67 {
68 sizeof (GtkImContextMultipressClass),
69 (GBaseInitFunc) NULL,
70 (GBaseFinalizeFunc) NULL,
71 (GClassInitFunc) &im_context_multipress_class_init,
72 NULL,
73 NULL,
74 sizeof (GtkImContextMultipress),
75 0,
76 (GInstanceInitFunc) &im_context_multipress_init,
77 0,
78 };
79
80 im_context_multipress_type =
81 g_type_module_register_type (type_module,
82 GTK_TYPE_IM_CONTEXT,
83 "GtkImContextMultipress",
84 &im_context_multipress_info, 0);
85 }
86
87 GType
gtk_im_context_multipress_get_type(void)88 gtk_im_context_multipress_get_type (void)
89 {
90 g_assert (im_context_multipress_type != 0);
91
92 return im_context_multipress_type;
93 }
94
95 static void
key_sequence_free(gpointer value)96 key_sequence_free (gpointer value)
97 {
98 KeySequence *seq = value;
99
100 if (seq != NULL)
101 {
102 g_strfreev (seq->characters);
103 g_slice_free (KeySequence, seq);
104 }
105 }
106
107 static void
im_context_multipress_class_init(GtkImContextMultipressClass * klass)108 im_context_multipress_class_init (GtkImContextMultipressClass *klass)
109 {
110 GtkIMContextClass *im_context_class;
111
112 /* Set this so we can use it later: */
113 im_context_multipress_parent_class = g_type_class_peek_parent (klass);
114
115 /* Specify our vfunc implementations: */
116 im_context_class = GTK_IM_CONTEXT_CLASS (klass);
117 im_context_class->filter_keypress = &vfunc_filter_keypress;
118 im_context_class->reset = &vfunc_reset;
119 im_context_class->get_preedit_string = &vfunc_get_preedit_string;
120
121 G_OBJECT_CLASS (klass)->finalize = &im_context_multipress_finalize;
122 }
123
124 static void
im_context_multipress_init(GtkImContextMultipress * self)125 im_context_multipress_init (GtkImContextMultipress *self)
126 {
127 self->key_sequences = g_hash_table_new_full (&g_direct_hash, &g_direct_equal,
128 NULL, &key_sequence_free);
129 load_config (self);
130 }
131
132 static void
im_context_multipress_finalize(GObject * obj)133 im_context_multipress_finalize (GObject *obj)
134 {
135 GtkImContextMultipress *self;
136
137 self = GTK_IM_CONTEXT_MULTIPRESS (obj);
138
139 /* Release the configuration data: */
140 if (self->key_sequences != NULL)
141 {
142 g_hash_table_destroy (self->key_sequences);
143 self->key_sequences = NULL;
144 }
145
146 (*im_context_multipress_parent_class->finalize) (obj);
147 }
148
149
150 GtkIMContext *
gtk_im_context_multipress_new(void)151 gtk_im_context_multipress_new (void)
152 {
153 return (GtkIMContext *)g_object_new (GTK_TYPE_IM_CONTEXT_MULTIPRESS, NULL);
154 }
155
156 static void
cancel_automatic_timeout_commit(GtkImContextMultipress * multipress_context)157 cancel_automatic_timeout_commit (GtkImContextMultipress *multipress_context)
158 {
159 if (multipress_context->timeout_id)
160 g_source_remove (multipress_context->timeout_id);
161
162 multipress_context->timeout_id = 0;
163 }
164
165
166 /* Clear the compose buffer, so we are ready to compose the next character.
167 */
168 static void
clear_compose_buffer(GtkImContextMultipress * multipress_context)169 clear_compose_buffer (GtkImContextMultipress *multipress_context)
170 {
171 multipress_context->key_last_entered = 0;
172 multipress_context->compose_count = 0;
173
174 cancel_automatic_timeout_commit (multipress_context);
175
176 if (multipress_context->tentative_match)
177 {
178 multipress_context->tentative_match = NULL;
179 g_signal_emit_by_name (multipress_context, "preedit-changed");
180 g_signal_emit_by_name (multipress_context, "preedit-end");
181 }
182 }
183
184 /* Finish composing, provide the character, and clear our compose buffer.
185 */
186 static void
accept_character(GtkImContextMultipress * multipress_context,const gchar * characters)187 accept_character (GtkImContextMultipress *multipress_context, const gchar *characters)
188 {
189 /* Clear the compose buffer, so we are ready to compose the next character.
190 * Note that if we emit "preedit-changed" after "commit", there's a segfault/
191 * invalid-write with GtkTextView in gtk_text_layout_free_line_display(), when
192 * destroying a PangoLayout (this can also be avoided by not using any Pango
193 * attributes in get_preedit_string(). */
194 clear_compose_buffer (multipress_context);
195
196 /* Provide the character to GTK+ */
197 g_signal_emit_by_name (multipress_context, "commit", characters);
198 }
199
200 static gboolean
on_timeout(gpointer data)201 on_timeout (gpointer data)
202 {
203 GtkImContextMultipress *multipress_context;
204
205 GDK_THREADS_ENTER ();
206
207 multipress_context = GTK_IM_CONTEXT_MULTIPRESS (data);
208
209 /* A certain amount of time has passed, so we will assume that the user
210 * really wants the currently chosen character */
211 accept_character (multipress_context, multipress_context->tentative_match);
212
213 multipress_context->timeout_id = 0;
214
215 GDK_THREADS_LEAVE ();
216
217 return FALSE; /* don't call me again */
218 }
219
220 static gboolean
vfunc_filter_keypress(GtkIMContext * context,GdkEventKey * event)221 vfunc_filter_keypress (GtkIMContext *context, GdkEventKey *event)
222 {
223 GtkIMContextClass *parent;
224 GtkImContextMultipress *multipress_context;
225
226 multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
227
228 if (event->type == GDK_KEY_PRESS)
229 {
230 KeySequence *possible;
231
232 /* Check whether the current key is the same as previously entered, because
233 * if it is not then we should accept the previous one, and start a new
234 * character. */
235 if (multipress_context->compose_count > 0
236 && multipress_context->key_last_entered != event->keyval
237 && multipress_context->tentative_match != NULL)
238 {
239 /* Accept the previously chosen character. This wipes
240 * the compose_count and key_last_entered. */
241 accept_character (multipress_context,
242 multipress_context->tentative_match);
243 }
244
245 /* Decide what character this key press would choose: */
246 possible = g_hash_table_lookup (multipress_context->key_sequences,
247 GUINT_TO_POINTER (event->keyval));
248 if (possible != NULL)
249 {
250 if (multipress_context->compose_count == 0)
251 g_signal_emit_by_name (multipress_context, "preedit-start");
252
253 /* Check whether we are at the end of a compose sequence, with no more
254 * possible characters. Cycle back to the start if necessary. */
255 if (multipress_context->compose_count >= possible->n_characters)
256 multipress_context->compose_count = 0;
257
258 /* Store the last key pressed in the compose sequence. */
259 multipress_context->key_last_entered = event->keyval;
260
261 /* Get the possible match for this number of presses of the key.
262 * compose_count starts at 1, so that 0 can mean not composing. */
263 multipress_context->tentative_match =
264 possible->characters[multipress_context->compose_count++];
265
266 /* Indicate the current possible character. This will cause our
267 * vfunc_get_preedit_string() vfunc to be called, which will provide
268 * the current possible character for the user to see. */
269 g_signal_emit_by_name (multipress_context, "preedit-changed");
270
271 /* Cancel any outstanding timeout, so we can start the timer again: */
272 cancel_automatic_timeout_commit (multipress_context);
273
274 /* Create a timeout that will cause the currently chosen character to
275 * be committed, if nothing happens for a certain amount of time: */
276 multipress_context->timeout_id =
277 g_timeout_add_seconds (AUTOMATIC_COMPOSE_TIMEOUT,
278 &on_timeout, multipress_context);
279
280 return TRUE; /* key handled */
281 }
282 else
283 {
284 guint32 keyval_uchar;
285
286 /* Just accept all other keypresses directly, but commit the
287 * current preedit content first. */
288 if (multipress_context->compose_count > 0
289 && multipress_context->tentative_match != NULL)
290 {
291 accept_character (multipress_context,
292 multipress_context->tentative_match);
293 }
294 keyval_uchar = gdk_keyval_to_unicode (event->keyval);
295
296 /* Convert to a string for accept_character(). */
297 if (keyval_uchar != 0)
298 {
299 /* max length of UTF-8 sequence = 6 + 1 for NUL termination */
300 gchar keyval_utf8[7];
301 gint length;
302
303 length = g_unichar_to_utf8 (keyval_uchar, keyval_utf8);
304 keyval_utf8[length] = '\0';
305
306 accept_character (multipress_context, keyval_utf8);
307
308 return TRUE; /* key handled */
309 }
310 }
311 }
312
313 parent = (GtkIMContextClass *)im_context_multipress_parent_class;
314
315 /* The default implementation just returns FALSE, but it is generally
316 * a good idea to call the base class implementation: */
317 if (parent->filter_keypress)
318 return (*parent->filter_keypress) (context, event);
319
320 return FALSE;
321 }
322
323 static void
vfunc_reset(GtkIMContext * context)324 vfunc_reset (GtkIMContext *context)
325 {
326 clear_compose_buffer (GTK_IM_CONTEXT_MULTIPRESS (context));
327 }
328
329 static void
vfunc_get_preedit_string(GtkIMContext * context,gchar ** str,PangoAttrList ** attrs,gint * cursor_pos)330 vfunc_get_preedit_string (GtkIMContext *context,
331 gchar **str,
332 PangoAttrList **attrs,
333 gint *cursor_pos)
334 {
335 gsize len_bytes = 0;
336 gsize len_utf8_chars = 0;
337
338 /* Show the user what character he will get if he accepts: */
339 if (str != NULL)
340 {
341 const gchar *match;
342
343 match = GTK_IM_CONTEXT_MULTIPRESS (context)->tentative_match;
344
345 if (match == NULL)
346 match = ""; /* *str must not be NUL */
347
348 len_bytes = strlen (match); /* byte count */
349 len_utf8_chars = g_utf8_strlen (match, len_bytes); /* character count */
350
351 *str = g_strndup (match, len_bytes);
352 }
353
354 /* Underline it, to show the user that he is in compose mode: */
355 if (attrs != NULL)
356 {
357 *attrs = pango_attr_list_new ();
358
359 if (len_bytes > 0)
360 {
361 PangoAttribute *attr;
362
363 attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
364 attr->start_index = 0;
365 attr->end_index = len_bytes;
366 pango_attr_list_insert (*attrs, attr);
367 }
368 }
369
370 if (cursor_pos)
371 *cursor_pos = len_utf8_chars;
372 }
373
374 /* Open the configuration file and fill in the key_sequences hash table
375 * with key/character-list pairs taken from the [keys] group of the file.
376 */
377 static void
load_config(GtkImContextMultipress * self)378 load_config (GtkImContextMultipress *self)
379 {
380 GKeyFile *key_file;
381 GError *error = NULL;
382 gchar **keys;
383 gsize n_keys = 0;
384 gsize i;
385
386 key_file = g_key_file_new ();
387
388 if (!g_key_file_load_from_file (key_file, CONFIGURATION_FILENAME,
389 G_KEY_FILE_NONE, &error))
390 {
391 g_warning ("Error while trying to open the %s configuration file: %s",
392 CONFIGURATION_FILENAME, error->message);
393 g_error_free (error);
394 g_key_file_free (key_file);
395 return;
396 }
397
398 keys = g_key_file_get_keys (key_file, "keys", &n_keys, &error);
399
400 if (error != NULL)
401 {
402 g_warning ("Error while trying to read the %s configuration file: %s",
403 CONFIGURATION_FILENAME, error->message);
404 g_error_free (error);
405 g_key_file_free (key_file);
406 return;
407 }
408
409 for (i = 0; i < n_keys; ++i)
410 {
411 KeySequence *seq;
412 guint keyval;
413
414 keyval = gdk_keyval_from_name (keys[i]);
415
416 if (keyval == GDK_VoidSymbol)
417 {
418 g_warning ("Error while trying to read the %s configuration file: "
419 "invalid key name \"%s\"",
420 CONFIGURATION_FILENAME, keys[i]);
421 continue;
422 }
423
424 seq = g_slice_new (KeySequence);
425 seq->characters = g_key_file_get_string_list (key_file, "keys", keys[i],
426 &seq->n_characters, &error);
427 if (error != NULL)
428 {
429 g_warning ("Error while trying to read the %s configuration file: %s",
430 CONFIGURATION_FILENAME, error->message);
431 g_error_free (error);
432 error = NULL;
433 g_slice_free (KeySequence, seq);
434 continue;
435 }
436
437 /* Ownership of the KeySequence is taken over by the hash table */
438 g_hash_table_insert (self->key_sequences, GUINT_TO_POINTER (keyval), seq);
439 }
440
441 g_strfreev (keys);
442 g_key_file_free (key_file);
443 }
444