summaryrefslogtreecommitdiff
path: root/modules/input/gtkimcontextmultipress.c
blob: 9cfe0289af2464a146bc9d9d137778d8fbd0b1e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
/*
 * Copyright (c) 2006-2009 Openismus GmbH
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "gtkimcontextmultipress.h"
#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtkimmodule.h>

#define AUTOMATIC_COMPOSE_TIMEOUT 1 /* seconds */
#define CONFIGURATION_FILENAME MULTIPRESS_CONFDIR G_DIR_SEPARATOR_S "im-multipress.conf"

/* This contains rows of characters that can be entered by pressing
 * a particular key repeatedly.  Each row has one key (such as GDK_a),
 * and an array of character strings, such as "a".
 */
typedef struct
{
  gchar **characters; /* array of strings */
  gsize n_characters; /* number of strings in the array */
}
KeySequence;

static GObjectClass *im_context_multipress_parent_class = NULL;
static GType         im_context_multipress_type = 0;

static void im_context_multipress_class_init (GtkImContextMultipressClass *klass);
static void im_context_multipress_init (GtkImContextMultipress *self);
static void im_context_multipress_finalize (GObject *obj);

static void load_config (GtkImContextMultipress *self);

static gboolean vfunc_filter_keypress (GtkIMContext *context,
                                       GdkEventKey  *event);
static void vfunc_reset (GtkIMContext *context);
static void vfunc_get_preedit_string (GtkIMContext   *context,
                                      gchar         **str,
                                      PangoAttrList **attrs,
                                      gint           *cursor_pos);

/* Notice that we have a *_register_type(GTypeModule*) function instead of a
 * *_get_type() function, because we must use g_type_module_register_type(),
 * providing the GTypeModule* that was provided to im_context_init(). That
 * is also why we are not using G_DEFINE_TYPE().
 */
void
gtk_im_context_multipress_register_type (GTypeModule* type_module)
{
  const GTypeInfo im_context_multipress_info =
    {
      sizeof (GtkImContextMultipressClass),
      (GBaseInitFunc) NULL,
      (GBaseFinalizeFunc) NULL,
      (GClassInitFunc) &im_context_multipress_class_init,
      NULL,
      NULL,
      sizeof (GtkImContextMultipress),
      0,
      (GInstanceInitFunc) &im_context_multipress_init,
      0,
    };

  im_context_multipress_type =
    g_type_module_register_type (type_module,
                                 GTK_TYPE_IM_CONTEXT,
                                 "GtkImContextMultipress",
                                 &im_context_multipress_info, 0);
}

GType
gtk_im_context_multipress_get_type (void)
{
  g_assert (im_context_multipress_type != 0);

  return im_context_multipress_type;
}

static void
key_sequence_free (gpointer value)
{
  KeySequence *seq = value;

  if (seq != NULL)
    {
      g_strfreev (seq->characters);
      g_slice_free (KeySequence, seq);
    }
}

static void
im_context_multipress_class_init (GtkImContextMultipressClass *klass)
{
  GtkIMContextClass *im_context_class;

  /* Set this so we can use it later: */
  im_context_multipress_parent_class = g_type_class_peek_parent (klass);

  /* Specify our vfunc implementations: */
  im_context_class = GTK_IM_CONTEXT_CLASS (klass);
  im_context_class->filter_keypress = &vfunc_filter_keypress;
  im_context_class->reset = &vfunc_reset;
  im_context_class->get_preedit_string = &vfunc_get_preedit_string;

  G_OBJECT_CLASS (klass)->finalize = &im_context_multipress_finalize;
}

static void
im_context_multipress_init (GtkImContextMultipress *self)
{
  self->key_sequences = g_hash_table_new_full (&g_direct_hash, &g_direct_equal,
                                               NULL, &key_sequence_free);
  load_config (self);
}

static void
im_context_multipress_finalize (GObject *obj)
{
  GtkImContextMultipress *self;

  self = GTK_IM_CONTEXT_MULTIPRESS (obj);

  /* Release the configuration data: */
  if (self->key_sequences != NULL)
    {
      g_hash_table_destroy (self->key_sequences);
      self->key_sequences = NULL;
    }

  (*im_context_multipress_parent_class->finalize) (obj);
}


GtkIMContext *
gtk_im_context_multipress_new (void)
{
  return (GtkIMContext *)g_object_new (GTK_TYPE_IM_CONTEXT_MULTIPRESS, NULL);
}

