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