]> Pileus Git - ~andy/gtk/blob - modules/input/gtkimcontextmultipress.c
Hacky support for combo boxes
[~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, 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
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
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
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
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
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
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 *
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
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
169 clear_compose_buffer (GtkImContextMultipress *multipress_context)
170 {
171   multipress_context->key_last_entered = 0;
172   multipress_context->compose_count = 0;
173
174   multipress_context->tentative_match = NULL;
175   cancel_automatic_timeout_commit (multipress_context);
176
177   g_signal_emit_by_name (multipress_context, "preedit-changed");
178   g_signal_emit_by_name (multipress_context, "preedit-end");
179 }
180
181 /* Finish composing, provide the character, and clear our compose buffer.
182  */
183 static void
184 accept_character (GtkImContextMultipress *multipress_context, const gchar *characters)
185 {
186   /* Clear the compose buffer, so we are ready to compose the next character.
187    * Note that if we emit "preedit-changed" after "commit", there's a segfault/
188    * invalid-write with GtkTextView in gtk_text_layout_free_line_display(), when
189    * destroying a PangoLayout (this can also be avoided by not using any Pango
190    * attributes in get_preedit_string(). */
191   clear_compose_buffer (multipress_context);
192
193   /* Provide the character to GTK+ */
194   g_signal_emit_by_name (multipress_context, "commit", characters);
195 }
196
197 static gboolean
198 on_timeout (gpointer data)
199 {
200   GtkImContextMultipress *multipress_context;
201
202   GDK_THREADS_ENTER ();
203
204   multipress_context = GTK_IM_CONTEXT_MULTIPRESS (data);
205
206   /* A certain amount of time has passed, so we will assume that the user
207    * really wants the currently chosen character */
208   accept_character (multipress_context, multipress_context->tentative_match);
209
210   multipress_context->timeout_id = 0;
211
212   GDK_THREADS_LEAVE ();
213
214   return FALSE; /* don't call me again */
215 }
216
217 static gboolean
218 vfunc_filter_keypress (GtkIMContext *context, GdkEventKey *event)
219 {
220   GtkIMContextClass      *parent;
221   GtkImContextMultipress *multipress_context;
222
223   multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
224
225   if (event->type == GDK_KEY_PRESS)
226     {
227       KeySequence *possible;
228
229       /* Check whether the current key is the same as previously entered, because
230        * if it is not then we should accept the previous one, and start a new
231        * character. */
232       if (multipress_context->compose_count > 0
233           && multipress_context->key_last_entered != event->keyval
234           && multipress_context->tentative_match != NULL)
235         {
236           /* Accept the previously chosen character.  This wipes
237            * the compose_count and key_last_entered. */
238           accept_character (multipress_context,
239                             multipress_context->tentative_match);
240         } 
241
242       /* Decide what character this key press would choose: */
243       possible = g_hash_table_lookup (multipress_context->key_sequences,
244                                       GUINT_TO_POINTER (event->keyval));
245       if (possible != NULL)
246         {
247           if (multipress_context->compose_count == 0)
248             g_signal_emit_by_name (multipress_context, "preedit-start");
249
250           /* Check whether we are at the end of a compose sequence, with no more
251            * possible characters.  Cycle back to the start if necessary. */
252           if (multipress_context->compose_count >= possible->n_characters)
253             multipress_context->compose_count = 0;
254
255           /* Store the last key pressed in the compose sequence. */
256           multipress_context->key_last_entered = event->keyval; 
257
258           /* Get the possible match for this number of presses of the key.
259            * compose_count starts at 1, so that 0 can mean not composing. */ 
260           multipress_context->tentative_match =
261             possible->characters[multipress_context->compose_count++];
262
263           /* Indicate the current possible character.  This will cause our
264            * vfunc_get_preedit_string() vfunc to be called, which will provide
265            * the current possible character for the user to see. */
266           g_signal_emit_by_name (multipress_context, "preedit-changed");
267
268           /* Cancel any outstanding timeout, so we can start the timer again: */
269           cancel_automatic_timeout_commit (multipress_context);
270
271           /* Create a timeout that will cause the currently chosen character to
272            * be committed, if nothing happens for a certain amount of time: */
273           multipress_context->timeout_id =
274             g_timeout_add_seconds (AUTOMATIC_COMPOSE_TIMEOUT,
275                                    &on_timeout, multipress_context);
276
277           return TRUE; /* key handled */
278         }
279       else
280         {
281           guint32 keyval_uchar;
282
283           /* Just accept all other keypresses directly, but commit the
284            * current preedit content first. */
285           if (multipress_context->compose_count > 0
286               && multipress_context->tentative_match != NULL)
287             {
288               accept_character (multipress_context,
289                                 multipress_context->tentative_match);
290             }
291           keyval_uchar = gdk_keyval_to_unicode (event->keyval);
292
293           /* Convert to a string for accept_character(). */
294           if (keyval_uchar != 0)
295             {
296               /* max length of UTF-8 sequence = 6 + 1 for NUL termination */
297               gchar keyval_utf8[7];
298               gint  length;
299
300               length = g_unichar_to_utf8 (keyval_uchar, keyval_utf8);
301               keyval_utf8[length] = '\0';
302
303               accept_character (multipress_context, keyval_utf8);
304
305               return TRUE; /* key handled */
306             }
307         }
308     }
309
310   parent = (GtkIMContextClass *)im_context_multipress_parent_class;
311
312   /* The default implementation just returns FALSE, but it is generally
313    * a good idea to call the base class implementation: */
314   if (parent->filter_keypress)
315     return (*parent->filter_keypress) (context, event);
316
317   return FALSE;
318 }
319
320 static void
321 vfunc_reset (GtkIMContext *context)
322 {
323   clear_compose_buffer (GTK_IM_CONTEXT_MULTIPRESS (context));
324 }
325
326 static void
327 vfunc_get_preedit_string (GtkIMContext   *context,
328                           gchar         **str,
329                           PangoAttrList **attrs,
330                           gint           *cursor_pos)
331 {
332   gsize len_bytes = 0;
333   gsize len_utf8_chars = 0;
334
335   /* Show the user what character he will get if he accepts: */
336   if (str != NULL)
337     {
338       const gchar *match;
339
340       match = GTK_IM_CONTEXT_MULTIPRESS (context)->tentative_match;
341
342       if (match == NULL)
343         match = ""; /* *str must not be NUL */
344
345       len_bytes = strlen (match); /* byte count */
346       len_utf8_chars = g_utf8_strlen (match, len_bytes); /* character count */
347
348       *str = g_strndup (match, len_bytes);
349     }
350
351   /* Underline it, to show the user that he is in compose mode: */
352   if (attrs != NULL)
353     {
354       *attrs = pango_attr_list_new ();
355
356       if (len_bytes > 0)
357         {
358           PangoAttribute *attr;
359
360           attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
361           attr->start_index = 0;
362           attr->end_index = len_bytes;
363           pango_attr_list_insert (*attrs, attr);
364         }
365     }
366
367   if (cursor_pos)
368     *cursor_pos = len_utf8_chars;
369 }
370
371 /* Open the configuration file and fill in the key_sequences hash table
372  * with key/character-list pairs taken from the [keys] group of the file.
373  */
374 static void
375 load_config (GtkImContextMultipress *self)
376 {
377   GKeyFile *key_file;
378   GError   *error = NULL;
379   gchar   **keys;
380   gsize     n_keys = 0;
381   gsize     i;
382
383   key_file = g_key_file_new ();
384
385   if (!g_key_file_load_from_file (key_file, CONFIGURATION_FILENAME,
386                                   G_KEY_FILE_NONE, &error))
387     {
388       g_warning ("Error while trying to open the %s configuration file: %s",
389                  CONFIGURATION_FILENAME, error->message);
390       g_error_free (error);
391       g_key_file_free (key_file);
392       return;
393     }
394
395   keys = g_key_file_get_keys (key_file, "keys", &n_keys, &error);
396
397   if (error != NULL)
398     {
399       g_warning ("Error while trying to read the %s configuration file: %s",
400                  CONFIGURATION_FILENAME, error->message);
401       g_error_free (error);
402       g_key_file_free (key_file);
403       return;
404     }
405
406   for (i = 0; i < n_keys; ++i)
407     {
408       KeySequence *seq;
409       guint        keyval;
410
411       keyval = gdk_keyval_from_name (keys[i]);
412
413       if (keyval == GDK_KEY_VoidSymbol)
414         {
415           g_warning ("Error while trying to read the %s configuration file: "
416                      "invalid key name \"%s\"",
417                      CONFIGURATION_FILENAME, keys[i]);
418           continue;
419         }
420
421       seq = g_slice_new (KeySequence);
422       seq->characters = g_key_file_get_string_list (key_file, "keys", keys[i],
423                                                     &seq->n_characters, &error);
424       if (error != NULL)
425         {
426           g_warning ("Error while trying to read the %s configuration file: %s",
427                      CONFIGURATION_FILENAME, error->message);
428           g_error_free (error);
429           error = NULL;
430           g_slice_free (KeySequence, seq);
431           continue;
432         }
433
434       /* Ownership of the KeySequence is taken over by the hash table */
435       g_hash_table_insert (self->key_sequences, GUINT_TO_POINTER (keyval), seq);
436     }
437
438   g_strfreev (keys);
439   g_key_file_free (key_file);
440 }