]> Pileus Git - ~andy/gtk/blob - modules/input/gtkimcontextmultipress.c
stylecontext: Do invalidation on first resize container
[~andy/gtk] / modules / input / gtkimcontextmultipress.c
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, see <http://www.gnu.org/licenses/>.
16  */
17
18 #include "gtkimcontextmultipress.h"
19 #include <string.h>
20 #include <gtk/gtk.h>
21 #include <gdk/gdkkeysyms.h>
22 #include <gtk/gtkimmodule.h>
23 #include <config.h>
24
25 #define AUTOMATIC_COMPOSE_TIMEOUT 1 /* seconds */
26 #define CONFIGURATION_FILENAME MULTIPRESS_CONFDIR G_DIR_SEPARATOR_S "im-multipress.conf"
27
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".
31  */
32 typedef struct
33 {
34   gchar **characters; /* array of strings */
35   gsize n_characters; /* number of strings in the array */
36 }
37 KeySequence;
38
39 static GObjectClass *im_context_multipress_parent_class = NULL;
40 static GType         im_context_multipress_type = 0;
41
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);
45
46 static void load_config (GtkImContextMultipress *self);
47
48 static gboolean vfunc_filter_keypress (GtkIMContext *context,
49                                        GdkEventKey  *event);
50 static void vfunc_reset (GtkIMContext *context);
51 static void vfunc_get_preedit_string (GtkIMContext   *context,
52                                       gchar         **str,
53                                       PangoAttrList **attrs,
54                                       gint           *cursor_pos);
55
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().
60  */
61 void
62 gtk_im_context_multipress_register_type (GTypeModule* type_module)
63 {
64   const GTypeInfo im_context_multipress_info =
65     {
66       sizeof (GtkImContextMultipressClass),
67       (GBaseInitFunc) NULL,
68       (GBaseFinalizeFunc) NULL,
69       (GClassInitFunc) &im_context_multipress_class_init,
70       NULL,
71       NULL,
72       sizeof (GtkImContextMultipress),
73       0,
74       (GInstanceInitFunc) &im_context_multipress_init,
75       0,
76     };
77
78   im_context_multipress_type =
79     g_type_module_register_type (type_module,
80                                  GTK_TYPE_IM_CONTEXT,
81                                  "GtkImContextMultipress",
82                                  &im_context_multipress_info, 0);
83 }
84
85 GType
86 gtk_im_context_multipress_get_type (void)
87 {
88   g_assert (im_context_multipress_type != 0);
89
90   return im_context_multipress_type;
91 }
92
93 static void
94 key_sequence_free (gpointer value)
95 {
96   KeySequence *seq = value;
97
98   if (seq != NULL)
99     {
100       g_strfreev (seq->characters);
101       g_slice_free (KeySequence, seq);
102     }
103 }
104
105 static void
106 im_context_multipress_class_init (GtkImContextMultipressClass *klass)
107 {
108   GtkIMContextClass *im_context_class;
109
110   /* Set this so we can use it later: */
111   im_context_multipress_parent_class = g_type_class_peek_parent (klass);
112
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;
118
119   G_OBJECT_CLASS (klass)->finalize = &im_context_multipress_finalize;
120 }
121
122 static void
123 im_context_multipress_init (GtkImContextMultipress *self)
124 {
125   self->key_sequences = g_hash_table_new_full (&g_direct_hash, &g_direct_equal,
126                                                NULL, &key_sequence_free);
127   load_config (self);
128 }
129
130 static void
131 im_context_multipress_finalize (GObject *obj)
132 {
133   GtkImContextMultipress *self;
134
135   self = GTK_IM_CONTEXT_MULTIPRESS (obj);
136
137   /* Release the configuration data: */
138   if (self->key_sequences != NULL)
139     {
140       g_hash_table_destroy (self->key_sequences);
141       self->key_sequences = NULL;
142     }
143
144   (*im_context_multipress_parent_class->finalize) (obj);
145 }
146
147
148 GtkIMContext *
149 gtk_im_context_multipress_new (void)
150 {
151   return (GtkIMContext *)g_object_new (GTK_TYPE_IM_CONTEXT_MULTIPRESS, NULL);
152 }
153
154 static void
155 cancel_automatic_timeout_commit (GtkImContextMultipress *multipress_context)
156 {
157   if (multipress_context->timeout_id)
158     g_source_remove (multipress_context->timeout_id);
159  
160   multipress_context->timeout_id = 0;
161 }
162
163
164 /* Clear the compose buffer, so we are ready to compose the next character.
165  */
166 static void
167 clear_compose_buffer (GtkImContextMultipress *multipress_context)
168 {
169   multipress_context->key_last_entered = 0;
170   multipress_context->compose_count = 0;
171
172   multipress_context->tentative_match = NULL;
173   cancel_automatic_timeout_commit (multipress_context);
174
175   g_signal_emit_by_name (multipress_context, "preedit-changed");
176   g_signal_emit_by_name (multipress_context, "preedit-end");
177 }
178
179 /* Finish composing, provide the character, and clear our compose buffer.
180  */
181 static void
182 accept_character (GtkImContextMultipress *multipress_context, const gchar *characters)
183 {
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);
190
191   /* Provide the character to GTK+ */
192   g_signal_emit_by_name (multipress_context, "commit", characters);
193 }
194
195 static gboolean
196 on_timeout (gpointer data)
197 {
198   GtkImContextMultipress *multipress_context;
199
200   gdk_threads_enter ();
201
202   multipress_context = GTK_IM_CONTEXT_MULTIPRESS (data);
203
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);
207
208   multipress_context->timeout_id = 0;
209
210   gdk_threads_leave ();
211
212   return G_SOURCE_REMOVE; /* don't call me again */
213 }
214
215 static gboolean
216 vfunc_filter_keypress (GtkIMContext *context, GdkEventKey *event)
217 {
218   GtkIMContextClass      *parent;
219   GtkImContextMultipress *multipress_context;
220
221   multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
222
223   if (event->type == GDK_KEY_PRESS)
224     {
225       KeySequence *possible;
226
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
229        * character. */
230       if (multipress_context->compose_count > 0
231           && multipress_context->key_last_entered != event->keyval
232           && multipress_context->tentative_match != NULL)
233         {
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);
238         } 
239
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)
244         {
245           if (multipress_context->compose_count == 0)
246             g_signal_emit_by_name (multipress_context, "preedit-start");
247
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;
252
253           /* Store the last key pressed in the compose sequence. */
254           multipress_context->key_last_entered = event->keyval; 
255
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++];
260
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");
265
266           /* Cancel any outstanding timeout, so we can start the timer again: */
267           cancel_automatic_timeout_commit (multipress_context);
268
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);
274
275           return TRUE; /* key handled */
276         }
277       else
278         {
279           guint32 keyval_uchar;
280
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)
285             {
286               accept_character (multipress_context,
287                                 multipress_context->tentative_match);
288             }
289           keyval_uchar = gdk_keyval_to_unicode (event->keyval);
290
291           /* Convert to a string for accept_character(). */
292           if (keyval_uchar != 0)
293             {
294               /* max length of UTF-8 sequence = 6 + 1 for NUL termination */
295               gchar keyval_utf8[7];
296               gint  length;
297
298               length = g_unichar_to_utf8 (keyval_uchar, keyval_utf8);
299               keyval_utf8[length] = '\0';
300
301               accept_character (multipress_context, keyval_utf8);
302
303               return TRUE; /* key handled */
304             }
305         }
306     }
307
308   parent = (GtkIMContextClass *)im_context_multipress_parent_class;
309
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);
314
315   return FALSE;
316 }
317
318 static void
319 vfunc_reset (GtkIMContext *context)
320 {
321   clear_compose_buffer (GTK_IM_CONTEXT_MULTIPRESS (context));
322 }
323
324 static void
325 vfunc_get_preedit_string (GtkIMContext   *context,
326                           gchar         **str,
327                           PangoAttrList **attrs,
328                           gint           *cursor_pos)
329 {
330   gsize len_bytes = 0;
331   gsize len_utf8_chars = 0;
332
333   /* Show the user what character he will get if he accepts: */
334   if (str != NULL)
335     {
336       const gchar *match;
337
338       match = GTK_IM_CONTEXT_MULTIPRESS (context)->tentative_match;
339
340       if (match == NULL)
341         match = ""; /* *str must not be NUL */
342
343       len_bytes = strlen (match); /* byte count */
344       len_utf8_chars = g_utf8_strlen (match, len_bytes); /* character count */
345
346       *str = g_strndup (match, len_bytes);
347     }
348
349   /* Underline it, to show the user that he is in compose mode: */
350   if (attrs != NULL)
351     {
352       *attrs = pango_attr_list_new ();
353
354       if (len_bytes > 0)
355         {
356           PangoAttribute *attr;
357
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);
362         }
363     }
364
365   if (cursor_pos)
366     *cursor_pos = len_utf8_chars;
367 }
368
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.
371  */
372 static void
373 load_config (GtkImContextMultipress *self)
374 {
375   GKeyFile *key_file;
376   GError   *error = NULL;
377   gchar   **keys;
378   gsize     n_keys = 0;
379   gsize     i;
380
381   key_file = g_key_file_new ();
382
383   if (!g_key_file_load_from_file (key_file, CONFIGURATION_FILENAME,
384                                   G_KEY_FILE_NONE, &error))
385     {
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);
390       return;
391     }
392
393   keys = g_key_file_get_keys (key_file, "keys", &n_keys, &error);
394
395   if (error != NULL)
396     {
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);
401       return;
402     }
403
404   for (i = 0; i < n_keys; ++i)
405     {
406       KeySequence *seq;
407       guint        keyval;
408
409       keyval = gdk_keyval_from_name (keys[i]);
410
411       if (keyval == GDK_KEY_VoidSymbol)
412         {
413           g_warning ("Error while trying to read the %s configuration file: "
414                      "invalid key name \"%s\"",
415                      CONFIGURATION_FILENAME, keys[i]);
416           continue;
417         }
418
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);
422       if (error != NULL)
423         {
424           g_warning ("Error while trying to read the %s configuration file: %s",
425                      CONFIGURATION_FILENAME, error->message);
426           g_error_free (error);
427           error = NULL;
428           g_slice_free (KeySequence, seq);
429           continue;
430         }
431
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);
434     }
435
436   g_strfreev (keys);
437   g_key_file_free (key_file);
438 }