2 * Copyright (c) 2006-2009 Openismus GmbH
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.
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.
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
18 #include "gtkimcontextmultipress.h"
21 #include <gdk/gdkkeysyms.h>
22 #include <gtk/gtkimmodule.h>
25 #define AUTOMATIC_COMPOSE_TIMEOUT 1 /* seconds */
26 #define CONFIGURATION_FILENAME MULTIPRESS_CONFDIR G_DIR_SEPARATOR_S "im-multipress.conf"
28 /* This contains rows of characters that can be entered by pressing
29 * a particular key repeatedly. Each row has one key (such as GDK_a),
30 * and an array of character strings, such as "a".
34 gchar **characters; /* array of strings */
35 gsize n_characters; /* number of strings in the array */
39 static GObjectClass *im_context_multipress_parent_class = NULL;
40 static GType im_context_multipress_type = 0;
42 static void im_context_multipress_class_init (GtkImContextMultipressClass *klass);
43 static void im_context_multipress_init (GtkImContextMultipress *self);
44 static void im_context_multipress_finalize (GObject *obj);
46 static void load_config (GtkImContextMultipress *self);
48 static gboolean vfunc_filter_keypress (GtkIMContext *context,
50 static void vfunc_reset (GtkIMContext *context);
51 static void vfunc_get_preedit_string (GtkIMContext *context,
53 PangoAttrList **attrs,
56 /* Notice that we have a *_register_type(GTypeModule*) function instead of a
57 * *_get_type() function, because we must use g_type_module_register_type(),
58 * providing the GTypeModule* that was provided to im_context_init(). That
59 * is also why we are not using G_DEFINE_TYPE().
62 gtk_im_context_multipress_register_type (GTypeModule* type_module)
64 const GTypeInfo im_context_multipress_info =
66 sizeof (GtkImContextMultipressClass),
68 (GBaseFinalizeFunc) NULL,
69 (GClassInitFunc) &im_context_multipress_class_init,
72 sizeof (GtkImContextMultipress),
74 (GInstanceInitFunc) &im_context_multipress_init,
78 im_context_multipress_type =
79 g_type_module_register_type (type_module,
81 "GtkImContextMultipress",
82 &im_context_multipress_info, 0);
86 gtk_im_context_multipress_get_type (void)
88 g_assert (im_context_multipress_type != 0);
90 return im_context_multipress_type;
94 key_sequence_free (gpointer value)
96 KeySequence *seq = value;
100 g_strfreev (seq->characters);
101 g_slice_free (KeySequence, seq);
106 im_context_multipress_class_init (GtkImContextMultipressClass *klass)
108 GtkIMContextClass *im_context_class;
110 /* Set this so we can use it later: */
111 im_context_multipress_parent_class = g_type_class_peek_parent (klass);
113 /* Specify our vfunc implementations: */
114 im_context_class = GTK_IM_CONTEXT_CLASS (klass);
115 im_context_class->filter_keypress = &vfunc_filter_keypress;
116 im_context_class->reset = &vfunc_reset;
117 im_context_class->get_preedit_string = &vfunc_get_preedit_string;
119 G_OBJECT_CLASS (klass)->finalize = &im_context_multipress_finalize;
123 im_context_multipress_init (GtkImContextMultipress *self)
125 self->key_sequences = g_hash_table_new_full (&g_direct_hash, &g_direct_equal,
126 NULL, &key_sequence_free);
131 im_context_multipress_finalize (GObject *obj)
133 GtkImContextMultipress *self;
135 self = GTK_IM_CONTEXT_MULTIPRESS (obj);
137 /* Release the configuration data: */
138 if (self->key_sequences != NULL)
140 g_hash_table_destroy (self->key_sequences);
141 self->key_sequences = NULL;
144 (*im_context_multipress_parent_class->finalize) (obj);
149 gtk_im_context_multipress_new (void)
151 return (GtkIMContext *)g_object_new (GTK_TYPE_IM_CONTEXT_MULTIPRESS, NULL);
155 cancel_automatic_timeout_commit (GtkImContextMultipress *multipress_context)
157 if (multipress_context->timeout_id)
158 g_source_remove (multipress_context->timeout_id);
160 multipress_context->timeout_id = 0;
164 /* Clear the compose buffer, so we are ready to compose the next character.
167 clear_compose_buffer (GtkImContextMultipress *multipress_context)
169 multipress_context->key_last_entered = 0;
170 multipress_context->compose_count = 0;
172 multipress_context->tentative_match = NULL;
173 cancel_automatic_timeout_commit (multipress_context);
175 g_signal_emit_by_name (multipress_context, "preedit-changed");
176 g_signal_emit_by_name (multipress_context, "preedit-end");
179 /* Finish composing, provide the character, and clear our compose buffer.
182 accept_character (GtkImContextMultipress *multipress_context, const gchar *characters)
184 /* Clear the compose buffer, so we are ready to compose the next character.
185 * Note that if we emit "preedit-changed" after "commit", there's a segfault/
186 * invalid-write with GtkTextView in gtk_text_layout_free_line_display(), when
187 * destroying a PangoLayout (this can also be avoided by not using any Pango
188 * attributes in get_preedit_string(). */
189 clear_compose_buffer (multipress_context);
191 /* Provide the character to GTK+ */
192 g_signal_emit_by_name (multipress_context, "commit", characters);
196 on_timeout (gpointer data)
198 GtkImContextMultipress *multipress_context;
200 gdk_threads_enter ();
202 multipress_context = GTK_IM_CONTEXT_MULTIPRESS (data);
204 /* A certain amount of time has passed, so we will assume that the user
205 * really wants the currently chosen character */
206 accept_character (multipress_context, multipress_context->tentative_match);
208 multipress_context->timeout_id = 0;
210 gdk_threads_leave ();
212 return G_SOURCE_REMOVE; /* don't call me again */
216 vfunc_filter_keypress (GtkIMContext *context, GdkEventKey *event)
218 GtkIMContextClass *parent;
219 GtkImContextMultipress *multipress_context;
221 multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
223 if (event->type == GDK_KEY_PRESS)
225 KeySequence *possible;
227 /* Check whether the current key is the same as previously entered, because
228 * if it is not then we should accept the previous one, and start a new
230 if (multipress_context->compose_count > 0
231 && multipress_context->key_last_entered != event->keyval
232 && multipress_context->tentative_match != NULL)
234 /* Accept the previously chosen character. This wipes
235 * the compose_count and key_last_entered. */
236 accept_character (multipress_context,
237 multipress_context->tentative_match);
240 /* Decide what character this key press would choose: */
241 possible = g_hash_table_lookup (multipress_context->key_sequences,
242 GUINT_TO_POINTER (event->keyval));
243 if (possible != NULL)
245 if (multipress_context->compose_count == 0)
246 g_signal_emit_by_name (multipress_context, "preedit-start");
248 /* Check whether we are at the end of a compose sequence, with no more
249 * possible characters. Cycle back to the start if necessary. */
250 if (multipress_context->compose_count >= possible->n_characters)
251 multipress_context->compose_count = 0;
253 /* Store the last key pressed in the compose sequence. */
254 multipress_context->key_last_entered = event->keyval;
256 /* Get the possible match for this number of presses of the key.
257 * compose_count starts at 1, so that 0 can mean not composing. */
258 multipress_context->tentative_match =
259 possible->characters[multipress_context->compose_count++];
261 /* Indicate the current possible character. This will cause our
262 * vfunc_get_preedit_string() vfunc to be called, which will provide
263 * the current possible character for the user to see. */
264 g_signal_emit_by_name (multipress_context, "preedit-changed");
266 /* Cancel any outstanding timeout, so we can start the timer again: */
267 cancel_automatic_timeout_commit (multipress_context);
269 /* Create a timeout that will cause the currently chosen character to
270 * be committed, if nothing happens for a certain amount of time: */
271 multipress_context->timeout_id =
272 g_timeout_add_seconds (AUTOMATIC_COMPOSE_TIMEOUT,
273 &on_timeout, multipress_context);
275 return TRUE; /* key handled */
279 guint32 keyval_uchar;
281 /* Just accept all other keypresses directly, but commit the
282 * current preedit content first. */
283 if (multipress_context->compose_count > 0
284 && multipress_context->tentative_match != NULL)
286 accept_character (multipress_context,
287 multipress_context->tentative_match);
289 keyval_uchar = gdk_keyval_to_unicode (event->keyval);
291 /* Convert to a string for accept_character(). */
292 if (keyval_uchar != 0)
294 /* max length of UTF-8 sequence = 6 + 1 for NUL termination */
295 gchar keyval_utf8[7];
298 length = g_unichar_to_utf8 (keyval_uchar, keyval_utf8);
299 keyval_utf8[length] = '\0';
301 accept_character (multipress_context, keyval_utf8);
303 return TRUE; /* key handled */
308 parent = (GtkIMContextClass *)im_context_multipress_parent_class;
310 /* The default implementation just returns FALSE, but it is generally
311 * a good idea to call the base class implementation: */
312 if (parent->filter_keypress)
313 return (*parent->filter_keypress) (context, event);
319 vfunc_reset (GtkIMContext *context)
321 clear_compose_buffer (GTK_IM_CONTEXT_MULTIPRESS (context));
325 vfunc_get_preedit_string (GtkIMContext *context,
327 PangoAttrList **attrs,
331 gsize len_utf8_chars = 0;
333 /* Show the user what character he will get if he accepts: */
338 match = GTK_IM_CONTEXT_MULTIPRESS (context)->tentative_match;
341 match = ""; /* *str must not be NUL */
343 len_bytes = strlen (match); /* byte count */
344 len_utf8_chars = g_utf8_strlen (match, len_bytes); /* character count */
346 *str = g_strndup (match, len_bytes);
349 /* Underline it, to show the user that he is in compose mode: */
352 *attrs = pango_attr_list_new ();
356 PangoAttribute *attr;
358 attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
359 attr->start_index = 0;
360 attr->end_index = len_bytes;
361 pango_attr_list_insert (*attrs, attr);
366 *cursor_pos = len_utf8_chars;
369 /* Open the configuration file and fill in the key_sequences hash table
370 * with key/character-list pairs taken from the [keys] group of the file.
373 load_config (GtkImContextMultipress *self)
376 GError *error = NULL;
381 key_file = g_key_file_new ();
383 if (!g_key_file_load_from_file (key_file, CONFIGURATION_FILENAME,
384 G_KEY_FILE_NONE, &error))
386 g_warning ("Error while trying to open the %s configuration file: %s",
387 CONFIGURATION_FILENAME, error->message);
388 g_error_free (error);
389 g_key_file_free (key_file);
393 keys = g_key_file_get_keys (key_file, "keys", &n_keys, &error);
397 g_warning ("Error while trying to read the %s configuration file: %s",
398 CONFIGURATION_FILENAME, error->message);
399 g_error_free (error);
400 g_key_file_free (key_file);
404 for (i = 0; i < n_keys; ++i)
409 keyval = gdk_keyval_from_name (keys[i]);
411 if (keyval == GDK_KEY_VoidSymbol)
413 g_warning ("Error while trying to read the %s configuration file: "
414 "invalid key name \"%s\"",
415 CONFIGURATION_FILENAME, keys[i]);
419 seq = g_slice_new (KeySequence);
420 seq->characters = g_key_file_get_string_list (key_file, "keys", keys[i],
421 &seq->n_characters, &error);
424 g_warning ("Error while trying to read the %s configuration file: %s",
425 CONFIGURATION_FILENAME, error->message);
426 g_error_free (error);
428 g_slice_free (KeySequence, seq);
432 /* Ownership of the KeySequence is taken over by the hash table */
433 g_hash_table_insert (self->key_sequences, GUINT_TO_POINTER (keyval), seq);
437 g_key_file_free (key_file);