diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:30:19 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:30:19 +0000 |
commit | 5c1676dfe6d2f3c837a5e074117b45613fd29a72 (patch) | |
tree | cbffb45144febf451e54061db2b21395faf94bfe /app/widgets/gimptagentry.c | |
parent | Initial commit. (diff) | |
download | gimp-5c1676dfe6d2f3c837a5e074117b45613fd29a72.tar.xz gimp-5c1676dfe6d2f3c837a5e074117b45613fd29a72.zip |
Adding upstream version 2.10.34.upstream/2.10.34upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'app/widgets/gimptagentry.c')
-rw-r--r-- | app/widgets/gimptagentry.c | 2207 |
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; +} |