summaryrefslogtreecommitdiffstats
path: root/app/widgets/gimptagentry.c
diff options
context:
space:
mode:
Diffstat (limited to 'app/widgets/gimptagentry.c')
-rw-r--r--app/widgets/gimptagentry.c2207
1 files changed, 2207 insertions, 0 deletions
diff --git a/app/widgets/gimptagentry.c b/app/widgets/gimptagentry.c
new file mode 100644
index 0000000..578899f
--- /dev/null
+++ b/app/widgets/gimptagentry.c
@@ -0,0 +1,2207 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagentry.c
+ * Copyright (C) 2008 Aurimas Juška <aurisj@svn.gnome.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gegl.h>
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "libgimpwidgets/gimpwidgets.h"
+
+#include "widgets-types.h"
+
+#include "core/gimp-utils.h"
+#include "core/gimpcontainer.h"
+#include "core/gimpcontext.h"
+#include "core/gimptag.h"
+#include "core/gimptagged.h"
+#include "core/gimptaggedcontainer.h"
+#include "core/gimpviewable.h"
+
+#include "gimptagentry.h"
+
+#include "gimp-intl.h"
+
+
+#define GIMP_TAG_ENTRY_QUERY_DESC _("filter")
+#define GIMP_TAG_ENTRY_ASSIGN_DESC _("enter tags")
+
+#define GIMP_TAG_ENTRY_MAX_RECENT_ITEMS 20
+
+
+typedef enum
+{
+ TAG_SEARCH_NONE,
+ TAG_SEARCH_LEFT,
+ TAG_SEARCH_RIGHT,
+} GimpTagSearchDir;
+
+enum
+{
+ PROP_0,
+ PROP_CONTAINER,
+ PROP_MODE
+};
+
+
+static void gimp_tag_entry_dispose (GObject *object);
+static void gimp_tag_entry_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+static void gimp_tag_entry_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+static void gimp_tag_entry_activate (GtkEntry *entry);
+static void gimp_tag_entry_changed (GtkEntry *entry);
+static void gimp_tag_entry_insert_text (GtkEditable *editable,
+ gchar *new_text,
+ gint text_length,
+ gint *position);
+static void gimp_tag_entry_delete_text (GtkEditable *editable,
+ gint start_pos,
+ gint end_pos);
+static gboolean gimp_tag_entry_focus_in (GtkWidget *widget,
+ GdkEventFocus *event);
+static gboolean gimp_tag_entry_focus_out (GtkWidget *widget,
+ GdkEventFocus *event);
+static void gimp_tag_entry_container_changed (GimpContainer *container,
+ GimpObject *object,
+ GimpTagEntry *entry);
+static gboolean gimp_tag_entry_button_release (GtkWidget *widget,
+ GdkEventButton *event);
+static gboolean gimp_tag_entry_key_press (GtkWidget *widget,
+ GdkEventKey *event);
+static gboolean gimp_tag_entry_query_tag (GimpTagEntry *entry);
+
+static void gimp_tag_entry_assign_tags (GimpTagEntry *entry);
+static void gimp_tag_entry_load_selection (GimpTagEntry *entry,
+ gboolean sort);
+static void gimp_tag_entry_find_common_tags (gpointer key,
+ gpointer value,
+ gpointer user_data);
+
+static gchar * gimp_tag_entry_get_completion_prefix (GimpTagEntry *entry);
+static GList * gimp_tag_entry_get_completion_candidates (GimpTagEntry *entry,
+ gchar **used_tags,
+ gchar *prefix);
+static gchar * gimp_tag_entry_get_completion_string (GimpTagEntry *entry,
+ GList *candidates,
+ gchar *prefix);
+static gboolean gimp_tag_entry_auto_complete (GimpTagEntry *entry);
+
+static void gimp_tag_entry_toggle_desc (GimpTagEntry *widget,
+ gboolean show);
+static gboolean gimp_tag_entry_expose (GtkWidget *widget,
+ GdkEventExpose *event);
+static void gimp_tag_entry_commit_region (GString *tags,
+ GString *mask);
+static void gimp_tag_entry_commit_tags (GimpTagEntry *entry);
+static gboolean gimp_tag_entry_commit_source_func (GimpTagEntry *entry);
+static gboolean gimp_tag_entry_select_jellybean (GimpTagEntry *entry,
+ gint selection_start,
+ gint selection_end,
+ GimpTagSearchDir search_dir);
+static gboolean gimp_tag_entry_try_select_jellybean (GimpTagEntry *entry);
+
+static gboolean gimp_tag_entry_add_to_recent (GimpTagEntry *entry,
+ const gchar *tags_string,
+ gboolean to_front);
+
+static void gimp_tag_entry_next_tag (GimpTagEntry *entry,
+ gboolean select);
+static void gimp_tag_entry_previous_tag (GimpTagEntry *entry,
+ gboolean select);
+
+static void gimp_tag_entry_select_for_deletion (GimpTagEntry *entry,
+ GimpTagSearchDir search_dir);
+static gboolean gimp_tag_entry_strip_extra_whitespace (GimpTagEntry *entry);
+
+
+
+G_DEFINE_TYPE (GimpTagEntry, gimp_tag_entry, GTK_TYPE_ENTRY)
+
+#define parent_class gimp_tag_entry_parent_class
+
+
+static void
+gimp_tag_entry_class_init (GimpTagEntryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gimp_tag_entry_dispose;
+ object_class->get_property = gimp_tag_entry_get_property;
+ object_class->set_property = gimp_tag_entry_set_property;
+
+ widget_class->button_release_event = gimp_tag_entry_button_release;
+
+ g_object_class_install_property (object_class,
+ PROP_CONTAINER,
+ g_param_spec_object ("container",
+ "Tagged container",
+ "The Tagged container",
+ GIMP_TYPE_TAGGED_CONTAINER,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_MODE,
+ g_param_spec_enum ("mode",
+ "Working mode",
+ "Mode in which to work.",
+ GIMP_TYPE_TAG_ENTRY_MODE,
+ GIMP_TAG_ENTRY_MODE_QUERY,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE));
+}
+
+static void
+gimp_tag_entry_init (GimpTagEntry *entry)
+{
+ entry->container = NULL;
+ entry->selected_items = NULL;
+ entry->common_tags = NULL;
+ entry->tab_completion_index = -1;
+ entry->mode = GIMP_TAG_ENTRY_MODE_QUERY;
+ entry->description_shown = FALSE;
+ entry->has_invalid_tags = FALSE;
+ entry->mask = g_string_new ("");
+
+ g_signal_connect (entry, "activate",
+ G_CALLBACK (gimp_tag_entry_activate),
+ NULL);
+ g_signal_connect (entry, "changed",
+ G_CALLBACK (gimp_tag_entry_changed),
+ NULL);
+ g_signal_connect (entry, "insert-text",
+ G_CALLBACK (gimp_tag_entry_insert_text),
+ NULL);
+ g_signal_connect (entry, "delete-text",
+ G_CALLBACK (gimp_tag_entry_delete_text),
+ NULL);
+ g_signal_connect (entry, "key-press-event",
+ G_CALLBACK (gimp_tag_entry_key_press),
+ NULL);
+ g_signal_connect (entry, "focus-in-event",
+ G_CALLBACK (gimp_tag_entry_focus_in),
+ NULL);
+ g_signal_connect (entry, "focus-out-event",
+ G_CALLBACK (gimp_tag_entry_focus_out),
+ NULL);
+ g_signal_connect_after (entry, "expose-event",
+ G_CALLBACK (gimp_tag_entry_expose),
+ NULL);
+}
+
+static void
+gimp_tag_entry_dispose (GObject *object)
+{
+ GimpTagEntry *entry = GIMP_TAG_ENTRY (object);
+
+ g_clear_pointer (&entry->selected_items, g_list_free);
+
+ if (entry->common_tags)
+ {
+ g_list_free_full (entry->common_tags, (GDestroyNotify) g_object_unref);
+ entry->common_tags = NULL;
+ }
+
+ if (entry->recent_list)
+ {
+ g_list_free_full (entry->recent_list, (GDestroyNotify) g_free);
+ entry->recent_list = NULL;
+ }
+
+ if (entry->container)
+ {
+ g_signal_handlers_disconnect_by_func (entry->container,
+ gimp_tag_entry_container_changed,
+ entry);
+ g_clear_object (&entry->container);
+ }
+
+ if (entry->mask)
+ {
+ g_string_free (entry->mask, TRUE);
+ entry->mask = NULL;
+ }
+
+ if (entry->tag_query_idle_id)
+ {
+ g_source_remove (entry->tag_query_idle_id);
+ entry->tag_query_idle_id = 0;
+ }
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gimp_tag_entry_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GimpTagEntry *entry = GIMP_TAG_ENTRY (object);
+
+ switch (property_id)
+ {
+ case PROP_CONTAINER:
+ entry->container = g_value_dup_object (value);
+ g_signal_connect (entry->container, "add",
+ G_CALLBACK (gimp_tag_entry_container_changed),
+ entry);
+ g_signal_connect (entry->container, "remove",
+ G_CALLBACK (gimp_tag_entry_container_changed),
+ entry);
+ break;
+
+ case PROP_MODE:
+ entry->mode = g_value_get_enum (value);
+ gimp_tag_entry_toggle_desc (entry, TRUE);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gimp_tag_entry_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GimpTagEntry *entry = GIMP_TAG_ENTRY (object);
+
+ switch (property_id)
+ {
+ case PROP_CONTAINER:
+ g_value_set_object (value, entry->container);
+ break;
+
+ case PROP_MODE:
+ g_value_set_enum (value, entry->mode);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/**
+ * gimp_tag_entry_new:
+ * @container: a #GimpTaggedContainer object
+ * @mode: #GimpTagEntryMode to work in.
+ *
+ * #GimpTagEntry is a widget which can query and assign tags to tagged objects.
+ * When operating in query mode, @container is kept up to date with
+ * tags selected. When operating in assignment mode, tags are assigned to
+ * objects selected and visible in @container.
+ *
+ * Return value: a new GimpTagEntry widget.
+ **/
+GtkWidget *
+gimp_tag_entry_new (GimpTaggedContainer *container,
+ GimpTagEntryMode mode)
+{
+ g_return_val_if_fail (GIMP_IS_TAGGED_CONTAINER (container), NULL);
+
+ return g_object_new (GIMP_TYPE_TAG_ENTRY,
+ "container", container,
+ "mode", mode,
+ NULL);
+}
+
+static void
+gimp_tag_entry_activate (GtkEntry *entry)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (entry);
+ gint selection_start;
+ gint selection_end;
+ GList *list;
+
+ gimp_tag_entry_toggle_desc (tag_entry, FALSE);
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &selection_start, &selection_end);
+ if (selection_start != selection_end)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_end, selection_end);
+ }
+
+ for (list = tag_entry->selected_items; list; list = g_list_next (list))
+ {
+ if (gimp_container_have (GIMP_CONTAINER (tag_entry->container),
+ GIMP_OBJECT (list->data)))
+ {
+ break;
+ }
+ }
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN && list)
+ {
+ gimp_tag_entry_assign_tags (GIMP_TAG_ENTRY (entry));
+ }
+}
+
+/**
+ * gimp_tag_entry_set_tag_string:
+ * @entry: a #GimpTagEntry object.
+ * @tag_string: string of tags, separated by any terminal punctuation
+ * character.
+ *
+ * Sets tags from @tag_string to @tag_entry. Given tags do not need to
+ * be valid as they can be fixed or dropped automatically. Depending on
+ * selected #GimpTagEntryMode, appropriate action is performed.
+ **/
+void
+gimp_tag_entry_set_tag_string (GimpTagEntry *entry,
+ const gchar *tag_string)
+{
+ g_return_if_fail (GIMP_IS_TAG_ENTRY (entry));
+
+ entry->internal_operation++;
+ entry->suppress_tag_query++;
+
+ gtk_entry_set_text (GTK_ENTRY (entry), tag_string);
+ gtk_editable_set_position (GTK_EDITABLE (entry), -1);
+
+ entry->suppress_tag_query--;
+ entry->internal_operation--;
+
+ gimp_tag_entry_commit_tags (entry);
+
+ if (entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ gimp_tag_entry_assign_tags (entry);
+ }
+ else if (entry->mode == GIMP_TAG_ENTRY_MODE_QUERY)
+ {
+ gimp_tag_entry_query_tag (entry);
+ }
+}
+
+static void
+gimp_tag_entry_changed (GtkEntry *entry)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (entry);
+ gchar *text;
+
+ text = g_strdup (gtk_entry_get_text (entry));
+ text = g_strstrip (text);
+
+ if (! gtk_widget_has_focus (GTK_WIDGET (entry)) &&
+ strlen (text) == 0)
+ {
+ gimp_tag_entry_toggle_desc (tag_entry, TRUE);
+ }
+ else
+ {
+ gimp_tag_entry_toggle_desc (tag_entry, FALSE);
+ }
+
+ g_free (text);
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_QUERY &&
+ ! tag_entry->suppress_tag_query &&
+ ! tag_entry->tag_query_idle_id)
+ {
+ tag_entry->tag_query_idle_id =
+ g_idle_add ((GSourceFunc) gimp_tag_entry_query_tag, entry);
+ }
+}
+
+static void
+gimp_tag_entry_insert_text (GtkEditable *editable,
+ gchar *new_text,
+ gint text_length,
+ gint *position)
+{
+ GimpTagEntry *entry = GIMP_TAG_ENTRY (editable);
+ gboolean is_tag[2];
+ gint i;
+ gint insert_pos = *position;
+ glong num_chars;
+
+ num_chars = g_utf8_strlen (new_text, text_length);
+
+ if (! entry->internal_operation)
+ {
+ /* suppress tag queries until auto completion runs */
+ entry->suppress_tag_query++;
+ }
+
+ is_tag[0] = FALSE;
+ if (*position > 0)
+ {
+ is_tag[0] = (entry->mask->str[*position - 1] == 't' ||
+ entry->mask->str[*position - 1] == 's');
+ }
+
+ is_tag[1] = (entry->mask->str[*position] == 't' ||
+ entry->mask->str[*position] == 's');
+
+ if (is_tag[0] && is_tag[1])
+ {
+ g_signal_stop_emission_by_name (editable, "insert-text");
+ }
+ else if (num_chars > 0)
+ {
+ gunichar c = g_utf8_get_char (new_text);
+
+ if (! entry->internal_operation &&
+ *position > 0 &&
+ entry->mask->str[*position - 1] == 's' &&
+ ! g_unichar_isspace (c))
+ {
+ if (! entry->suppress_mask_update)
+ {
+ g_string_insert_c (entry->mask, *position, 'u');
+ }
+
+ g_signal_handlers_block_by_func (editable,
+ gimp_tag_entry_insert_text,
+ NULL);
+
+ gtk_editable_insert_text (editable, " ", 1, position);
+ gtk_editable_insert_text (editable, new_text, text_length, position);
+
+ g_signal_handlers_unblock_by_func (editable,
+ gimp_tag_entry_insert_text,
+ NULL);
+
+ g_signal_stop_emission_by_name (editable, "insert-text");
+ }
+ else if (! entry->internal_operation &&
+ num_chars == 1 &&
+ *position < entry->mask->len &&
+ entry->mask->str[*position] == 't' &&
+ ! g_unichar_isspace (c))
+ {
+ if (! entry->suppress_mask_update)
+ {
+ g_string_insert_c (entry->mask, *position, 'u');
+ }
+
+ g_signal_handlers_block_by_func (editable,
+ gimp_tag_entry_insert_text,
+ NULL);
+
+ gtk_editable_insert_text (editable, new_text, text_length, position);
+ gtk_editable_insert_text (editable, " ", 1, position);
+ (*position)--;
+
+ g_signal_handlers_unblock_by_func (editable,
+ gimp_tag_entry_insert_text,
+ NULL);
+
+ g_signal_stop_emission_by_name (editable, "insert-text");
+ }
+
+ if (! entry->suppress_mask_update)
+ {
+ for (i = 0; i < num_chars; i++)
+ {
+ g_string_insert_c (entry->mask, insert_pos + i, 'u');
+ }
+ }
+ }
+
+ if (! entry->internal_operation)
+ {
+ entry->tab_completion_index = -1;
+ g_idle_add ((GSourceFunc) gimp_tag_entry_auto_complete, editable);
+ }
+}
+
+static void
+gimp_tag_entry_delete_text (GtkEditable *editable,
+ gint start_pos,
+ gint end_pos)
+{
+ GimpTagEntry *entry = GIMP_TAG_ENTRY (editable);
+
+ if (! entry->internal_operation)
+ {
+ g_signal_handlers_block_by_func (editable,
+ gimp_tag_entry_delete_text,
+ NULL);
+
+ if (end_pos > start_pos &&
+ (entry->mask->str[end_pos - 1] == 't' ||
+ entry->mask->str[end_pos - 1] == 's'))
+ {
+ while (end_pos <= entry->mask->len &&
+ (entry->mask->str[end_pos] == 's'))
+ {
+ end_pos++;
+ }
+ }
+
+ gtk_editable_delete_text (editable, start_pos, end_pos);
+ if (! entry->suppress_mask_update)
+ {
+ g_string_erase (entry->mask, start_pos, end_pos - start_pos);
+ }
+
+ g_signal_handlers_unblock_by_func (editable,
+ gimp_tag_entry_delete_text,
+ NULL);
+
+ g_signal_stop_emission_by_name (editable, "delete-text");
+ }
+ else
+ {
+ if (! entry->suppress_mask_update)
+ {
+ g_string_erase (entry->mask, start_pos, end_pos - start_pos);
+ }
+ }
+}
+
+static gboolean
+gimp_tag_entry_query_tag (GimpTagEntry *entry)
+{
+ gchar **parsed_tags;
+ gint count;
+ gint i;
+ GList *query_list = NULL;
+ gboolean has_invalid_tags;
+
+ entry->tag_query_idle_id = 0;
+
+ if (entry->suppress_tag_query)
+ return FALSE;
+
+ has_invalid_tags = FALSE;
+
+ parsed_tags = gimp_tag_entry_parse_tags (entry);
+ count = g_strv_length (parsed_tags);
+ for (i = 0; i < count; i++)
+ {
+ if (strlen (parsed_tags[i]) > 0)
+ {
+ GimpTag *tag = gimp_tag_try_new (parsed_tags[i]);
+
+ if (! tag)
+ has_invalid_tags = TRUE;
+
+ query_list = g_list_append (query_list, tag);
+ }
+ }
+ g_strfreev (parsed_tags);
+
+ gimp_tagged_container_set_filter (GIMP_TAGGED_CONTAINER (entry->container),
+ query_list);
+
+ g_list_free_full (query_list, (GDestroyNotify) gimp_tag_or_null_unref);
+
+ if (has_invalid_tags != entry->has_invalid_tags)
+ {
+ entry->has_invalid_tags = has_invalid_tags;
+ gtk_widget_queue_draw (GTK_WIDGET (entry));
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_auto_complete (GimpTagEntry *tag_entry)
+{
+ GtkEntry *entry = GTK_ENTRY (tag_entry);
+ gchar *completion_prefix;
+ GList *completion_candidates;
+ gint candidate_count = 0;
+ gchar **tags;
+ gchar *completion;
+ gint start_position;
+ gint end_position;
+
+ tag_entry->suppress_tag_query--;
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_QUERY)
+ {
+ /* tag query was suppressed until we got to auto completion (here),
+ * now queue tag query
+ */
+ tag_entry->tag_query_idle_id =
+ g_idle_add ((GSourceFunc) gimp_tag_entry_query_tag, tag_entry);
+ }
+
+ if (tag_entry->tab_completion_index >= 0)
+ {
+ tag_entry->internal_operation++;
+ tag_entry->suppress_tag_query++;
+ gtk_editable_delete_selection (GTK_EDITABLE (tag_entry));
+ tag_entry->suppress_tag_query--;
+ tag_entry->internal_operation--;
+ }
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry),
+ &start_position, &end_position);
+ if (start_position != end_position)
+ {
+ /* only autocomplete what user types,
+ * not was autocompleted in the previous step.
+ */
+ return FALSE;
+ }
+
+ completion_prefix =
+ gimp_tag_entry_get_completion_prefix (GIMP_TAG_ENTRY (entry));
+ tags = gimp_tag_entry_parse_tags (GIMP_TAG_ENTRY (entry));
+ completion_candidates =
+ gimp_tag_entry_get_completion_candidates (GIMP_TAG_ENTRY (entry),
+ tags,
+ completion_prefix);
+ completion_candidates = g_list_sort (completion_candidates,
+ gimp_tag_compare_func);
+
+ if (tag_entry->tab_completion_index >= 0 && completion_candidates)
+ {
+ GimpTag *the_chosen_one;
+
+ candidate_count = g_list_length (completion_candidates);
+ tag_entry->tab_completion_index %= candidate_count;
+ the_chosen_one = g_list_nth_data (completion_candidates,
+ tag_entry->tab_completion_index);
+ g_list_free (completion_candidates);
+ completion_candidates = NULL;
+ completion_candidates = g_list_append (completion_candidates,
+ the_chosen_one);
+ }
+
+ completion = gimp_tag_entry_get_completion_string (GIMP_TAG_ENTRY (entry),
+ completion_candidates,
+ completion_prefix);
+
+ if (completion && strlen (completion) > 0)
+ {
+ start_position = gtk_editable_get_position (GTK_EDITABLE (entry));
+ end_position = start_position;
+ tag_entry->internal_operation++;
+ gtk_editable_insert_text (GTK_EDITABLE (entry),
+ completion, strlen (completion),
+ &end_position);
+ tag_entry->internal_operation--;
+ if (tag_entry->tab_completion_index >= 0 &&
+ candidate_count == 1)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (entry), end_position);
+ }
+ else
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ start_position, end_position);
+ }
+ }
+
+ g_free (completion);
+ g_strfreev (tags);
+ g_list_free (completion_candidates);
+ g_free (completion_prefix);
+
+ return FALSE;
+}
+
+static void
+gimp_tag_entry_assign_tags (GimpTagEntry *tag_entry)
+{
+ gchar **parsed_tags;
+ gint count;
+ gint i;
+ GList *resource_iter;
+ GList *tag_iter;
+ GList *selected_items;
+ GList *dont_remove_list = NULL;
+ GList *remove_list = NULL;
+ GList *add_list = NULL;
+ GList *common_tags = NULL;
+
+ parsed_tags = gimp_tag_entry_parse_tags (tag_entry);
+
+ count = g_strv_length (parsed_tags);
+ for (i = 0; i < count; i++)
+ {
+ GimpTag *tag = gimp_tag_new (parsed_tags[i]);
+
+ if (tag)
+ {
+ if (g_list_find_custom (tag_entry->common_tags, tag,
+ gimp_tag_compare_func))
+ {
+ dont_remove_list = g_list_prepend (dont_remove_list, tag);
+ }
+ else
+ {
+ add_list = g_list_prepend (add_list, g_object_ref (tag));
+ }
+
+ common_tags = g_list_prepend (common_tags, tag);
+ }
+ }
+
+ g_strfreev (parsed_tags);
+
+ /* find common tags which were removed. */
+ for (tag_iter = tag_entry->common_tags;
+ tag_iter;
+ tag_iter = g_list_next (tag_iter))
+ {
+ if (! g_list_find_custom (dont_remove_list, tag_iter->data,
+ gimp_tag_compare_func))
+ {
+ remove_list = g_list_prepend (remove_list,
+ g_object_ref (tag_iter->data));
+ }
+ }
+
+ g_list_free (dont_remove_list);
+
+ /* duplicate tag_entry->selected_items for the add/remove loop
+ * because adding/removing can change tag_entry->selected_items.
+ * See Issue #2227.
+ */
+ selected_items = g_list_copy_deep (tag_entry->selected_items,
+ (GCopyFunc) g_object_ref, NULL);
+
+ for (resource_iter = selected_items;
+ resource_iter;
+ resource_iter = g_list_next (resource_iter))
+ {
+ GimpTagged *tagged = GIMP_TAGGED (resource_iter->data);
+
+ for (tag_iter = remove_list; tag_iter; tag_iter = g_list_next (tag_iter))
+ {
+ gimp_tagged_remove_tag (tagged, tag_iter->data);
+ }
+
+ for (tag_iter = add_list; tag_iter; tag_iter = g_list_next (tag_iter))
+ {
+ gimp_tagged_add_tag (tagged, tag_iter->data);
+ }
+ }
+
+ g_list_free_full (selected_items, (GDestroyNotify) g_object_unref);
+
+ g_list_free_full (add_list, (GDestroyNotify) g_object_unref);
+ g_list_free_full (remove_list, (GDestroyNotify) g_object_unref);
+
+ /* common tags list with changes applied. */
+ g_list_free_full (tag_entry->common_tags, (GDestroyNotify) g_object_unref);
+ tag_entry->common_tags = common_tags;
+}
+
+/**
+ * gimp_tag_entry_parse_tags:
+ * @entry: a #GimpTagEntry widget.
+ *
+ * Parses currently entered tags from @entry. Tags do not need to be
+ * valid as they are fixed when necessary. Only valid tags are
+ * returned.
+ *
+ * Return value: a newly allocated NULL terminated list of strings. It
+ * should be freed using g_strfreev().
+ **/
+gchar **
+gimp_tag_entry_parse_tags (GimpTagEntry *entry)
+{
+ gchar **parsed_tags;
+ gint length;
+ gint i;
+ GString *parsed_tag;
+ const gchar *cursor;
+ GList *tag_list = NULL;
+ GList *iterator;
+ gunichar c;
+
+ g_return_val_if_fail (GIMP_IS_TAG_ENTRY (entry), NULL);
+
+ parsed_tag = g_string_new ("");
+ cursor = gtk_entry_get_text (GTK_ENTRY (entry));
+ do
+ {
+ c = g_utf8_get_char (cursor);
+ cursor = g_utf8_next_char (cursor);
+
+ if (! c || gimp_tag_is_tag_separator (c))
+ {
+ if (parsed_tag->len > 0)
+ {
+ gchar *validated_tag = gimp_tag_string_make_valid (parsed_tag->str);
+
+ if (validated_tag)
+ {
+ tag_list = g_list_append (tag_list, validated_tag);
+ }
+
+ g_string_set_size (parsed_tag, 0);
+ }
+ }
+ else
+ {
+ g_string_append_unichar (parsed_tag, c);
+ }
+ }
+ while (c);
+
+ g_string_free (parsed_tag, TRUE);
+
+ length = g_list_length (tag_list);
+ parsed_tags = g_malloc ((length + 1) * sizeof (gchar **));
+ iterator = tag_list;
+ for (i = 0; i < length; i++)
+ {
+ parsed_tags[i] = (gchar *) iterator->data;
+
+ iterator = g_list_next (iterator);
+ }
+ parsed_tags[length] = NULL;
+
+ g_list_free (tag_list);
+
+ return parsed_tags;
+}
+
+/**
+ * gimp_tag_entry_set_selected_items:
+ * @tag_entry: a #GimpTagEntry widget.
+ * @items: a list of #GimpTagged objects.
+ *
+ * Set list of currently selected #GimpTagged objects. Only selected and
+ * visible (not filtered out) #GimpTagged objects are assigned tags when
+ * operating in tag assignment mode.
+ **/
+void
+gimp_tag_entry_set_selected_items (GimpTagEntry *tag_entry,
+ GList *items)
+{
+ g_return_if_fail (GIMP_IS_TAG_ENTRY (tag_entry));
+
+ if (tag_entry->selected_items)
+ {
+ g_list_free (tag_entry->selected_items);
+ tag_entry->selected_items = NULL;
+ }
+
+ if (tag_entry->common_tags)
+ {
+ g_list_free_full (tag_entry->common_tags, (GDestroyNotify) g_object_unref);
+ tag_entry->common_tags = NULL;
+ }
+
+ tag_entry->selected_items = g_list_copy (items);
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ gimp_tag_entry_load_selection (tag_entry, TRUE);
+ }
+}
+
+static void
+gimp_tag_entry_load_selection (GimpTagEntry *tag_entry,
+ gboolean sort)
+{
+ GList *list;
+ gint insert_pos;
+ GHashTable *refcounts;
+ GList *resource;
+ GList *tag;
+
+ tag_entry->internal_operation++;
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), 0, -1);
+ tag_entry->internal_operation--;
+
+ if (! tag_entry->selected_items)
+ {
+ gimp_tag_entry_toggle_desc (tag_entry, FALSE);
+ return;
+ }
+
+ refcounts = g_hash_table_new ((GHashFunc) gimp_tag_get_hash,
+ (GEqualFunc) gimp_tag_equals);
+
+ /* find set of tags common to all resources. */
+ for (resource = tag_entry->selected_items;
+ resource;
+ resource = g_list_next (resource))
+ {
+ for (tag = gimp_tagged_get_tags (GIMP_TAGGED (resource->data));
+ tag;
+ tag = g_list_next (tag))
+ {
+ /* count refcount for each tag */
+ guint refcount = GPOINTER_TO_UINT (g_hash_table_lookup (refcounts,
+ tag->data));
+
+ g_hash_table_insert (refcounts, tag->data,
+ GUINT_TO_POINTER (refcount + 1));
+ }
+ }
+
+ g_hash_table_foreach (refcounts, gimp_tag_entry_find_common_tags, tag_entry);
+
+ g_hash_table_destroy (refcounts);
+
+ tag_entry->common_tags = g_list_sort (tag_entry->common_tags,
+ gimp_tag_compare_func);
+
+ insert_pos = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+
+ for (list = tag_entry->common_tags; list; list = g_list_next (list))
+ {
+ GimpTag *tag = list->data;
+ gchar *text;
+
+ text = g_strdup_printf ("%s%s ",
+ gimp_tag_get_name (tag),
+ gimp_tag_entry_get_separator ());
+
+ tag_entry->internal_operation++;
+ gtk_editable_insert_text (GTK_EDITABLE (tag_entry), text, strlen (text),
+ &insert_pos);
+ tag_entry->internal_operation--;
+
+ g_free (text);
+ }
+
+ gimp_tag_entry_commit_tags (tag_entry);
+}
+
+static void
+gimp_tag_entry_find_common_tags (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ guint ref_count = GPOINTER_TO_UINT (value);
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (user_data);
+
+ /* FIXME: more efficient list length */
+ if (ref_count == g_list_length (tag_entry->selected_items))
+ {
+ tag_entry->common_tags = g_list_prepend (tag_entry->common_tags,
+ g_object_ref (key));
+ }
+}
+
+static gchar *
+gimp_tag_entry_get_completion_prefix (GimpTagEntry *entry)
+{
+ gchar *original_string;
+ gchar *prefix_start;
+ gchar *prefix;
+ gchar *cursor;
+ gint position;
+ gint i;
+
+ position = gtk_editable_get_position (GTK_EDITABLE (entry));
+ if (position < 1 ||
+ entry->mask->str[position - 1] != 'u')
+ {
+ return g_strdup ("");
+ }
+
+ original_string = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
+ cursor = original_string;
+ prefix_start = original_string;
+ for (i = 0; i < position; i++)
+ {
+ gunichar c;
+
+ c = g_utf8_get_char (cursor);
+ cursor = g_utf8_next_char (cursor);
+
+ if (gimp_tag_is_tag_separator (c))
+ prefix_start = cursor;
+ }
+ *cursor = '\0';
+
+ prefix = g_strdup (g_strchug (prefix_start));
+ g_free (original_string);
+
+ return prefix;
+}
+
+static GList *
+gimp_tag_entry_get_completion_candidates (GimpTagEntry *tag_entry,
+ gchar **used_tags,
+ gchar *src_prefix)
+{
+ GList *candidates = NULL;
+ GList *all_tags;
+ GList *list;
+ gint length;
+ gchar *prefix;
+
+ if (! src_prefix || strlen (src_prefix) < 1)
+ return NULL;
+
+ prefix = g_utf8_normalize (src_prefix, -1, G_NORMALIZE_ALL);
+ if (! prefix)
+ return NULL;
+
+ all_tags = g_hash_table_get_keys (tag_entry->container->tag_ref_counts);
+ length = g_strv_length (used_tags);
+
+ for (list = all_tags; list; list = g_list_next (list))
+ {
+ GimpTag *tag = list->data;
+
+ if (gimp_tag_has_prefix (tag, prefix))
+ {
+ gint i;
+
+ /* check if tag is not already entered */
+ for (i = 0; i < length; i++)
+ {
+ if (! gimp_tag_compare_with_string (tag, used_tags[i]))
+ break;
+ }
+
+ if (i == length)
+ candidates = g_list_append (candidates, list->data);
+ }
+ }
+
+ g_list_free (all_tags);
+
+ g_free (prefix);
+
+ return candidates;
+}
+
+static gchar *
+gimp_tag_entry_get_completion_string (GimpTagEntry *tag_entry,
+ GList *candidates,
+ gchar *prefix)
+{
+ const gchar **completions;
+ guint length;
+ guint i;
+ GList *candidate_iterator;
+ const gchar *candidate_string;
+ gint prefix_length;
+ gunichar c;
+ gint num_chars_match;
+ gchar *completion;
+ gchar *completion_end;
+ gint completion_length;
+ gchar *normalized_prefix;
+
+ if (! candidates)
+ return NULL;
+
+ normalized_prefix = g_utf8_normalize (prefix, -1, G_NORMALIZE_ALL);
+ if (! normalized_prefix)
+ return NULL;
+
+ prefix_length = strlen (normalized_prefix);
+ g_free (normalized_prefix);
+
+ length = g_list_length (candidates);
+ if (length < 2)
+ {
+ candidate_string = gimp_tag_get_name (GIMP_TAG (candidates->data));
+ return g_strdup (candidate_string + prefix_length);
+ }
+
+ completions = g_malloc (length * sizeof (gchar*));
+ candidate_iterator = candidates;
+ for (i = 0; i < length; i++)
+ {
+ candidate_string = gimp_tag_get_name (GIMP_TAG (candidate_iterator->data));
+ completions[i] = candidate_string + prefix_length;
+ candidate_iterator = g_list_next (candidate_iterator);
+ }
+
+ num_chars_match = 0;
+ do
+ {
+ c = g_utf8_get_char (completions[0]);
+ if (! c)
+ break;
+
+ for (i = 1; i < length; i++)
+ {
+ gunichar d = g_utf8_get_char (completions[i]);
+
+ if (c != d)
+ {
+ candidate_string = gimp_tag_get_name (GIMP_TAG (candidates->data));
+ candidate_string += prefix_length;
+ completion_end = g_utf8_offset_to_pointer (candidate_string,
+ num_chars_match);
+ completion_length = completion_end - candidate_string;
+ completion = g_malloc (completion_length + 1);
+ memcpy (completion, candidate_string, completion_length);
+ completion[completion_length] = '\0';
+
+ g_free (completions);
+
+ return completion;
+ }
+
+ completions[i] = g_utf8_next_char (completions[i]);
+ }
+
+ completions[0] = g_utf8_next_char (completions[0]);
+ num_chars_match++;
+ }
+ while (c);
+
+ g_free (completions);
+
+ candidate_string = gimp_tag_get_name (GIMP_TAG (candidates->data));
+
+ return g_strdup (candidate_string + prefix_length);
+}
+
+static gboolean
+gimp_tag_entry_focus_in (GtkWidget *widget,
+ GdkEventFocus *event)
+{
+ gimp_tag_entry_toggle_desc (GIMP_TAG_ENTRY (widget), FALSE);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_focus_out (GtkWidget *widget,
+ GdkEventFocus *event)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (widget);
+
+ gimp_tag_entry_commit_tags (tag_entry);
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ gimp_tag_entry_assign_tags (GIMP_TAG_ENTRY (widget));
+ }
+
+ gimp_tag_entry_add_to_recent (tag_entry,
+ gtk_entry_get_text (GTK_ENTRY (widget)),
+ TRUE);
+
+ gimp_tag_entry_toggle_desc (tag_entry, TRUE);
+
+ return FALSE;
+}
+
+static void
+gimp_tag_entry_container_changed (GimpContainer *container,
+ GimpObject *object,
+ GimpTagEntry *tag_entry)
+{
+ GList *list;
+
+ if (! gimp_container_have (GIMP_CONTAINER (tag_entry->container),
+ object))
+ {
+ GList *selected_items = NULL;
+
+ for (list = tag_entry->selected_items; list; list = g_list_next (list))
+ {
+ if (list->data != object)
+ selected_items = g_list_prepend (selected_items, list->data);
+ }
+
+ selected_items = g_list_reverse (selected_items);
+ gimp_tag_entry_set_selected_items (tag_entry, selected_items);
+ g_list_free (selected_items);
+ }
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ for (list = tag_entry->selected_items; list; list = g_list_next (list))
+ {
+ if (gimp_tagged_get_tags (GIMP_TAGGED (list->data)) &&
+ gimp_container_have (GIMP_CONTAINER (tag_entry->container),
+ GIMP_OBJECT (list->data)))
+ {
+ break;
+ }
+ }
+
+ if (! list)
+ {
+ tag_entry->internal_operation++;
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), 0, -1);
+ tag_entry->internal_operation--;
+ }
+ }
+}
+
+static void
+gimp_tag_entry_toggle_desc (GimpTagEntry *tag_entry,
+ gboolean show)
+{
+ GtkWidget *widget = GTK_WIDGET (tag_entry);
+
+ if (! (show ^ tag_entry->description_shown))
+ return;
+
+ if (show)
+ {
+ gchar *current_text;
+ size_t len;
+
+ current_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (tag_entry)));
+ current_text = g_strstrip (current_text);
+ len = strlen (current_text);
+ g_free (current_text);
+
+ if (len > 0)
+ {
+ return;
+ }
+
+ tag_entry->description_shown = TRUE;
+ gtk_widget_queue_draw (widget);
+ }
+ else
+ {
+ tag_entry->description_shown = FALSE;
+ gtk_widget_queue_draw (widget);
+ }
+}
+
+static gboolean
+gimp_tag_entry_expose (GtkWidget *widget,
+ GdkEventExpose *event)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (widget);
+ PangoLayout *layout;
+ PangoAttrList *attr_list;
+ PangoAttribute *attribute;
+ gint layout_width;
+ gint layout_height;
+ gint window_width;
+ gint window_height;
+ gint offset;
+ const char *display_text;
+
+ /* eeeeeek */
+ if (event->window != gtk_entry_get_text_window (GTK_ENTRY (widget)))
+ return FALSE;
+
+ if (! GIMP_TAG_ENTRY (widget)->description_shown)
+ return FALSE;
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_QUERY)
+ {
+ display_text = GIMP_TAG_ENTRY_QUERY_DESC;
+ }
+ else
+ {
+ display_text = GIMP_TAG_ENTRY_ASSIGN_DESC;
+ }
+
+ layout = gtk_widget_create_pango_layout (widget, display_text);
+
+ attr_list = pango_attr_list_new ();
+ attribute = pango_attr_style_new (PANGO_STYLE_ITALIC);
+ pango_attr_list_insert (attr_list, attribute);
+
+ pango_layout_set_attributes (layout, attr_list);
+ pango_attr_list_unref (attr_list);
+
+ window_width = gdk_window_get_width (event->window);
+ window_height = gdk_window_get_height (event->window);
+ pango_layout_get_size (layout,
+ &layout_width, &layout_height);
+ offset = (window_height - PANGO_PIXELS (layout_height)) / 2;
+
+ gtk_paint_layout (gtk_widget_get_style (widget),
+ event->window,
+ GTK_STATE_INSENSITIVE,
+ TRUE,
+ &event->area,
+ widget,
+ NULL,
+ (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) ?
+ window_width - PANGO_PIXELS (layout_width) - offset :
+ offset,
+ offset,
+ layout);
+
+ g_object_unref (layout);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_key_press (GtkWidget *widget,
+ GdkEventKey *event)
+{
+ GimpTagEntry *entry = GIMP_TAG_ENTRY (widget);
+ GdkModifierType extend_mask;
+ guchar c;
+
+ extend_mask =
+ gtk_widget_get_modifier_mask (widget,
+ GDK_MODIFIER_INTENT_EXTEND_SELECTION);
+
+ c = gdk_keyval_to_unicode (event->keyval);
+ if (gimp_tag_is_tag_separator (c))
+ {
+ g_idle_add ((GSourceFunc) gimp_tag_entry_commit_source_func, entry);
+ return FALSE;
+ }
+
+ switch (event->keyval)
+ {
+ case GDK_KEY_Tab:
+ case GDK_KEY_KP_Tab:
+ case GDK_KEY_ISO_Left_Tab:
+ /* allow to leave the widget with Ctrl+Tab */
+ if (! (event->state & GDK_CONTROL_MASK))
+ {
+ entry->tab_completion_index++;
+ entry->suppress_tag_query++;
+ g_idle_add ((GSourceFunc) gimp_tag_entry_auto_complete, entry);
+ }
+ else
+ {
+ gimp_tag_entry_commit_tags (entry);
+ g_signal_emit_by_name (widget, "move-focus",
+ (event->state & GDK_SHIFT_MASK) ?
+ GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD);
+ }
+ return TRUE;
+
+ case GDK_KEY_Return:
+ gimp_tag_entry_commit_tags (entry);
+ break;
+
+ case GDK_KEY_Left:
+ gimp_tag_entry_previous_tag (entry,
+ (event->state & extend_mask) ? TRUE : FALSE);
+ return TRUE;
+
+ case GDK_KEY_Right:
+ gimp_tag_entry_next_tag (entry,
+ (event->state & extend_mask) ? TRUE : FALSE);
+ return TRUE;
+
+ case GDK_KEY_BackSpace:
+ {
+ gint selection_start;
+ gint selection_end;
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &selection_start, &selection_end);
+ if (gimp_tag_entry_select_jellybean (entry,
+ selection_start, selection_end,
+ TAG_SEARCH_LEFT))
+ {
+ return TRUE;
+ }
+ else
+ {
+ gimp_tag_entry_select_for_deletion (entry, TAG_SEARCH_LEFT);
+ /* FIXME: need to remove idle handler in dispose */
+ g_idle_add ((GSourceFunc) gimp_tag_entry_strip_extra_whitespace,
+ entry);
+ }
+ }
+ break;
+
+ case GDK_KEY_Delete:
+ {
+ gint selection_start;
+ gint selection_end;
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &selection_start, &selection_end);
+ if (gimp_tag_entry_select_jellybean (entry,
+ selection_start, selection_end,
+ TAG_SEARCH_RIGHT))
+ {
+ return TRUE;
+ }
+ else
+ {
+ gimp_tag_entry_select_for_deletion (entry, TAG_SEARCH_RIGHT);
+ /* FIXME: need to remove idle handler in dispose */
+ g_idle_add ((GSourceFunc) gimp_tag_entry_strip_extra_whitespace,
+ entry);
+ }
+ }
+ break;
+
+ case GDK_KEY_Up:
+ case GDK_KEY_Down:
+ if (entry->recent_list != NULL)
+ {
+ gchar *recent_item;
+ gchar *very_recent_item;
+
+ very_recent_item = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
+ gimp_tag_entry_add_to_recent (entry, very_recent_item, TRUE);
+ g_free (very_recent_item);
+
+ if (event->keyval == GDK_KEY_Up)
+ {
+ recent_item = (gchar *) g_list_first (entry->recent_list)->data;
+ entry->recent_list = g_list_remove (entry->recent_list, recent_item);
+ entry->recent_list = g_list_append (entry->recent_list, recent_item);
+ }
+ else
+ {
+ recent_item = (gchar *) g_list_last (entry->recent_list)->data;
+ entry->recent_list = g_list_remove (entry->recent_list, recent_item);
+ entry->recent_list = g_list_prepend (entry->recent_list, recent_item);
+ }
+
+ recent_item = (gchar *) g_list_first (entry->recent_list)->data;
+ entry->internal_operation++;
+ gtk_entry_set_text (GTK_ENTRY (entry), recent_item);
+ gtk_editable_set_position (GTK_EDITABLE (entry), -1);
+ entry->internal_operation--;
+ }
+ return TRUE;
+
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_button_release (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ if (event->button == 1)
+ {
+ /* FIXME: need to remove idle handler in dispose */
+ g_idle_add ((GSourceFunc) gimp_tag_entry_try_select_jellybean,
+ widget);
+ }
+
+ return GTK_WIDGET_CLASS (parent_class)->button_release_event (widget, event);
+}
+
+static gboolean
+gimp_tag_entry_try_select_jellybean (GimpTagEntry *entry)
+{
+ gint selection_start;
+ gint selection_end;
+ gint selection_pos = gtk_editable_get_position (GTK_EDITABLE (entry));
+ gint char_count = g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (entry)), -1);
+
+ if (selection_pos == char_count)
+ {
+ return FALSE;
+ }
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &selection_start, &selection_end);
+ gimp_tag_entry_select_jellybean (entry, selection_start, selection_end,
+ TAG_SEARCH_NONE);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_select_jellybean (GimpTagEntry *entry,
+ gint selection_start,
+ gint selection_end,
+ GimpTagSearchDir search_dir)
+{
+ gint prev_selection_start;
+ gint prev_selection_end;
+
+ if (! entry->mask->len)
+ {
+ return FALSE;
+ }
+
+ if (selection_start >= entry->mask->len)
+ {
+ selection_start = entry->mask->len - 1;
+ selection_end = selection_start;
+ }
+
+ if (entry->mask->str[selection_start] == 'u')
+ {
+ return FALSE;
+ }
+
+ switch (search_dir)
+ {
+ case TAG_SEARCH_NONE:
+ if (selection_start > 0 &&
+ entry->mask->str[selection_start] == 's')
+ {
+ selection_start--;
+ }
+
+ if (selection_start > 0 &&
+ (entry->mask->str[selection_start - 1] == 'w') &&
+ (entry->mask->str[selection_start] == 't'))
+ {
+ /* between whitespace and tag,
+ * should allow to select tag.
+ */
+ selection_start--;
+ }
+ break;
+
+ case TAG_SEARCH_LEFT:
+ if (selection_start == selection_end)
+ {
+ if (selection_start > 0 &&
+ entry->mask->str[selection_start] == 't' &&
+ entry->mask->str[selection_start - 1] == 'w')
+ {
+ selection_start--;
+ }
+
+ if ((entry->mask->str[selection_start] == 'w' ||
+ entry->mask->str[selection_start] == 's') &&
+ selection_start > 0)
+ {
+ while ((entry->mask->str[selection_start] == 'w' ||
+ entry->mask->str[selection_start] == 's') &&
+ selection_start > 0)
+ {
+ selection_start--;
+ }
+
+ selection_end = selection_start + 1;
+ }
+ }
+ break;
+
+ case TAG_SEARCH_RIGHT:
+ if (selection_start == selection_end)
+ {
+ if ((entry->mask->str[selection_start] == 'w' ||
+ entry->mask->str[selection_start] == 's') &&
+ selection_start < entry->mask->len - 1)
+ {
+ while ((entry->mask->str[selection_start] == 'w' ||
+ entry->mask->str[selection_start] == 's') &&
+ selection_start < entry->mask->len - 1)
+ {
+ selection_start++;
+ }
+
+ selection_end = selection_start + 1;
+ }
+ }
+ break;
+ }
+
+ if (selection_start < entry->mask->len &&
+ selection_start == selection_end)
+ {
+ selection_end = selection_start + 1;
+ }
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &prev_selection_start,
+ &prev_selection_end);
+
+ if (entry->mask->str[selection_start] == 't')
+ {
+ while (selection_start > 0 &&
+ (entry->mask->str[selection_start - 1] == 't'))
+ {
+ selection_start--;
+ }
+ }
+
+ if (selection_end > selection_start &&
+ (entry->mask->str[selection_end - 1] == 't'))
+ {
+ while (selection_end <= entry->mask->len &&
+ (entry->mask->str[selection_end] == 't'))
+ {
+ selection_end++;
+ }
+ }
+
+ if (search_dir == TAG_SEARCH_NONE &&
+ selection_end - selection_start == 1 &&
+ entry->mask->str[selection_start] == 'w')
+ {
+ gtk_editable_set_position (GTK_EDITABLE (entry), selection_end);
+ return TRUE;
+ }
+
+ if ((selection_start != prev_selection_start ||
+ selection_end != prev_selection_end) &&
+ (entry->mask->str[selection_start] == 't') &&
+ selection_start < selection_end)
+ {
+ if (search_dir == TAG_SEARCH_LEFT)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_end, selection_start);
+ }
+ else
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_start, selection_end);
+ }
+
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+static gboolean
+gimp_tag_entry_add_to_recent (GimpTagEntry *entry,
+ const gchar *tags_string,
+ gboolean to_front)
+{
+ gchar *recent_item = NULL;
+ gchar *stripped_string;
+ gint stripped_length;
+ GList *list;
+
+ if (entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ return FALSE;
+
+ stripped_string = g_strdup (tags_string);
+ stripped_string = g_strstrip (stripped_string);
+ stripped_length = strlen (stripped_string);
+ g_free (stripped_string);
+
+ if (stripped_length <= 0)
+ {
+ /* there is no content in the string,
+ * therefore don't add to recent list.
+ */
+ return FALSE;
+ }
+
+ if (g_list_length (entry->recent_list) >= GIMP_TAG_ENTRY_MAX_RECENT_ITEMS)
+ {
+ gchar *last_item = g_list_last (entry->recent_list)->data;
+ entry->recent_list = g_list_remove (entry->recent_list, last_item);
+ g_free (last_item);
+ }
+
+ for (list = entry->recent_list; list; list = g_list_next (list))
+ {
+ if (! strcmp (tags_string, list->data))
+ {
+ recent_item = list->data;
+ entry->recent_list = g_list_remove (entry->recent_list, recent_item);
+ break;
+ }
+ }
+
+ if (! recent_item)
+ {
+ recent_item = g_strdup (tags_string);
+ }
+
+ if (to_front)
+ {
+ entry->recent_list = g_list_prepend (entry->recent_list, recent_item);
+ }
+ else
+ {
+ entry->recent_list = g_list_append (entry->recent_list, recent_item);
+ }
+
+ return TRUE;
+}
+
+/**
+ * gimp_tag_entry_get_separator:
+ *
+ * Tag separator is a single Unicode terminal punctuation
+ * character.
+ *
+ * Return value: returns locale dependent tag separator.
+ **/
+const gchar *
+gimp_tag_entry_get_separator (void)
+{
+ /* Separator for tags
+ * IMPORTANT: use only one of Unicode terminal punctuation chars.
+ * http://unicode.org/review/pr-23.html
+ */
+ return _(",");
+}
+
+static void
+gimp_tag_entry_commit_region (GString *tags,
+ GString *mask)
+{
+ gint i = 0;
+ gint j;
+ gint stage = 0;
+ gunichar c;
+ gchar *cursor;
+ GString *out_tags;
+ GString *out_mask;
+ GString *tag_buffer;
+
+ out_tags = g_string_new ("");
+ out_mask = g_string_new ("");
+ tag_buffer = g_string_new ("");
+
+ cursor = tags->str;
+ for (i = 0; i <= mask->len; i++)
+ {
+ c = g_utf8_get_char (cursor);
+ cursor = g_utf8_next_char (cursor);
+
+ if (stage == 0)
+ {
+ /* whitespace before tag */
+ if (g_unichar_isspace (c))
+ {
+ g_string_append_unichar (out_tags, c);
+ g_string_append_c (out_mask, 'w');
+ }
+ else
+ {
+ stage++;
+ }
+ }
+
+ if (stage == 1)
+ {
+ /* tag */
+ if (c && ! gimp_tag_is_tag_separator (c))
+ {
+ g_string_append_unichar (tag_buffer, c);
+ }
+ else
+ {
+ gchar *valid_tag = gimp_tag_string_make_valid (tag_buffer->str);
+ gsize tag_length;
+
+ if (valid_tag)
+ {
+ tag_length = g_utf8_strlen (valid_tag, -1);
+ g_string_append (out_tags, valid_tag);
+ for (j = 0; j < tag_length; j++)
+ {
+ g_string_append_c (out_mask, 't');
+ }
+ g_free (valid_tag);
+
+ if (! c)
+ {
+ g_string_append (out_tags, gimp_tag_entry_get_separator ());
+ g_string_append_c (out_mask, 's');
+ }
+
+ stage++;
+ }
+ else
+ {
+ stage = 0;
+ }
+
+ g_string_set_size (tag_buffer, 0);
+
+ }
+ }
+
+ if (stage == 2)
+ {
+ if (gimp_tag_is_tag_separator (c))
+ {
+ g_string_append_unichar (out_tags, c);
+ g_string_append_c (out_mask, 's');
+ }
+ else
+ {
+ if (g_unichar_isspace (c))
+ {
+ g_string_append_unichar (out_tags, c);
+ g_string_append_c (out_mask, 'w');
+ }
+
+ stage = 0;
+ }
+ }
+ }
+
+ g_string_assign (tags, out_tags->str);
+ g_string_assign (mask, out_mask->str);
+
+ g_string_free (tag_buffer, TRUE);
+ g_string_free (out_tags, TRUE);
+ g_string_free (out_mask, TRUE);
+}
+
+static void
+gimp_tag_entry_commit_tags (GimpTagEntry *entry)
+{
+ gboolean found_region;
+ gint cursor_position;
+
+ cursor_position = gtk_editable_get_position (GTK_EDITABLE (entry));
+
+ do
+ {
+ gint region_start;
+ gint region_end;
+ gint position;
+ glong length_before;
+ gint i;
+
+ found_region = FALSE;
+
+ for (i = 0; i < entry->mask->len; i++)
+ {
+ if (entry->mask->str[i] == 'u')
+ {
+ found_region = TRUE;
+ region_start = i;
+ region_end = i + 1;
+ for (i++; i < entry->mask->len; i++)
+ {
+ if (entry->mask->str[i] == 'u')
+ {
+ region_end = i + 1;
+ }
+ else
+ {
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ if (found_region)
+ {
+ gchar *tags_string;
+ GString *tags;
+ GString *mask;
+
+ tags_string = gtk_editable_get_chars (GTK_EDITABLE (entry),
+ region_start, region_end);
+ tags = g_string_new (tags_string);
+ g_free (tags_string);
+ length_before = region_end - region_start;
+
+ mask = g_string_new_len (entry->mask->str + region_start, region_end - region_start);
+
+ gimp_tag_entry_commit_region (tags, mask);
+
+ /* prepend space before if needed */
+ if (region_start > 0
+ && entry->mask->str[region_start - 1] != 'w'
+ && mask->len > 0
+ && mask->str[0] != 'w')
+ {
+ g_string_prepend_c (tags, ' ');
+ g_string_prepend_c (mask, 'w');
+ }
+
+ /* append space after if needed */
+ if (region_end <= entry->mask->len
+ && entry->mask->str[region_end] != 'w'
+ && mask->len > 0
+ && mask->str[mask->len - 1] != 'w')
+ {
+ g_string_append_c (tags, ' ');
+ g_string_append_c (mask, 'w');
+ }
+
+ if (cursor_position >= region_start)
+ {
+ cursor_position += g_utf8_strlen (tags->str, tags->len) - length_before;
+ }
+
+ entry->internal_operation++;
+ entry->suppress_mask_update++;
+ entry->suppress_tag_query++;
+ gtk_editable_delete_text (GTK_EDITABLE (entry),
+ region_start, region_end);
+ position = region_start;
+ gtk_editable_insert_text (GTK_EDITABLE (entry),
+ tags->str, tags->len, &position);
+ entry->suppress_tag_query--;
+ entry->suppress_mask_update--;
+ entry->internal_operation--;
+
+ g_string_erase (entry->mask, region_start, region_end - region_start);
+ g_string_insert_len (entry->mask, region_start, mask->str, mask->len);
+
+ g_string_free (mask, TRUE);
+ g_string_free (tags, TRUE);
+ }
+ }
+ while (found_region);
+
+ gtk_editable_set_position (GTK_EDITABLE (entry), cursor_position);
+ gimp_tag_entry_strip_extra_whitespace (entry);
+}
+
+static gboolean
+gimp_tag_entry_commit_source_func (GimpTagEntry *entry)
+{
+ gimp_tag_entry_commit_tags (entry);
+
+ return FALSE;
+}
+
+static void
+gimp_tag_entry_next_tag (GimpTagEntry *entry,
+ gboolean select)
+{
+ gint position = gtk_editable_get_position (GTK_EDITABLE (entry));
+
+ if (entry->mask->str[position] != 'u')
+ {
+ while (position < entry->mask->len &&
+ (entry->mask->str[position] != 'w'))
+ {
+ position++;
+ }
+
+ if (entry->mask->str[position] == 'w')
+ {
+ position++;
+ }
+ }
+ else if (position < entry->mask->len)
+ {
+ position++;
+ }
+
+ if (select)
+ {
+ gint current_position;
+ gint selection_start;
+ gint selection_end;
+
+ current_position = gtk_editable_get_position (GTK_EDITABLE (entry));
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &selection_start, &selection_end);
+
+ if (current_position == selection_end)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_start, position);
+ }
+ else if (current_position == selection_start)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_end, position);
+ }
+ }
+ else
+ {
+ gtk_editable_set_position (GTK_EDITABLE (entry), position);
+ }
+}
+
+static void
+gimp_tag_entry_previous_tag (GimpTagEntry *entry,
+ gboolean select)
+{
+ gint position = gtk_editable_get_position (GTK_EDITABLE (entry));
+
+ if (position >= 1 &&
+ entry->mask->str[position - 1] == 'w')
+ {
+ position--;
+ }
+ if (position < 1)
+ {
+ return;
+ }
+ if (entry->mask->str[position - 1] != 'u')
+ {
+ while (position > 0 &&
+ (entry->mask->str[position - 1] != 'w'))
+ {
+ if (entry->mask->str[position - 1] == 'u')
+ {
+ break;
+ }
+
+ position--;
+ }
+ }
+ else
+ {
+ position--;
+ }
+
+ if (select)
+ {
+ gint current_position;
+ gint selection_start;
+ gint selection_end;
+
+ current_position = gtk_editable_get_position (GTK_EDITABLE (entry));
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &selection_start, &selection_end);
+
+ if (current_position == selection_start)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_end, position);
+ }
+ else if (current_position == selection_end)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry),
+ selection_start, position);
+ }
+ }
+ else
+ {
+ gtk_editable_set_position (GTK_EDITABLE (entry), position);
+ }
+}
+
+static void
+gimp_tag_entry_select_for_deletion (GimpTagEntry *entry,
+ GimpTagSearchDir search_dir)
+{
+ gint start_pos;
+ gint end_pos;
+
+ /* make sure the whole tag is selected,
+ * including a separator
+ */
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (entry),
+ &start_pos, &end_pos);
+ while (start_pos > 0 &&
+ (entry->mask->str[start_pos - 1] == 't'))
+ {
+ start_pos--;
+ }
+
+ if (end_pos > start_pos &&
+ (entry->mask->str[end_pos - 1] == 't' ||
+ entry->mask->str[end_pos - 1] == 's'))
+ {
+ while (end_pos <= entry->mask->len &&
+ (entry->mask->str[end_pos] == 's'))
+ {
+ end_pos++;
+ }
+ }
+
+ /* ensure there is no unnecessary whitespace selected */
+ while (start_pos < end_pos &&
+ entry->mask->str[start_pos] == 'w')
+ {
+ start_pos++;
+ }
+
+ while (start_pos < end_pos &&
+ entry->mask->str[end_pos - 1] == 'w')
+ {
+ end_pos--;
+ }
+
+ /* delete spaces in one side */
+ if (search_dir == TAG_SEARCH_LEFT)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry), end_pos, start_pos);
+ }
+ else if (end_pos > start_pos &&
+ search_dir == TAG_SEARCH_RIGHT &&
+ (entry->mask->str[end_pos - 1] == 't' ||
+ entry->mask->str[end_pos - 1] == 's'))
+ {
+ gtk_editable_select_region (GTK_EDITABLE (entry), start_pos, end_pos);
+ }
+}
+
+static gboolean
+gimp_tag_entry_strip_extra_whitespace (GimpTagEntry *entry)
+{
+ gint i;
+ gint position;
+
+ position = gtk_editable_get_position (GTK_EDITABLE (entry));
+
+ entry->internal_operation++;
+ entry->suppress_tag_query++;
+
+ /* strip whitespace in front */
+ while (entry->mask->len > 0 &&
+ entry->mask->str[0] == 'w')
+ {
+ gtk_editable_delete_text (GTK_EDITABLE (entry), 0, 1);
+ }
+
+ /* strip whitespace in back */
+ while (entry->mask->len > 1 &&
+ entry->mask->str[entry->mask->len - 1] == 'w' &&
+ entry->mask->str[entry->mask->len - 2] == 'w')
+ {
+ gtk_editable_delete_text (GTK_EDITABLE (entry),
+ entry->mask->len - 1, entry->mask->len);
+
+ if (position == entry->mask->len)
+ {
+ position--;
+ }
+ }
+
+ /* strip extra whitespace in the middle */
+ for (i = entry->mask->len - 1; i > 0; i--)
+ {
+ if (entry->mask->str[i] == 'w' &&
+ entry->mask->str[i - 1] == 'w')
+ {
+ gtk_editable_delete_text (GTK_EDITABLE (entry), i, i + 1);
+
+ if (position >= i)
+ {
+ position--;
+ }
+ }
+ }
+
+ /* special case when cursor is in the last position:
+ * it must be positioned after the last whitespace.
+ */
+ if (position == entry->mask->len - 1 &&
+ entry->mask->str[position] == 'w')
+ {
+ position++;
+ }
+
+ gtk_editable_set_position (GTK_EDITABLE (entry), position);
+
+ entry->suppress_tag_query--;
+ entry->internal_operation--;
+
+ return FALSE;
+}