diff options
Diffstat (limited to 'e-util/e-category-completion.c')
-rw-r--r-- | e-util/e-category-completion.c | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/e-util/e-category-completion.c b/e-util/e-category-completion.c new file mode 100644 index 0000000000..095df50b45 --- /dev/null +++ b/e-util/e-category-completion.c @@ -0,0 +1,505 @@ +/* + * This program 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) version 3. + * + * This program 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 the program; if not, see <http://www.gnu.org/licenses/> + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + */ + +#include "e-category-completion.h" + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <string.h> +#include <glib/gi18n-lib.h> + +#include <libedataserver/libedataserver.h> + +#define E_CATEGORY_COMPLETION_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_CATEGORY_COMPLETION, ECategoryCompletionPrivate)) + +struct _ECategoryCompletionPrivate { + GtkWidget *last_known_entry; + gchar *create; + gchar *prefix; +}; + +enum { + COLUMN_PIXBUF, + COLUMN_CATEGORY, + COLUMN_NORMALIZED, + NUM_COLUMNS +}; + +G_DEFINE_TYPE ( + ECategoryCompletion, + e_category_completion, + GTK_TYPE_ENTRY_COMPLETION) + +/* Forward Declarations */ + +static void +category_completion_track_entry (GtkEntryCompletion *completion); + +static void +category_completion_build_model (GtkEntryCompletion *completion) +{ + GtkListStore *store; + GList *list; + + store = gtk_list_store_new ( + NUM_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING); + + list = e_categories_get_list (); + while (list != NULL) { + const gchar *category = list->data; + const gchar *filename; + gchar *normalized; + gchar *casefolded; + GdkPixbuf *pixbuf = NULL; + GtkTreeIter iter; + + /* Only add user-visible categories. */ + if (!e_categories_is_searchable (category)) { + list = g_list_delete_link (list, list); + continue; + } + + filename = e_categories_get_icon_file_for (category); + if (filename != NULL && *filename != '\0') + pixbuf = gdk_pixbuf_new_from_file (filename, NULL); + + normalized = g_utf8_normalize ( + category, -1, G_NORMALIZE_DEFAULT); + casefolded = g_utf8_casefold (normalized, -1); + + gtk_list_store_append (store, &iter); + + gtk_list_store_set ( + store, &iter, COLUMN_PIXBUF, pixbuf, + COLUMN_CATEGORY, category, COLUMN_NORMALIZED, + casefolded, -1); + + g_free (normalized); + g_free (casefolded); + + if (pixbuf != NULL) + g_object_unref (pixbuf); + + list = g_list_delete_link (list, list); + } + + gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store)); +} + +static void +category_completion_categories_changed_cb (GObject *some_private_object, + GtkEntryCompletion *completion) +{ + category_completion_build_model (completion); +} + +static void +category_completion_complete (GtkEntryCompletion *completion, + const gchar *category) +{ + GtkEditable *editable; + GtkWidget *entry; + const gchar *text; + const gchar *cp; + gint start_pos; + gint end_pos; + glong offset; + + entry = gtk_entry_completion_get_entry (completion); + + editable = GTK_EDITABLE (entry); + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + /* Get the cursor position as a character offset. */ + offset = gtk_editable_get_position (editable); + + /* Find the rightmost comma before the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strrchr (text, (gssize) (cp - text), ','); + + /* Calculate the selection start position as a character offset. */ + if (cp == NULL) + offset = 0; + else { + cp = g_utf8_next_char (cp); + if (g_unichar_isspace (g_utf8_get_char (cp))) + cp = g_utf8_next_char (cp); + offset = g_utf8_pointer_to_offset (text, cp); + } + start_pos = (gint) offset; + + /* Find the leftmost comma after the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strchr (cp, -1, ','); + + /* Calculate the selection end position as a character offset. */ + if (cp == NULL) + offset = -1; + else { + cp = g_utf8_next_char (cp); + if (g_unichar_isspace (g_utf8_get_char (cp))) + cp = g_utf8_next_char (cp); + offset = g_utf8_pointer_to_offset (text, cp); + } + end_pos = (gint) offset; + + /* Complete the partially typed category. */ + gtk_editable_delete_text (editable, start_pos, end_pos); + gtk_editable_insert_text (editable, category, -1, &start_pos); + gtk_editable_insert_text (editable, ",", 1, &start_pos); + gtk_editable_set_position (editable, start_pos); +} + +static gboolean +category_completion_is_match (GtkEntryCompletion *completion, + const gchar *key, + GtkTreeIter *iter) +{ + ECategoryCompletionPrivate *priv; + GtkTreeModel *model; + GtkWidget *entry; + GValue value = { 0, }; + gboolean match; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + entry = gtk_entry_completion_get_entry (completion); + model = gtk_entry_completion_get_model (completion); + + /* XXX This would be easier if GtkEntryCompletion had an 'entry' + * property that we could listen to for notifications. */ + if (entry != priv->last_known_entry) + category_completion_track_entry (completion); + + if (priv->prefix == NULL) + return FALSE; + + gtk_tree_model_get_value (model, iter, COLUMN_NORMALIZED, &value); + match = g_str_has_prefix (g_value_get_string (&value), priv->prefix); + g_value_unset (&value); + + return match; +} + +static void +category_completion_update_prefix (GtkEntryCompletion *completion) +{ + ECategoryCompletionPrivate *priv; + GtkEditable *editable; + GtkTreeModel *model; + GtkWidget *entry; + GtkTreeIter iter; + const gchar *text; + const gchar *start; + const gchar *end; + const gchar *cp; + gboolean valid; + gchar *input; + glong offset; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + entry = gtk_entry_completion_get_entry (completion); + model = gtk_entry_completion_get_model (completion); + + /* XXX This would be easier if GtkEntryCompletion had an 'entry' + * property that we could listen to for notifications. */ + if (entry != priv->last_known_entry) { + category_completion_track_entry (completion); + return; + } + + editable = GTK_EDITABLE (entry); + text = gtk_entry_get_text (GTK_ENTRY (entry)); + + /* Get the cursor position as a character offset. */ + offset = gtk_editable_get_position (editable); + + /* Find the rightmost comma before the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strrchr (text, (gsize) (cp - text), ','); + + /* Mark the start of the prefix. */ + if (cp == NULL) + start = text; + else { + cp = g_utf8_next_char (cp); + if (g_unichar_isspace (g_utf8_get_char (cp))) + cp = g_utf8_next_char (cp); + start = cp; + } + + /* Find the leftmost comma after the cursor. */ + cp = g_utf8_offset_to_pointer (text, offset); + cp = g_utf8_strchr (cp, -1, ','); + + /* Mark the end of the prefix. */ + if (cp == NULL) + end = text + strlen (text); + else + end = cp; + + if (priv->create != NULL) + gtk_entry_completion_delete_action (completion, 0); + + g_free (priv->create); + priv->create = NULL; + + g_free (priv->prefix); + priv->prefix = NULL; + + if (start == end) + return; + + input = g_strstrip (g_strndup (start, end - start)); + priv->create = input; + + input = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT); + priv->prefix = g_utf8_casefold (input, -1); + g_free (input); + + if (*priv->create == '\0') { + g_free (priv->create); + priv->create = NULL; + return; + } + + valid = gtk_tree_model_get_iter_first (model, &iter); + while (valid) { + GValue value = { 0, }; + + gtk_tree_model_get_value ( + model, &iter, COLUMN_NORMALIZED, &value); + if (strcmp (g_value_get_string (&value), priv->prefix) == 0) { + g_value_unset (&value); + g_free (priv->create); + priv->create = NULL; + return; + } + g_value_unset (&value); + + valid = gtk_tree_model_iter_next (model, &iter); + } + + input = g_strdup_printf (_("Create category \"%s\""), priv->create); + gtk_entry_completion_insert_action_text (completion, 0, input); + g_free (input); +} + +static gboolean +category_completion_sanitize_suffix (GtkEntry *entry, + GdkEventFocus *event, + GtkEntryCompletion *completion) +{ + const gchar *text; + + g_return_val_if_fail (entry != NULL, FALSE); + g_return_val_if_fail (completion != NULL, FALSE); + + text = gtk_entry_get_text (entry); + if (text) { + gint len = strlen (text), old_len = len; + + while (len > 0 && (text[len -1] == ' ' || text[len - 1] == ',')) + len--; + + if (old_len != len) { + gchar *tmp = g_strndup (text, len); + + gtk_entry_set_text (entry, tmp); + + g_free (tmp); + } + } + + return FALSE; +} + +static void +category_completion_track_entry (GtkEntryCompletion *completion) +{ + ECategoryCompletionPrivate *priv; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + + if (priv->last_known_entry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->last_known_entry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, completion); + g_object_unref (priv->last_known_entry); + } + + g_free (priv->prefix); + priv->prefix = NULL; + + priv->last_known_entry = gtk_entry_completion_get_entry (completion); + if (priv->last_known_entry == NULL) + return; + + g_object_ref (priv->last_known_entry); + + g_signal_connect_swapped ( + priv->last_known_entry, "notify::cursor-position", + G_CALLBACK (category_completion_update_prefix), completion); + + g_signal_connect_swapped ( + priv->last_known_entry, "notify::text", + G_CALLBACK (category_completion_update_prefix), completion); + + g_signal_connect ( + priv->last_known_entry, "focus-out-event", + G_CALLBACK (category_completion_sanitize_suffix), completion); + + category_completion_update_prefix (completion); +} + +static void +category_completion_constructed (GObject *object) +{ + GtkCellRenderer *renderer; + GtkEntryCompletion *completion; + + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_category_completion_parent_class)->constructed (object); + + completion = GTK_ENTRY_COMPLETION (object); + + gtk_entry_completion_set_match_func ( + completion, (GtkEntryCompletionMatchFunc) + category_completion_is_match, NULL, NULL); + + gtk_entry_completion_set_text_column (completion, COLUMN_CATEGORY); + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start ( + GTK_CELL_LAYOUT (completion), renderer, FALSE); + gtk_cell_layout_add_attribute ( + GTK_CELL_LAYOUT (completion), + renderer, "pixbuf", COLUMN_PIXBUF); + gtk_cell_layout_reorder ( + GTK_CELL_LAYOUT (completion), renderer, 0); + + e_categories_register_change_listener ( + G_CALLBACK (category_completion_categories_changed_cb), + completion); + + category_completion_build_model (completion); +} + +static void +category_completion_dispose (GObject *object) +{ + ECategoryCompletionPrivate *priv; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object); + + if (priv->last_known_entry != NULL) { + g_signal_handlers_disconnect_matched ( + priv->last_known_entry, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, object); + g_object_unref (priv->last_known_entry); + priv->last_known_entry = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_category_completion_parent_class)->dispose (object); +} + +static void +category_completion_finalize (GObject *object) +{ + ECategoryCompletionPrivate *priv; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (object); + + g_free (priv->create); + g_free (priv->prefix); + + e_categories_unregister_change_listener ( + G_CALLBACK (category_completion_categories_changed_cb), + object); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_category_completion_parent_class)->finalize (object); +} + +static gboolean +category_completion_match_selected (GtkEntryCompletion *completion, + GtkTreeModel *model, + GtkTreeIter *iter) +{ + GValue value = { 0, }; + + gtk_tree_model_get_value (model, iter, COLUMN_CATEGORY, &value); + category_completion_complete (completion, g_value_get_string (&value)); + g_value_unset (&value); + + return TRUE; +} + +static void +category_completion_action_activated (GtkEntryCompletion *completion, + gint index) +{ + ECategoryCompletionPrivate *priv; + gchar *category; + + priv = E_CATEGORY_COMPLETION_GET_PRIVATE (completion); + + category = g_strdup (priv->create); + e_categories_add (category, NULL, NULL, TRUE); + category_completion_complete (completion, category); + g_free (category); +} + +static void +e_category_completion_class_init (ECategoryCompletionClass *class) +{ + GObjectClass *object_class; + GtkEntryCompletionClass *entry_completion_class; + + g_type_class_add_private (class, sizeof (ECategoryCompletionPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->constructed = category_completion_constructed; + object_class->dispose = category_completion_dispose; + object_class->finalize = category_completion_finalize; + + entry_completion_class = GTK_ENTRY_COMPLETION_CLASS (class); + entry_completion_class->match_selected = category_completion_match_selected; + entry_completion_class->action_activated = category_completion_action_activated; +} + +static void +e_category_completion_init (ECategoryCompletion *category_completion) +{ + category_completion->priv = + E_CATEGORY_COMPLETION_GET_PRIVATE (category_completion); +} + +/** + * e_category_completion_new: + * + * Since: 2.26 + **/ +GtkEntryCompletion * +e_category_completion_new (void) +{ + return g_object_new (E_TYPE_CATEGORY_COMPLETION, NULL); +} |