static void
cancel_automatic_timeout_commit (GtkImContextMultipress *multipress_context)
{
  if (multipress_context->timeout_id)
    g_source_remove (multipress_context->timeout_id);
 
  multipress_context->timeout_id = 0;
}


/* Clear the compose buffer, so we are ready to compose the next character.
 */
static void
clear_compose_buffer (GtkImContextMultipress *multipress_context)
{
  multipress_context->key_last_entered = 0;
  multipress_context->compose_count = 0;

  multipress_context->tentative_match = NULL;
  cancel_automatic_timeout_commit (multipress_context);

  g_signal_emit_by_name (multipress_context, "preedit-changed");
  g_signal_emit_by_name (multipress_context, "preedit-end");
}

/* Finish composing, provide the character, and clear our compose buffer.
 */
static void
accept_character (GtkImContextMultipress *multipress_context, const gchar *characters)
{
  /* Clear the compose buffer, so we are ready to compose the next character.
   * Note that if we emit "preedit-changed" after "commit", there's a segfault/
   * invalid-write with GtkTextView in gtk_text_layout_free_line_display(), when
   * destroying a PangoLayout (this can also be avoided by not using any Pango
   * attributes in get_preedit_string(). */
  clear_compose_buffer (multipress_context);

  /* Provide the character to GTK+ */
  g_signal_emit_by_name (multipress_context, "commit", characters);
}

static gboolean
on_timeout (gpointer data)
{
  GtkImContextMultipress *multipress_context;

  gdk_threads_enter ();

  multipress_context = GTK_IM_CONTEXT_MULTIPRESS (data);

  /* A certain amount of time has passed, so we will assume that the user
   * really wants the currently chosen character */
  accept_character (multipress_context, multipress_context->tentative_match);

  multipress_context->timeout_id = 0;

  gdk_threads_leave ();

  return G_SOURCE_REMOVE; /* don't call me again */
}

static gboolean
vfunc_filter_keypress (GtkIMContext *context, GdkEventKey *event)
{
  GtkIMContextClass      *parent;
  GtkImContextMultipress *multipress_context;

  multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);

  if (event->type == GDK_KEY_PRESS)
    {
      KeySequence *possible;

      /* Check whether the current key is the same as previously entered, because
       * if it is not then we should accept the previous one, and start a new
       * character. */
      if (multipress_context->compose_count > 0
          && multipress_context->key_last_entered != event->keyval
          && multipress_context->tentative_match != NULL)
        {
          /* Accept the previously chosen character.  This wipes
           * the compose_count and key_last_entered. */
          accept_character (multipress_context,
                            multipress_context->tentative_match);
        } 

      /* Decide what character this key press would choose: */
      possible = g_hash_table_lookup (multipress_context->key_sequences,
                                      GUINT_TO_POINTER (event->keyval));
      if (possible != NULL)
        {
          if (multipress_context->compose_count == 0)
            g_signal_emit_by_name (multipress_context, "preedit-start");

          /* Check whether we are at the end of a compose sequence, with no more
           * possible characters.  Cycle back to the start if necessary. */
          if (multipress_context->compose_count >= possible->n_characters)
            multipress_context->compose_count = 0;

          /* Store the last key pressed in the compose sequence. */
          multipress_context->key_last_entered = event->keyval; 

          /* Get the possible match for this number of presses of the key.
           * compose_count starts at 1, so that 0 can mean not composing. */ 
          multipress_context->tentative_match =
            possible->characters[multipress_context->compose_count++];

          /* Indicate the current possible character.  This will cause our
           * vfunc_get_preedit_string() vfunc to be called, which will provide
           * the current possible character for the user to see. */
          g_signal_emit_by_name (multipress_context, "preedit-changed");

          /* Cancel any outstanding timeout, so we can start the timer again: */
          cancel_automatic_timeout_commit (multipress_context);

          /* Create a timeout that will cause the currently chosen character to
           * be committed, if nothing happens for a certain amount of time: */
          multipress_context->timeout_id =
            g_timeout_add_seconds (AUTOMATIC_COMPOSE_TIMEOUT,
                                   &on_timeout, multipress_context);
          g_source_set_name_by_id (multipress_context->timeout_id, "[gtk+] on_timeout");

          return TRUE; /* key handled */
        }
      else
        {
          guint32 keyval_uchar;

          /* Just accept all other keypresses directly, but commit the
           * current preedit content first. */
          if (multipress_context->compose_count > 0
              && multipress_context->tentative_match != NULL)
            {
              accept_character (multipress_context,
                                multipress_context->tentative_match);
            }
          keyval_uchar = gdk_keyval_to_unicode (event->keyval);

          /* Convert to a string for accept_character(). */
          if (keyval_uchar != 0)
            {
              /* max length of UTF-8 sequence = 6 + 1 for NUL termination */
              gchar keyval_utf8[7];
              gint  length;

              length = g_unichar_to_utf8 (keyval_uchar, keyval_utf8);
              keyval_utf8[length] = '\0';

              accept_character (multipress_context, keyval_utf8);

              return TRUE; /* key handled */
            }
        }
    }

  parent = (GtkIMContextClass *)im_context_multipress_parent_class;

  /* The default implementation just returns FALSE, but it is generally
   * a good idea to call the base class implementation: */
  if (parent->filter_keypress)
    return (*parent->filter_keypress) (context, event);

  return FALSE;
}

