/* GIMP - The GNU Image Manipulation Program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * gimptagentry.c * Copyright (C) 2008 Aurimas Juška * * 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 . */ #include "config.h" #include #include #include #include #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; }