PIKApp/app/widgets/pikatagentry.c

2207 lines
65 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* pikatagentry.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 "libpikawidgets/pikawidgets.h"
#include "widgets-types.h"
#include "core/pika-utils.h"
#include "core/pikacontainer.h"
#include "core/pikacontext.h"
#include "core/pikatag.h"
#include "core/pikatagged.h"
#include "core/pikataggedcontainer.h"
#include "core/pikaviewable.h"
#include "pikatagentry.h"
#include "pika-intl.h"
#define PIKA_TAG_ENTRY_QUERY_DESC _("filter")
#define PIKA_TAG_ENTRY_ASSIGN_DESC _("enter tags")
#define PIKA_TAG_ENTRY_MAX_RECENT_ITEMS 20
typedef enum
{
TAG_SEARCH_NONE,
TAG_SEARCH_LEFT,
TAG_SEARCH_RIGHT,
} PikaTagSearchDir;
enum
{
PROP_0,
PROP_CONTAINER,
PROP_MODE
};
static void pika_tag_entry_dispose (GObject *object);
static void pika_tag_entry_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static void pika_tag_entry_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void pika_tag_entry_activate (GtkEntry *entry);
static void pika_tag_entry_changed (GtkEntry *entry);
static void pika_tag_entry_insert_text (GtkEditable *editable,
gchar *new_text,
gint text_length,
gint *position);
static void pika_tag_entry_delete_text (GtkEditable *editable,
gint start_pos,
gint end_pos);
static gboolean pika_tag_entry_focus_in (GtkWidget *widget,
GdkEventFocus *event);
static gboolean pika_tag_entry_focus_out (GtkWidget *widget,
GdkEventFocus *event);
static void pika_tag_entry_container_changed (PikaContainer *container,
PikaObject *object,
PikaTagEntry *entry);
static gboolean pika_tag_entry_button_release (GtkWidget *widget,
GdkEventButton *event);
static gboolean pika_tag_entry_key_press (GtkWidget *widget,
GdkEventKey *event);
static gboolean pika_tag_entry_query_tag (PikaTagEntry *entry);
static void pika_tag_entry_assign_tags (PikaTagEntry *entry);
static void pika_tag_entry_load_selection (PikaTagEntry *entry,
gboolean sort);
static void pika_tag_entry_find_common_tags (gpointer key,
gpointer value,
gpointer user_data);
static gchar * pika_tag_entry_get_completion_prefix (PikaTagEntry *entry);
static GList * pika_tag_entry_get_completion_candidates (PikaTagEntry *entry,
gchar **used_tags,
gchar *prefix);
static gchar * pika_tag_entry_get_completion_string (PikaTagEntry *entry,
GList *candidates,
gchar *prefix);
static gboolean pika_tag_entry_auto_complete (PikaTagEntry *entry);
static void pika_tag_entry_toggle_desc (PikaTagEntry *widget,
gboolean show);
static gboolean pika_tag_entry_draw (GtkWidget *widget,
cairo_t *cr);
static void pika_tag_entry_commit_region (GString *tags,
GString *mask);
static void pika_tag_entry_commit_tags (PikaTagEntry *entry);
static gboolean pika_tag_entry_commit_source_func (PikaTagEntry *entry);
static gboolean pika_tag_entry_select_jellybean (PikaTagEntry *entry,
gint selection_start,
gint selection_end,
PikaTagSearchDir search_dir);
static gboolean pika_tag_entry_try_select_jellybean (PikaTagEntry *entry);
static gboolean pika_tag_entry_add_to_recent (PikaTagEntry *entry,
const gchar *tags_string,
gboolean to_front);
static void pika_tag_entry_next_tag (PikaTagEntry *entry,
gboolean select);
static void pika_tag_entry_previous_tag (PikaTagEntry *entry,
gboolean select);
static void pika_tag_entry_select_for_deletion (PikaTagEntry *entry,
PikaTagSearchDir search_dir);
static gboolean pika_tag_entry_strip_extra_whitespace (PikaTagEntry *entry);
G_DEFINE_TYPE (PikaTagEntry, pika_tag_entry, GTK_TYPE_ENTRY)
#define parent_class pika_tag_entry_parent_class
static void
pika_tag_entry_class_init (PikaTagEntryClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->dispose = pika_tag_entry_dispose;
object_class->get_property = pika_tag_entry_get_property;
object_class->set_property = pika_tag_entry_set_property;
widget_class->button_release_event = pika_tag_entry_button_release;
g_object_class_install_property (object_class,
PROP_CONTAINER,
g_param_spec_object ("container",
"Tagged container",
"The Tagged container",
PIKA_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.",
PIKA_TYPE_TAG_ENTRY_MODE,
PIKA_TAG_ENTRY_MODE_QUERY,
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_READWRITE));
}
static void
pika_tag_entry_init (PikaTagEntry *entry)
{
entry->container = NULL;
entry->selected_items = NULL;
entry->common_tags = NULL;
entry->tab_completion_index = -1;
entry->mode = PIKA_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 (pika_tag_entry_activate),
NULL);
g_signal_connect (entry, "changed",
G_CALLBACK (pika_tag_entry_changed),
NULL);
g_signal_connect (entry, "insert-text",
G_CALLBACK (pika_tag_entry_insert_text),
NULL);
g_signal_connect (entry, "delete-text",
G_CALLBACK (pika_tag_entry_delete_text),
NULL);
g_signal_connect (entry, "key-press-event",
G_CALLBACK (pika_tag_entry_key_press),
NULL);
g_signal_connect (entry, "focus-in-event",
G_CALLBACK (pika_tag_entry_focus_in),
NULL);
g_signal_connect (entry, "focus-out-event",
G_CALLBACK (pika_tag_entry_focus_out),
NULL);
g_signal_connect_after (entry, "draw",
G_CALLBACK (pika_tag_entry_draw),
NULL);
}
static void
pika_tag_entry_dispose (GObject *object)
{
PikaTagEntry *entry = PIKA_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,
pika_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
pika_tag_entry_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
PikaTagEntry *entry = PIKA_TAG_ENTRY (object);
switch (property_id)
{
case PROP_CONTAINER:
entry->container = g_value_dup_object (value);
g_signal_connect (entry->container, "add",
G_CALLBACK (pika_tag_entry_container_changed),
entry);
g_signal_connect (entry->container, "remove",
G_CALLBACK (pika_tag_entry_container_changed),
entry);
break;
case PROP_MODE:
entry->mode = g_value_get_enum (value);
pika_tag_entry_toggle_desc (entry, TRUE);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
pika_tag_entry_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
PikaTagEntry *entry = PIKA_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;
}
}
/**
* pika_tag_entry_new:
* @container: a #PikaTaggedContainer object
* @mode: #PikaTagEntryMode to work in.
*
* #PikaTagEntry 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.
*
* Returns: a new PikaTagEntry widget.
**/
GtkWidget *
pika_tag_entry_new (PikaTaggedContainer *container,
PikaTagEntryMode mode)
{
g_return_val_if_fail (PIKA_IS_TAGGED_CONTAINER (container), NULL);
return g_object_new (PIKA_TYPE_TAG_ENTRY,
"container", container,
"mode", mode,
NULL);
}
static void
pika_tag_entry_activate (GtkEntry *entry)
{
PikaTagEntry *tag_entry = PIKA_TAG_ENTRY (entry);
gint selection_start;
gint selection_end;
GList *list;
pika_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 (pika_container_have (PIKA_CONTAINER (tag_entry->container),
PIKA_OBJECT (list->data)))
{
break;
}
}
if (tag_entry->mode == PIKA_TAG_ENTRY_MODE_ASSIGN && list)
{
pika_tag_entry_assign_tags (PIKA_TAG_ENTRY (entry));
}
}
/**
* pika_tag_entry_set_tag_string:
* @entry: a #PikaTagEntry 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 #PikaTagEntryMode, appropriate action is performed.
**/
void
pika_tag_entry_set_tag_string (PikaTagEntry *entry,
const gchar *tag_string)
{
g_return_if_fail (PIKA_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--;
pika_tag_entry_commit_tags (entry);
if (entry->mode == PIKA_TAG_ENTRY_MODE_ASSIGN)
{
pika_tag_entry_assign_tags (entry);
}
else if (entry->mode == PIKA_TAG_ENTRY_MODE_QUERY)
{
pika_tag_entry_query_tag (entry);
}
}
static void
pika_tag_entry_changed (GtkEntry *entry)
{
PikaTagEntry *tag_entry = PIKA_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)
{
pika_tag_entry_toggle_desc (tag_entry, TRUE);
}
else
{
pika_tag_entry_toggle_desc (tag_entry, FALSE);
}
g_free (text);
if (tag_entry->mode == PIKA_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) pika_tag_entry_query_tag, entry);
}
}
static void
pika_tag_entry_insert_text (GtkEditable *editable,
gchar *new_text,
gint text_length,
gint *position)
{
PikaTagEntry *entry = PIKA_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,
pika_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,
pika_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,
pika_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,
pika_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) pika_tag_entry_auto_complete, editable);
}
}
static void
pika_tag_entry_delete_text (GtkEditable *editable,
gint start_pos,
gint end_pos)
{
PikaTagEntry *entry = PIKA_TAG_ENTRY (editable);
if (! entry->internal_operation)
{
g_signal_handlers_block_by_func (editable,
pika_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,
pika_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
pika_tag_entry_query_tag (PikaTagEntry *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 = pika_tag_entry_parse_tags (entry);
count = g_strv_length (parsed_tags);
for (i = 0; i < count; i++)
{
if (strlen (parsed_tags[i]) > 0)
{
PikaTag *tag = pika_tag_try_new (parsed_tags[i]);
if (! tag)
has_invalid_tags = TRUE;
query_list = g_list_append (query_list, tag);
}
}
g_strfreev (parsed_tags);
pika_tagged_container_set_filter (PIKA_TAGGED_CONTAINER (entry->container),
query_list);
g_list_free_full (query_list, (GDestroyNotify) pika_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
pika_tag_entry_auto_complete (PikaTagEntry *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 == PIKA_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) pika_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 =
pika_tag_entry_get_completion_prefix (PIKA_TAG_ENTRY (entry));
tags = pika_tag_entry_parse_tags (PIKA_TAG_ENTRY (entry));
completion_candidates =
pika_tag_entry_get_completion_candidates (PIKA_TAG_ENTRY (entry),
tags,
completion_prefix);
completion_candidates = g_list_sort (completion_candidates,
pika_tag_compare_func);
if (tag_entry->tab_completion_index >= 0 && completion_candidates)
{
PikaTag *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 = pika_tag_entry_get_completion_string (PIKA_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
pika_tag_entry_assign_tags (PikaTagEntry *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 = pika_tag_entry_parse_tags (tag_entry);
count = g_strv_length (parsed_tags);
for (i = 0; i < count; i++)
{
PikaTag *tag = pika_tag_new (parsed_tags[i]);
if (tag)
{
if (g_list_find_custom (tag_entry->common_tags, tag,
pika_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,
pika_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))
{
PikaTagged *tagged = PIKA_TAGGED (resource_iter->data);
for (tag_iter = remove_list; tag_iter; tag_iter = g_list_next (tag_iter))
{
pika_tagged_remove_tag (tagged, tag_iter->data);
}
for (tag_iter = add_list; tag_iter; tag_iter = g_list_next (tag_iter))
{
pika_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;
}
/**
* pika_tag_entry_parse_tags:
* @entry: a #PikaTagEntry 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.
*
* Returns: a newly allocated NULL terminated list of strings. It
* should be freed using g_strfreev().
**/
gchar **
pika_tag_entry_parse_tags (PikaTagEntry *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 (PIKA_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 || pika_tag_is_tag_separator (c))
{
if (parsed_tag->len > 0)
{
gchar *validated_tag = pika_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;
}
/**
* pika_tag_entry_set_selected_items:
* @tag_entry: a #PikaTagEntry widget.
* @items: a list of #PikaTagged objects.
*
* Set list of currently selected #PikaTagged objects. Only selected and
* visible (not filtered out) #PikaTagged objects are assigned tags when
* operating in tag assignment mode.
**/
void
pika_tag_entry_set_selected_items (PikaTagEntry *tag_entry,
GList *items)
{
g_return_if_fail (PIKA_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 == PIKA_TAG_ENTRY_MODE_ASSIGN)
{
pika_tag_entry_load_selection (tag_entry, TRUE);
}
}
static void
pika_tag_entry_load_selection (PikaTagEntry *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)
{
pika_tag_entry_toggle_desc (tag_entry, FALSE);
return;
}
refcounts = g_hash_table_new ((GHashFunc) pika_tag_get_hash,
(GEqualFunc) pika_tag_equals);
/* find set of tags common to all resources. */
for (resource = tag_entry->selected_items;
resource;
resource = g_list_next (resource))
{
for (tag = pika_tagged_get_tags (PIKA_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, pika_tag_entry_find_common_tags, tag_entry);
g_hash_table_destroy (refcounts);
tag_entry->common_tags = g_list_sort (tag_entry->common_tags,
pika_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))
{
PikaTag *tag = list->data;
gchar *text;
text = g_strdup_printf ("%s%s ",
pika_tag_get_name (tag),
pika_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);
}
pika_tag_entry_commit_tags (tag_entry);
}
static void
pika_tag_entry_find_common_tags (gpointer key,
gpointer value,
gpointer user_data)
{
guint ref_count = GPOINTER_TO_UINT (value);
PikaTagEntry *tag_entry = PIKA_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 *
pika_tag_entry_get_completion_prefix (PikaTagEntry *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 (pika_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 *
pika_tag_entry_get_completion_candidates (PikaTagEntry *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))
{
PikaTag *tag = list->data;
if (pika_tag_has_prefix (tag, prefix))
{
gint i;
/* check if tag is not already entered */
for (i = 0; i < length; i++)
{
if (! pika_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 *
pika_tag_entry_get_completion_string (PikaTagEntry *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 = pika_tag_get_name (PIKA_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 = pika_tag_get_name (PIKA_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 = pika_tag_get_name (PIKA_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 = pika_tag_get_name (PIKA_TAG (candidates->data));
return g_strdup (candidate_string + prefix_length);
}
static gboolean
pika_tag_entry_focus_in (GtkWidget *widget,
GdkEventFocus *event)
{
pika_tag_entry_toggle_desc (PIKA_TAG_ENTRY (widget), FALSE);
return FALSE;
}
static gboolean
pika_tag_entry_focus_out (GtkWidget *widget,
GdkEventFocus *event)
{
PikaTagEntry *tag_entry = PIKA_TAG_ENTRY (widget);
pika_tag_entry_commit_tags (tag_entry);
if (tag_entry->mode == PIKA_TAG_ENTRY_MODE_ASSIGN)
{
pika_tag_entry_assign_tags (PIKA_TAG_ENTRY (widget));
}
pika_tag_entry_add_to_recent (tag_entry,
gtk_entry_get_text (GTK_ENTRY (widget)),
TRUE);
pika_tag_entry_toggle_desc (tag_entry, TRUE);
return FALSE;
}
static void
pika_tag_entry_container_changed (PikaContainer *container,
PikaObject *object,
PikaTagEntry *tag_entry)
{
GList *list;
if (! pika_container_have (PIKA_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);
pika_tag_entry_set_selected_items (tag_entry, selected_items);
g_list_free (selected_items);
}
if (tag_entry->mode == PIKA_TAG_ENTRY_MODE_ASSIGN)
{
for (list = tag_entry->selected_items; list; list = g_list_next (list))
{
if (pika_tagged_get_tags (PIKA_TAGGED (list->data)) &&
pika_container_have (PIKA_CONTAINER (tag_entry->container),
PIKA_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
pika_tag_entry_toggle_desc (PikaTagEntry *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
pika_tag_entry_draw (GtkWidget *widget,
cairo_t *cr)
{
PikaTagEntry *tag_entry = PIKA_TAG_ENTRY (widget);
GtkStyleContext *style = gtk_widget_get_style_context (widget);
GdkRectangle text_area;
PangoLayout *layout;
PangoAttrList *attr_list;
PangoAttribute *attribute;
gint layout_width;
gint layout_height;
gint offset;
const char *display_text;
if (! PIKA_TAG_ENTRY (widget)->description_shown)
return FALSE;
gtk_entry_get_text_area (GTK_ENTRY (widget), &text_area);
if (tag_entry->mode == PIKA_TAG_ENTRY_MODE_QUERY)
{
display_text = PIKA_TAG_ENTRY_QUERY_DESC;
}
else
{
display_text = PIKA_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);
pango_layout_get_pixel_size (layout, &layout_width, &layout_height);
offset = (text_area.height - layout_height) / 2;
gtk_style_context_save (style);
gtk_style_context_set_state (style, GTK_STATE_FLAG_INSENSITIVE);
gtk_render_layout (style, cr,
(gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) ?
text_area.width - layout_width - offset :
text_area.x + offset,
text_area.y + offset,
layout);
gtk_style_context_restore (style);
g_object_unref (layout);
return FALSE;
}
static gboolean
pika_tag_entry_key_press (GtkWidget *widget,
GdkEventKey *event)
{
PikaTagEntry *entry = PIKA_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 (pika_tag_is_tag_separator (c))
{
g_idle_add ((GSourceFunc) pika_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) pika_tag_entry_auto_complete, entry);
}
else
{
pika_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:
pika_tag_entry_commit_tags (entry);
break;
case GDK_KEY_Left:
pika_tag_entry_previous_tag (entry,
(event->state & extend_mask) ? TRUE : FALSE);
return TRUE;
case GDK_KEY_Right:
pika_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 (pika_tag_entry_select_jellybean (entry,
selection_start, selection_end,
TAG_SEARCH_LEFT))
{
return TRUE;
}
else
{
pika_tag_entry_select_for_deletion (entry, TAG_SEARCH_LEFT);
/* FIXME: need to remove idle handler in dispose */
g_idle_add ((GSourceFunc) pika_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 (pika_tag_entry_select_jellybean (entry,
selection_start, selection_end,
TAG_SEARCH_RIGHT))
{
return TRUE;
}
else
{
pika_tag_entry_select_for_deletion (entry, TAG_SEARCH_RIGHT);
/* FIXME: need to remove idle handler in dispose */
g_idle_add ((GSourceFunc) pika_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)));
pika_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
pika_tag_entry_button_release (GtkWidget *widget,
GdkEventButton *event)
{
if (event->button == 1)
{
/* FIXME: need to remove idle handler in dispose */
g_idle_add ((GSourceFunc) pika_tag_entry_try_select_jellybean,
widget);
}
return GTK_WIDGET_CLASS (parent_class)->button_release_event (widget, event);
}
static gboolean
pika_tag_entry_try_select_jellybean (PikaTagEntry *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);
pika_tag_entry_select_jellybean (entry, selection_start, selection_end,
TAG_SEARCH_NONE);
return FALSE;
}
static gboolean
pika_tag_entry_select_jellybean (PikaTagEntry *entry,
gint selection_start,
gint selection_end,
PikaTagSearchDir 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
pika_tag_entry_add_to_recent (PikaTagEntry *entry,
const gchar *tags_string,
gboolean to_front)
{
gchar *recent_item = NULL;
gchar *stripped_string;
gint stripped_length;
GList *list;
if (entry->mode == PIKA_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) >= PIKA_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;
}
/**
* pika_tag_entry_get_separator:
*
* Tag separator is a single Unicode terminal punctuation
* character.
*
* Returns: returns locale dependent tag separator.
**/
const gchar *
pika_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
pika_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 && ! pika_tag_is_tag_separator (c))
{
g_string_append_unichar (tag_buffer, c);
}
else
{
gchar *valid_tag = pika_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, pika_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 (pika_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
pika_tag_entry_commit_tags (PikaTagEntry *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);
pika_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);
pika_tag_entry_strip_extra_whitespace (entry);
}
static gboolean
pika_tag_entry_commit_source_func (PikaTagEntry *entry)
{
pika_tag_entry_commit_tags (entry);
return FALSE;
}
static void
pika_tag_entry_next_tag (PikaTagEntry *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
pika_tag_entry_previous_tag (PikaTagEntry *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
pika_tag_entry_select_for_deletion (PikaTagEntry *entry,
PikaTagSearchDir 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
pika_tag_entry_strip_extra_whitespace (PikaTagEntry *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;
}