static void
vfunc_reset (GtkIMContext *context)
{
  clear_compose_buffer (GTK_IM_CONTEXT_MULTIPRESS (context));
}

static void
vfunc_get_preedit_string (GtkIMContext   *context,
                          gchar         **str,
                          PangoAttrList **attrs,
                          gint           *cursor_pos)
{
  gsize len_bytes = 0;
  gsize len_utf8_chars = 0;

  /* Show the user what character he will get if he accepts: */
  if (str != NULL)
    {
      const gchar *match;

      match = GTK_IM_CONTEXT_MULTIPRESS (context)->tentative_match;

      if (match == NULL)
        match = ""; /* *str must not be NUL */

      len_bytes = strlen (match); /* byte count */
      len_utf8_chars = g_utf8_strlen (match, len_bytes); /* character count */

      *str = g_strndup (match, len_bytes);
    }

  /* Underline it, to show the user that he is in compose mode: */
  if (attrs != NULL)
    {
      *attrs = pango_attr_list_new ();

      if (len_bytes > 0)
        {
          PangoAttribute *attr;

          attr = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
          attr->start_index = 0;
          attr->end_index = len_bytes;
          pango_attr_list_insert (*attrs, attr);
        }
    }

  if (cursor_pos)
    *cursor_pos = len_utf8_chars;
}

/* Open the configuration file and fill in the key_sequences hash table
 * with key/character-list pairs taken from the [keys] group of the file.
 */
static void
load_config (GtkImContextMultipress *self)
{
  GKeyFile *key_file;
  GError   *error = NULL;
  gchar   **keys;
  gsize     n_keys = 0;
  gsize     i;

  key_file = g_key_file_new ();

  if (!g_key_file_load_from_file (key_file, CONFIGURATION_FILENAME,
                                  G_KEY_FILE_NONE, &error))
    {
      g_warning ("Error while trying to open the %s configuration file: %s",
                 CONFIGURATION_FILENAME, error->message);
      g_error_free (error);
      g_key_file_free (key_file);
      return;
    }

  keys = g_key_file_get_keys (key_file, "keys", &n_keys, &error);

  if (error != NULL)
    {
      g_warning ("Error while trying to read the %s configuration file: %s",
                 CONFIGURATION_FILENAME, error->message);
      g_error_free (error);
      g_key_file_free (key_file);
      return;
    }

  for (i = 0; i < n_keys; ++i)
    {
      KeySequence *seq;
      guint        keyval;

      keyval = gdk_keyval_from_name (keys[i]);

      if (keyval == GDK_KEY_VoidSymbol)
        {
          g_warning ("Error while trying to read the %s configuration file: "
                     "invalid key name \"%s\"",
                     CONFIGURATION_FILENAME, keys[i]);
          continue;
        }

      seq = g_slice_new (KeySequence);
      seq->characters = g_key_file_get_string_list (key_file, "keys", keys[i],
                                                    &seq->n_characters, &error);
      if (error != NULL)
        {
          g_warning ("Error while trying to read the %s configuration file: %s",
                     CONFIGURATION_FILENAME, error->message);
          g_error_free (error);
          error = NULL;
          g_slice_free (KeySequence, seq);
          continue;
        }

      /* Ownership of the KeySequence is taken over by the hash table */
      g_hash_table_insert (self->key_sequences, GUINT_TO_POINTER (keyval), seq);
    }

  g_strfreev (keys);
  g_key_file_free (key_file);
}