gtk2/modules/input/gtkimcontextmultipress.c
Daniel Elstner 3c5a7c3b70 Maintenance of Multipress input method by Openismus GmbH:
* modules/input/gtkimcontextmultipress.[ch]: Clean up the code
a bit to follow the GTK+ coding style more closely.  Fix the code
to emit "preedit-start" and "preedit-end", too, rather than only
"preedit-changed".
(GTK_IM_CONTEXT_MULTIPRESS*): Rename incorrectly spelled macros
gtk_im_context_multipress*. Shouldn't break API or ABI as it's
only used internally.
* modules/input/immultipress.c: More cleanup,
* modules/input/README.multipress: ditto.

svn path=/trunk/; revision=21864
2008-12-10 11:58:37 +00:00

587 lines
18 KiB
C

/* Copyright (C) 2006 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, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#include <config.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtkimmodule.h>
#include "gtkimcontextmultipress.h"
#define AUTOMATIC_COMPOSE_TIMEOUT 1 /* seconds */
#define CONFIGURATION_FILENAME MULTIPRESS_CONFDIR G_DIR_SEPARATOR_S "im-multipress.conf"
#define MULTIPRESS_PASSTHROUGH_FLAG "multipress-passthrough-flag"
/** This contains rows of characters that can be inputed by pressing a particular key repeatedly.
* Each row has one key (such as GDK_A), and an array of characters, such as 'a'.
*/
struct _KeySequence
{
gunichar key_press; /* Such as 'a' (== GDK_a) */
gchar **characters; /* Array of strings. */
gsize characters_length; /* size of the array of strings. */
};
static void gtk_im_context_multipress_class_init (GtkImContextMultipressClass *klass);
static void gtk_im_context_multipress_init (GtkImContextMultipress *self);
static void gtk_im_context_multipress_finalize (GObject *obj);
static void gtk_im_context_multipress_load_config (GtkImContextMultipress *self);
static GObjectClass *gtk_im_context_multipress_parent_class = NULL;
static GType gtk_im_multi_press_im_context_type = 0;
/* 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)
{
static const GTypeInfo im_context_multipress_info =
{
sizeof (GtkImContextMultipressClass),
(GBaseInitFunc) NULL,
(GBaseFinalizeFunc) NULL,
(GClassInitFunc) gtk_im_context_multipress_class_init,
NULL,
NULL,
sizeof (GtkImContextMultipress),
0,
(GInstanceInitFunc) gtk_im_context_multipress_init,
0,
};
gtk_im_multi_press_im_context_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 (gtk_im_multi_press_im_context_type != 0);
return gtk_im_multi_press_im_context_type;
}
/*
* Returns TRUE if the passthrough flag is set on the currently focused
* child of the widget that owns the GDK window. In order to turn on
* passthrough mode, call:
* g_object_set_data (widget, "multipress-passthrough-flag", GINT_TO_POINTER (1));
*/
static gboolean
passthrough_enabled_for_window (GdkWindow* window)
{
gpointer event_widget = NULL;
g_return_val_if_fail (window != NULL, FALSE);
/*
* For historical reasons, GTK+ assumes the user data attached to a GdkWindow
* to point to the GtkWidget that owns the window. It's even documented:
* http://developer.gnome.org/doc/API/2.0/gdk/gdk-Windows.html#gdk-window-set-user-data
* So we are really lucky here, as this allows us to attach IM state
* information to a widget in a fairly straightforward manner.
*/
gdk_window_get_user_data (window, &event_widget);
if (event_widget && GTK_IS_WIDGET (event_widget))
{
GtkWidget *toplevel;
GtkWidget *focus_widget;
/*
* The event window for key presses will usually belong to the toplevel
* GtkWindow, but that might not be true for synthetic events. In any
* case we need to find the currently focused child widget.
*/
toplevel = gtk_widget_get_toplevel ((GtkWidget *)event_widget);
g_return_val_if_fail (toplevel != NULL && GTK_IS_WINDOW (toplevel), FALSE);
focus_widget = gtk_window_get_focus ((GtkWindow *)toplevel);
if (focus_widget)
{
static GQuark quark_passthrough_flag = 0;
if (!quark_passthrough_flag)
quark_passthrough_flag = g_quark_from_string (MULTIPRESS_PASSTHROUGH_FLAG);
if (g_object_get_qdata (G_OBJECT (focus_widget), quark_passthrough_flag))
return TRUE;
}
}
return FALSE;
}
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);
static void
gtk_im_context_multipress_class_init (GtkImContextMultipressClass *klass)
{
GtkIMContextClass* im_context_class;
/* Set this so we can use it later: */
gtk_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 = gtk_im_context_multipress_finalize;
}
static void
gtk_im_context_multipress_init (GtkImContextMultipress *self)
{
gtk_im_context_multipress_load_config (self);
}
static void
gtk_im_context_multipress_finalize (GObject *obj)
{
GtkImContextMultipress *self = GTK_IM_CONTEXT_MULTIPRESS (obj);
/* Release the configuration data: */
/* Free each item: */
gsize i = 0;
for (i = 0; i < self->key_sequences_count; ++i)
{
KeySequence *item = self->key_sequences[i];
/* Free the array of strings in the item: */
/* This is only for null-terminated arrays: g_strfreev(item->characters); */
gsize i = 0;
for (i = 0; i < item->characters_length; ++i)
{
gchar *str = item->characters[i];
g_free (str);
item->characters[i] = NULL;
}
g_free (item->characters);
item->characters = NULL;
item->characters_length = 0;
/* Free the item itself: */
g_free (item);
}
/* Free the array of pointers: */
g_free (self->key_sequences);
self->key_sequences = NULL;
self->key_sequences_count = 0;
gtk_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);
}
/* Lookup a compose sequence for the key press, from the table. The result is
* a null-terminated array of gchar*. It should not be freed by the caller.
*/
static KeySequence *
lookup_characters (GtkImContextMultipress *multipress_context, guint keypress)
{
/* Find the matching KeySequence, so that the caller can look at the possible
* characters for this keypress: */
gsize i = 0;
for (i = 0; i < multipress_context->key_sequences_count; ++i)
{
KeySequence *item = multipress_context->key_sequences[i];
/* Just compare the first item, to match the keyval: */
if (keypress == item->key_press)
return item;
}
return NULL;
}
static void
cancel_automatic_timeout_commit (GtkImContextMultipress *multipress_context)
{
/* printf("debug: cancelling timeout\n"); */
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();
/* printf("debug: on_timeout\n"); */
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 FALSE; /* don't call me again */
}
static gboolean
vfunc_filter_keypress (GtkIMContext *context, GdkEventKey *event)
{
GtkIMContextClass *parent;
GtkImContextMultipress *multipress_context;
/* printf("debug: vfunc_filter_keypress:\n"); */
multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
if (event->type == GDK_KEY_PRESS)
{
KeySequence *possible = NULL;
/* printf("debug: multipress_context->compose_count=%d\n", multipress_context->compose_count); */
/* 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)
{
/* Accept the previously chosen character: */
if (multipress_context->tentative_match)
{
/* 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: */
if (!passthrough_enabled_for_window (event->window))
possible = lookup_characters (multipress_context, event->keyval); /* Not to be freed. */
if (possible)
{
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->characters_length)
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);
return TRUE; /* TRUE means that the event was 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)
accept_character (multipress_context, multipress_context->tentative_match);
keyval_uchar = gdk_keyval_to_unicode (event->keyval);
/*
* Convert to a string, because that's what accept_character() needs.
*/
if (keyval_uchar)
{
gchar keyval_utf8[7]; /* max length of UTF-8 sequence + 1 for NUL termination */
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 */
}
}
}
/* The default implementation just returns FALSE,
* but it is generally a good idea to call the base class implementation:
*/
parent = (GtkIMContextClass *)gtk_im_context_multipress_parent_class;
if (parent->filter_keypress)
return parent->filter_keypress (context, event);
else
return FALSE;
}
static void
vfunc_reset (GtkIMContext *context)
{
GtkImContextMultipress *multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
clear_compose_buffer (multipress_context);
}
static void
vfunc_get_preedit_string (GtkIMContext *context,
gchar **str,
PangoAttrList **attrs,
gint *cursor_pos)
{
/* printf("debug: get_preedit_string:\n"); */
GtkImContextMultipress *multipress_context = GTK_IM_CONTEXT_MULTIPRESS (context);
/* Show the user what character he will get if he accepts: */
gsize len_bytes = 0;
gsize len_utf8_chars = 0;
if (str)
{
if (multipress_context->tentative_match)
{
/*
printf("debug: vfunc_get_preedit_string(): tentative_match != NULL\n");
printf("debug: vfunc_get_preedit_string(): tentative_match=%s\n", multipress_context->tentative_match);
*/
*str = g_strdup (multipress_context->tentative_match);
}
else
{
/* *str can never be NULL - that crashes the caller, which doesn't check for it: */
*str = g_strdup("");
}
if (*str)
{
len_utf8_chars = g_utf8_strlen (*str, -1); /* For the cursor pos, which seems need to be UTF-8 characters (GtkEntry clamps it.) */
len_bytes = strlen (*str); /* The number of bytes, not the number of UTF-8 characters. For the PangoAttribute. */
}
}
/* Underline it, to show the user that he is in compose mode: */
if (attrs)
{
*attrs = pango_attr_list_new ();
if (len_bytes)
{
PangoAttribute *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;
}
static void
gtk_im_context_multipress_load_config (GtkImContextMultipress *self)
{
/* Open the configuration file: */
GKeyFile *key_file;
GError *error = NULL;
GArray *array;
gboolean found;
guint key_suffix_num = 0;
gboolean keep_looking = TRUE;
key_file = g_key_file_new ();
found = g_key_file_load_from_file (key_file, CONFIGURATION_FILENAME, G_KEY_FILE_NONE, &error);
if (!found || error)
{
if (error)
{
g_warning ("Error while trying to open the %s configuration file: %s", CONFIGURATION_FILENAME, error->message);
g_error_free (error);
error = NULL;
/*debug_output_possible_data_dirs();*/
}
g_key_file_free (key_file);
return;
}
/* Get data from the file:
* Each KP_* key should have a value consiting of ;-separated values: */
array = g_array_sized_new (FALSE /* Not zero_terminated */, TRUE /* clear */,
sizeof(KeySequence*), 10 /* reserved size */);
/* Look at each KP_* key in sequence, until there are no more KP_* keys: */
while (keep_looking)
{
gchar* key_name;
gchar** values;
gsize length_values = 0;
key_name = g_strdup_printf ("KP_%d", key_suffix_num);
values = g_key_file_get_string_list (key_file, "keys", key_name, &length_values, &error);
if (error)
{
/* Only show the warning if this was the first key. It's OK to fail when trying to find subsequent keys: */
if (key_suffix_num == 0)
{
g_warning ("Error while trying to read key values from the configuration file: %s", error->message);
}
g_error_free (error);
error = NULL;
}
if (!values)
{
/* printf("debug: No values found for key %s\n", key_name); */
keep_looking = FALSE; /* This must be the last in the array of keys. */
/* debug_output_possible_config_keys(key_file); */
}
else
{
KeySequence *key_sequence;
GArray *array_characters;
gsize value_index = 0;
key_sequence = g_new0 (KeySequence, 1);
g_array_append_val (array, key_sequence);
array_characters = g_array_sized_new (FALSE /* Not zero_terminated */, TRUE /* clear */,
sizeof(gchar*), 10 /* reserved size */);
for (value_index = 0; value_index < length_values; ++value_index)
{
gchar *value;
gchar *value_copy;
value = values[value_index];
if (value_index == 0)
{
g_assert (strlen(value) > 0);
key_sequence->key_press = g_utf8_get_char (value);
}
value_copy = g_strdup (value);
g_array_append_val (array_characters, value_copy);
/* printf("debug: Key=%s, value=%s\n", key_name, value); */
}
g_strfreev (values);
key_sequence->characters_length = array_characters->len;
key_sequence->characters = (gchar **)g_array_free(array_characters,
FALSE /* Don't free items - return a real array of them. */);
}
g_free (key_name);
++key_suffix_num;
}
g_key_file_free (key_file);
self->key_sequences_count = array->len;
self->key_sequences = (KeySequence **)g_array_free (array,
FALSE /* Don't free items - return a real array of them. */);
/* debug_output_key_sequences_array(self); */
}