gimp r27816 - in trunk: . app/core app/widgets devel-docs devel-docs/app po
- From: martinn svn gnome org
- To: svn-commits-list gnome org
- Subject: gimp r27816 - in trunk: . app/core app/widgets devel-docs devel-docs/app po
- Date: Sat, 20 Dec 2008 14:46:54 +0000 (UTC)
Author: martinn
Date: Sat Dec 20 14:46:54 2008
New Revision: 27816
URL: http://svn.gnome.org/viewvc/gimp?rev=27816&view=rev
Log:
Bug 555954 â Merge Tagging of Gimp Resources GSoC Project
Merge the rest of the tagging code developed on the tagging branch
by Aurimas JuÅka. Development will now continue in trunk.
* app/core/gimptag.[ch]: New files (not strictly true but almost)
implementing the represention of a tag.
* app/core/gimptagcache.[ch]: New files implementing functionality
for loading and saving tags to tags.xml, and assigning loaded tags
to tagged objects.
* app/core/gimpfilteredcontainer.[ch]: New files implementing a
tag filtered GimpContainer.
* app/widgets/gimptagentry.[ch]: New files implementing a
GtkEntry-like widget for entering tags.
* app/widgets/gimpcombotagentry.[ch]: New files implementing a
combobox-like widget for selecting tags.
* app/widgets/gimptagpopup.[ch]: New files implementing a popup of
all available tags that can be selected and combined in a
checkbox-like way.
* app/core/gimp.[ch]: Add a GimpTagCache member and manage tag
assignment and saving and loading to/from tags.xml.
* app/widgets/gimpdatafactoryview.c: Add the tag query and tag
assignment widgets to the UI and show the tag filtered items
instead of all items.
* app/core/Makefile.am
* app/widgets/Makefile.am: Add new files.
* app/core/core-types.h
* app/widgets/widgets-types.h: Add new types.
Added:
trunk/app/core/gimpfilteredcontainer.c
trunk/app/core/gimpfilteredcontainer.h
trunk/app/core/gimptagcache.c
trunk/app/core/gimptagcache.h
trunk/app/widgets/gimpcombotagentry.c
trunk/app/widgets/gimpcombotagentry.h
trunk/app/widgets/gimptagentry.c
trunk/app/widgets/gimptagentry.h
trunk/app/widgets/gimptagpopup.c
trunk/app/widgets/gimptagpopup.h
trunk/devel-docs/tagging.txt
Modified:
trunk/ChangeLog
trunk/app/core/Makefile.am
trunk/app/core/core-types.h
trunk/app/core/gimp.c
trunk/app/core/gimp.h
trunk/app/core/gimptag.c
trunk/app/core/gimptag.h
trunk/app/widgets/Makefile.am
trunk/app/widgets/gimpdatafactoryview.c
trunk/app/widgets/widgets-types.h
trunk/devel-docs/ChangeLog
trunk/devel-docs/app/app-docs.sgml
trunk/devel-docs/app/app-sections.txt
trunk/devel-docs/app/app.types
trunk/po/ChangeLog
trunk/po/POTFILES.in
Modified: trunk/app/core/Makefile.am
==============================================================================
--- trunk/app/core/Makefile.am (original)
+++ trunk/app/core/Makefile.am Sat Dec 20 14:46:54 2008
@@ -168,6 +168,8 @@
gimperror.h \
gimpfilloptions.c \
gimpfilloptions.h \
+ gimpfilteredcontainer.c \
+ gimpfilteredcontainer.h \
gimpfloatingselundo.c \
gimpfloatingselundo.h \
gimpgradient.c \
@@ -325,6 +327,8 @@
gimpsubprogress.h \
gimptag.c \
gimptag.h \
+ gimptagcache.c \
+ gimptagcache.h \
gimptagged.c \
gimptagged.h \
gimptemplate.c \
Modified: trunk/app/core/core-types.h
==============================================================================
--- trunk/app/core/core-types.h (original)
+++ trunk/app/core/core-types.h Sat Dec 20 14:46:54 2008
@@ -67,12 +67,13 @@
/* containers */
-typedef struct _GimpContainer GimpContainer;
-typedef struct _GimpDocumentList GimpDocumentList;
-typedef struct _GimpDrawableStack GimpDrawableStack;
-typedef struct _GimpItemStack GimpItemStack;
-typedef struct _GimpList GimpList;
-typedef struct _GimpToolPresets GimpToolPresets;
+typedef struct _GimpContainer GimpContainer;
+typedef struct _GimpDocumentList GimpDocumentList;
+typedef struct _GimpDrawableStack GimpDrawableStack;
+typedef struct _GimpFilteredContainer GimpFilteredContainer;
+typedef struct _GimpItemStack GimpItemStack;
+typedef struct _GimpList GimpList;
+typedef struct _GimpToolPresets GimpToolPresets;
/* context objects */
@@ -102,6 +103,7 @@
typedef struct _GimpPalette GimpPalette;
typedef struct _GimpPattern GimpPattern;
typedef struct _GimpPatternClipboard GimpPatternClipboard;
+typedef struct _GimpTagCache GimpTagCache;
/* drawable objects */
Modified: trunk/app/core/gimp.c
==============================================================================
--- trunk/app/core/gimp.c (original)
+++ trunk/app/core/gimp.c Sat Dec 20 14:46:54 2008
@@ -58,6 +58,7 @@
#include "gimpbuffer.h"
#include "gimpcontext.h"
#include "gimpdatafactory.h"
+#include "gimptagcache.h"
#include "gimpdocumentlist.h"
#include "gimpgradient-load.h"
#include "gimpgradient.h"
@@ -242,6 +243,8 @@
gimp->gradient_factory = NULL;
gimp->palette_factory = NULL;
+ gimp->tag_cache = NULL;
+
gimp->pdb = gimp_pdb_new (gimp);
xcf_init (gimp);
@@ -355,6 +358,12 @@
gimp->palette_factory = NULL;
}
+ if (gimp->tag_cache)
+ {
+ g_object_unref (gimp->tag_cache);
+ gimp->tag_cache = NULL;
+ }
+
if (gimp->fonts)
{
g_object_unref (gimp->fonts);
@@ -477,6 +486,9 @@
memsize += gimp_object_get_memsize (GIMP_OBJECT (gimp->palette_factory),
gui_size);
+ memsize += gimp_object_get_memsize (GIMP_OBJECT (gimp->tag_cache),
+ gui_size);
+
memsize += gimp_object_get_memsize (GIMP_OBJECT (gimp->pdb), gui_size);
memsize += gimp_object_get_memsize (GIMP_OBJECT (gimp->tool_info_list),
@@ -588,6 +600,8 @@
gimp_object_set_static_name (GIMP_OBJECT (gimp->palette_factory),
"palette factory");
+ gimp->tag_cache = gimp_tag_cache_new ();
+
gimp_paint_init (gimp);
/* Set the last values used to default values. */
@@ -649,6 +663,8 @@
gimp_plug_in_manager_exit (gimp->plug_in_manager);
gimp_modules_unload (gimp);
+ gimp_tag_cache_save (gimp->tag_cache);
+
gimp_data_factory_data_save (gimp->brush_factory);
gimp_data_factory_data_save (gimp->pattern_factory);
gimp_data_factory_data_save (gimp->gradient_factory);
@@ -865,6 +881,18 @@
status_callback (NULL, _("Modules"), 0.7);
gimp_modules_load (gimp);
+ /* update tag cache */
+ status_callback (NULL, _("Updating tag cache"), 0.8);
+ gimp_tag_cache_load (gimp->tag_cache);
+ gimp_tag_cache_add_container (gimp->tag_cache,
+ gimp_data_factory_get_container (gimp->brush_factory));
+ gimp_tag_cache_add_container (gimp->tag_cache,
+ gimp_data_factory_get_container (gimp->pattern_factory));
+ gimp_tag_cache_add_container (gimp->tag_cache,
+ gimp_data_factory_get_container (gimp->gradient_factory));
+ gimp_tag_cache_add_container (gimp->tag_cache,
+ gimp_data_factory_get_container (gimp->palette_factory));
+
g_signal_emit (gimp, gimp_signals[RESTORE], 0, status_callback);
}
Modified: trunk/app/core/gimp.h
==============================================================================
--- trunk/app/core/gimp.h (original)
+++ trunk/app/core/gimp.h Sat Dec 20 14:46:54 2008
@@ -95,6 +95,8 @@
GimpDataFactory *gradient_factory;
GimpDataFactory *palette_factory;
+ GimpTagCache *tag_cache;
+
GimpPDB *pdb;
GimpContainer *tool_info_list;
Added: trunk/app/core/gimpfilteredcontainer.c
==============================================================================
--- (empty file)
+++ trunk/app/core/gimpfilteredcontainer.c Sat Dec 20 14:46:54 2008
@@ -0,0 +1,607 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
+ *
+ * gimpfilteredcontainer.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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h> /* strcmp */
+
+#include <glib-object.h>
+
+#include "core-types.h"
+
+#include "gimp.h"
+#include "gimpmarshal.h"
+#include "gimptag.h"
+#include "gimptagged.h"
+#include "gimplist.h"
+#include "gimpfilteredcontainer.h"
+
+enum
+{
+ TAG_COUNT_CHANGED,
+ LAST_SIGNAL
+};
+
+enum
+{
+ PROP_0,
+ PROP_SRC_CONTAINER,
+};
+
+typedef struct MatchParams_
+{
+ GimpFilteredContainer *filtered_container;
+ GList *items_to_add;
+ GList *items_to_remove;
+} MatchParams;
+
+
+static GObject* gimp_filtered_container_constructor (GType type,
+ guint n_construct_params,
+ GObjectConstructParam *construct_params);
+
+static void gimp_filtered_container_dispose (GObject *object);
+static void gimp_filtered_container_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+static void gimp_filtered_container_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+
+static gint64 gimp_filtered_container_get_memsize (GimpObject *object,
+ gint64 *gui_size);
+
+static gboolean gimp_filtered_container_object_matches (GimpFilteredContainer *filtered_container,
+ GimpObject *object);
+
+static void gimp_filtered_container_filter (GimpFilteredContainer *filtered_container);
+
+static void gimp_filtered_container_src_add (GimpContainer *src_container,
+ GimpObject *obj,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_src_remove (GimpContainer *src_container,
+ GimpObject *obj,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_src_freeze (GimpContainer *src_container,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_src_thaw (GimpContainer *src_container,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_tag_added (GimpTagged *tagged,
+ GimpTag *tag,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_tag_removed (GimpTagged *tagged,
+ GimpTag *tag,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_tagged_item_added (GimpTagged *tagged,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_tagged_item_removed(GimpTagged *tagged,
+ GimpFilteredContainer *filtered_container);
+static void gimp_filtered_container_tag_count_changed (GimpFilteredContainer *filtered_container,
+ gint tag_count);
+
+
+G_DEFINE_TYPE (GimpFilteredContainer, gimp_filtered_container, GIMP_TYPE_LIST)
+
+#define parent_class gimp_filtered_container_parent_class
+
+static guint gimp_filtered_container_signals[LAST_SIGNAL] = { 0, };
+
+
+static void
+gimp_filtered_container_class_init (GimpFilteredContainerClass *klass)
+{
+ GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
+ GimpObjectClass *gimp_object_class = GIMP_OBJECT_CLASS (klass);
+
+ g_object_class->constructor = gimp_filtered_container_constructor;
+ g_object_class->dispose = gimp_filtered_container_dispose;
+ g_object_class->set_property = gimp_filtered_container_set_property;
+ g_object_class->get_property = gimp_filtered_container_get_property;
+
+ gimp_object_class->get_memsize = gimp_filtered_container_get_memsize;
+
+ klass->tag_count_changed = gimp_filtered_container_tag_count_changed;
+
+ gimp_filtered_container_signals[TAG_COUNT_CHANGED] =
+ g_signal_new ("tag-count-changed",
+ GIMP_TYPE_FILTERED_CONTAINER,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GimpFilteredContainerClass, tag_count_changed),
+ NULL, NULL,
+ gimp_marshal_VOID__INT,
+ G_TYPE_NONE, 1,
+ G_TYPE_INT);
+
+ g_object_class_install_property (g_object_class, PROP_SRC_CONTAINER,
+ g_param_spec_object ("src-container", NULL, NULL,
+ GIMP_TYPE_CONTAINER,
+ GIMP_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+gimp_filtered_container_init (GimpFilteredContainer *filtered_container)
+{
+ filtered_container->src_container = NULL;
+ filtered_container->filter = NULL;
+ filtered_container->tag_ref_counts = NULL;
+ filtered_container->tag_count = 0;
+}
+
+static GObject*
+gimp_filtered_container_constructor (GType type,
+ guint n_construct_params,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+ GimpFilteredContainer *filtered_container;
+
+ object = G_OBJECT_CLASS (parent_class)->constructor (type,
+ n_construct_params,
+ construct_params);
+
+ filtered_container = GIMP_FILTERED_CONTAINER (object);
+ filtered_container->tag_ref_counts =
+ g_hash_table_new ((GHashFunc)gimp_tag_get_hash,
+ (GEqualFunc)gimp_tag_equals);
+
+ gimp_container_foreach (filtered_container->src_container,
+ (GFunc) gimp_filtered_container_tagged_item_added,
+ filtered_container);
+ gimp_filtered_container_filter (filtered_container);
+
+ return object;
+}
+
+static void
+gimp_filtered_container_dispose (GObject *object)
+{
+ GimpFilteredContainer *filtered_container = GIMP_FILTERED_CONTAINER (object);
+
+ if (filtered_container->src_container)
+ {
+ g_signal_handlers_disconnect_by_func (filtered_container->src_container,
+ gimp_filtered_container_src_add,
+ filtered_container);
+ g_signal_handlers_disconnect_by_func (filtered_container->src_container,
+ gimp_filtered_container_src_remove,
+ filtered_container);
+ g_signal_handlers_disconnect_by_func (filtered_container->src_container,
+ gimp_filtered_container_src_freeze,
+ filtered_container);
+ g_signal_handlers_disconnect_by_func (filtered_container->src_container,
+ gimp_filtered_container_src_thaw,
+ filtered_container);
+ g_object_unref (filtered_container->src_container);
+ filtered_container->src_container = NULL;
+ }
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gimp_filtered_container_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GimpFilteredContainer *filtered_container = GIMP_FILTERED_CONTAINER (object);
+
+ switch (property_id)
+ {
+ case PROP_SRC_CONTAINER:
+ {
+ filtered_container->src_container = g_value_get_object (value);
+ g_object_ref (filtered_container->src_container);
+
+ g_signal_connect (filtered_container->src_container, "add",
+ G_CALLBACK (gimp_filtered_container_src_add),
+ filtered_container);
+ g_signal_connect (filtered_container->src_container, "remove",
+ G_CALLBACK (gimp_filtered_container_src_remove),
+ filtered_container);
+ g_signal_connect (filtered_container->src_container, "freeze",
+ G_CALLBACK (gimp_filtered_container_src_freeze),
+ filtered_container);
+ g_signal_connect (filtered_container->src_container, "thaw",
+ G_CALLBACK (gimp_filtered_container_src_thaw),
+ filtered_container);
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gimp_filtered_container_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GimpFilteredContainer *filtered_container = GIMP_FILTERED_CONTAINER (object);
+
+ switch (property_id)
+ {
+ case PROP_SRC_CONTAINER:
+ g_value_set_object (value, filtered_container->src_container);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static gint64
+gimp_filtered_container_get_memsize (GimpObject *object,
+ gint64 *gui_size)
+{
+ GimpFilteredContainer *filtered_container = GIMP_FILTERED_CONTAINER (object);
+ gint64 memsize = 0;
+
+ memsize += (gimp_container_get_n_children (GIMP_CONTAINER (filtered_container)) *
+ sizeof (GList));
+
+ return memsize + GIMP_OBJECT_CLASS (parent_class)->get_memsize (object,
+ gui_size);
+}
+
+/**
+ * gimp_filtered_container_new:
+ * @src_container: container to be filtered.
+ *
+ * Creates a new #GimpFilteredContainer object which creates filtered
+ * data view of #GimpTagged objects. It filters @src_container for objects
+ * containing all of the filtering tags. Syncronization with @src_container
+ * data is performed automatically.
+ *
+ * Return value: a new #GimpFilteredContainer object.
+ **/
+GimpContainer *
+gimp_filtered_container_new (GimpContainer *src_container,
+ GCompareFunc sort_func)
+{
+ GimpFilteredContainer *filtered_container;
+ GType children_type;
+
+ g_return_val_if_fail (GIMP_IS_CONTAINER (src_container), NULL);
+
+ children_type = gimp_container_get_children_type (src_container);
+
+ filtered_container = g_object_new (GIMP_TYPE_FILTERED_CONTAINER,
+ "sort-func", sort_func,
+ "children-type", children_type,
+ "policy", GIMP_CONTAINER_POLICY_WEAK,
+ "unique-names", FALSE,
+ "src-container", src_container,
+ NULL);
+
+ return GIMP_CONTAINER (filtered_container);
+}
+
+/**
+ * gimp_filtered_container_set_filter:
+ * @filtered_container: a #GimpFilteredContainer object.
+ * @tags: list of #GimpTag objects.
+ *
+ * Sets list of tags to be used for filtering. Only objects which have all of
+ * the tags assigned match filtering criteria.
+ **/
+void
+gimp_filtered_container_set_filter (GimpFilteredContainer *filtered_container,
+ GList *tags)
+{
+ g_return_if_fail (GIMP_IS_FILTERED_CONTAINER (filtered_container));
+
+ filtered_container->filter = tags;
+
+ gimp_container_freeze (GIMP_CONTAINER (filtered_container));
+ gimp_filtered_container_filter (filtered_container);
+ gimp_container_thaw (GIMP_CONTAINER (filtered_container));
+}
+
+/**
+ * gimp_filtered_container_get_filter:
+ * @filtered_container: a #GimpFilteredContainer object.
+ *
+ * Returns current tag filter. Tag filter is a list of GimpTag objects, which
+ * must be contained by each object matching filter criteria.
+ *
+ * Return value: a list of GimpTag objects used as filter. This value should
+ * not be modified or freed.
+ **/
+const GList *
+gimp_filtered_container_get_filter (GimpFilteredContainer *filtered_container)
+{
+ g_return_val_if_fail (GIMP_IS_FILTERED_CONTAINER (filtered_container), NULL);
+ return filtered_container->filter;
+}
+
+static gboolean
+gimp_filtered_container_object_matches (GimpFilteredContainer *filtered_container,
+ GimpObject *object)
+{
+ GList *filter_tag;
+ GList *object_tag;
+
+ filter_tag = filtered_container->filter;
+ while (filter_tag)
+ {
+ if (! filter_tag->data)
+ {
+ /* invalid tag - does not match */
+ return FALSE;
+ }
+
+ object_tag = gimp_tagged_get_tags (GIMP_TAGGED (object));
+ while (object_tag)
+ {
+ if (gimp_tag_equals (GIMP_TAG (object_tag->data),
+ GIMP_TAG (filter_tag->data)))
+ {
+ /* found match for the tag */
+ break;
+ }
+ object_tag = g_list_next (object_tag);
+ }
+
+ if (! object_tag)
+ {
+ /* match for the tag was not found.
+ * since query is of type AND, it whole fails. */
+ return FALSE;
+ }
+
+ filter_tag = g_list_next (filter_tag);
+ }
+
+ return TRUE;
+}
+
+static void
+gimp_filtered_container_check_needs_remove (GimpObject *object,
+ MatchParams *match_params)
+{
+ if (! gimp_filtered_container_object_matches (match_params->filtered_container,
+ object))
+ {
+ match_params->items_to_remove = g_list_prepend (match_params->items_to_remove,
+ object);
+ }
+}
+
+static void
+gimp_filtered_container_check_needs_add (GimpObject *object,
+ MatchParams *match_params)
+{
+ if (gimp_filtered_container_object_matches (match_params->filtered_container, object)
+ && !gimp_container_have (GIMP_CONTAINER (match_params->filtered_container),
+ object))
+ {
+ match_params->items_to_add = g_list_prepend (match_params->items_to_add,
+ object);
+ }
+}
+
+static void
+gimp_filtered_container_filter (GimpFilteredContainer *filtered_container)
+{
+ MatchParams match_params;
+ GList *iterator;
+
+ match_params.filtered_container = filtered_container;
+ match_params.items_to_add = NULL;
+ match_params.items_to_remove = NULL;
+
+ gimp_container_foreach (GIMP_CONTAINER (filtered_container),
+ (GFunc) gimp_filtered_container_check_needs_remove,
+ &match_params);
+ gimp_container_foreach (GIMP_CONTAINER (filtered_container->src_container),
+ (GFunc) gimp_filtered_container_check_needs_add,
+ &match_params);
+
+ for (iterator = match_params.items_to_remove; iterator;
+ iterator = g_list_next (iterator))
+ {
+ gimp_container_remove (GIMP_CONTAINER (filtered_container),
+ GIMP_OBJECT (iterator->data));
+ }
+
+ for (iterator = match_params.items_to_add; iterator;
+ iterator = g_list_next (iterator))
+ {
+ gimp_container_add (GIMP_CONTAINER (filtered_container),
+ GIMP_OBJECT (iterator->data));
+ }
+
+ g_list_free (match_params.items_to_add);
+ g_list_free (match_params.items_to_remove);
+}
+
+static void
+gimp_filtered_container_src_add (GimpContainer *src_container,
+ GimpObject *obj,
+ GimpFilteredContainer *filtered_container)
+{
+ gimp_filtered_container_tagged_item_added (GIMP_TAGGED (obj),
+ filtered_container);
+
+ if (! gimp_container_frozen (src_container))
+ {
+ gimp_container_add (GIMP_CONTAINER (filtered_container), obj);
+ }
+}
+
+static void
+gimp_filtered_container_src_remove (GimpContainer *src_container,
+ GimpObject *obj,
+ GimpFilteredContainer *filtered_container)
+{
+ gimp_filtered_container_tagged_item_removed (GIMP_TAGGED (obj),
+ filtered_container);
+
+ if (! gimp_container_frozen (src_container)
+ && gimp_container_have (GIMP_CONTAINER (filtered_container), obj))
+ {
+ gimp_container_remove (GIMP_CONTAINER (filtered_container), obj);
+ }
+}
+
+static void
+gimp_filtered_container_src_freeze (GimpContainer *src_container,
+ GimpFilteredContainer *filtered_container)
+{
+ gimp_container_freeze (GIMP_CONTAINER (filtered_container));
+ gimp_container_clear (GIMP_CONTAINER (filtered_container));
+}
+
+static void
+gimp_filtered_container_src_thaw (GimpContainer *src_container,
+ GimpFilteredContainer *filtered_container)
+{
+ gimp_filtered_container_filter (filtered_container);
+ gimp_container_thaw (GIMP_CONTAINER (filtered_container));
+}
+
+static void
+gimp_filtered_container_tagged_item_added (GimpTagged *tagged,
+ GimpFilteredContainer *filtered_container)
+{
+ GList *tag_iterator;
+
+ for (tag_iterator = gimp_tagged_get_tags (tagged); tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ gimp_filtered_container_tag_added (tagged,
+ GIMP_TAG (tag_iterator->data),
+ filtered_container);
+ }
+
+ g_signal_connect (tagged, "tag-added",
+ G_CALLBACK (gimp_filtered_container_tag_added),
+ filtered_container);
+ g_signal_connect (tagged, "tag-removed",
+ G_CALLBACK (gimp_filtered_container_tag_removed),
+ filtered_container);
+}
+
+static void
+gimp_filtered_container_tagged_item_removed (GimpTagged *tagged,
+ GimpFilteredContainer *filtered_container)
+{
+ GList *tag_iterator;
+
+ g_signal_handlers_disconnect_by_func (tagged,
+ G_CALLBACK (gimp_filtered_container_tag_added),
+ filtered_container);
+ g_signal_handlers_disconnect_by_func (tagged,
+ G_CALLBACK (gimp_filtered_container_tag_removed),
+ filtered_container);
+
+ for (tag_iterator = gimp_tagged_get_tags (tagged); tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ gimp_filtered_container_tag_removed (tagged,
+ GIMP_TAG (tag_iterator->data),
+ filtered_container);
+ }
+
+}
+
+static void
+gimp_filtered_container_tag_added (GimpTagged *tagged,
+ GimpTag *tag,
+ GimpFilteredContainer *filtered_container)
+{
+ gint ref_count;
+
+ ref_count = GPOINTER_TO_INT (g_hash_table_lookup (filtered_container->tag_ref_counts,
+ tag));
+ ref_count++;
+ g_hash_table_insert (filtered_container->tag_ref_counts,
+ tag, GINT_TO_POINTER (ref_count));
+ if (ref_count == 1)
+ {
+ filtered_container->tag_count++;
+ g_signal_emit (filtered_container,
+ gimp_filtered_container_signals[TAG_COUNT_CHANGED],
+ 0, filtered_container->tag_count);
+ }
+}
+
+static void
+gimp_filtered_container_tag_removed (GimpTagged *tagged,
+ GimpTag *tag,
+ GimpFilteredContainer *filtered_container)
+{
+ gint ref_count;
+
+ ref_count = GPOINTER_TO_INT (g_hash_table_lookup (filtered_container->tag_ref_counts,
+ tag));
+ ref_count--;
+ if (ref_count > 0)
+ {
+ g_hash_table_insert (filtered_container->tag_ref_counts,
+ tag, GINT_TO_POINTER (ref_count));
+ }
+ else
+ {
+ if (g_hash_table_remove (filtered_container->tag_ref_counts, tag))
+ {
+ filtered_container->tag_count--;
+ g_signal_emit (filtered_container,
+ gimp_filtered_container_signals[TAG_COUNT_CHANGED],
+ 0, filtered_container->tag_count);
+ }
+ }
+}
+
+static void
+gimp_filtered_container_tag_count_changed (GimpFilteredContainer *container,
+ gint tag_count)
+{
+}
+
+/**
+ * gimp_filtered_container_get_tag_count:
+ * @container: a #GimpFilteredContainer object.
+ *
+ * Get number of distinct tags that are currently assigned to all objects
+ * in the container. The count is independent of currently used filter, it
+ * is provided for all available objects (ie. empty filter).
+ *
+ * Return value: number of distinct tags assigned to all objects in the
+ * container.
+ **/
+gint
+gimp_filtered_container_get_tag_count (GimpFilteredContainer *container)
+{
+ g_return_val_if_fail (GIMP_IS_FILTERED_CONTAINER (container), 0);
+
+ return container->tag_count;
+}
+
Added: trunk/app/core/gimpfilteredcontainer.h
==============================================================================
--- (empty file)
+++ trunk/app/core/gimpfilteredcontainer.h Sat Dec 20 14:46:54 2008
@@ -0,0 +1,69 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
+ *
+ * gimpfilteredcontainer.h
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GIMP_FILTERED_CONTAINER_H__
+#define __GIMP_FILTERED_CONTAINER_H__
+
+
+#include "gimplist.h"
+
+
+#define GIMP_TYPE_FILTERED_CONTAINER (gimp_filtered_container_get_type ())
+#define GIMP_FILTERED_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_FILTERED_CONTAINER, GimpFilteredContainer))
+#define GIMP_FILTERED_CONTAINER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_FILTERED_CONTAINER, GimpFilteredContainerClass))
+#define GIMP_IS_FILTERED_CONTAINER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_FILTERED_CONTAINER))
+#define GIMP_FILTERED_CONTAINER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_FILTERED_CONTAINER, GimpFilteredContainerClass))
+
+
+typedef struct _GimpFilteredContainerClass GimpFilteredContainerClass;
+
+struct _GimpFilteredContainer
+{
+ GimpList parent_instance;
+
+ GimpContainer *src_container;
+ GList *filter;
+ GHashTable *tag_ref_counts;
+ gint tag_count;
+};
+
+struct _GimpFilteredContainerClass
+{
+ GimpContainerClass parent_class;
+
+ void (* tag_count_changed) (GimpFilteredContainer *container,
+ gint count);
+};
+
+
+GType gimp_filtered_container_get_type (void) G_GNUC_CONST;
+
+GimpContainer * gimp_filtered_container_new (GimpContainer *src_container,
+ GCompareFunc sort_func);
+
+void gimp_filtered_container_set_filter (GimpFilteredContainer *filtered_container,
+ GList *tags);
+
+const GList * gimp_filtered_container_get_filter (GimpFilteredContainer *filtered_container);
+
+gint gimp_filtered_container_get_tag_count (GimpFilteredContainer *container);
+
+#endif /* __GIMP_FILTERED_CONTAINER_H__ */
Modified: trunk/app/core/gimptag.c
==============================================================================
--- trunk/app/core/gimptag.c (original)
+++ trunk/app/core/gimptag.c Sat Dec 20 14:46:54 2008
@@ -22,12 +22,16 @@
#include "config.h"
#include <glib-object.h>
+#include <string.h>
#include "core-types.h"
#include "gimptag.h"
+#define GIMP_TAG_INTERNAL_PREFIX "gimp:"
+
+
G_DEFINE_TYPE (GimpTag, gimp_tag, G_TYPE_OBJECT)
#define parent_class gimp_tag_parent_class
@@ -41,6 +45,133 @@
static void
gimp_tag_init (GimpTag *tag)
{
+ tag->tag = 0;
+ tag->collate_key = 0;
+}
+
+/**
+ * gimp_tag_new:
+ * @tag_string: a tag name.
+ *
+ * If given tag name is not valid, an attempt will be made to fix it.
+ *
+ * Return value: a new #GimpTag object, or NULL if tag string is invalid and
+ * cannot be fixed.
+ **/
+GimpTag *
+gimp_tag_new (const char *tag_string)
+{
+ GimpTag *tag;
+ gchar *tag_name;
+ gchar *case_folded;
+ gchar *collate_key;
+
+ g_return_val_if_fail (tag_string != NULL, NULL);
+
+ tag_name = gimp_tag_string_make_valid (tag_string);
+ if (! tag_name)
+ {
+ return NULL;
+ }
+
+ tag = g_object_new (GIMP_TYPE_TAG, NULL);
+
+ tag->tag = g_quark_from_string (tag_name);
+
+ case_folded = g_utf8_casefold (tag_name, -1);
+ collate_key = g_utf8_collate_key (case_folded, -1);
+ tag->collate_key = g_quark_from_string (collate_key);
+ g_free (collate_key);
+ g_free (case_folded);
+ g_free (tag_name);
+
+ return tag;
+}
+
+/**
+ * gimp_tag_try_new:
+ * @tag_string: a tag name.
+ *
+ * Similar to gimp_tag_new(), but returns NULL if tag is surely not equal
+ * to any of currently created tags. It is useful for tag querying to avoid
+ * unneeded comparisons. If tag is created, however, it does not mean that
+ * it would necessarily match with some other tag.
+ *
+ * Return value: new #GimpTag object, or NULL if tag will not match with any
+ * other #GimpTag.
+ **/
+GimpTag *
+gimp_tag_try_new (const char *tag_string)
+{
+ GimpTag *tag;
+ gchar *tag_name;
+ gchar *case_folded;
+ gchar *collate_key;
+ GQuark tag_quark;
+ GQuark collate_key_quark;
+
+ tag_name = gimp_tag_string_make_valid (tag_string);
+ if (! tag_name)
+ {
+ return NULL;
+ }
+
+ case_folded = g_utf8_casefold (tag_name, -1);
+ collate_key = g_utf8_collate_key (case_folded, -1);
+ collate_key_quark = g_quark_try_string (collate_key);
+ g_free (collate_key);
+ g_free (case_folded);
+
+ if (! collate_key_quark)
+ {
+ g_free (tag_name);
+ return NULL;
+ }
+
+ tag_quark = g_quark_from_string (tag_name);
+ g_free (tag_name);
+ if (! tag_quark)
+ {
+ return NULL;
+ }
+
+ tag = g_object_new (GIMP_TYPE_TAG, NULL);
+ tag->tag = tag_quark;
+ tag->collate_key = collate_key_quark;
+ return tag;
+}
+
+/**
+ * gimp_tag_get_name:
+ * @tag: a gimp tag.
+ *
+ * Retrieve name of the tag.
+ *
+ * Return value: name of tag.
+ **/
+const gchar *
+gimp_tag_get_name (GimpTag *tag)
+{
+ g_return_val_if_fail (GIMP_IS_TAG (tag), NULL);
+
+ return g_quark_to_string (tag->tag);
+}
+
+/**
+ * gimp_tag_get_hash:
+ * @tag: a gimp tag.
+ *
+ * Hashing function which is useful, for example, to store #GimpTag in
+ * a #GHashTable.
+ *
+ * Return value: hash value for tag.
+ **/
+guint
+gimp_tag_get_hash (GimpTag *tag)
+{
+ g_return_val_if_fail (GIMP_IS_TAG (tag), -1);
+
+ return tag->collate_key;
}
/**
@@ -59,5 +190,156 @@
g_return_val_if_fail (GIMP_IS_TAG (tag), FALSE);
g_return_val_if_fail (GIMP_IS_TAG (other), FALSE);
- return FALSE;
+ return tag->collate_key == other->collate_key;
+}
+
+/**
+ * gimp_tag_compare_func:
+ * @p1: pointer to left-hand #GimpTag object.
+ * @p2: pointer to right-hand #GimpTag object.
+ *
+ * Compares tags according to tag comparison rules. Useful for sorting
+ * functions.
+ *
+ * Return value: meaning of return value is the same as in strcmp().
+ **/
+int
+gimp_tag_compare_func (const void *p1,
+ const void *p2)
+{
+ GimpTag *t1 = GIMP_TAG (p1);
+ GimpTag *t2 = GIMP_TAG (p2);
+
+ return g_strcmp0 (g_quark_to_string (t1->collate_key),
+ g_quark_to_string (t2->collate_key));
+}
+
+/**
+ * gimp_tag_compare_with_string:
+ * @tag: a #GimpTag object.
+ * @tag_string: pointer to right-hand #GimpTag object.
+ *
+ * Compares tag and a string according to tag comparison rules. Similar to
+ * gimp_tag_compare_func(), but can be used without creating temporary tag
+ * object.
+ *
+ * Return value: meaning of return value is the same as in strcmp().
+ **/
+gint
+gimp_tag_compare_with_string (GimpTag *tag,
+ const char *tag_string)
+{
+ gchar *case_folded;
+ const gchar *collate_key;
+ gchar *collate_key2;
+ gint result;
+
+ g_return_val_if_fail (GIMP_IS_TAG (tag), 0);
+ g_return_val_if_fail (tag_string != NULL, 0);
+
+ collate_key = g_quark_to_string (tag->collate_key);
+ case_folded = g_utf8_casefold (tag_string, -1);
+ collate_key2 = g_utf8_collate_key (case_folded, -1);
+ result = g_strcmp0 (collate_key, collate_key2);
+ g_free (collate_key2);
+ g_free (case_folded);
+
+ return result;
+}
+
+/**
+ * gimp_tag_string_make_valid:
+ * @tag_string: a text string.
+ *
+ * Tries to create a valid tag string from given @tag_string.
+ *
+ * Return value: a newly allocated tag string in case given @tag_string was
+ * valid or could be fixed, otherwise NULL. Allocated value should be freed
+ * using g_free().
+ **/
+gchar *
+gimp_tag_string_make_valid (const gchar *tag_string)
+{
+ gchar *tag;
+ GString *buffer;
+ gchar *tag_cursor;
+ gunichar c;
+
+ g_return_val_if_fail (tag_string, NULL);
+
+ tag = g_utf8_normalize (tag_string, -1, G_NORMALIZE_ALL);
+ if (! tag)
+ {
+ return NULL;
+ }
+
+ tag = g_strstrip (tag);
+ if (! *tag)
+ {
+ g_free (tag);
+ return NULL;
+ }
+
+ buffer = g_string_new ("");
+ tag_cursor = tag;
+ if (g_str_has_prefix (tag_cursor, GIMP_TAG_INTERNAL_PREFIX))
+ {
+ tag_cursor += strlen (GIMP_TAG_INTERNAL_PREFIX);
+ }
+ do
+ {
+ c = g_utf8_get_char (tag_cursor);
+ tag_cursor = g_utf8_next_char (tag_cursor);
+ if (g_unichar_isprint (c)
+ && ! gimp_tag_is_tag_separator (c))
+ {
+ g_string_append_unichar (buffer, c);
+ }
+ } while (c);
+
+ g_free (tag);
+ tag = g_string_free (buffer, FALSE);
+ tag = g_strstrip (tag);
+
+ if (! *tag)
+ {
+ g_free (tag);
+ return NULL;
+ }
+
+ return tag;
+}
+
+/**
+ * gimp_tag_is_tag_separator:
+ * @c: Unicode character.
+ *
+ * Defines a set of characters that are considered tag separators. The
+ * tag separators are hand-picked from the set of characters with the
+ * Terminal_Punctuation property as specified in the version 5.1.0 of
+ * the Unicode Standard.
+ *
+ * Return value: %TRUE if the character is a tag separator.
+ */
+gboolean
+gimp_tag_is_tag_separator (gunichar c)
+{
+ switch (c)
+ {
+ case 0x002C: /* COMMA */
+ case 0x060C: /* ARABIC COMMA */
+ case 0x07F8: /* NKO COMMA */
+ case 0x1363: /* ETHIOPIC COMMA */
+ case 0x1802: /* MONGOLIAN COMMA */
+ case 0x1808: /* MONGOLIAN MANCHU COMMA */
+ case 0x3001: /* IDEOGRAPHIC COMMA */
+ case 0xA60D: /* VAI COMMA */
+ case 0xFE50: /* SMALL COMMA */
+ case 0xFF0C: /* FULLWIDTH COMMA */
+ case 0xFF64: /* HALFWIDTH IDEOGRAPHIC COMMA */
+ return TRUE;
+
+ default:
+ return FALSE;
+ }
}
Modified: trunk/app/core/gimptag.h
==============================================================================
--- trunk/app/core/gimptag.h (original)
+++ trunk/app/core/gimptag.h Sat Dec 20 14:46:54 2008
@@ -39,6 +39,9 @@
struct _GimpTag
{
GObject parent_instance;
+
+ GQuark tag;
+ GQuark collate_key;
};
struct _GimpTagClass
@@ -46,12 +49,21 @@
GObjectClass parent_class;
};
+GType gimp_tag_get_type (void) G_GNUC_CONST;
-GType gimp_tag_get_type (void) G_GNUC_CONST;
+GimpTag * gimp_tag_new (const gchar *tag_string);
+GimpTag * gimp_tag_try_new (const gchar *tag_string);
-gboolean gimp_tag_equals (const GimpTag *tag,
- const GimpTag *other);
+const gchar * gimp_tag_get_name (GimpTag *tag);
+guint gimp_tag_get_hash (GimpTag *tag);
+gboolean gimp_tag_equals (const GimpTag *tag,
+ const GimpTag *other);
+gint gimp_tag_compare_func (const void *p1,
+ const void *p2);
+gint gimp_tag_compare_with_string (GimpTag *tag,
+ const gchar *tag_string);
+gchar * gimp_tag_string_make_valid (const gchar *tag_string);
+gboolean gimp_tag_is_tag_separator (gunichar c);
#endif /* __GIMP_TAG_H__ */
-
Added: trunk/app/core/gimptagcache.c
==============================================================================
--- (empty file)
+++ trunk/app/core/gimptagcache.c Sat Dec 20 14:46:54 2008
@@ -0,0 +1,593 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagcache.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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib-object.h>
+
+#include "libgimpbase/gimpbase.h"
+#include "libgimpmath/gimpmath.h"
+#include "libgimpconfig/gimpconfig.h"
+
+#include "core-types.h"
+
+#include "config/gimpxmlparser.h"
+
+#include "gimp-utils.h"
+#include "gimpcontext.h"
+#include "gimpdata.h"
+#include "gimplist.h"
+#include "gimptag.h"
+#include "gimptagcache.h"
+#include "gimptagged.h"
+
+#include "gimp-intl.h"
+
+
+#define GIMP_TAG_CACHE_FILE "tags.xml"
+
+
+enum
+{
+ PROP_0,
+ PROP_GIMP,
+};
+
+
+typedef struct
+{
+ GQuark identifier;
+ GQuark checksum;
+ GList *tags;
+ guint referenced : 1;
+} GimpTagCacheRecord;
+
+typedef struct
+{
+ GArray *records;
+ GimpTagCacheRecord current_record;
+} GimpTagCacheParseData;
+
+struct _GimpTagCachePriv
+{
+ GArray *records;
+ GList *containers;
+};
+
+
+static void gimp_tag_cache_finalize (GObject *object);
+
+static gint64 gimp_tag_cache_get_memsize (GimpObject *object,
+ gint64 *gui_size);
+static void gimp_tag_cache_object_initialize (GimpTagged *tagged,
+ GimpTagCache *cache);
+static void gimp_tag_cache_add_object (GimpTagCache *cache,
+ GimpTagged *tagged);
+
+static void gimp_tag_cache_load_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error);
+static void gimp_tag_cache_load_end_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data,
+ GError **error);
+static void gimp_tag_cache_load_text (GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error);
+static void gimp_tag_cache_load_error (GMarkupParseContext *context,
+ GError *error,
+ gpointer user_data);
+static const gchar * gimp_tag_cache_attribute_name_to_value
+ (const gchar **attribute_names,
+ const gchar **attribute_values,
+ const gchar *name);
+
+static GQuark gimp_tag_cache_get_error_domain (void);
+
+
+G_DEFINE_TYPE (GimpTagCache, gimp_tag_cache, GIMP_TYPE_OBJECT)
+
+#define parent_class gimp_tag_cache_parent_class
+
+
+static void
+gimp_tag_cache_class_init (GimpTagCacheClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GimpObjectClass *gimp_object_class = GIMP_OBJECT_CLASS (klass);
+ GimpTagCacheClass *gimp_tag_cache_class = GIMP_TAG_CACHE_CLASS (klass);
+
+ object_class->finalize = gimp_tag_cache_finalize;
+
+ gimp_object_class->get_memsize = gimp_tag_cache_get_memsize;
+
+ g_type_class_add_private (gimp_tag_cache_class,
+ sizeof (GimpTagCachePriv));
+}
+
+static void
+gimp_tag_cache_init (GimpTagCache *cache)
+{
+ cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (cache,
+ GIMP_TYPE_TAG_CACHE,
+ GimpTagCachePriv);
+
+ cache->priv->records = g_array_new (FALSE, FALSE, sizeof(GimpTagCacheRecord));
+ cache->priv->containers = NULL;
+}
+
+static void
+gimp_tag_cache_finalize (GObject *object)
+{
+ GimpTagCache *cache = GIMP_TAG_CACHE (object);
+ gint i;
+
+ if (cache->priv->records)
+ {
+ for (i = 0; i < cache->priv->records->len; i++)
+ {
+ GimpTagCacheRecord *rec =
+ &g_array_index (cache->priv->records, GimpTagCacheRecord, i);
+
+ g_list_foreach (rec->tags, (GFunc) g_object_unref, NULL);
+ g_list_free (rec->tags);
+ }
+
+ g_array_free (cache->priv->records, TRUE);
+ cache->priv->records = NULL;
+ }
+
+ if (cache->priv->containers)
+ {
+ g_list_free (cache->priv->containers);
+ cache->priv->containers = NULL;
+ }
+
+ G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static gint64
+gimp_tag_cache_get_memsize (GimpObject *object,
+ gint64 *gui_size)
+{
+ GimpTagCache *cache = GIMP_TAG_CACHE (object);
+ gint64 memsize = 0;
+
+ memsize += gimp_g_list_get_memsize (cache->priv->containers, 0);
+ memsize += cache->priv->records->len * sizeof (GimpTagCacheRecord);
+
+ return memsize + GIMP_OBJECT_CLASS (parent_class)->get_memsize (object,
+ gui_size);
+}
+
+/**
+ * gimp_tag_cache_new:
+ *
+ * Return value: creates new GimpTagCache object.
+ **/
+GimpTagCache *
+gimp_tag_cache_new (void)
+{
+ return g_object_new (GIMP_TYPE_TAG_CACHE, NULL);
+}
+
+static void
+gimp_tag_cache_container_add_callback (GimpTagCache *cache,
+ GimpTagged *tagged,
+ GimpContainer *not_used)
+{
+ gimp_tag_cache_add_object (cache, tagged);
+}
+
+/**
+ * gimp_tag_cache_add_container:
+ * @cache: a GimpTagCache object.
+ * @container: container containing GimpTagged objects.
+ *
+ * Adds container of GimpTagged objects to tag cache. Before calling this
+ * function tag cache must be loaded using gimp_tag_cache_load(). When tag
+ * cache is saved to file, tags are collected from objects in priv->containers.
+ **/
+void
+gimp_tag_cache_add_container (GimpTagCache *cache,
+ GimpContainer *container)
+{
+ g_return_if_fail (GIMP_IS_TAG_CACHE (cache));
+ g_return_if_fail (GIMP_IS_CONTAINER (container));
+
+ cache->priv->containers = g_list_append (cache->priv->containers, container);
+ gimp_container_foreach (container, (GFunc) gimp_tag_cache_object_initialize, cache);
+
+ g_signal_connect_swapped (container, "add",
+ G_CALLBACK (gimp_tag_cache_container_add_callback),
+ cache);
+}
+
+static void
+gimp_tag_cache_add_object (GimpTagCache *cache,
+ GimpTagged *tagged)
+{
+ gchar *identifier;
+ GQuark identifier_quark = 0;
+ gchar *checksum_string;
+ GQuark checksum_quark;
+ GList *tag_iterator;
+ gint i;
+
+ identifier = gimp_tagged_get_identifier (tagged);
+ identifier_quark = g_quark_try_string (identifier);
+ g_free (identifier);
+
+ if (! gimp_tagged_get_tags (tagged))
+ {
+ if (identifier_quark)
+ {
+ for (i = 0; i < cache->priv->records->len; i++)
+ {
+ GimpTagCacheRecord *rec = &g_array_index (cache->priv->records,
+ GimpTagCacheRecord, i);
+
+ if (rec->identifier == identifier_quark)
+ {
+ for (tag_iterator = rec->tags; tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ gimp_tagged_add_tag (tagged, GIMP_TAG (tag_iterator->data));
+ }
+ rec->referenced = TRUE;
+ return;
+ }
+ }
+ }
+
+ checksum_string = gimp_tagged_get_checksum (tagged);
+ checksum_quark = g_quark_try_string (checksum_string);
+ g_free (checksum_string);
+
+ if (checksum_quark)
+ {
+ for (i = 0; i < cache->priv->records->len; i++)
+ {
+ GimpTagCacheRecord *rec = &g_array_index (cache->priv->records,
+ GimpTagCacheRecord, i);
+
+ if (rec->checksum == checksum_quark)
+ {
+ printf ("remapping identifier: %s ==> %s\n",
+ g_quark_to_string (rec->identifier),
+ g_quark_to_string (identifier_quark));
+ rec->identifier = identifier_quark;
+
+ for (tag_iterator = rec->tags; tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ gimp_tagged_add_tag (tagged, GIMP_TAG (tag_iterator->data));
+ }
+ rec->referenced = TRUE;
+ return;
+ }
+ }
+ }
+ }
+}
+
+static void
+gimp_tag_cache_object_initialize (GimpTagged *tagged,
+ GimpTagCache *cache)
+{
+ gimp_tag_cache_add_object (cache, tagged);
+}
+
+static void
+gimp_tag_cache_tagged_to_cache_record_foreach (GimpTagged *tagged,
+ GList **cache_records)
+{
+ gchar *identifier;
+ gchar *checksum;
+ GimpTagCacheRecord *cache_rec;
+
+ identifier = gimp_tagged_get_identifier (tagged);
+ if (identifier)
+ {
+ cache_rec = (GimpTagCacheRecord*) g_malloc (sizeof (GimpTagCacheRecord));
+ cache_rec->identifier = g_quark_from_string (identifier);
+ checksum = gimp_tagged_get_checksum (tagged);
+ cache_rec->checksum = g_quark_from_string (checksum);
+ g_free (checksum);
+ cache_rec->tags = g_list_copy (gimp_tagged_get_tags (tagged));
+ *cache_records = g_list_append (*cache_records, cache_rec);
+ }
+ g_free (identifier);
+}
+
+/**
+ * gimp_tag_cache_save:
+ * @cache: a GimpTagCache object.
+ *
+ * Saves tag cache to cache file.
+ **/
+void
+gimp_tag_cache_save (GimpTagCache *cache)
+{
+ GString *buf;
+ GList *saved_records;
+ GList *iterator;
+ gchar *filename;
+ GError *error;
+ gint i;
+
+ g_return_if_fail (GIMP_IS_TAG_CACHE (cache));
+
+ saved_records = NULL;
+ for (i = 0; i < cache->priv->records->len; i++)
+ {
+ GimpTagCacheRecord *current_record = &g_array_index(cache->priv->records, GimpTagCacheRecord, i);
+ if (! current_record->referenced
+ && current_record->tags)
+ {
+ /* keep tagged objects which have tags assigned
+ * but were not loaded. */
+ GimpTagCacheRecord *record_copy = (GimpTagCacheRecord*) g_malloc (sizeof (GimpTagCacheRecord));
+ record_copy->identifier = current_record->identifier;
+ record_copy->checksum = current_record->checksum;
+ record_copy->tags = g_list_copy (current_record->tags);
+ saved_records = g_list_append (saved_records, record_copy);
+ }
+ }
+
+ for (iterator = cache->priv->containers; iterator;
+ iterator = g_list_next (iterator))
+ {
+ gimp_container_foreach (GIMP_CONTAINER (iterator->data),
+ (GFunc) gimp_tag_cache_tagged_to_cache_record_foreach,
+ &saved_records);
+ }
+
+ buf = g_string_new ("");
+ g_string_append (buf, "<?xml version='1.0' encoding='UTF-8'?>\n");
+ g_string_append (buf, "<tags>\n");
+ for (iterator = saved_records; iterator;
+ iterator = g_list_next (iterator))
+ {
+ GimpTagCacheRecord *cache_rec = (GimpTagCacheRecord*) iterator->data;
+ GList *tag_iterator;
+ gchar *identifier_string;
+ gchar *tag_string;
+
+ identifier_string = g_markup_escape_text (g_quark_to_string (cache_rec->identifier), -1);
+ g_string_append_printf (buf, "\n <resource identifier=\"%s\" checksum=\"%s\">\n",
+ identifier_string,
+ g_quark_to_string (cache_rec->checksum));
+ g_free (identifier_string);
+
+ for (tag_iterator = cache_rec->tags; tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ tag_string = g_markup_escape_text (gimp_tag_get_name (GIMP_TAG (tag_iterator->data)), -1);
+ g_string_append_printf (buf, " <tag>%s</tag>\n", tag_string);
+ g_free (tag_string);
+ }
+ g_string_append (buf, " </resource>\n");
+ }
+ g_string_append (buf, "</tags>\n");
+
+ filename = g_build_filename (gimp_directory (), GIMP_TAG_CACHE_FILE, NULL);
+ error = NULL;
+ if (!g_file_set_contents (filename, buf->str, buf->len, &error))
+ {
+ printf ("Error while saving tag cache: %s\n", error->message);
+ g_error_free (error);
+ }
+
+ g_free (filename);
+ g_string_free (buf, TRUE);
+
+ for (iterator = saved_records; iterator;
+ iterator = g_list_next (iterator))
+ {
+ GimpTagCacheRecord *cache_rec = (GimpTagCacheRecord*) iterator->data;
+ g_list_free (cache_rec->tags);
+ g_free (cache_rec);
+ }
+ g_list_free (saved_records);
+}
+
+/**
+ * gimp_tag_cache_load:
+ * @cache: a GimpTagCache object.
+ *
+ * Loads tag cache from file.
+ **/
+void
+gimp_tag_cache_load (GimpTagCache *cache)
+{
+ gchar *filename;
+ GError *error;
+ GMarkupParser markup_parser;
+ GimpXmlParser *xml_parser;
+ GimpTagCacheParseData parse_data;
+
+ g_return_if_fail (GIMP_IS_TAG_CACHE (cache));
+
+ /* clear any previous priv->records */
+ cache->priv->records = g_array_set_size (cache->priv->records, 0);
+
+ filename = g_build_filename (gimp_directory (), GIMP_TAG_CACHE_FILE, NULL);
+ error = NULL;
+
+ parse_data.records = g_array_new (FALSE, FALSE, sizeof (GimpTagCacheRecord));
+ memset (&parse_data.current_record, 0, sizeof (GimpTagCacheRecord));
+
+ markup_parser.start_element = gimp_tag_cache_load_start_element;
+ markup_parser.end_element = gimp_tag_cache_load_end_element;
+ markup_parser.text = gimp_tag_cache_load_text;
+ markup_parser.passthrough = NULL;
+ markup_parser.error = gimp_tag_cache_load_error;
+ xml_parser = gimp_xml_parser_new (&markup_parser, &parse_data);
+ if (! gimp_xml_parser_parse_file (xml_parser, filename, &error))
+ {
+ printf ("Failed to parse tag cache.\n");
+ }
+ else
+ {
+ cache->priv->records = g_array_append_vals (cache->priv->records,
+ parse_data.records->data,
+ parse_data.records->len);
+ }
+
+ g_free (filename);
+ gimp_xml_parser_free (xml_parser);
+ g_array_free (parse_data.records, TRUE);
+}
+
+static void
+gimp_tag_cache_load_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **attribute_names,
+ const gchar **attribute_values,
+ gpointer user_data,
+ GError **error)
+{
+ GimpTagCacheParseData *parse_data = (GimpTagCacheParseData*) user_data;
+
+ if (! strcmp (element_name, "resource"))
+ {
+ const gchar *identifier;
+ const gchar *checksum;
+
+ identifier = gimp_tag_cache_attribute_name_to_value (attribute_names, attribute_values,
+ "identifier");
+ checksum = gimp_tag_cache_attribute_name_to_value (attribute_names, attribute_values,
+ "checksum");
+
+ if (! identifier)
+ {
+ g_set_error (error,
+ gimp_tag_cache_get_error_domain (),
+ 1001,
+ "Resource tag does not contain required attribute identifier.");
+ return;
+ }
+
+ memset (&parse_data->current_record, 0, sizeof (GimpTagCacheRecord));
+
+ parse_data->current_record.identifier = g_quark_from_string (identifier);
+ parse_data->current_record.checksum = g_quark_from_string (checksum);
+ }
+}
+
+static void
+gimp_tag_cache_load_end_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ gpointer user_data,
+ GError **error)
+{
+ GimpTagCacheParseData *parse_data = (GimpTagCacheParseData*) user_data;
+
+ if (strcmp (element_name, "resource") == 0)
+ {
+ parse_data->records = g_array_append_val (parse_data->records,
+ parse_data->current_record);
+ memset (&parse_data->current_record, 0, sizeof (GimpTagCacheRecord));
+ }
+}
+
+static void
+gimp_tag_cache_load_text (GMarkupParseContext *context,
+ const gchar *text,
+ gsize text_len,
+ gpointer user_data,
+ GError **error)
+{
+ GimpTagCacheParseData *parse_data = (GimpTagCacheParseData*) user_data;
+ const gchar *current_element;
+ gchar buffer[2048];
+ GimpTag *tag;
+
+ current_element = g_markup_parse_context_get_element (context);
+
+ if (current_element &&
+ strcmp (current_element, "tag") == 0)
+ {
+ if (text_len >= sizeof (buffer))
+ {
+ g_set_error (error, gimp_tag_cache_get_error_domain (), 1002,
+ "Tag value is too long.");
+ return;
+ }
+
+ memcpy (buffer, text, text_len);
+ buffer[text_len] = '\0';
+
+ tag = gimp_tag_new (buffer);
+ if (tag)
+ {
+ parse_data->current_record.tags = g_list_append (parse_data->current_record.tags,
+ tag);
+ }
+ else
+ {
+ g_warning ("dropping invalid tag '%s' from '%s'\n", buffer,
+ g_quark_to_string (parse_data->current_record.identifier));
+ }
+ }
+}
+
+static void
+gimp_tag_cache_load_error (GMarkupParseContext *context,
+ GError *error,
+ gpointer user_data)
+{
+ printf ("Tag cache parse error: %s\n", error->message);
+}
+
+static const gchar*
+gimp_tag_cache_attribute_name_to_value (const gchar **attribute_names,
+ const gchar **attribute_values,
+ const gchar *name)
+{
+ while (*attribute_names)
+ {
+ if (! strcmp (*attribute_names, name))
+ {
+ return *attribute_values;
+ }
+
+ attribute_names++;
+ attribute_values++;
+ }
+
+ return NULL;
+}
+
+static GQuark
+gimp_tag_cache_get_error_domain (void)
+{
+ return g_quark_from_static_string ("gimp-tag-cache-error-quark");
+}
Added: trunk/app/core/gimptagcache.h
==============================================================================
--- (empty file)
+++ trunk/app/core/gimptagcache.h Sat Dec 20 14:46:54 2008
@@ -0,0 +1,62 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagcache.h
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GIMP_TAG_CACHE_H__
+#define __GIMP_TAG_CACHE_H__
+
+
+#include "gimpobject.h"
+
+#define GIMP_TYPE_TAG_CACHE (gimp_tag_cache_get_type ())
+#define GIMP_TAG_CACHE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_TAG_CACHE, GimpTagCache))
+#define GIMP_TAG_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_TAG_CACHE, GimpTagCacheClass))
+#define GIMP_IS_TAG_CACHE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_TAG_CACHE))
+#define GIMP_IS_TAG_CACHE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_TAG_CACHE))
+#define GIMP_TAG_CACHE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_TAG_CACHE, GimpTagCacheClass))
+
+
+typedef struct _GimpTagCacheClass GimpTagCacheClass;
+typedef struct _GimpTagCachePriv GimpTagCachePriv;
+
+struct _GimpTagCache
+{
+ GimpObject parent_instance;
+
+ GimpTagCachePriv *priv;
+};
+
+struct _GimpTagCacheClass
+{
+ GimpObjectClass parent_class;
+};
+
+
+GType gimp_tag_cache_get_type (void) G_GNUC_CONST;
+
+GimpTagCache * gimp_tag_cache_new (void);
+
+void gimp_tag_cache_save (GimpTagCache *cache);
+void gimp_tag_cache_load (GimpTagCache *cache);
+
+void gimp_tag_cache_add_container (GimpTagCache *cache,
+ GimpContainer *container);
+
+#endif /* __GIMP_TAG_CACHE_H__ */
Modified: trunk/app/widgets/Makefile.am
==============================================================================
--- trunk/app/widgets/Makefile.am (original)
+++ trunk/app/widgets/Makefile.am Sat Dec 20 14:46:54 2008
@@ -65,6 +65,8 @@
gimpcolorpanel.h \
gimpcolorselectorpalette.c \
gimpcolorselectorpalette.h \
+ gimpcombotagentry.c \
+ gimpcombotagentry.h \
gimpcomponenteditor.c \
gimpcomponenteditor.h \
gimpcontainerbox.c \
@@ -265,6 +267,10 @@
gimpstringaction.h \
gimpstrokeeditor.c \
gimpstrokeeditor.h \
+ gimptagentry.c \
+ gimptagentry.h \
+ gimptagpopup.c \
+ gimptagpopup.h \
gimptemplateeditor.c \
gimptemplateeditor.h \
gimptemplateview.c \
Added: trunk/app/widgets/gimpcombotagentry.c
==============================================================================
--- (empty file)
+++ trunk/app/widgets/gimpcombotagentry.c Sat Dec 20 14:46:54 2008
@@ -0,0 +1,408 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpcombotagentry.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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gtk/gtk.h>
+
+#include "widgets-types.h"
+
+#include "core/gimpcontainer.h"
+#include "core/gimpfilteredcontainer.h"
+#include "core/gimpcontext.h"
+#include "core/gimpviewable.h"
+#include "core/gimptag.h"
+#include "core/gimptagged.h"
+
+#include "gimptagentry.h"
+#include "gimptagpopup.h"
+#include "gimpcombotagentry.h"
+
+static GObject* gimp_combo_tag_entry_constructor (GType type,
+ guint n_params,
+ GObjectConstructParam *params);
+static void gimp_combo_tag_entry_dispose (GObject *object);
+static gboolean gimp_combo_tag_entry_expose_event (GtkWidget *widget,
+ GdkEventExpose *event,
+ gpointer user_data);
+static gboolean gimp_combo_tag_entry_event (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer user_data);
+static void gimp_combo_tag_entry_style_set (GtkWidget *widget,
+ GtkStyle *previous_style);
+
+static void gimp_combo_tag_entry_popup_list (GimpComboTagEntry *combo_entry);
+static void gimp_combo_tag_entry_popup_destroy (GtkObject *object,
+ GimpComboTagEntry *combo_entry);
+
+static void gimp_combo_tag_entry_tag_count_changed (GimpFilteredContainer *container,
+ gint tag_count,
+ GimpComboTagEntry *combo_entry);
+
+static void gimp_combo_tag_entry_get_arrow_rect (GimpComboTagEntry *combo_entry,
+ GdkRectangle *arrow_rect);
+
+
+G_DEFINE_TYPE (GimpComboTagEntry, gimp_combo_tag_entry, GIMP_TYPE_TAG_ENTRY);
+
+#define parent_class gimp_combo_tag_entry_parent_class
+
+
+static void
+gimp_combo_tag_entry_class_init (GimpComboTagEntryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructor = gimp_combo_tag_entry_constructor;
+ object_class->dispose = gimp_combo_tag_entry_dispose;
+
+ widget_class->style_set = gimp_combo_tag_entry_style_set;
+}
+
+static void
+gimp_combo_tag_entry_init (GimpComboTagEntry *combo_entry)
+{
+ GtkBorder border;
+
+ combo_entry->popup = NULL;
+ combo_entry->focus_width = 0;
+ combo_entry->interior_focus = FALSE;
+ combo_entry->normal_item_attr = NULL;
+ combo_entry->selected_item_attr = NULL;
+ combo_entry->insensitive_item_attr = NULL;
+
+ gtk_widget_add_events (GTK_WIDGET (combo_entry),
+ GDK_BUTTON_PRESS_MASK);
+
+ if (gtk_widget_get_direction (GTK_WIDGET (combo_entry)) == GTK_TEXT_DIR_RTL)
+ {
+ border.left = 18;
+ border.right = 2;
+ }
+ else
+ {
+ border.left = 2;
+ border.right = 18;
+ }
+ border.top = 2;
+ border.bottom = 2;
+ gtk_entry_set_inner_border (GTK_ENTRY (combo_entry), &border);
+
+
+ g_signal_connect_after (combo_entry, "expose-event",
+ G_CALLBACK (gimp_combo_tag_entry_expose_event),
+ NULL);
+ g_signal_connect (combo_entry, "style-set",
+ G_CALLBACK (gimp_combo_tag_entry_style_set),
+ NULL);
+ g_signal_connect (combo_entry, "event",
+ G_CALLBACK (gimp_combo_tag_entry_event),
+ NULL);
+}
+
+static GObject*
+gimp_combo_tag_entry_constructor (GType type,
+ guint n_params,
+ GObjectConstructParam *params)
+{
+ GObject *object;
+ GimpComboTagEntry *combo_entry;
+
+ object = G_OBJECT_CLASS (parent_class)->constructor (type,
+ n_params,
+ params);
+
+ combo_entry = GIMP_COMBO_TAG_ENTRY (object);
+ combo_entry->filtered_container =
+ GIMP_TAG_ENTRY (combo_entry)->filtered_container;
+ g_object_ref (combo_entry->filtered_container);
+
+ g_signal_connect (combo_entry->filtered_container,
+ "tag-count-changed",
+ G_CALLBACK (gimp_combo_tag_entry_tag_count_changed),
+ combo_entry);
+
+ return object;
+}
+
+static void
+gimp_combo_tag_entry_dispose (GObject *object)
+{
+ GimpComboTagEntry *combo_entry = GIMP_COMBO_TAG_ENTRY (object);
+
+ if (combo_entry->normal_item_attr)
+ {
+ pango_attr_list_unref (combo_entry->normal_item_attr);
+ combo_entry->normal_item_attr = NULL;
+ }
+ if (combo_entry->selected_item_attr)
+ {
+ pango_attr_list_unref (combo_entry->selected_item_attr);
+ combo_entry->selected_item_attr = NULL;
+ }
+ if (combo_entry->insensitive_item_attr)
+ {
+ pango_attr_list_unref (combo_entry->insensitive_item_attr);
+ combo_entry->insensitive_item_attr = NULL;
+ }
+
+ if (combo_entry->filtered_container)
+ {
+ g_signal_handlers_disconnect_by_func (combo_entry->filtered_container,
+ G_CALLBACK (gimp_combo_tag_entry_tag_count_changed),
+ combo_entry);
+
+ g_object_unref (combo_entry->filtered_container);
+ combo_entry->filtered_container = NULL;
+ }
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+/**
+ * gimp_combo_tag_entry_new:
+ * @filtered_container: a filtered container to be used.
+ * @mode: tag entry mode to work in.
+ *
+ * Creates a new #GimpComboTagEntry widget which extends #GimpTagEntry by
+ * adding ability to pick tags using popup window (similar to combo box).
+ *
+ * Return value: a new #GimpComboTagEntry widget.
+ **/
+GtkWidget *
+gimp_combo_tag_entry_new (GimpFilteredContainer *filtered_container,
+ GimpTagEntryMode mode)
+{
+ GimpComboTagEntry *combo_entry;
+
+ g_return_val_if_fail (GIMP_IS_FILTERED_CONTAINER (filtered_container), NULL);
+
+ combo_entry = g_object_new (GIMP_TYPE_COMBO_TAG_ENTRY,
+ "filtered-container", filtered_container,
+ "tag-entry-mode", mode,
+ NULL);
+ return GTK_WIDGET (combo_entry);
+}
+
+static gboolean
+gimp_combo_tag_entry_expose_event (GtkWidget *widget,
+ GdkEventExpose *event,
+ gpointer user_data)
+{
+ GimpComboTagEntry *combo_entry = GIMP_COMBO_TAG_ENTRY (widget);
+ GdkRectangle arrow_rect;
+ gint tag_count;
+ gint window_width;
+ gint window_height;
+ GtkStateType arrow_state;
+
+ if (widget->window == event->window)
+ {
+ return FALSE;
+ }
+
+ gimp_combo_tag_entry_get_arrow_rect (combo_entry, &arrow_rect);
+ tag_count = gimp_filtered_container_get_tag_count (combo_entry->filtered_container);
+
+ gdk_drawable_get_size (GDK_DRAWABLE (event->window), &window_width, &window_height);
+ if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ {
+ gdk_draw_rectangle (event->window, widget->style->base_gc[widget->state],
+ TRUE, 0, 0, 14, window_height);
+ }
+ else
+ {
+ gdk_draw_rectangle (event->window, widget->style->base_gc[widget->state],
+ TRUE, window_width - 14, 0, 14, window_height);
+ }
+
+ if (tag_count > 0
+ && ! GIMP_TAG_ENTRY (combo_entry)->has_invalid_tags)
+ {
+ arrow_state = GTK_STATE_NORMAL;
+ }
+ else
+ {
+ arrow_state = GTK_STATE_INSENSITIVE;
+ }
+
+ gtk_paint_arrow (widget->style,
+ event->window, arrow_state,
+ GTK_SHADOW_NONE, &event->area, widget, NULL,
+ GTK_ARROW_DOWN, TRUE,
+ arrow_rect.x + arrow_rect.width / 2 - 4,
+ arrow_rect.y + arrow_rect.height / 2 - 4, 8, 8);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_combo_tag_entry_event (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ GimpComboTagEntry *combo_entry = GIMP_COMBO_TAG_ENTRY (widget);
+
+ if (event->type == GDK_BUTTON_PRESS)
+ {
+ GdkEventButton *button_event;
+ gint x;
+ gint y;
+ GdkRectangle arrow_rect;
+
+ button_event = (GdkEventButton *) event;
+ x = button_event->x;
+ y = button_event->y;
+
+ gimp_combo_tag_entry_get_arrow_rect (combo_entry, &arrow_rect);
+ if (x > arrow_rect.x
+ && y > arrow_rect.y
+ && x < arrow_rect.x + arrow_rect.width
+ && y < arrow_rect.y + arrow_rect.height)
+ {
+ if (! combo_entry->popup)
+ {
+ gimp_combo_tag_entry_popup_list (combo_entry);
+ }
+ else
+ {
+ gtk_widget_destroy (combo_entry->popup);
+ }
+
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+gimp_combo_tag_entry_popup_list (GimpComboTagEntry *combo_entry)
+{
+ gint tag_count;
+
+ tag_count = gimp_filtered_container_get_tag_count (combo_entry->filtered_container);
+ if (tag_count > 0
+ && ! GIMP_TAG_ENTRY (combo_entry)->has_invalid_tags)
+ {
+ combo_entry->popup = gimp_tag_popup_new (combo_entry);
+ g_signal_connect (combo_entry->popup, "destroy",
+ G_CALLBACK (gimp_combo_tag_entry_popup_destroy),
+ combo_entry);
+ gimp_tag_popup_show (GIMP_TAG_POPUP (combo_entry->popup));
+ }
+}
+
+
+static void
+gimp_combo_tag_entry_popup_destroy (GtkObject *object,
+ GimpComboTagEntry *combo_entry)
+{
+ combo_entry->popup = NULL;
+ gtk_widget_grab_focus (GTK_WIDGET (combo_entry));
+}
+
+static void
+gimp_combo_tag_entry_tag_count_changed (GimpFilteredContainer *container,
+ gint tag_count,
+ GimpComboTagEntry *combo_entry)
+{
+ gtk_widget_queue_draw (GTK_WIDGET (combo_entry));
+}
+
+static void
+gimp_combo_tag_entry_style_set (GtkWidget *widget,
+ GtkStyle *previous_style)
+{
+ GimpComboTagEntry *combo_entry = GIMP_COMBO_TAG_ENTRY (widget);
+ GtkStyle *style;
+ GdkColor color;
+ PangoAttribute *attribute;
+
+ style = widget->style;
+ if (combo_entry->normal_item_attr)
+ {
+ pango_attr_list_unref (combo_entry->normal_item_attr);
+ }
+ combo_entry->normal_item_attr = pango_attr_list_new ();
+ if (style->font_desc)
+ {
+ attribute = pango_attr_font_desc_new (style->font_desc);
+ pango_attr_list_insert (combo_entry->normal_item_attr, attribute);
+ }
+ color = style->text[GTK_STATE_NORMAL];
+ attribute = pango_attr_foreground_new (color.red, color.green, color.blue);
+ pango_attr_list_insert (combo_entry->normal_item_attr, attribute);
+
+ if (combo_entry->selected_item_attr)
+ {
+ pango_attr_list_unref (combo_entry->selected_item_attr);
+ }
+ combo_entry->selected_item_attr = pango_attr_list_copy (combo_entry->normal_item_attr);
+ color = style->text[GTK_STATE_SELECTED];
+ attribute = pango_attr_foreground_new (color.red, color.green, color.blue);
+ pango_attr_list_insert (combo_entry->selected_item_attr, attribute);
+ color = style->base[GTK_STATE_SELECTED];
+ attribute = pango_attr_background_new (color.red, color.green, color.blue);
+ pango_attr_list_insert (combo_entry->selected_item_attr, attribute);
+
+ if (combo_entry->insensitive_item_attr)
+ {
+ pango_attr_list_unref (combo_entry->insensitive_item_attr);
+ }
+ combo_entry->insensitive_item_attr = pango_attr_list_copy (combo_entry->normal_item_attr);
+ color = style->text[GTK_STATE_INSENSITIVE];
+ attribute = pango_attr_foreground_new (color.red, color.green, color.blue);
+ pango_attr_list_insert (combo_entry->insensitive_item_attr, attribute);
+ color = style->base[GTK_STATE_INSENSITIVE];
+ attribute = pango_attr_background_new (color.red, color.green, color.blue);
+ pango_attr_list_insert (combo_entry->insensitive_item_attr, attribute);
+
+ combo_entry->selected_item_color = style->base[GTK_STATE_SELECTED];
+
+ if (GTK_WIDGET_CLASS (parent_class))
+ {
+ GTK_WIDGET_CLASS (parent_class)->style_set (widget, previous_style);
+ }
+}
+
+static void
+gimp_combo_tag_entry_get_arrow_rect (GimpComboTagEntry *combo_entry,
+ GdkRectangle *arrow_rect)
+{
+ GtkWidget *widget = GTK_WIDGET (combo_entry);
+
+ if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ {
+ arrow_rect->x = widget->style->xthickness;
+ }
+ else
+ {
+ arrow_rect->x = widget->allocation.width - 16 - widget->style->xthickness * 2;
+ }
+ arrow_rect->y = 0;
+ arrow_rect->width = 12;
+ arrow_rect->height = widget->allocation.height - widget->style->ythickness * 2;
+}
+
Added: trunk/app/widgets/gimpcombotagentry.h
==============================================================================
--- (empty file)
+++ trunk/app/widgets/gimpcombotagentry.h Sat Dec 20 14:46:54 2008
@@ -0,0 +1,62 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimpcombotagentry.h
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GIMP_COMBO_TAG_ENTRY_H__
+#define __GIMP_COMBO_TAG_ENTRY_H__
+
+#include "gimptagentry.h"
+
+#define GIMP_TYPE_COMBO_TAG_ENTRY (gimp_combo_tag_entry_get_type ())
+#define GIMP_COMBO_TAG_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_COMBO_TAG_ENTRY, GimpComboTagEntry))
+#define GIMP_COMBO_TAG_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_COMBO_TAG_ENTRY, GimpComboTagEntryClass))
+#define GIMP_IS_COMBO_TAG_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_COMBO_TAG_ENTRY))
+#define GIMP_IS_COMBO_TAG_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_COMBO_TAG_ENTRY))
+#define GIMP_COMBO_TAG_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_COMBO_TAG_ENTRY, GimpComboTagEntryClass))
+
+
+typedef struct _GimpComboTagEntryClass GimpComboTagEntryClass;
+
+struct _GimpComboTagEntry
+{
+ GimpTagEntry parent_instance;
+
+ GtkWidget *popup;
+ GimpFilteredContainer *filtered_container;
+ gint focus_width;
+ PangoAttrList *normal_item_attr;
+ PangoAttrList *selected_item_attr;
+ PangoAttrList *insensitive_item_attr;
+ GdkColor selected_item_color;
+ gboolean interior_focus;
+};
+
+struct _GimpComboTagEntryClass
+{
+ GimpTagEntryClass parent_class;
+};
+
+
+GType gimp_combo_tag_entry_get_type (void) G_GNUC_CONST;
+
+GtkWidget * gimp_combo_tag_entry_new (GimpFilteredContainer *filtered_container,
+ GimpTagEntryMode mode);
+
+#endif /* __GIMP_COMBO_TAG_ENTRY_H__ */
Modified: trunk/app/widgets/gimpdatafactoryview.c
==============================================================================
--- trunk/app/widgets/gimpdatafactoryview.c (original)
+++ trunk/app/widgets/gimpdatafactoryview.c Sat Dec 20 14:46:54 2008
@@ -36,13 +36,17 @@
#include "core/gimpcontext.h"
#include "core/gimpdata.h"
#include "core/gimpdatafactory.h"
+#include "core/gimpfilteredcontainer.h"
+#include "core/gimplist.h"
#include "core/gimpmarshal.h"
+#include "gimpcombotagentry.h"
#include "gimpcontainergridview.h"
#include "gimpcontainertreeview.h"
#include "gimpcontainerview.h"
#include "gimpdatafactoryview.h"
#include "gimpdnd.h"
+#include "gimptagentry.h"
#include "gimpuimanager.h"
#include "gimpviewrenderer.h"
#include "gimpwidgets-utils.h"
@@ -54,6 +58,11 @@
{
GimpDataFactory *factory;
+ GimpContainer *tag_filtered_container;
+ GtkWidget *query_tag_entry;
+ GtkWidget *assign_tag_entry;
+ GList *selected_items;
+
GtkWidget *edit_button;
GtkWidget *new_button;
GtkWidget *duplicate_button;
@@ -64,6 +73,8 @@
static void gimp_data_factory_view_activate_item (GimpContainerEditor *editor,
GimpViewable *viewable);
+static void gimp_data_factory_view_select_item (GimpContainerEditor *editor,
+ GimpViewable *viewable);
static void gimp_data_factory_view_tree_name_edited (GtkCellRendererText *cell,
const gchar *path,
const gchar *name,
@@ -81,6 +92,7 @@
{
GimpContainerEditorClass *editor_class = GIMP_CONTAINER_EDITOR_CLASS (klass);
+ editor_class->select_item = gimp_data_factory_view_select_item;
editor_class->activate_item = gimp_data_factory_view_activate_item;
g_type_class_add_private (klass, sizeof (GimpDataFactoryViewPriv));
@@ -92,11 +104,16 @@
view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
GIMP_TYPE_DATA_FACTORY_VIEW,
GimpDataFactoryViewPriv);
- view->priv->edit_button = NULL;
- view->priv->new_button = NULL;
- view->priv->duplicate_button = NULL;
- view->priv->delete_button = NULL;
- view->priv->refresh_button = NULL;
+
+ view->priv->tag_filtered_container = NULL;
+ view->priv->query_tag_entry = NULL;
+ view->priv->assign_tag_entry = NULL;
+ view->priv->selected_items = NULL;
+ view->priv->edit_button = NULL;
+ view->priv->new_button = NULL;
+ view->priv->duplicate_button = NULL;
+ view->priv->delete_button = NULL;
+ view->priv->refresh_button = NULL;
}
GtkWidget *
@@ -207,9 +224,13 @@
factory_view->priv->factory = factory;
+ factory_view->priv->tag_filtered_container =
+ gimp_filtered_container_new (gimp_data_factory_get_container (factory),
+ (GCompareFunc) gimp_data_compare);
+
if (! gimp_container_editor_construct (GIMP_CONTAINER_EDITOR (factory_view),
view_type,
- gimp_data_factory_get_container (factory), context,
+ factory_view->priv->tag_filtered_container, context,
view_size, view_border_width,
menu_factory, menu_identifier,
ui_identifier))
@@ -263,6 +284,30 @@
str, NULL);
g_free (str);
+ /* Query tag entry */
+ factory_view->priv->query_tag_entry =
+ gimp_combo_tag_entry_new (GIMP_FILTERED_CONTAINER (factory_view->priv->tag_filtered_container),
+ GIMP_TAG_ENTRY_MODE_QUERY);
+ gtk_widget_show (factory_view->priv->query_tag_entry);
+ gtk_box_pack_start (GTK_BOX (editor->view),
+ factory_view->priv->query_tag_entry,
+ FALSE, FALSE, 0);
+ gtk_box_reorder_child (GTK_BOX (editor->view),
+ factory_view->priv->query_tag_entry, 0);
+
+ /* Assign tag entry */
+ factory_view->priv->assign_tag_entry =
+ gimp_combo_tag_entry_new (GIMP_FILTERED_CONTAINER (factory_view->priv->tag_filtered_container),
+ GIMP_TAG_ENTRY_MODE_ASSIGN);
+ gimp_tag_entry_set_selected_items (GIMP_TAG_ENTRY (factory_view->priv->assign_tag_entry),
+ factory_view->priv->selected_items);
+ g_list_free (factory_view->priv->selected_items);
+ factory_view->priv->selected_items = NULL;
+ gtk_widget_show (factory_view->priv->assign_tag_entry);
+ gtk_box_pack_start (GTK_BOX (editor->view),
+ factory_view->priv->assign_tag_entry,
+ FALSE, FALSE, 0);
+
gimp_container_view_enable_dnd (editor->view,
GTK_BUTTON (factory_view->priv->edit_button),
gimp_container_get_children_type (gimp_data_factory_get_container (factory)));
@@ -279,6 +324,33 @@
}
static void
+gimp_data_factory_view_select_item (GimpContainerEditor *editor,
+ GimpViewable *viewable)
+{
+ GimpDataFactoryView *view = GIMP_DATA_FACTORY_VIEW (editor);
+
+ if (GIMP_CONTAINER_EDITOR_CLASS (parent_class)->select_item)
+ GIMP_CONTAINER_EDITOR_CLASS (parent_class)->select_item (editor, viewable);
+
+ if (view->priv->assign_tag_entry)
+ {
+ GList *active_items = NULL;
+
+ if (viewable)
+ {
+ active_items = g_list_append (active_items, viewable);
+ }
+ gimp_tag_entry_set_selected_items (GIMP_TAG_ENTRY (view->priv->assign_tag_entry),
+ active_items);
+ g_list_free (active_items);
+ }
+ else
+ {
+ view->priv->selected_items = g_list_append (view->priv->selected_items, viewable);
+ }
+}
+
+static void
gimp_data_factory_view_activate_item (GimpContainerEditor *editor,
GimpViewable *viewable)
{
Added: trunk/app/widgets/gimptagentry.c
==============================================================================
--- (empty file)
+++ trunk/app/widgets/gimptagentry.c Sat Dec 20 14:46:54 2008
@@ -0,0 +1,2129 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagentry.c
+ * Copyright (C) 2008 Aurimas JuÅka <aurisj svn gnome org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "widgets-types.h"
+
+#include "core/gimp-utils.h"
+#include "core/gimpcontainer.h"
+#include "core/gimpcontext.h"
+#include "core/gimpfilteredcontainer.h"
+#include "core/gimptag.h"
+#include "core/gimptagged.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 GimpTagSearchDir_
+{
+ TAG_SEARCH_NONE,
+ TAG_SEARCH_LEFT,
+ TAG_SEARCH_RIGHT,
+} GimpTagSearchDir;
+
+enum
+{
+ PROP_0,
+
+ PROP_FILTERED_CONTAINER,
+ PROP_TAG_ENTRY_MODE,
+};
+
+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_dispose (GObject *object);
+static void gimp_tag_entry_activate (GtkEntry *entry,
+ gpointer unused);
+static void gimp_tag_entry_changed (GtkEntry *entry,
+ gpointer unused);
+static void gimp_tag_entry_insert_text (GtkEditable *editable,
+ gchar *new_text,
+ gint text_length,
+ gint *position,
+ gpointer user_data);
+static void gimp_tag_entry_delete_text (GtkEditable *editable,
+ gint start_pos,
+ gint end_pos,
+ gpointer user_data);
+static gboolean gimp_tag_entry_focus_in (GtkWidget *widget,
+ GdkEventFocus *event,
+ gpointer user_data);
+static gboolean gimp_tag_entry_focus_out (GtkWidget *widget,
+ GdkEventFocus *event,
+ gpointer user_data);
+static void gimp_tag_entry_container_changed (GimpContainer *container,
+ GimpObject *object,
+ GimpTagEntry *tag_entry);
+static gboolean gimp_tag_entry_button_release (GtkWidget *widget,
+ GdkEventButton *event);
+static gboolean gimp_tag_entry_key_press (GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static gboolean gimp_tag_entry_query_tag (GimpTagEntry *entry);
+
+static void gimp_tag_entry_assign_tags (GimpTagEntry *tag_entry);
+static void gimp_tag_entry_item_set_tags (GimpTagged *entry,
+ GList *tags);
+static void gimp_tag_entry_load_selection (GimpTagEntry *tag_entry,
+ gboolean sort);
+
+static gchar* gimp_tag_entry_get_completion_prefix (GimpTagEntry *entry);
+static GList * gimp_tag_entry_get_completion_candidates (GimpTagEntry *tag_entry,
+ gchar **used_tags,
+ gchar *prefix);
+static gchar * gimp_tag_entry_get_completion_string (GimpTagEntry *tag_entry,
+ GList *candidates,
+ gchar *prefix);
+static gboolean gimp_tag_entry_auto_complete (GimpTagEntry *tag_entry);
+
+static void gimp_tag_entry_toggle_desc (GimpTagEntry *widget,
+ gboolean show);
+static gboolean gimp_tag_entry_expose (GtkWidget *widget,
+ GdkEventExpose *event,
+ gpointer user_data);
+static void gimp_tag_entry_commit_region (GString *tags,
+ GString *mask);
+static void gimp_tag_entry_commit_tags (GimpTagEntry *tag_entry);
+static gboolean gimp_tag_entry_commit_source_func (GimpTagEntry *tag_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 *tag_entry);
+
+static gboolean gimp_tag_entry_add_to_recent (GimpTagEntry *tag_entry,
+ const gchar *tags_string,
+ gboolean to_front);
+
+static void gimp_tag_entry_next_tag (GimpTagEntry *tag_entry,
+ gboolean select);
+static void gimp_tag_entry_previous_tag (GimpTagEntry *tag_entry,
+ gboolean select);
+
+static void gimp_tag_entry_select_for_deletion (GimpTagEntry *tag_entry,
+ GimpTagSearchDir search_dir);
+static gboolean gimp_tag_entry_strip_extra_whitespace (GimpTagEntry *tag_entry);
+
+
+GType
+gimp_tag_entry_mode_get_type (void)
+{
+ static const GEnumValue values[] =
+ {
+ { GIMP_TAG_ENTRY_MODE_QUERY, "GIMP_TAG_ENTRY_MODE_QUERY", "query" },
+ { GIMP_TAG_ENTRY_MODE_ASSIGN, "GIMP_TAG_ENTRY_MODE_ASSIGN", "assign" },
+ { 0, NULL, NULL }
+ };
+
+ static const GimpEnumDesc descs[] =
+ {
+ { GIMP_TAG_ENTRY_MODE_QUERY, N_("Query"), NULL },
+ { GIMP_TAG_ENTRY_MODE_ASSIGN, N_("Assign"), NULL },
+ { 0, NULL, NULL }
+ };
+
+ static GType type = 0;
+
+ if (! type)
+ {
+ type = g_enum_register_static ("GimpTagEntryMode", values);
+ gimp_enum_set_value_descriptions (type, descs);
+ }
+
+ return type;
+}
+
+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_FILTERED_CONTAINER,
+ g_param_spec_object ("filtered-container",
+ ("Filtered container"),
+ ("The Filtered container"),
+ GIMP_TYPE_FILTERED_CONTAINER,
+ G_PARAM_CONSTRUCT_ONLY
+ | G_PARAM_WRITABLE
+ | G_PARAM_READABLE));
+
+ g_object_class_install_property (object_class,
+ PROP_TAG_ENTRY_MODE,
+ g_param_spec_enum ("tag-entry-mode",
+ ("Working mode"),
+ ("Mode in which to work."),
+ GIMP_TYPE_TAG_ENTRY_MODE,
+ GIMP_TAG_ENTRY_MODE_QUERY,
+ G_PARAM_CONSTRUCT_ONLY
+ | G_PARAM_WRITABLE
+ | G_PARAM_READABLE));
+}
+
+static void
+gimp_tag_entry_init (GimpTagEntry *entry)
+{
+ entry->filtered_container = NULL;
+ entry->selected_items = 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 *tag_entry = GIMP_TAG_ENTRY (object);
+
+ if (tag_entry->selected_items)
+ {
+ g_list_free (tag_entry->selected_items);
+ tag_entry->selected_items = NULL;
+ }
+
+ if (tag_entry->recent_list)
+ {
+ g_list_foreach (tag_entry->recent_list, (GFunc) g_free, NULL);
+ g_list_free (tag_entry->recent_list);
+ tag_entry->recent_list = NULL;
+ }
+
+ if (tag_entry->filtered_container)
+ {
+ g_signal_handlers_disconnect_by_func (tag_entry->filtered_container,
+ gimp_tag_entry_container_changed,
+ tag_entry);
+ g_object_unref (tag_entry->filtered_container);
+ tag_entry->filtered_container = NULL;
+ }
+
+ if (tag_entry->mask)
+ {
+ g_string_free (tag_entry->mask, TRUE);
+ tag_entry->mask = NULL;
+ }
+
+ 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 *tag_entry = GIMP_TAG_ENTRY (object);
+
+ switch (property_id)
+ {
+ case PROP_FILTERED_CONTAINER:
+ tag_entry->filtered_container = g_value_get_object (value);
+ g_assert (GIMP_IS_FILTERED_CONTAINER (tag_entry->filtered_container));
+ g_object_ref (tag_entry->filtered_container);
+ g_signal_connect (tag_entry->filtered_container, "add",
+ G_CALLBACK (gimp_tag_entry_container_changed),
+ tag_entry);
+ g_signal_connect (tag_entry->filtered_container, "remove",
+ G_CALLBACK (gimp_tag_entry_container_changed),
+ tag_entry);
+ break;
+
+ case PROP_TAG_ENTRY_MODE:
+ tag_entry->mode = g_value_get_enum (value);
+ gimp_tag_entry_toggle_desc (tag_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 *tag_entry = GIMP_TAG_ENTRY (object);
+
+ switch (property_id)
+ {
+ case PROP_FILTERED_CONTAINER:
+ g_value_set_object (value, tag_entry->filtered_container);
+ break;
+
+ case PROP_TAG_ENTRY_MODE:
+ g_value_set_enum (value, tag_entry->mode);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/**
+ * gimp_tag_entry_new:
+ * @filtered_container: a #GimpFilteredContainer object
+ * @mode: #GimpTagEntryMode to work in.
+ *
+ * #GimpTagEntry is a widget which can query and assign tags to tagged objects.
+ * When operating in query mode, @filtered_container is kept up to date with
+ * tags selected. When operating in assignment mode, tags are assigned to
+ * objects selected and visible in @filtered_container.
+ *
+ * Return value: a new GimpTagEntry widget.
+ **/
+GtkWidget *
+gimp_tag_entry_new (GimpFilteredContainer *filtered_container,
+ GimpTagEntryMode mode)
+{
+ GimpTagEntry *entry;
+
+ g_return_val_if_fail (GIMP_IS_FILTERED_CONTAINER (filtered_container),
+ NULL);
+
+ entry = g_object_new (GIMP_TYPE_TAG_ENTRY,
+ "filtered-container", filtered_container,
+ "tag-entry-mode", mode,
+ NULL);
+ return GTK_WIDGET (entry);
+}
+
+static void
+gimp_tag_entry_activate (GtkEntry *entry,
+ gpointer unused)
+{
+ GimpTagEntry *tag_entry;
+ gint selection_start;
+ gint selection_end;
+ GList *iterator;
+
+ tag_entry = GIMP_TAG_ENTRY (entry);
+
+ 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 (iterator = tag_entry->selected_items; iterator;
+ iterator = g_list_next (iterator))
+ {
+ if (gimp_container_have (GIMP_CONTAINER (tag_entry->filtered_container),
+ GIMP_OBJECT(iterator->data)))
+ {
+ break;
+ }
+ }
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN
+ && iterator)
+ {
+ gimp_tag_entry_assign_tags (GIMP_TAG_ENTRY (entry));
+ }
+}
+
+/**
+ * gimp_tag_entry_set_tag_string:
+ * @tag_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 peformed.
+ **/
+void
+gimp_tag_entry_set_tag_string (GimpTagEntry *tag_entry,
+ const gchar *tag_string)
+{
+ g_return_if_fail (GIMP_IS_TAG_ENTRY (tag_entry));
+
+ tag_entry->internal_operation++;
+ gtk_entry_set_text (GTK_ENTRY (tag_entry), tag_string);
+ gtk_editable_set_position (GTK_EDITABLE (tag_entry), -1);
+ tag_entry->internal_operation--;
+ gimp_tag_entry_commit_tags (tag_entry);
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ gimp_tag_entry_assign_tags (tag_entry);
+ }
+}
+
+static void
+gimp_tag_entry_changed (GtkEntry *entry,
+ gpointer unused)
+{
+ 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_pending)
+ {
+ tag_entry->tag_query_pending = TRUE;
+ g_idle_add ((GSourceFunc)gimp_tag_entry_query_tag,
+ GIMP_TAG_ENTRY (entry));
+ }
+}
+
+static void
+gimp_tag_entry_insert_text (GtkEditable *editable,
+ gchar *new_text,
+ gint text_length,
+ gint *position,
+ gpointer user_data)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (editable);
+ const gchar *entry_text;
+ gboolean is_tag[2];
+ gint i;
+ gint insert_pos = *position;
+
+ entry_text = gtk_entry_get_text (GTK_ENTRY (editable));
+
+ if (! tag_entry->internal_operation)
+ {
+ /* suppress tag queries until auto completion runs */
+ tag_entry->suppress_tag_query++;
+ }
+
+ is_tag[0] = FALSE;
+ if (*position > 0)
+ {
+ is_tag[0] = (tag_entry->mask->str[*position - 1] == 't' || tag_entry->mask->str[*position - 1] == 's');
+ }
+ is_tag[1] = (tag_entry->mask->str[*position] == 't' || tag_entry->mask->str[*position] == 's');
+ if (is_tag[0] && is_tag[1])
+ {
+ g_signal_stop_emission_by_name (editable, "insert_text");
+ }
+ else if (text_length > 0)
+ {
+ gunichar c = g_utf8_get_char (new_text);
+
+ if (! tag_entry->internal_operation
+ && *position > 0
+ && tag_entry->mask->str[*position - 1] == 's'
+ && ! g_unichar_isspace (c))
+ {
+ if (! tag_entry->suppress_mask_update)
+ {
+ g_string_insert_c (tag_entry->mask, *position, 'u');
+ }
+
+ g_signal_handlers_block_by_func (editable,
+ G_CALLBACK (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,
+ G_CALLBACK (gimp_tag_entry_insert_text),
+ NULL);
+
+ g_signal_stop_emission_by_name (editable, "insert_text");
+ }
+ else if (! tag_entry->internal_operation
+ && text_length == 1
+ && *position < tag_entry->mask->len
+ && tag_entry->mask->str[*position] == 't'
+ && ! g_unichar_isspace (c))
+ {
+ if (! tag_entry->suppress_mask_update)
+ {
+ g_string_insert_c (tag_entry->mask, *position, 'u');
+ }
+
+ g_signal_handlers_block_by_func (editable,
+ G_CALLBACK (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,
+ G_CALLBACK (gimp_tag_entry_insert_text),
+ NULL);
+
+ g_signal_stop_emission_by_name (editable, "insert_text");
+ }
+
+ if (! tag_entry->suppress_mask_update)
+ {
+ for (i = 0; i < text_length; i++)
+ {
+ g_string_insert_c (tag_entry->mask, insert_pos + i, 'u');
+ }
+ }
+ }
+
+ if (! tag_entry->internal_operation)
+ {
+ tag_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,
+ gpointer user_data)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (editable);
+
+ if (! tag_entry->internal_operation)
+ {
+ g_signal_handlers_block_by_func (editable,
+ gimp_tag_entry_delete_text,
+ NULL);
+
+ if (end_pos > start_pos
+ && (tag_entry->mask->str[end_pos - 1] == 't'
+ || tag_entry->mask->str[end_pos - 1] == 's'))
+ {
+ while (end_pos <= tag_entry->mask->len
+ && (tag_entry->mask->str[end_pos] == 's'))
+ {
+ end_pos++;
+ }
+ }
+
+ gtk_editable_delete_text (editable, start_pos, end_pos);
+ if (! tag_entry->suppress_mask_update)
+ {
+ g_string_erase (tag_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 (! tag_entry->suppress_mask_update)
+ {
+ g_string_erase (tag_entry->mask, start_pos, end_pos - start_pos);
+ }
+ }
+}
+
+static gboolean
+gimp_tag_entry_query_tag (GimpTagEntry *entry)
+{
+ gchar **parsed_tags;
+ gint count;
+ gint i;
+ GimpTag *tag;
+ GList *query_list = NULL;
+ gboolean has_invalid_tags;
+
+ if (entry->suppress_tag_query)
+ {
+ entry->tag_query_pending = FALSE;
+ 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)
+ {
+ 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_filtered_container_set_filter (GIMP_FILTERED_CONTAINER (entry->filtered_container),
+ query_list);
+
+ if (has_invalid_tags != entry->has_invalid_tags)
+ {
+ entry->has_invalid_tags = has_invalid_tags;
+ gtk_widget_queue_draw (GTK_WIDGET (entry));
+ }
+
+ entry->tag_query_pending = FALSE;
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_auto_complete (GimpTagEntry *tag_entry)
+{
+ gchar *completion_prefix;
+ GList *completion_candidates;
+ gint candidate_count;
+ gchar **tags;
+ gchar *completion;
+ gint start_position;
+ gint end_position;
+ GtkEntry *entry;
+
+ 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_pending = TRUE;
+ g_idle_add ((GSourceFunc)gimp_tag_entry_query_tag,
+ tag_entry);
+ }
+
+ entry = GTK_ENTRY (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 = (GimpTag *) 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)
+{
+ GList *selected_iterator = NULL;
+ GimpTagged *selected_item;
+ gchar **parsed_tags;
+ gint count;
+ gint i;
+ GimpTag *tag;
+ GList *tag_list = NULL;
+
+ parsed_tags = gimp_tag_entry_parse_tags (tag_entry);
+ count = g_strv_length (parsed_tags);
+ for (i = 0; i < count; i++)
+ {
+ tag = gimp_tag_new (parsed_tags[i]);
+ if (tag)
+ {
+ tag_list = g_list_append (tag_list, tag);
+ }
+ }
+ g_strfreev (parsed_tags);
+
+ for (selected_iterator = tag_entry->selected_items; selected_iterator;
+ selected_iterator = g_list_next (selected_iterator))
+ {
+ selected_item = GIMP_TAGGED (selected_iterator->data);
+ gimp_tag_entry_item_set_tags (selected_item, tag_list);
+ }
+ g_list_free (tag_list);
+}
+
+static void
+gimp_tag_entry_item_set_tags (GimpTagged *tagged,
+ GList *tags)
+{
+ GList *old_tags;
+ GList *tags_iterator;
+
+ old_tags = g_list_copy (gimp_tagged_get_tags (tagged));
+ for (tags_iterator = old_tags; tags_iterator;
+ tags_iterator = g_list_next (tags_iterator))
+ {
+ gimp_tagged_remove_tag (tagged, GIMP_TAG (tags_iterator->data));
+ }
+ g_list_free (old_tags);
+
+ for (tags_iterator = tags; tags_iterator;
+ tags_iterator = g_list_next (tags_iterator))
+ {
+ gimp_tagged_add_tag (tagged, GIMP_TAG (tags_iterator->data));
+ }
+}
+
+/**
+ * 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;
+
+ 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)
+{
+ GList *iterator;
+
+ 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;
+ }
+
+ tag_entry->selected_items = g_list_copy (items);
+
+ for (iterator = tag_entry->selected_items; iterator;
+ iterator = g_list_next (iterator))
+ {
+ if (gimp_tagged_get_tags (GIMP_TAGGED (iterator->data))
+ && gimp_container_have (GIMP_CONTAINER (tag_entry->filtered_container),
+ GIMP_OBJECT(iterator->data)))
+ {
+ break;
+ }
+ }
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ if (iterator)
+ {
+ gimp_tag_entry_load_selection (tag_entry, TRUE);
+ gimp_tag_entry_toggle_desc (tag_entry, FALSE);
+ }
+ else
+ {
+ tag_entry->internal_operation++;
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), 0, -1);
+ tag_entry->internal_operation--;
+ gimp_tag_entry_toggle_desc (tag_entry, TRUE);
+ }
+ }
+}
+
+static void
+gimp_tag_entry_load_selection (GimpTagEntry *tag_entry,
+ gboolean sort)
+{
+ GimpTagged *selected_item;
+ GList *tag_list;
+ GList *tag_iterator;
+ gint insert_pos;
+ GimpTag *tag;
+ gchar *text;
+
+ tag_entry->internal_operation++;
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), 0, -1);
+ tag_entry->internal_operation--;
+
+ if (! tag_entry->selected_items)
+ {
+ return;
+ }
+
+ selected_item = GIMP_TAGGED (tag_entry->selected_items->data);
+ insert_pos = 0;
+
+ tag_list = g_list_copy (gimp_tagged_get_tags (selected_item));
+ if (sort)
+ {
+ tag_list = g_list_sort (tag_list, gimp_tag_compare_func);
+ }
+ for (tag_iterator = tag_list; tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ tag = GIMP_TAG (tag_iterator->data);
+ 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);
+ }
+ g_list_free (tag_list);
+
+ gimp_tag_entry_commit_tags (tag_entry);
+}
+
+static gchar*
+gimp_tag_entry_get_completion_prefix (GimpTagEntry *entry)
+{
+ gchar *original_string;
+ gchar *prefix_start;
+ gchar *prefix;
+ gchar *cursor;
+ gint position;
+ gint i;
+ gunichar c;
+
+ 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++)
+ {
+ 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 *tag_iterator;
+ GimpTag *tag;
+ const gchar *tag_name;
+ gint i;
+ 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->filtered_container->tag_ref_counts);
+ length = g_strv_length (used_tags);
+ for (tag_iterator = all_tags; tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ tag = GIMP_TAG (tag_iterator->data);
+ tag_name = gimp_tag_get_name (tag);
+ if (g_str_has_prefix (tag_name, prefix))
+ {
+ /* 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, tag_iterator->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;
+ gunichar d;
+ gint num_chars_match;
+ gchar *completion;
+ gchar *completion_end;
+ gint completion_length;
+
+ if (! candidates)
+ {
+ return NULL;
+ }
+
+ prefix_length = strlen (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++)
+ {
+ 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,
+ gpointer user_data)
+{
+ gimp_tag_entry_toggle_desc (GIMP_TAG_ENTRY (widget), FALSE);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_focus_out (GtkWidget *widget,
+ GdkEventFocus *event,
+ gpointer user_data)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (widget);
+
+ gimp_tag_entry_commit_tags (tag_entry);
+ 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)
+{
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_ASSIGN)
+ {
+ GList *selected_iterator = tag_entry->selected_items;
+
+ for (selected_iterator = tag_entry->selected_items; selected_iterator;
+ selected_iterator = g_list_next (selected_iterator))
+ {
+ if (gimp_tagged_get_tags (GIMP_TAGGED (selected_iterator->data))
+ && gimp_container_have (GIMP_CONTAINER (tag_entry->filtered_container),
+ GIMP_OBJECT(selected_iterator->data)))
+ {
+ break;
+ }
+ }
+ if (! selected_iterator)
+ {
+ 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);
+ const gchar *display_text;
+
+ if (! (show ^ tag_entry->description_shown))
+ {
+ return;
+ }
+
+ if (tag_entry->mode == GIMP_TAG_ENTRY_MODE_QUERY)
+ {
+ display_text = GIMP_TAG_ENTRY_QUERY_DESC;
+ }
+ else
+ {
+ display_text = GIMP_TAG_ENTRY_ASSIGN_DESC;
+ }
+
+ 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,
+ gpointer user_data)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (widget);
+ PangoContext *context;
+ PangoLayout *layout;
+ PangoAttrList *attr_list;
+ PangoAttribute *attribute;
+ PangoRenderer *renderer;
+ gint layout_width;
+ gint layout_height;
+ gint window_width;
+ gint window_height;
+ gint offset;
+ const char *display_text;
+
+ /* eeeeeek */
+ if (widget->window == event->window)
+ {
+ 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;
+ }
+
+ context = gtk_widget_create_pango_context (GTK_WIDGET (widget));
+ layout = pango_layout_new (context);
+ 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);
+ GTK_IS_WIDGET (widget);
+ renderer = gdk_pango_renderer_get_default (gtk_widget_get_screen (widget));
+ gdk_pango_renderer_set_drawable (GDK_PANGO_RENDERER (renderer), event->window);
+ gdk_pango_renderer_set_gc (GDK_PANGO_RENDERER (renderer),
+ widget->style->text_gc[GTK_STATE_INSENSITIVE]);
+ pango_layout_set_text (layout, display_text, -1);
+ gdk_drawable_get_size (GDK_DRAWABLE (event->window),
+ &window_width, &window_height);
+ pango_layout_get_size (layout,
+ &layout_width, &layout_height);
+ offset = (window_height * PANGO_SCALE - layout_height) / 2;
+ if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ {
+ pango_renderer_draw_layout (renderer, layout,
+ window_width * PANGO_SCALE - layout_width - offset,
+ offset);
+ }
+ else
+ {
+ pango_renderer_draw_layout (renderer, layout, offset, offset);
+ }
+ gdk_pango_renderer_set_drawable (GDK_PANGO_RENDERER (renderer), NULL);
+ gdk_pango_renderer_set_gc (GDK_PANGO_RENDERER (renderer), NULL);
+ g_object_unref (layout);
+ g_object_unref (context);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_key_press (GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data)
+{
+ GimpTagEntry *tag_entry = GIMP_TAG_ENTRY (widget);
+ guchar c;
+
+ c = gdk_keyval_to_unicode (event->keyval);
+ if (gimp_tag_is_tag_separator (c))
+ {
+ g_idle_add ((GSourceFunc) gimp_tag_entry_commit_source_func, tag_entry);
+ return FALSE;
+ }
+
+ switch (event->keyval)
+ {
+ case GDK_Tab:
+ {
+ tag_entry->tab_completion_index++;
+ tag_entry->suppress_tag_query++;
+ g_idle_add ((GSourceFunc)gimp_tag_entry_auto_complete,
+ tag_entry);
+ }
+ return TRUE;
+
+ case GDK_Return:
+ gimp_tag_entry_commit_tags (tag_entry);
+ break;
+
+ case GDK_Left:
+ gimp_tag_entry_previous_tag (tag_entry,
+ (event->state & GDK_SHIFT_MASK) ? TRUE : FALSE);
+ return TRUE;
+
+ case GDK_Right:
+ gimp_tag_entry_next_tag (tag_entry,
+ (event->state & GDK_SHIFT_MASK) ? TRUE : FALSE);
+ return TRUE;
+
+ case GDK_BackSpace:
+ {
+ gint selection_start;
+ gint selection_end;
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry),
+ &selection_start, &selection_end);
+ if (gimp_tag_entry_select_jellybean (tag_entry,
+ selection_start, selection_end,
+ TAG_SEARCH_LEFT))
+ {
+ return TRUE;
+ }
+ else
+ {
+ gimp_tag_entry_select_for_deletion (tag_entry, TAG_SEARCH_LEFT);
+ g_idle_add ((GSourceFunc) gimp_tag_entry_strip_extra_whitespace,
+ tag_entry);
+ }
+ }
+ break;
+
+ case GDK_Delete:
+ {
+ gint selection_start;
+ gint selection_end;
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry),
+ &selection_start, &selection_end);
+ if (gimp_tag_entry_select_jellybean (tag_entry,
+ selection_start, selection_end,
+ TAG_SEARCH_RIGHT))
+ {
+ return TRUE;
+ }
+ else
+ {
+ gimp_tag_entry_select_for_deletion (tag_entry, TAG_SEARCH_RIGHT);
+ g_idle_add ((GSourceFunc) gimp_tag_entry_strip_extra_whitespace,
+ tag_entry);
+ }
+ }
+ break;
+
+ case GDK_Up:
+ case GDK_Down:
+ if (tag_entry->recent_list != NULL)
+ {
+ gchar *recent_item;
+ gchar *very_recent_item;
+
+ very_recent_item = g_strdup (gtk_entry_get_text (GTK_ENTRY (tag_entry)));
+ gimp_tag_entry_add_to_recent (tag_entry, very_recent_item, TRUE);
+ g_free (very_recent_item);
+
+ if (event->keyval == GDK_Up)
+ {
+ recent_item = (gchar *) g_list_first (tag_entry->recent_list)->data;
+ tag_entry->recent_list = g_list_remove (tag_entry->recent_list, recent_item);
+ tag_entry->recent_list = g_list_append (tag_entry->recent_list, recent_item);
+ }
+ else
+ {
+ recent_item = (gchar *) g_list_last (tag_entry->recent_list)->data;
+ tag_entry->recent_list = g_list_remove (tag_entry->recent_list, recent_item);
+ tag_entry->recent_list = g_list_prepend (tag_entry->recent_list, recent_item);
+ }
+
+ recent_item = (gchar *) g_list_first (tag_entry->recent_list)->data;
+ tag_entry->internal_operation++;
+ gtk_entry_set_text (GTK_ENTRY (tag_entry), recent_item);
+ gtk_editable_set_position (GTK_EDITABLE (tag_entry), -1);
+ tag_entry->internal_operation--;
+ }
+ return TRUE;
+
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_button_release (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ if (event->button == 1)
+ {
+ 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 *tag_entry)
+{
+ gint selection_start;
+ gint selection_end;
+ gint selection_pos = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+ gint char_count = g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (tag_entry)), -1);
+ if (selection_pos == char_count)
+ {
+ return FALSE;
+ }
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry),
+ &selection_start, &selection_end);
+ gimp_tag_entry_select_jellybean (tag_entry, selection_start, selection_end, TAG_SEARCH_NONE);
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_entry_select_jellybean (GimpTagEntry *tag_entry,
+ gint selection_start,
+ gint selection_end,
+ GimpTagSearchDir search_dir)
+{
+ gint prev_selection_start;
+ gint prev_selection_end;
+
+ if (! tag_entry->mask->len)
+ {
+ return FALSE;
+ }
+
+ if (selection_start >= tag_entry->mask->len)
+ {
+ selection_start = tag_entry->mask->len - 1;
+ selection_end = selection_start;
+ }
+
+ if (tag_entry->mask->str[selection_start] == 'u')
+ {
+ return FALSE;
+ }
+
+ switch (search_dir)
+ {
+ case TAG_SEARCH_NONE:
+ {
+ if (selection_start > 0
+ && tag_entry->mask->str[selection_start] == 's')
+ {
+ selection_start--;
+ }
+
+ if (selection_start > 0
+ && (tag_entry->mask->str[selection_start - 1] == 'w')
+ && (tag_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
+ && tag_entry->mask->str[selection_start] == 't'
+ && tag_entry->mask->str[selection_start - 1] == 'w')
+ {
+ selection_start--;
+ }
+ if ((tag_entry->mask->str[selection_start] == 'w'
+ || tag_entry->mask->str[selection_start] == 's')
+ && selection_start > 0)
+ {
+ while ((tag_entry->mask->str[selection_start] == 'w'
+ || tag_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 ((tag_entry->mask->str[selection_start] == 'w'
+ || tag_entry->mask->str[selection_start] == 's')
+ && selection_start < tag_entry->mask->len - 1)
+ {
+ while ((tag_entry->mask->str[selection_start] == 'w'
+ || tag_entry->mask->str[selection_start] == 's')
+ && selection_start < tag_entry->mask->len - 1)
+ {
+ selection_start++;
+ }
+ selection_end = selection_start + 1;
+ }
+ }
+ }
+ break;
+ }
+
+ if (selection_start < tag_entry->mask->len
+ && selection_start == selection_end)
+ {
+ selection_end = selection_start + 1;
+ }
+
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry),
+ &prev_selection_start,
+ &prev_selection_end);
+
+ if (tag_entry->mask->str[selection_start] == 't')
+ {
+ while (selection_start > 0
+ && (tag_entry->mask->str[selection_start - 1] == 't'))
+ {
+ selection_start--;
+ }
+ }
+
+ if (selection_end > selection_start
+ && (tag_entry->mask->str[selection_end - 1] == 't'))
+ {
+ while (selection_end <= tag_entry->mask->len
+ && (tag_entry->mask->str[selection_end] == 't'))
+ {
+ selection_end++;
+ }
+ }
+
+ if (search_dir == TAG_SEARCH_NONE
+ && selection_end - selection_start == 1
+ && tag_entry->mask->str[selection_start] == 'w')
+ {
+ gtk_editable_set_position (GTK_EDITABLE (tag_entry), selection_end);
+ return TRUE;
+ }
+
+ if ((selection_start != prev_selection_start
+ || selection_end != prev_selection_end)
+ && (tag_entry->mask->str[selection_start] == 't')
+ && selection_start < selection_end)
+ {
+ if (search_dir == TAG_SEARCH_LEFT)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry),
+ selection_end, selection_start);
+ }
+ else
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry),
+ selection_start, selection_end);
+ }
+
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+static gboolean
+gimp_tag_entry_add_to_recent (GimpTagEntry *tag_entry,
+ const gchar *tags_string,
+ gboolean to_front)
+{
+ gchar *recent_item = NULL;
+ GList *tags_iterator;
+ gchar *stripped_string;
+ gint stripped_length;
+
+ if (tag_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 (tag_entry->recent_list) >= GIMP_TAG_ENTRY_MAX_RECENT_ITEMS)
+ {
+ gchar *last_item = (gchar *) g_list_last (tag_entry->recent_list)->data;
+ tag_entry->recent_list = g_list_remove (tag_entry->recent_list, last_item);
+ g_free (last_item);
+ }
+
+ for (tags_iterator = tag_entry->recent_list; tags_iterator;
+ tags_iterator = g_list_next (tags_iterator))
+ {
+ if (! strcmp (tags_string, tags_iterator->data))
+ {
+ recent_item = tags_iterator->data;
+ tag_entry->recent_list = g_list_remove (tag_entry->recent_list,
+ recent_item);
+ break;
+ }
+ }
+
+ if (! recent_item)
+ {
+ recent_item = g_strdup (tags_string);
+ }
+
+ if (to_front)
+ {
+ tag_entry->recent_list = g_list_prepend (tag_entry->recent_list,
+ recent_item);
+ }
+ else
+ {
+ tag_entry->recent_list = g_list_append (tag_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)
+{
+ /* 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 *tag_entry)
+{
+ gint i;
+ gint region_start;
+ gint region_end;
+ gint position;
+ gboolean found_region;
+ gint cursor_position;
+
+ cursor_position = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+
+ do
+ {
+ found_region = FALSE;
+
+ for (i = 0; i < tag_entry->mask->len; i++)
+ {
+ if (tag_entry->mask->str[i] == 'u')
+ {
+ found_region = TRUE;
+ region_start = i;
+ region_end = i + 1;
+ for (i++; i < tag_entry->mask->len; i++)
+ {
+ if (tag_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 (tag_entry), region_start, region_end);
+ tags = g_string_new (tags_string);
+ g_free (tags_string);
+
+ mask = g_string_new_len (tag_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
+ && tag_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 <= tag_entry->mask->len
+ && tag_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 += mask->len - (region_end - region_start);
+ }
+
+ tag_entry->internal_operation++;
+ tag_entry->suppress_mask_update++;
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), region_start, region_end);
+ position = region_start;
+ gtk_editable_insert_text (GTK_EDITABLE (tag_entry), tags->str, mask->len, &position);
+ tag_entry->suppress_mask_update--;
+ tag_entry->internal_operation--;
+
+ g_string_erase (tag_entry->mask, region_start, region_end - region_start);
+ g_string_insert_len (tag_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 (tag_entry), cursor_position);
+ gimp_tag_entry_strip_extra_whitespace (tag_entry);
+}
+
+static gboolean
+gimp_tag_entry_commit_source_func (GimpTagEntry *tag_entry)
+{
+ gimp_tag_entry_commit_tags (GIMP_TAG_ENTRY (tag_entry));
+ return FALSE;
+}
+
+
+static void
+gimp_tag_entry_next_tag (GimpTagEntry *tag_entry,
+ gboolean select)
+{
+ gint position = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+ if (tag_entry->mask->str[position] != 'u')
+ {
+ while (position < tag_entry->mask->len
+ && (tag_entry->mask->str[position] != 'w'))
+ {
+ position++;
+ }
+
+ if (tag_entry->mask->str[position] == 'w')
+ {
+ position++;
+ }
+ }
+ else if (position < tag_entry->mask->len)
+ {
+ position++;
+ }
+
+ if (select)
+ {
+ gint current_position;
+ gint selection_start;
+ gint selection_end;
+
+ current_position = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry), &selection_start, &selection_end);
+ if (current_position == selection_end)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry), selection_start, position);
+ }
+ else if (current_position == selection_start)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry), selection_end, position);
+ }
+ }
+ else
+ {
+ gtk_editable_set_position (GTK_EDITABLE (tag_entry), position);
+ }
+}
+
+static void
+gimp_tag_entry_previous_tag (GimpTagEntry *tag_entry,
+ gboolean select)
+{
+ gint position = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+
+ if (position >= 1
+ && tag_entry->mask->str[position - 1] == 'w')
+ {
+ position--;
+ }
+ if (position < 1)
+ {
+ return;
+ }
+ if (tag_entry->mask->str[position - 1] != 'u')
+ {
+ while (position > 0
+ && (tag_entry->mask->str[position - 1] != 'w'))
+ {
+ if (tag_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 (tag_entry));
+ gtk_editable_get_selection_bounds (GTK_EDITABLE (tag_entry), &selection_start, &selection_end);
+ if (current_position == selection_start)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry), selection_end, position);
+ }
+ else if (current_position == selection_end)
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry), selection_start, position);
+ }
+ }
+ else
+ {
+ gtk_editable_set_position (GTK_EDITABLE (tag_entry), position);
+ }
+}
+
+static void
+gimp_tag_entry_select_for_deletion (GimpTagEntry *tag_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 (tag_entry), &start_pos, &end_pos);
+ while (start_pos > 0
+ && (tag_entry->mask->str[start_pos - 1] == 't'))
+ {
+ start_pos--;
+ }
+
+ if (end_pos > start_pos
+ && (tag_entry->mask->str[end_pos - 1] == 't'
+ || tag_entry->mask->str[end_pos - 1] == 's'))
+ {
+ while (end_pos <= tag_entry->mask->len
+ && (tag_entry->mask->str[end_pos] == 's'))
+ {
+ end_pos++;
+ }
+ }
+
+ /* ensure there is no unnecessary whitespace selected */
+ while (start_pos < end_pos
+ && tag_entry->mask->str[start_pos] == 'w')
+ {
+ start_pos++;
+ }
+ while (start_pos < end_pos
+ && tag_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 (tag_entry), end_pos, start_pos);
+ }
+ else if (end_pos > start_pos
+ && search_dir == TAG_SEARCH_RIGHT
+ && (tag_entry->mask->str[end_pos - 1] == 't'
+ || tag_entry->mask->str[end_pos - 1] == 's'))
+ {
+ gtk_editable_select_region (GTK_EDITABLE (tag_entry), start_pos, end_pos);
+ }
+}
+
+static gboolean
+gimp_tag_entry_strip_extra_whitespace (GimpTagEntry *tag_entry)
+{
+ gint i;
+ gint position;
+
+ position = gtk_editable_get_position (GTK_EDITABLE (tag_entry));
+
+ /* strip whitespace in front */
+ while (tag_entry->mask->len > 0
+ && tag_entry->mask->str[0] == 'w')
+ {
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), 0, 1);
+ }
+
+ /* strip whitespace in back */
+ while (tag_entry->mask->len > 1
+ && tag_entry->mask->str[tag_entry->mask->len - 1] == 'w'
+ && tag_entry->mask->str[tag_entry->mask->len - 2] == 'w')
+ {
+ gtk_editable_delete_text (GTK_EDITABLE (tag_entry), tag_entry->mask->len - 1, tag_entry->mask->len);
+
+ if (position == tag_entry->mask->len)
+ {
+ position--;
+ }
+ }
+
+ /* strip extra whitespace in the middle */
+ for (i = tag_entry->mask->len - 1; i > 0; i--)
+ {
+ if (tag_entry->mask->str[i] == 'w'
+ && tag_entry->mask->str[i - 1] == 'w')
+ {
+ gtk_editable_delete_text (GTK_EDITABLE (tag_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 == tag_entry->mask->len - 1
+ && tag_entry->mask->str[position] == 'w')
+ {
+ position++;
+ }
+
+ gtk_editable_set_position (GTK_EDITABLE (tag_entry), position);
+
+ return FALSE;
+}
+
Added: trunk/app/widgets/gimptagentry.h
==============================================================================
--- (empty file)
+++ trunk/app/widgets/gimptagentry.h Sat Dec 20 14:46:54 2008
@@ -0,0 +1,93 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagentry.h
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GIMP_TAG_ENTRY_H__
+#define __GIMP_TAG_ENTRY_H__
+
+
+#define GIMP_TYPE_TAG_ENTRY (gimp_tag_entry_get_type ())
+#define GIMP_TAG_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_TAG_ENTRY, GimpTagEntry))
+#define GIMP_TAG_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIMP_TYPE_TAG_ENTRY, GimpTagEntryClass))
+#define GIMP_IS_TAG_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_TAG_ENTRY))
+#define GIMP_IS_TAG_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_TAG_ENTRY))
+#define GIMP_TAG_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_TAG_ENTRY, GimpTagEntryClass))
+
+#define GIMP_TYPE_TAG_ENTRY_MODE (gimp_tag_entry_mode_get_type ())
+
+GType gimp_tag_entry_mode_get_type (void) G_GNUC_CONST;
+
+typedef enum
+{
+ GIMP_TAG_ENTRY_MODE_QUERY, /*< desc="Query" >*/
+ GIMP_TAG_ENTRY_MODE_ASSIGN, /*< desc="Assign" >*/
+} GimpTagEntryMode;
+
+typedef struct _GimpTagEntryClass GimpTagEntryClass;
+
+struct _GimpTagEntry
+{
+ GtkEntry parent_instance;
+
+ GimpFilteredContainer *filtered_container;
+ /* mask describes the meaning of each char in GimpTagEntry.
+ * It is maintained automatically on insert-text and delete-text
+ * events. If manual mask modification is desired, then
+ * suppress_mask_update must be increased before calling any
+ * function changing entry contents.
+ * Meaning of mask chars:
+ * u - undefined / unknown (just typed unparsed text)
+ * t - tag
+ * s - separator
+ * w - whitespace.
+ */
+ GString *mask;
+ GList *selected_items;
+ GList *recent_list;
+ gint tab_completion_index;
+ gint internal_operation;
+ gint suppress_mask_update;
+ gint suppress_tag_query;
+ GimpTagEntryMode mode;
+ gboolean description_shown;
+ gboolean has_invalid_tags;
+ gboolean tag_query_pending;
+};
+
+struct _GimpTagEntryClass
+{
+ GtkEntryClass parent_class;
+};
+
+
+GType gimp_tag_entry_get_type (void) G_GNUC_CONST;
+
+GtkWidget * gimp_tag_entry_new (GimpFilteredContainer *tagged_container,
+ GimpTagEntryMode mode);
+
+void gimp_tag_entry_set_selected_items (GimpTagEntry *tag_entry,
+ GList *items);
+gchar ** gimp_tag_entry_parse_tags (GimpTagEntry *entry);
+void gimp_tag_entry_set_tag_string (GimpTagEntry *tag_entry,
+ const gchar *tag_string);
+
+const gchar * gimp_tag_entry_get_separator (void);
+
+#endif /* __GIMP_TAG_ENTRY_H__ */
Added: trunk/app/widgets/gimptagpopup.c
==============================================================================
--- (empty file)
+++ trunk/app/widgets/gimptagpopup.c Sat Dec 20 14:46:54 2008
@@ -0,0 +1,1477 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagentry.c
+ * Copyright (C) 2008 Aurimas JuÅka <aurisj svn gnome org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <gtk/gtk.h>
+
+#include "widgets-types.h"
+
+#include "core/gimpcontainer.h"
+#include "core/gimpfilteredcontainer.h"
+#include "core/gimpcontext.h"
+#include "core/gimpviewable.h"
+#include "core/gimptag.h"
+#include "core/gimptagged.h"
+
+#include "gimptagentry.h"
+#include "gimptagpopup.h"
+#include "gimpcombotagentry.h"
+
+#include "gimp-intl.h"
+
+enum
+{
+ PROP_0,
+ PROP_OWNER,
+};
+
+#define MENU_SCROLL_STEP1 8
+#define MENU_SCROLL_STEP2 15
+#define MENU_SCROLL_FAST_ZONE 8
+#define MENU_SCROLL_TIMEOUT1 50
+#define MENU_SCROLL_TIMEOUT2 20
+
+#define GIMP_TAG_POPUP_MARGIN 5
+
+static GObject* gimp_tag_popup_constructor (GType type,
+ guint n_construct_params,
+ GObjectConstructParam *construct_params);
+static void gimp_tag_popup_dispose (GObject *object);
+static void gimp_tag_popup_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+static void gimp_tag_popup_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec);
+
+static gboolean gimp_tag_popup_border_expose (GtkWidget *widget,
+ GdkEventExpose *event,
+ GimpTagPopup *tag_popup);
+static gboolean gimp_tag_popup_list_expose (GtkWidget *widget,
+ GdkEventExpose *event,
+ GimpTagPopup *tag_popup);
+static gboolean gimp_tag_popup_border_event (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer user_data);
+static gboolean gimp_tag_popup_list_event (GtkWidget *widget,
+ GdkEvent *event,
+ GimpTagPopup *tag_popup);
+static void gimp_tag_popup_toggle_tag (GimpTagPopup *tag_popup,
+ PopupTagData *tag_data);
+static void gimp_tag_popup_check_can_toggle (GimpTagged *tagged,
+ GimpTagPopup *tag_popup);
+static gint gimp_tag_popup_layout_tags (GimpTagPopup *tag_popup,
+ gint width);
+static void gimp_tag_popup_do_timeout_scroll (GimpTagPopup *tag_popup,
+ gboolean touchscreen_mode);
+static gboolean gimp_tag_popup_scroll_timeout (gpointer data);
+static void gimp_tag_popup_remove_scroll_timeout (GimpTagPopup *tag_popup);
+static gboolean gimp_tag_popup_scroll_timeout_initial (gpointer data);
+static void gimp_tag_popup_start_scrolling (GimpTagPopup *tag_popup);
+static void gimp_tag_popup_stop_scrolling (GimpTagPopup *tag_popup);
+static void gimp_tag_popup_scroll_by (GimpTagPopup *tag_popup,
+ gint step);
+static void gimp_tag_popup_handle_scrolling (GimpTagPopup *tag_popup,
+ gint x,
+ gint y,
+ gboolean enter,
+ gboolean motion);
+
+static gboolean gimp_tag_popup_button_scroll (GimpTagPopup *tag_popup,
+ GdkEventButton *event);
+
+static void get_arrows_visible_area (GimpTagPopup *combo_entry,
+ GdkRectangle *border,
+ GdkRectangle *upper,
+ GdkRectangle *lower,
+ gint *arrow_space);
+static void get_arrows_sensitive_area (GimpTagPopup *tag_popup,
+ GdkRectangle *upper,
+ GdkRectangle *lower);
+
+
+G_DEFINE_TYPE (GimpTagPopup, gimp_tag_popup, GTK_TYPE_WINDOW);
+
+#define parent_class gimp_tag_popup_parent_class
+
+
+static void
+gimp_tag_popup_class_init (GimpTagPopupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructor = gimp_tag_popup_constructor;
+ object_class->dispose = gimp_tag_popup_dispose;
+ object_class->set_property = gimp_tag_popup_set_property;
+ object_class->get_property = gimp_tag_popup_get_property;
+
+ g_object_class_install_property (object_class, PROP_OWNER,
+ g_param_spec_object ("owner", NULL, NULL,
+ GIMP_TYPE_COMBO_TAG_ENTRY,
+ GIMP_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+gimp_tag_popup_init (GimpTagPopup *tag_popup)
+{
+}
+
+static GObject*
+gimp_tag_popup_constructor (GType type,
+ guint n_construct_params,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+ GimpTagPopup *popup;
+ GtkWidget *alignment;
+ GtkWidget *drawing_area;
+ GtkWidget *frame;
+ gint x;
+ gint y;
+ gint width;
+ gint height;
+ gint popup_height;
+ GHashTable *tag_hash;
+ GList *tag_list;
+ GList *tag_iterator;
+ gint i;
+ gint j;
+ gint max_height;
+ gint screen_height;
+ gchar **current_tags;
+ gint current_count;
+ const gchar *list_tag;
+ GdkRectangle popup_rects[2]; /* variants of popup placement */
+ GdkRectangle popup_rect; /* best popup rect in screen coordinates */
+
+
+ object = G_OBJECT_CLASS (parent_class)->constructor (type,
+ n_construct_params,
+ construct_params);
+ popup = GIMP_TAG_POPUP (object);
+
+ gtk_widget_add_events (GTK_WIDGET (popup),
+ GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK
+ | GDK_KEY_RELEASE_MASK
+ | GDK_SCROLL_MASK);
+ gtk_window_set_screen (GTK_WINDOW (popup),
+ gtk_widget_get_screen (GTK_WIDGET (popup->combo_entry)));
+
+ frame = gtk_frame_new (NULL);
+ gtk_container_add (GTK_CONTAINER (popup), frame);
+
+ alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
+ gtk_container_add (GTK_CONTAINER (frame), alignment);
+
+ drawing_area = gtk_drawing_area_new ();
+ gtk_widget_add_events (GTK_WIDGET (drawing_area),
+ GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK);
+ gtk_container_add (GTK_CONTAINER (alignment), drawing_area);
+
+ popup->alignment = alignment;
+ popup->drawing_area = drawing_area;
+ popup->context = gtk_widget_create_pango_context (GTK_WIDGET (popup));
+ popup->layout = pango_layout_new (popup->context);
+ popup->prelight = NULL;
+ popup->upper_arrow_state = GTK_STATE_NORMAL;
+ popup->lower_arrow_state = GTK_STATE_NORMAL;
+ gtk_widget_style_get (GTK_WIDGET (popup),
+ "scroll-arrow-vlength", &popup->scroll_arrow_height,
+ NULL);
+
+ pango_layout_set_attributes (popup->layout,
+ popup->combo_entry->normal_item_attr);
+
+ current_tags = gimp_tag_entry_parse_tags (GIMP_TAG_ENTRY (popup->combo_entry));
+ current_count = g_strv_length (current_tags);
+
+ tag_hash = popup->combo_entry->filtered_container->tag_ref_counts;
+ tag_list = g_hash_table_get_keys (tag_hash);
+ tag_list = g_list_sort (tag_list, gimp_tag_compare_func);
+ popup->tag_count = g_list_length (tag_list);
+ popup->tag_data = g_malloc (sizeof (PopupTagData) * popup->tag_count);
+ tag_iterator = tag_list;
+ for (i = 0; i < popup->tag_count; i++)
+ {
+ popup->tag_data[i].tag = GIMP_TAG (tag_iterator->data);
+ popup->tag_data[i].state = GTK_STATE_NORMAL;
+ list_tag = gimp_tag_get_name (popup->tag_data[i].tag);
+ for (j = 0; j < current_count; j++)
+ {
+ if (! strcmp (current_tags[j], list_tag))
+ {
+ popup->tag_data[i].state = GTK_STATE_SELECTED;
+ break;
+ }
+ }
+ tag_iterator = g_list_next (tag_iterator);
+ }
+ g_list_free (tag_list);
+ g_strfreev (current_tags);
+
+ if (GIMP_TAG_ENTRY (popup->combo_entry)->mode == GIMP_TAG_ENTRY_MODE_QUERY)
+ {
+ for (i = 0; i < popup->tag_count; i++)
+ {
+ if (popup->tag_data[i].state != GTK_STATE_SELECTED)
+ {
+ popup->tag_data[i].state = GTK_STATE_INSENSITIVE;
+ }
+ }
+ gimp_container_foreach (GIMP_CONTAINER (popup->combo_entry->filtered_container),
+ (GFunc) gimp_tag_popup_check_can_toggle, popup);
+ }
+
+ width = GTK_WIDGET (popup->combo_entry)->allocation.width - frame->style->xthickness * 2;
+ height = gimp_tag_popup_layout_tags (popup, width);
+ gdk_window_get_origin (GTK_WIDGET (popup->combo_entry)->window, &x, &y);
+ max_height = GTK_WIDGET (popup->combo_entry)->allocation.height * 7;
+ screen_height = gdk_screen_get_height (gtk_widget_get_screen (GTK_WIDGET (popup->combo_entry)));
+ height += frame->style->ythickness * 2;
+ popup_height = height;
+ popup_rects[0].x = x;
+ popup_rects[0].y = 0;
+ popup_rects[0].width = GTK_WIDGET (popup->combo_entry)->allocation.width;
+ popup_rects[0].height = y + GTK_WIDGET (popup->combo_entry)->allocation.height;
+ popup_rects[1].x = popup_rects[0].x;
+ popup_rects[1].y = y;
+ popup_rects[1].width = popup_rects[0].width;
+ popup_rects[1].height = screen_height - popup_rects[0].height;
+ if (popup_rects[0].height >= popup_height)
+ {
+ popup_rect = popup_rects[0];
+ popup_rect.y += popup_rects[0].height - popup_height;
+ popup_rect.height = popup_height;
+ }
+ else if (popup_rects[1].height >= popup_height)
+ {
+ popup_rect = popup_rects[1];
+ popup_rect.height = popup_height;
+ }
+ else
+ {
+ if (popup_rects[0].height >= popup_rects[1].height)
+ {
+ popup_rect = popup_rects[0];
+ popup_rect.y += popup->scroll_arrow_height + frame->style->ythickness;
+ }
+ else
+ {
+ popup_rect = popup_rects[1];
+ popup_rect.y -= popup->scroll_arrow_height + frame->style->ythickness;
+ }
+
+ popup->arrows_visible = TRUE;
+ popup->upper_arrow_state = GTK_STATE_INSENSITIVE;
+ gtk_alignment_set_padding (GTK_ALIGNMENT (alignment),
+ popup->scroll_arrow_height + 2,
+ popup->scroll_arrow_height + 2, 0, 0);
+ popup_height = popup_rect.height - popup->scroll_arrow_height * 2 + 4;
+ popup->scroll_height = height - popup_rect.height;
+ popup->scroll_y = 0;
+ popup->scroll_step = 0;
+ }
+
+ drawing_area->requisition.width = width;
+ drawing_area->requisition.height = popup_height;
+
+ gtk_window_move (GTK_WINDOW (popup), popup_rect.x, popup_rect.y);
+ gtk_window_resize (GTK_WINDOW (popup), popup_rect.width, popup_rect.height);
+
+ gtk_widget_show_all (GTK_WIDGET (popup));
+
+ g_signal_connect (alignment, "expose-event",
+ G_CALLBACK (gimp_tag_popup_border_expose),
+ popup);
+ g_signal_connect (popup, "event",
+ G_CALLBACK (gimp_tag_popup_border_event),
+ NULL);
+ g_signal_connect (drawing_area, "expose-event",
+ G_CALLBACK (gimp_tag_popup_list_expose),
+ popup);
+ g_signal_connect (drawing_area, "event",
+ G_CALLBACK (gimp_tag_popup_list_event),
+ popup);
+
+ return object;
+}
+
+static void
+gimp_tag_popup_dispose (GObject *object)
+{
+ GimpTagPopup *tag_popup = GIMP_TAG_POPUP (object);
+
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+
+ if (tag_popup->combo_entry)
+ {
+ g_object_unref (tag_popup->combo_entry);
+ tag_popup->combo_entry = NULL;
+ }
+
+ if (tag_popup->layout)
+ {
+ g_object_unref (tag_popup->layout);
+ tag_popup->layout = NULL;
+ }
+
+ if (tag_popup->context)
+ {
+ g_object_unref (tag_popup->context);
+ tag_popup->context = NULL;
+ }
+
+ if (tag_popup->close_rectangles)
+ {
+ g_list_foreach (tag_popup->close_rectangles, (GFunc) g_free, NULL);
+ g_list_free (tag_popup->close_rectangles);
+ tag_popup->close_rectangles = NULL;
+ }
+
+ g_free (tag_popup->tag_data);
+ tag_popup->tag_data = NULL;
+
+ G_OBJECT_CLASS (parent_class)->dispose (object);
+}
+
+static void
+gimp_tag_popup_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GimpTagPopup *tag_popup = GIMP_TAG_POPUP (object);
+
+ switch (property_id)
+ {
+ case PROP_OWNER:
+ {
+ tag_popup->combo_entry = g_value_get_object (value);
+ g_object_ref (tag_popup->combo_entry);
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gimp_tag_popup_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GimpTagPopup *tag_popup = GIMP_TAG_POPUP (object);
+
+ switch (property_id)
+ {
+ case PROP_OWNER:
+ g_value_set_object (value, tag_popup->combo_entry);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/**
+ * gimp_tag_popup_new:
+ * @combo_entry: #GimpComboTagEntry which is owner of the popup
+ * window.
+ *
+ * Tag popup widget is only useful for for #GimpComboTagEntry and
+ * should not be used elsewhere.
+ *
+ * Return value: a newly created #GimpTagPopup widget.
+ **/
+GtkWidget *
+gimp_tag_popup_new (GimpComboTagEntry *combo_entry)
+{
+ g_return_val_if_fail (GIMP_IS_COMBO_TAG_ENTRY (combo_entry), NULL);
+
+ return g_object_new (GIMP_TYPE_TAG_POPUP,
+ "type", GTK_WINDOW_POPUP,
+ "owner", combo_entry,
+ NULL);
+}
+
+/**
+ * gimp_tag_popup_show:
+ * @tag_popup: an instance of #GimpTagPopup
+ *
+ * Show tag popup widget. If mouse grab cannot be obtained for widget,
+ * it is destroyed.
+ **/
+void
+gimp_tag_popup_show (GimpTagPopup *popup)
+{
+
+ GdkGrabStatus grab_status;
+
+ g_return_if_fail (popup);
+
+ gtk_widget_show_all (GTK_WIDGET (popup));
+
+ gtk_grab_add (GTK_WIDGET (popup));
+ gtk_widget_grab_focus (GTK_WIDGET (popup));
+ grab_status = gdk_pointer_grab (GTK_WIDGET (popup)->window, TRUE,
+ GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK, NULL, NULL,
+ GDK_CURRENT_TIME);
+ if (grab_status != GDK_GRAB_SUCCESS)
+ {
+ /* pointer grab must be attained otherwise user would have
+ * problems closing the popup window. */
+ gtk_grab_remove (GTK_WIDGET (popup));
+ gtk_widget_destroy (GTK_WIDGET (popup));
+ }
+}
+
+static gint
+gimp_tag_popup_layout_tags (GimpTagPopup *tag_popup,
+ gint width)
+{
+ gint x;
+ gint y;
+ gint height = 0;
+ gint i;
+ gint line_height;
+ gint space_width;
+ PangoFontMetrics *font_metrics;
+
+ x = GIMP_TAG_POPUP_MARGIN;
+ y = GIMP_TAG_POPUP_MARGIN;
+ font_metrics = pango_context_get_metrics (tag_popup->context,
+ pango_context_get_font_description (tag_popup->context),
+ NULL);
+ line_height = pango_font_metrics_get_ascent (font_metrics) +
+ pango_font_metrics_get_descent (font_metrics);
+ space_width = pango_font_metrics_get_approximate_char_width (font_metrics);
+ line_height /= PANGO_SCALE;
+ space_width /= PANGO_SCALE;
+ pango_font_metrics_unref (font_metrics);
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ pango_layout_set_text (tag_popup->layout,
+ gimp_tag_get_name (tag_popup->tag_data[i].tag), -1);
+ pango_layout_get_size (tag_popup->layout,
+ &tag_popup->tag_data[i].bounds.width,
+ &tag_popup->tag_data[i].bounds.height);
+ tag_popup->tag_data[i].bounds.width /= PANGO_SCALE;
+ tag_popup->tag_data[i].bounds.height /= PANGO_SCALE;
+ if (tag_popup->tag_data[i].bounds.width + x + 3 +GIMP_TAG_POPUP_MARGIN > width)
+ {
+ if (tag_popup->tag_data[i].bounds.width + line_height + GIMP_TAG_POPUP_MARGIN < width)
+ {
+ GdkRectangle *close_rect = g_malloc (sizeof (GdkRectangle));
+ close_rect->x = x - space_width - 5;
+ close_rect->y = y;
+ close_rect->width = width - close_rect->x;
+ close_rect->height = line_height + 2;
+ tag_popup->close_rectangles = g_list_append (tag_popup->close_rectangles,
+ close_rect);
+ }
+ x = GIMP_TAG_POPUP_MARGIN;
+ y += line_height + 2;
+ }
+
+ tag_popup->tag_data[i].bounds.x = x;
+ tag_popup->tag_data[i].bounds.y = y;
+
+ x += tag_popup->tag_data[i].bounds.width + space_width + 5;
+ }
+
+ if (tag_popup->tag_count > 0
+ && (width - x) > line_height + GIMP_TAG_POPUP_MARGIN)
+ {
+ GdkRectangle *close_rect = g_malloc (sizeof (GdkRectangle));
+ close_rect->x = x - space_width - 5;
+ close_rect->y = y;
+ close_rect->width = width - close_rect->x;
+ close_rect->height = line_height + 2;
+ tag_popup->close_rectangles = g_list_append (tag_popup->close_rectangles,
+ close_rect);
+ }
+
+ if (gtk_widget_get_direction (GTK_WIDGET (tag_popup)) == GTK_TEXT_DIR_RTL)
+ {
+ GList *iterator;
+
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ PopupTagData *tag_data = &tag_popup->tag_data[i];
+ tag_data->bounds.x = width - tag_data->bounds.x - tag_data->bounds.width;
+ }
+
+ for (iterator = tag_popup->close_rectangles; iterator;
+ iterator = g_list_next (iterator))
+ {
+ GdkRectangle *rect = (GdkRectangle *) iterator->data;
+ rect->x = width - rect->x - rect->width;
+ }
+ }
+ height = y + line_height + GIMP_TAG_POPUP_MARGIN;
+
+ return height;
+}
+
+static gboolean
+gimp_tag_popup_border_expose (GtkWidget *widget,
+ GdkEventExpose *event,
+ GimpTagPopup *tag_popup)
+{
+ GdkGC *gc;
+ GdkRectangle border;
+ GdkRectangle upper;
+ GdkRectangle lower;
+ gint arrow_space;
+
+ if (event->window == widget->window)
+ {
+ gc = gdk_gc_new (GDK_DRAWABLE (widget->window));
+
+ get_arrows_visible_area (tag_popup, &border, &upper, &lower, &arrow_space);
+
+ if (event->window == widget->window)
+ {
+ gint arrow_size = 0.7 * arrow_space;
+
+ gtk_paint_box (widget->style,
+ widget->window,
+ GTK_STATE_NORMAL,
+ GTK_SHADOW_OUT,
+ &event->area, widget, "menu",
+ 0, 0, -1, -1);
+
+ if (tag_popup->arrows_visible)
+ {
+ gtk_paint_box (widget->style,
+ widget->window,
+ tag_popup->upper_arrow_state,
+ GTK_SHADOW_OUT,
+ &event->area, widget, "menu",
+ upper.x,
+ upper.y,
+ upper.width,
+ upper.height);
+
+ gtk_paint_arrow (widget->style,
+ widget->window,
+ tag_popup->upper_arrow_state,
+ GTK_SHADOW_OUT,
+ &event->area, widget, "menu_scroll_arrow_up",
+ GTK_ARROW_UP,
+ TRUE,
+ upper.x + (upper.width - arrow_size) / 2,
+ upper.y + widget->style->ythickness + (arrow_space - arrow_size) / 2,
+ arrow_size, arrow_size);
+ }
+
+ if (tag_popup->arrows_visible)
+ {
+ gtk_paint_box (widget->style,
+ widget->window,
+ tag_popup->lower_arrow_state,
+ GTK_SHADOW_OUT,
+ &event->area, widget, "menu",
+ lower.x,
+ lower.y,
+ lower.width,
+ lower.height);
+
+ gtk_paint_arrow (widget->style,
+ widget->window,
+ tag_popup->lower_arrow_state,
+ GTK_SHADOW_OUT,
+ &event->area, widget, "menu_scroll_arrow_down",
+ GTK_ARROW_DOWN,
+ TRUE,
+ lower.x + (lower.width - arrow_size) / 2,
+ lower.y + widget->style->ythickness + (arrow_space - arrow_size) / 2,
+ arrow_size, arrow_size);
+ }
+ }
+
+ g_object_unref (gc);
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_popup_border_event (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ GimpTagPopup *tag_popup = GIMP_TAG_POPUP (widget);
+
+ if (event->type == GDK_BUTTON_PRESS)
+ {
+ GdkEventButton *button_event;
+ gint x;
+ gint y;
+
+ button_event = (GdkEventButton *) event;
+
+ if (button_event->window == widget->window
+ && gimp_tag_popup_button_scroll (tag_popup, button_event))
+ {
+ return TRUE;
+ }
+
+ gdk_window_get_pointer (widget->window, &x, &y, NULL);
+
+ if (button_event->window != tag_popup->drawing_area->window
+ && (x < widget->allocation.y
+ || y < widget->allocation.x
+ || x > widget->allocation.x + widget->allocation.width
+ || y > widget->allocation.y + widget->allocation.height))
+ {
+ /* user has clicked outside the popup area,
+ * which means it should be hidden. */
+ gtk_grab_remove (widget);
+ gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
+ GDK_CURRENT_TIME);
+ gtk_widget_destroy (widget);
+ }
+ }
+ else if (event->type == GDK_MOTION_NOTIFY)
+ {
+ gint x;
+ gint y;
+
+ gdk_window_get_pointer (widget->window, &x, &y, NULL);
+ x += widget->allocation.x;
+ y += widget->allocation.y;
+ tag_popup->ignore_button_release = FALSE;
+ gimp_tag_popup_handle_scrolling (tag_popup, x, y,
+ tag_popup->timeout_id == 0, TRUE);
+ }
+ else if (event->type == GDK_BUTTON_RELEASE)
+ {
+ tag_popup->single_select_disabled = TRUE;
+
+ if (((GdkEventButton *)event)->window == widget->window
+ && ! tag_popup->ignore_button_release
+ && gimp_tag_popup_button_scroll (tag_popup, (GdkEventButton *) event))
+ {
+ return TRUE;
+ }
+ }
+ else if (event->type == GDK_GRAB_BROKEN)
+ {
+ gtk_grab_remove (widget);
+ gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
+ GDK_CURRENT_TIME);
+ gtk_widget_destroy (widget);
+ }
+ else if (event->type == GDK_KEY_PRESS)
+ {
+ gtk_widget_destroy (GTK_WIDGET (tag_popup));
+ }
+ else if (event->type == GDK_SCROLL)
+ {
+ GdkEventScroll *scroll_event = (GdkEventScroll *) event;
+
+ switch (scroll_event->direction)
+ {
+ case GDK_SCROLL_RIGHT:
+ case GDK_SCROLL_DOWN:
+ gimp_tag_popup_scroll_by (tag_popup, MENU_SCROLL_STEP2);
+ return TRUE;
+
+ case GDK_SCROLL_LEFT:
+ case GDK_SCROLL_UP:
+ gimp_tag_popup_scroll_by (tag_popup, - MENU_SCROLL_STEP2);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_popup_list_expose (GtkWidget *widget,
+ GdkEventExpose *event,
+ GimpTagPopup *tag_popup)
+{
+ GdkGC *gc;
+ PangoRenderer *renderer;
+ gint i;
+ PangoAttribute *attribute;
+ PangoAttrList *attributes;
+
+ renderer = gdk_pango_renderer_get_default (gtk_widget_get_screen (widget));
+ gdk_pango_renderer_set_gc (GDK_PANGO_RENDERER (renderer), widget->style->black_gc);
+ gdk_pango_renderer_set_drawable (GDK_PANGO_RENDERER (renderer),
+ widget->window);
+
+ gc = gdk_gc_new (GDK_DRAWABLE (widget->window));
+ gdk_gc_set_rgb_fg_color (gc, &tag_popup->combo_entry->selected_item_color);
+ gdk_gc_set_line_attributes (gc, 5, GDK_LINE_SOLID, GDK_CAP_ROUND,
+ GDK_JOIN_ROUND);
+
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ pango_layout_set_text (tag_popup->layout,
+ gimp_tag_get_name (tag_popup->tag_data[i].tag), -1);
+ if (tag_popup->tag_data[i].state == GTK_STATE_SELECTED)
+ {
+ attributes = pango_attr_list_copy (tag_popup->combo_entry->selected_item_attr);
+ }
+ else if (tag_popup->tag_data[i].state == GTK_STATE_INSENSITIVE)
+ {
+ attributes = pango_attr_list_copy (tag_popup->combo_entry->insensitive_item_attr);
+ }
+ else
+ {
+ attributes = pango_attr_list_copy (tag_popup->combo_entry->normal_item_attr);
+ }
+
+ if (&tag_popup->tag_data[i] == tag_popup->prelight
+ && tag_popup->tag_data[i].state != GTK_STATE_INSENSITIVE)
+ {
+ attribute = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
+ pango_attr_list_insert (attributes, attribute);
+ }
+
+ pango_layout_set_attributes (tag_popup->layout, attributes);
+ pango_attr_list_unref (attributes);
+
+ if (tag_popup->tag_data[i].state == GTK_STATE_SELECTED)
+ {
+ gdk_draw_rectangle (widget->window, gc, FALSE,
+ tag_popup->tag_data[i].bounds.x - 1,
+ tag_popup->tag_data[i].bounds.y - tag_popup->scroll_y + 1,
+ tag_popup->tag_data[i].bounds.width + 2,
+ tag_popup->tag_data[i].bounds.height - 2);
+ }
+ pango_renderer_draw_layout (renderer, tag_popup->layout,
+ (tag_popup->tag_data[i].bounds.x) * PANGO_SCALE,
+ (tag_popup->tag_data[i].bounds.y - tag_popup->scroll_y) * PANGO_SCALE);
+
+ if (&tag_popup->tag_data[i] == tag_popup->prelight
+ && tag_popup->tag_data[i].state != GTK_STATE_INSENSITIVE
+ && ! tag_popup->single_select_disabled)
+ {
+ gtk_paint_focus (widget->style, widget->window,
+ tag_popup->tag_data[i].state,
+ &event->area, widget, NULL,
+ tag_popup->tag_data[i].bounds.x,
+ tag_popup->tag_data[i].bounds.y - tag_popup->scroll_y,
+ tag_popup->tag_data[i].bounds.width,
+ tag_popup->tag_data[i].bounds.height);
+ }
+ }
+
+ g_object_unref (gc);
+
+ gdk_pango_renderer_set_drawable (GDK_PANGO_RENDERER (renderer), NULL);
+ gdk_pango_renderer_set_gc (GDK_PANGO_RENDERER (renderer), NULL);
+
+ return FALSE;
+}
+
+static gboolean
+gimp_tag_popup_list_event (GtkWidget *widget,
+ GdkEvent *event,
+ GimpTagPopup *tag_popup)
+{
+ if (event->type == GDK_BUTTON_PRESS)
+ {
+ GdkEventButton *button_event;
+ gint x;
+ gint y;
+ gint i;
+ GdkRectangle *bounds;
+ GimpTag *tag;
+
+ tag_popup->single_select_disabled = TRUE;
+
+ button_event = (GdkEventButton *) event;
+ x = button_event->x;
+ y = button_event->y;
+
+ y += tag_popup->scroll_y;
+
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ bounds = &tag_popup->tag_data[i].bounds;
+ if (x >= bounds->x
+ && y >= bounds->y
+ && x < bounds->x + bounds->width
+ && y < bounds->y + bounds->height)
+ {
+ tag = tag_popup->tag_data[i].tag;
+ gimp_tag_popup_toggle_tag (tag_popup,
+ &tag_popup->tag_data[i]);
+ gtk_widget_queue_draw (widget);
+ break;
+ }
+ }
+
+ if (i == tag_popup->tag_count)
+ {
+ GList *iterator;
+
+ for (iterator = tag_popup->close_rectangles; iterator;
+ iterator = g_list_next (iterator))
+ {
+ bounds = (GdkRectangle *) iterator->data;
+ if (x >= bounds->x
+ && y >= bounds->y
+ && x < bounds->x + bounds->width
+ && y < bounds->y + bounds->height)
+ {
+ gtk_widget_destroy (GTK_WIDGET (tag_popup));
+ break;
+ }
+ }
+ }
+ }
+ else if (event->type == GDK_MOTION_NOTIFY)
+ {
+ GdkEventMotion *motion_event;
+ gint x;
+ gint y;
+ gint i;
+ GdkRectangle *bounds;
+ PopupTagData *previous_prelight = tag_popup->prelight;
+
+ motion_event = (GdkEventMotion*) event;
+ x = motion_event->x;
+ y = motion_event->y;
+ y += tag_popup->scroll_y;
+
+ tag_popup->prelight = NULL;
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ bounds = &tag_popup->tag_data[i].bounds;
+ if (x >= bounds->x
+ && y >= bounds->y
+ && x < bounds->x + bounds->width
+ && y < bounds->y + bounds->height)
+ {
+ tag_popup->prelight = &tag_popup->tag_data[i];
+ break;
+ }
+ }
+
+ if (previous_prelight != tag_popup->prelight)
+ {
+ gtk_widget_queue_draw (widget);
+ }
+ }
+ else if (event->type == GDK_BUTTON_RELEASE
+ && !tag_popup->single_select_disabled)
+ {
+ GdkEventButton *button_event;
+ gint x;
+ gint y;
+ gint i;
+ GdkRectangle *bounds;
+ GimpTag *tag;
+
+ tag_popup->single_select_disabled = TRUE;
+
+ button_event = (GdkEventButton *) event;
+ x = button_event->x;
+ y = button_event->y;
+
+ y += tag_popup->scroll_y;
+
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ bounds = &tag_popup->tag_data[i].bounds;
+ if (x >= bounds->x
+ && y >= bounds->y
+ && x < bounds->x + bounds->width
+ && y < bounds->y + bounds->height)
+ {
+ tag = tag_popup->tag_data[i].tag;
+ gimp_tag_popup_toggle_tag (tag_popup,
+ &tag_popup->tag_data[i]);
+ gtk_widget_destroy (GTK_WIDGET (tag_popup));
+ break;
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+gimp_tag_popup_toggle_tag (GimpTagPopup *tag_popup,
+ PopupTagData *tag_data)
+{
+ gchar **current_tags;
+ GString *tag_str;
+ const gchar *tag;
+ gint length;
+ gint i;
+ gboolean tag_toggled_off = FALSE;
+
+ if (tag_data->state == GTK_STATE_NORMAL)
+ {
+ tag_data->state = GTK_STATE_SELECTED;
+ }
+ else if (tag_data->state == GTK_STATE_SELECTED)
+ {
+ tag_data->state = GTK_STATE_NORMAL;
+ }
+ else
+ {
+ return;
+ }
+
+ tag = gimp_tag_get_name (tag_data->tag);
+ current_tags = gimp_tag_entry_parse_tags (GIMP_TAG_ENTRY (tag_popup->combo_entry));
+ tag_str = g_string_new ("");
+ length = g_strv_length (current_tags);
+ for (i = 0; i < length; i++)
+ {
+ if (! strcmp (current_tags[i], tag))
+ {
+ tag_toggled_off = TRUE;
+ }
+ else
+ {
+ if (tag_str->len)
+ {
+ g_string_append (tag_str, gimp_tag_entry_get_separator ());
+ g_string_append_c (tag_str, ' ');
+ }
+ g_string_append (tag_str, current_tags[i]);
+ }
+ }
+
+ if (! tag_toggled_off)
+ {
+ /* this tag was not selected yet,
+ * so it needs to be toggled on. */
+ if (tag_str->len)
+ {
+ g_string_append (tag_str, gimp_tag_entry_get_separator ());
+ g_string_append_c (tag_str, ' ');
+ }
+ g_string_append (tag_str, tag);
+ }
+
+ gimp_tag_entry_set_tag_string (GIMP_TAG_ENTRY (tag_popup->combo_entry),
+ tag_str->str);
+
+ g_string_free (tag_str, TRUE);
+ g_strfreev (current_tags);
+
+ if (GIMP_TAG_ENTRY (tag_popup->combo_entry)->mode == GIMP_TAG_ENTRY_MODE_QUERY)
+ {
+ for (i = 0; i < tag_popup->tag_count; i++)
+ {
+ if (tag_popup->tag_data[i].state != GTK_STATE_SELECTED)
+ {
+ tag_popup->tag_data[i].state = GTK_STATE_INSENSITIVE;
+ }
+ }
+ gimp_container_foreach (GIMP_CONTAINER (tag_popup->combo_entry->filtered_container),
+ (GFunc) gimp_tag_popup_check_can_toggle, tag_popup);
+ }
+}
+
+static int
+gimp_tag_popup_data_compare (const void *a, const void *b)
+{
+ return gimp_tag_compare_func (GIMP_TAG (((PopupTagData *) a)->tag),
+ GIMP_TAG (((PopupTagData *) b)->tag));
+}
+
+static void
+gimp_tag_popup_check_can_toggle (GimpTagged *tagged,
+ GimpTagPopup *tag_popup)
+{
+ GList *tag_iterator;
+ PopupTagData search_key;
+ PopupTagData *search_result;
+
+ for (tag_iterator = gimp_tagged_get_tags (tagged); tag_iterator;
+ tag_iterator = g_list_next (tag_iterator))
+ {
+ search_key.tag = GIMP_TAG (tag_iterator->data);
+ search_result =
+ (PopupTagData *) bsearch (&search_key, tag_popup->tag_data, tag_popup->tag_count,
+ sizeof (PopupTagData), gimp_tag_popup_data_compare);
+ if (search_result)
+ {
+ if (search_result->state == GTK_STATE_INSENSITIVE)
+ {
+ search_result->state = GTK_STATE_NORMAL;
+ }
+ }
+ }
+}
+
+static gboolean
+gimp_tag_popup_scroll_timeout (gpointer data)
+{
+ GimpTagPopup *tag_popup;
+ gboolean touchscreen_mode;
+
+ tag_popup = (GimpTagPopup*) data;
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (tag_popup)),
+ "gtk-touchscreen-mode", &touchscreen_mode,
+ NULL);
+
+ gimp_tag_popup_do_timeout_scroll (tag_popup, touchscreen_mode);
+
+ return TRUE;
+}
+
+static void
+gimp_tag_popup_remove_scroll_timeout (GimpTagPopup *tag_popup)
+{
+ if (tag_popup->timeout_id)
+ {
+ g_source_remove (tag_popup->timeout_id);
+ tag_popup->timeout_id = 0;
+ }
+}
+
+static gboolean
+gimp_tag_popup_scroll_timeout_initial (gpointer data)
+{
+ GimpTagPopup *tag_popup;
+ guint timeout;
+ gboolean touchscreen_mode;
+
+ tag_popup = (GimpTagPopup*) (data);
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (tag_popup)),
+ "gtk-timeout-repeat", &timeout,
+ "gtk-touchscreen-mode", &touchscreen_mode,
+ NULL);
+
+ gimp_tag_popup_do_timeout_scroll (tag_popup, touchscreen_mode);
+
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+
+ tag_popup->timeout_id = gdk_threads_add_timeout (timeout,
+ gimp_tag_popup_scroll_timeout,
+ tag_popup);
+
+ return FALSE;
+}
+
+static void
+gimp_tag_popup_start_scrolling (GimpTagPopup *tag_popup)
+{
+ guint timeout;
+ gboolean touchscreen_mode;
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (tag_popup)),
+ "gtk-timeout-repeat", &timeout,
+ "gtk-touchscreen-mode", &touchscreen_mode,
+ NULL);
+
+ gimp_tag_popup_do_timeout_scroll (tag_popup, touchscreen_mode);
+
+ tag_popup->timeout_id = gdk_threads_add_timeout (timeout,
+ gimp_tag_popup_scroll_timeout_initial,
+ tag_popup);
+}
+
+static void
+gimp_tag_popup_stop_scrolling (GimpTagPopup *tag_popup)
+{
+ gboolean touchscreen_mode;
+
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (tag_popup)),
+ "gtk-touchscreen-mode", &touchscreen_mode,
+ NULL);
+
+ if (!touchscreen_mode)
+ {
+ tag_popup->upper_arrow_prelight = FALSE;
+ tag_popup->lower_arrow_prelight = FALSE;
+ }
+}
+
+static void
+gimp_tag_popup_scroll_by (GimpTagPopup *tag_popup,
+ gint step)
+{
+ gint new_scroll_y = tag_popup->scroll_y + step;
+
+ if (new_scroll_y < 0)
+ {
+ new_scroll_y = 0;
+ if (tag_popup->upper_arrow_state != GTK_STATE_INSENSITIVE)
+ {
+ gimp_tag_popup_stop_scrolling (tag_popup);
+ gtk_widget_queue_draw (GTK_WIDGET (tag_popup));
+ }
+ tag_popup->upper_arrow_state = GTK_STATE_INSENSITIVE;
+ }
+ else
+ {
+ tag_popup->upper_arrow_state = tag_popup->upper_arrow_prelight ?
+ GTK_STATE_PRELIGHT : GTK_STATE_NORMAL;
+ }
+
+ if (new_scroll_y >= tag_popup->scroll_height)
+ {
+ new_scroll_y = tag_popup->scroll_height - 1;
+ if (tag_popup->lower_arrow_state != GTK_STATE_INSENSITIVE)
+ {
+ gimp_tag_popup_stop_scrolling (tag_popup);
+ gtk_widget_queue_draw (GTK_WIDGET (tag_popup));
+ }
+ tag_popup->lower_arrow_state = GTK_STATE_INSENSITIVE;
+ }
+ else
+ {
+ tag_popup->lower_arrow_state = tag_popup->lower_arrow_prelight ?
+ GTK_STATE_PRELIGHT : GTK_STATE_NORMAL;
+ }
+
+ if (new_scroll_y != tag_popup->scroll_y)
+ {
+ tag_popup->scroll_y = new_scroll_y;
+ gdk_window_scroll (tag_popup->drawing_area->window, 0, -step);
+ }
+}
+
+static void
+gimp_tag_popup_do_timeout_scroll (GimpTagPopup *tag_popup,
+ gboolean touchscreen_mode)
+{
+ gimp_tag_popup_scroll_by (tag_popup, tag_popup->scroll_step);
+}
+
+static void
+gimp_tag_popup_handle_scrolling (GimpTagPopup *tag_popup,
+ gint x,
+ gint y,
+ gboolean enter,
+ gboolean motion)
+{
+ GdkRectangle rect;
+ gboolean in_arrow;
+ gboolean scroll_fast = FALSE;
+ gboolean touchscreen_mode;
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (tag_popup)),
+ "gtk-touchscreen-mode", &touchscreen_mode,
+ NULL);
+
+ /* upper arrow handling */
+
+ get_arrows_sensitive_area (tag_popup, &rect, NULL);
+
+ in_arrow = FALSE;
+ if (tag_popup->arrows_visible &&
+ (x >= rect.x) && (x < rect.x + rect.width) &&
+ (y >= rect.y) && (y < rect.y + rect.height))
+ {
+ in_arrow = TRUE;
+ }
+
+ if (touchscreen_mode)
+ tag_popup->upper_arrow_prelight = in_arrow;
+
+ if (tag_popup->upper_arrow_state != GTK_STATE_INSENSITIVE)
+ {
+ gboolean arrow_pressed = FALSE;
+
+ if (tag_popup->arrows_visible)
+ {
+ if (touchscreen_mode)
+ {
+ if (enter && tag_popup->upper_arrow_prelight)
+ {
+ if (tag_popup->timeout_id == 0)
+ {
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+ tag_popup->scroll_step = -MENU_SCROLL_STEP2; /* always fast */
+
+ if (!motion)
+ {
+ /* Only do stuff on click. */
+ gimp_tag_popup_start_scrolling (tag_popup);
+ arrow_pressed = TRUE;
+ }
+ }
+ else
+ {
+ arrow_pressed = TRUE;
+ }
+ }
+ else if (!enter)
+ {
+ gimp_tag_popup_stop_scrolling (tag_popup);
+ }
+ }
+ else /* !touchscreen_mode */
+ {
+ scroll_fast = (y < rect.y + MENU_SCROLL_FAST_ZONE);
+
+ if (enter && in_arrow &&
+ (!tag_popup->upper_arrow_prelight ||
+ tag_popup->scroll_fast != scroll_fast))
+ {
+ tag_popup->upper_arrow_prelight = TRUE;
+ tag_popup->scroll_fast = scroll_fast;
+
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+ tag_popup->scroll_step = scroll_fast ?
+ -MENU_SCROLL_STEP2 : -MENU_SCROLL_STEP1;
+
+ tag_popup->timeout_id =
+ gdk_threads_add_timeout (scroll_fast ?
+ MENU_SCROLL_TIMEOUT2 :
+ MENU_SCROLL_TIMEOUT1,
+ gimp_tag_popup_scroll_timeout, tag_popup);
+ }
+ else if (!enter && !in_arrow && tag_popup->upper_arrow_prelight)
+ {
+ gimp_tag_popup_stop_scrolling (tag_popup);
+ }
+ }
+ }
+
+ /* gimp_tag_popup_start_scrolling() might have hit the top of the
+ * tag_popup, so check if the button isn't insensitive before
+ * changing it to something else.
+ */
+ if (tag_popup->upper_arrow_state != GTK_STATE_INSENSITIVE)
+ {
+ GtkStateType arrow_state = GTK_STATE_NORMAL;
+
+ if (arrow_pressed)
+ arrow_state = GTK_STATE_ACTIVE;
+ else if (tag_popup->upper_arrow_prelight)
+ arrow_state = GTK_STATE_PRELIGHT;
+
+ if (arrow_state != tag_popup->upper_arrow_state)
+ {
+ tag_popup->upper_arrow_state = arrow_state;
+
+ gdk_window_invalidate_rect (GTK_WIDGET (tag_popup)->window,
+ &rect, FALSE);
+ }
+ }
+ }
+
+ /* lower arrow handling */
+
+ get_arrows_sensitive_area (tag_popup, NULL, &rect);
+
+ in_arrow = FALSE;
+ if (tag_popup->arrows_visible &&
+ (x >= rect.x) && (x < rect.x + rect.width) &&
+ (y >= rect.y) && (y < rect.y + rect.height))
+ {
+ in_arrow = TRUE;
+ }
+
+ if (touchscreen_mode)
+ tag_popup->lower_arrow_prelight = in_arrow;
+
+ if (tag_popup->lower_arrow_state != GTK_STATE_INSENSITIVE)
+ {
+ gboolean arrow_pressed = FALSE;
+
+ if (tag_popup->arrows_visible)
+ {
+ if (touchscreen_mode)
+ {
+ if (enter && tag_popup->lower_arrow_prelight)
+ {
+ if (tag_popup->timeout_id == 0)
+ {
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+ tag_popup->scroll_step = MENU_SCROLL_STEP2; /* always fast */
+
+ if (!motion)
+ {
+ /* Only do stuff on click. */
+ gimp_tag_popup_start_scrolling (tag_popup);
+ arrow_pressed = TRUE;
+ }
+ }
+ else
+ {
+ arrow_pressed = TRUE;
+ }
+ }
+ else if (!enter)
+ {
+ gimp_tag_popup_stop_scrolling (tag_popup);
+ }
+ }
+ else /* !touchscreen_mode */
+ {
+ scroll_fast = (y > rect.y + rect.height - MENU_SCROLL_FAST_ZONE);
+
+ if (enter && in_arrow &&
+ (!tag_popup->lower_arrow_prelight ||
+ tag_popup->scroll_fast != scroll_fast))
+ {
+ tag_popup->lower_arrow_prelight = TRUE;
+ tag_popup->scroll_fast = scroll_fast;
+
+ gimp_tag_popup_remove_scroll_timeout (tag_popup);
+ tag_popup->scroll_step = scroll_fast ?
+ MENU_SCROLL_STEP2 : MENU_SCROLL_STEP1;
+
+ tag_popup->timeout_id =
+ gdk_threads_add_timeout (scroll_fast ?
+ MENU_SCROLL_TIMEOUT2 :
+ MENU_SCROLL_TIMEOUT1,
+ gimp_tag_popup_scroll_timeout, tag_popup);
+ }
+ else if (!enter && !in_arrow && tag_popup->lower_arrow_prelight)
+ {
+ gimp_tag_popup_stop_scrolling (tag_popup);
+ }
+ }
+ }
+
+ /* gimp_tag_popup_start_scrolling() might have hit the bottom of the
+ * tag_popup, so check if the button isn't insensitive before
+ * changing it to something else.
+ */
+ if (tag_popup->lower_arrow_state != GTK_STATE_INSENSITIVE)
+ {
+ GtkStateType arrow_state = GTK_STATE_NORMAL;
+
+ if (arrow_pressed)
+ arrow_state = GTK_STATE_ACTIVE;
+ else if (tag_popup->lower_arrow_prelight)
+ arrow_state = GTK_STATE_PRELIGHT;
+
+ if (arrow_state != tag_popup->lower_arrow_state)
+ {
+ tag_popup->lower_arrow_state = arrow_state;
+
+ gdk_window_invalidate_rect (GTK_WIDGET (tag_popup)->window,
+ &rect, FALSE);
+ }
+ }
+ }
+}
+
+static gboolean
+gimp_tag_popup_button_scroll (GimpTagPopup *tag_popup,
+ GdkEventButton *event)
+{
+ if (tag_popup->upper_arrow_prelight
+ || tag_popup->lower_arrow_prelight)
+ {
+ gboolean touchscreen_mode;
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (tag_popup)),
+ "gtk-touchscreen-mode", &touchscreen_mode,
+ NULL);
+
+ if (touchscreen_mode)
+ gimp_tag_popup_handle_scrolling (tag_popup,
+ event->x_root, event->y_root,
+ event->type == GDK_BUTTON_PRESS,
+ FALSE);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+get_arrows_visible_area (GimpTagPopup *tag_popup,
+ GdkRectangle *border,
+ GdkRectangle *upper,
+ GdkRectangle *lower,
+ gint *arrow_space)
+{
+ GtkWidget *widget = GTK_WIDGET (tag_popup->alignment);
+ gint scroll_arrow_height = tag_popup->scroll_arrow_height;
+ guint padding_top;
+ guint padding_bottom;
+ guint padding_left;
+ guint padding_right;
+
+ gtk_alignment_get_padding (GTK_ALIGNMENT (tag_popup->alignment),
+ &padding_top, &padding_bottom,
+ &padding_left, &padding_right);
+
+ *border = widget->allocation;
+
+ upper->x = border->x + padding_left;
+ upper->y = border->y;
+ upper->width = border->width - padding_left - padding_right;
+ upper->height = padding_top;
+
+ lower->x = border->x + padding_left;
+ lower->y = border->y + border->height - padding_bottom;
+ lower->width = border->width - padding_left - padding_right;
+ lower->height = padding_bottom;
+
+ *arrow_space = scroll_arrow_height;
+}
+
+static void
+get_arrows_sensitive_area (GimpTagPopup *tag_popup,
+ GdkRectangle *upper,
+ GdkRectangle *lower)
+{
+ GdkRectangle tmp_border;
+ GdkRectangle tmp_upper;
+ GdkRectangle tmp_lower;
+ gint tmp_arrow_space;
+
+ get_arrows_visible_area (tag_popup, &tmp_border, &tmp_upper, &tmp_lower, &tmp_arrow_space);
+ if (upper)
+ {
+ *upper = tmp_upper;
+ }
+ if (lower)
+ {
+ *lower = tmp_lower;
+ }
+}
+
+
Added: trunk/app/widgets/gimptagpopup.h
==============================================================================
--- (empty file)
+++ trunk/app/widgets/gimptagpopup.h Sat Dec 20 14:46:54 2008
@@ -0,0 +1,80 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimptagpopup.h
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __GIMP_TAG_POPUP_H__
+#define __GIMP_TAG_POPUP_H__
+
+
+#define GIMP_TYPE_TAG_POPUP (gimp_tag_popup_get_type ())
+#define GIMP_TAG_POPUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIMP_TYPE_TAG_POPUP, GimpTagPopup))
+#define GIMP_IS_TAG_POPUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIMP_TYPE_TAG_POPUP))
+#define GIMP_IS_TAG_POPUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIMP_TYPE_TAG_POPUP))
+#define GIMP_TAG_POPUP_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIMP_TYPE_TAG_POPUP, GimpTagPopupClass))
+
+
+typedef struct _GimpTagPopupClass GimpTagPopupClass;
+
+typedef struct
+{
+ GimpTag *tag;
+ GdkRectangle bounds;
+ GtkStateType state;
+} PopupTagData;
+
+struct _GimpTagPopup
+{
+ GtkWindow parent_instance;
+ GimpComboTagEntry *combo_entry;
+ GtkWidget *alignment;
+ GtkWidget *drawing_area;
+ PangoContext *context;
+ PangoLayout *layout;
+ PopupTagData *tag_data;
+ PopupTagData *prelight;
+ gint tag_count;
+ GList *close_rectangles;
+ guint timeout_id;
+ gint scroll_height;
+ gint scroll_y;
+ gint scroll_step;
+ gint scroll_arrow_height;
+ gboolean scroll_fast;
+ gboolean arrows_visible;
+ gboolean ignore_button_release;
+ gboolean upper_arrow_prelight;
+ gboolean lower_arrow_prelight;
+ gboolean single_select_disabled;
+ GtkStateType upper_arrow_state;
+ GtkStateType lower_arrow_state;
+};
+
+struct _GimpTagPopupClass
+{
+ GtkWindowClass parent_class;
+};
+
+
+GType gimp_tag_popup_get_type (void) G_GNUC_CONST;
+
+GtkWidget * gimp_tag_popup_new (GimpComboTagEntry *combo_entry);
+void gimp_tag_popup_show (GimpTagPopup *popup);
+
+#endif /* __GIMP_TAG_POPUP_H__ */
Modified: trunk/app/widgets/widgets-types.h
==============================================================================
--- trunk/app/widgets/widgets-types.h (original)
+++ trunk/app/widgets/widgets-types.h Sat Dec 20 14:46:54 2008
@@ -155,6 +155,7 @@
typedef struct _GimpColorDisplayEditor GimpColorDisplayEditor;
typedef struct _GimpColorFrame GimpColorFrame;
typedef struct _GimpColorPanel GimpColorPanel;
+typedef struct _GimpComboTagEntry GimpComboTagEntry;
typedef struct _GimpControllerEditor GimpControllerEditor;
typedef struct _GimpControllerList GimpControllerList;
typedef struct _GimpCurveView GimpCurveView;
@@ -180,6 +181,8 @@
typedef struct _GimpSettingsEditor GimpSettingsEditor;
typedef struct _GimpSizeBox GimpSizeBox;
typedef struct _GimpStrokeEditor GimpStrokeEditor;
+typedef struct _GimpTagEntry GimpTagEntry;
+typedef struct _GimpTagPopup GimpTagPopup;
typedef struct _GimpTemplateEditor GimpTemplateEditor;
typedef struct _GimpThumbBox GimpThumbBox;
typedef struct _GimpUnitStore GimpUnitStore;
Modified: trunk/devel-docs/app/app-docs.sgml
==============================================================================
--- trunk/devel-docs/app/app-docs.sgml (original)
+++ trunk/devel-docs/app/app-docs.sgml Sat Dec 20 14:46:54 2008
@@ -336,6 +336,13 @@
<xi:include href="xml/gimpsamplepointundo.xml" />
</chapter>
+ <chapter id="app-core-tagging">
+ <title>The Resource Tagging System</title>
+ <xi:include href="xml/gimptag.xml" />
+ <xi:include href="xml/gimptagcache.xml" />
+ <xi:include href="xml/gimpfilteredcontainer.xml" />
+ </chapter>
+
<chapter id="app-core-misc-objects">
<title>Misc. Objects</title>
<xi:include href="xml/gimpbuffer.xml" />
@@ -859,6 +866,13 @@
<xi:include href="xml/gimpviewablebox.xml" />
</chapter>
+ <chapter id="app-widgets-resource-tagging">
+ <title>Resource Tagging Widgets</title>
+ <xi:include href="xml/gimptagentry.xml" />
+ <xi:include href="xml/gimpcombotagentry.xml" />
+ <xi:include href="xml/gimptagpopup.xml" />
+ </chapter>
+
<chapter id="app-widgets-container-editors">
<title>GimpContainerEditor Widgets</title>
<xi:include href="xml/gimpcontainereditor.xml" />
Modified: trunk/devel-docs/app/app-sections.txt
==============================================================================
--- trunk/devel-docs/app/app-sections.txt (original)
+++ trunk/devel-docs/app/app-sections.txt Sat Dec 20 14:46:54 2008
@@ -2138,13 +2138,72 @@
</SECTION>
<SECTION>
+<FILE>gimptag</FILE>
+<TITLE>GimpTag</TITLE>
+gimp_tag_new
+gimp_tag_try_new
+gimp_tag_get_name
+gimp_tag_get_hash
+gimp_tag_equals
+gimp_tag_compare_func
+gimp_tag_compare_with_string
+gimp_tag_string_make_valid
+<SUBSECTION Standard>
+GimpTagClass
+GIMP_TAG
+GIMP_IS_TAG
+GIMP_TYPE_TAG
+gimp_tag_get_type
+GIMP_TAG_CLASS
+GIMP_IS_TAG_CLASS
+GIMP_TAG_GET_CLASS
+</SECTION>
+
+<SECTION>
+<FILE>gimptagcache</FILE>
+<TITLE>GimpTagCache</TITLE>
+gimp_tag_cache_new
+gimp_tag_cache_load
+gimp_tag_cache_save
+gimp_tag_cache_add_container
+<SUBSECTION Standard>
+GimpTagCacheClass
+GIMP_TAG_CACHE
+GIMP_IS_TAG_CACHE
+GIMP_TYPE_TAG_CACHE
+gimp_tag_cache_get_type
+GIMP_TAG_CACHE_CLASS
+GIMP_IS_TAG_CACHE_CLASS
+GIMP_TAG_CACHE_GET_CLASS
+</SECTION>
+
+<SECTION>
+<FILE>gimptagentry</FILE>
+<TITLE>GimpTagEntry</TITLE>
+gimp_tag_entry_new
+gimp_tag_entry_set_selected_items
+gimp_tag_entry_parse_tags
+gimp_tag_entry_set_tag_string
+gimp_tag_entry_get_separator
+<SUBSECTION Standard>
+GimpTagEntryClass
+GIMP_TAG_ENTRY
+GIMP_IS_TAG_ENTRY
+GIMP_TYPE_TAG_ENTRY
+gimp_tag_entry_get_type
+GIMP_TAG_ENTRY_CLASS
+GIMP_IS_TAG_ENTRY_CLASS
+GIMP_TAG_ENTRY_GET_CLASS
+</SECTION>
+
+<SECTION>
<FILE>gimptagged</FILE>
<TITLE>GimpTagged</TITLE>
GimpTagged
GimpTaggedInterface
gimp_tagged_add_tag
gimp_tagged_remove_tag
-gimp_tagged_get_get_tags
+gimp_tagged_get_tags
<SUBSECTION Standard>
GIMP_TAGGED
GIMP_IS_TAGGED
@@ -2154,6 +2213,21 @@
</SECTION>
<SECTION>
+<FILE>gimptagpopup</FILE>
+<TITLE>GimpTagPopup</TITLE>
+gimp_tag_popup_new
+<SUBSECTION Standard>
+GimpTagPopupClass
+GIMP_TAG_POPUP
+GIMP_IS_TAG_POPUP
+GIMP_TYPE_TAG_POPUP
+gimp_tag_popup_get_type
+GIMP_TAG_POPUP_CLASS
+GIMP_IS_TAG_POPUP_CLASS
+GIMP_TAG_POPUP_GET_CLASS
+</SECTION>
+
+<SECTION>
<FILE>gimptemplate</FILE>
<TITLE>GimpTemplate</TITLE>
GIMP_DEFAULT_IMAGE_WIDTH
@@ -2649,6 +2723,21 @@
</SECTION>
<SECTION>
+<FILE>gimpcombotagentry</FILE>
+<TITLE>GimpComboTagEntry</TITLE>
+gimp_combo_tag_entry_new
+<SUBSECTION Standard>
+GimpComboTagEntryClass
+GIMP_COMBO_TAG_ENTRY
+GIMP_IS_COMBO_TAG_ENTRY
+GIMP_TYPE_COMBO_TAG_ENTRY
+gimp_combo_tag_entry_get_type
+GIMP_COMBO_TAG_ENTRY_CLASS
+GIMP_IS_COMBO_TAG_ENTRY_CLASS
+GIMP_COMBO_TAG_ENTRY_GET_CLASS
+</SECTION>
+
+<SECTION>
<FILE>gimpdisplay</FILE>
<TITLE>GimpDisplay</TITLE>
GimpDisplay
@@ -3538,6 +3627,24 @@
</SECTION>
<SECTION>
+<FILE>gimpfilteredcontainer</FILE>
+<TITLE>GimpFilteredContainer</TITLE>
+gimp_filtered_container_new
+gimp_filtered_container_get_filter
+gimp_filtered_container_set_filter
+gimp_filtered_container_get_tag_count
+<SUBSECTION Standard>
+GimpFilteredContainerClass
+GIMP_FILTERED_CONTAINER
+GIMP_IS_FILTERED_CONTAINER
+GIMP_TYPE_FILTERED_CONTAINER
+gimp_filtered_container_get_type
+GIMP_FILTERED_CONTAINER_CLASS
+GIMP_IS_FILTERED_CONTAINER_CLASS
+GIMP_FILTERED_CONTAINER_GET_CLASS
+</SECTION>
+
+<SECTION>
<FILE>gimpfont</FILE>
<TITLE>GimpFont</TITLE>
GimpFont
@@ -8512,9 +8619,6 @@
GIMP_COORDS_DEFAULT_WHEEL
GIMP_COORDS_DEFAULT_VALUES
GimpTattoo
-GimpTag
-gimp_tag_get_name
-gimp_tag_new
GimpInitStatusFunc
GimpObjectFilterFunc
GimpMemsizeFunc
Modified: trunk/devel-docs/app/app.types
==============================================================================
--- trunk/devel-docs/app/app.types (original)
+++ trunk/devel-docs/app/app.types Sat Dec 20 14:46:54 2008
@@ -132,6 +132,7 @@
gimp_fg_bg_view_get_type
gimp_file_dialog_get_type
gimp_file_proc_view_get_type
+gimp_filtered_container_get_type
gimp_flip_options_get_type
gimp_flip_tool_get_type
gimp_floating_sel_undo_get_type
@@ -299,6 +300,11 @@
gimp_stroke_get_type
gimp_stroke_options_get_type
gimp_sub_progress_get_type
+gimp_tag_get_type
+gimp_tag_cache_get_type
+gimp_combo_tag_entry_get_type
+gimp_tag_entry_get_type
+gimp_tag_popup_get_type
gimp_tagged_interface_get_type
gimp_template_editor_get_type
gimp_template_get_type
Added: trunk/devel-docs/tagging.txt
==============================================================================
--- (empty file)
+++ trunk/devel-docs/tagging.txt Sat Dec 20 14:46:54 2008
@@ -0,0 +1,148 @@
+=============================================================
+How resource tagging in Gimp works?
+=============================================================
+
+
+GimpTagged
+
+Tagging is not limited to a concrete class hierarchy, but any class
+implementing GimpTagged interface can be tagged. In addition to
+methods for adding/removing/enumerating tags it also requires
+GimpTagged objects to identify themselves:
+
+* gimp_tagged_get_identifier: used to get a unique identifier of
+GimpTagged object. For objects which are stored in a file it will
+usually be a filename.
+
+* gimp_tagged_get_checksum: identifier mentioned above has a problem
+that it can change during sessions (for example, user moves or renames
+a resource file). Therefore, there needs to be a way to get other
+identifier from data of the tagged object, so that tags stored between
+session could be properly remapped.
+
+
+GimpTag
+
+Tags are represented by GimpTag object. There are no limitations for
+tags names except they cannot contain a selected set of terminal
+punctuations characters (used to separate tags), no whitespace at the
+end or front and cannot begin with a reserved prefix for internal tags
+('gimp:'). These conditions are ensured when creating tag object from
+tag string. The only reason for tag creation to fail is when there are
+no characters left after applying trying to fix a tag according to the
+rules above. Tag names are displayed as user typed them (case
+sensitive), but tag comparing is done case insensitively.
+
+Tags are immutable, ie. when tag is created with one name string, it
+cannot be changed, but new tag has to be created instead.
+
+There are methods provided for convenient use with GLib: compare
+function which can be used to sort tag list and functions for storing
+tags in GHashTable.
+
+
+GimpTagCache
+
+Between sessions tags assigned to objects are stored in a cache
+file. Cache file is a simple XML file, which lists all resources and
+tags which are added to them. Resources which have no tags assigned
+are listed here too, so that when we check the cache we know that they
+have no tags assigned instead trying to find out if the resource file
+has been renamed.
+
+When session ends, list or all resources and tags they have assigned
+is constructed. Resources which were not loaded during this session,
+but had tags assigned are also added to the list (they are saved
+because they could be useful in the next session, for example, when
+temporarily disconnected network directory is reconnected). The list
+is then written to a tag cache file in user home directory.
+
+When session starts, previously saved resource and tag mapping has to
+be loaded and assigned to GimpTagged objects. First tag cache is
+loaded from file, and then containers are added (GimpContainer objects
+where contained items implement GimpTagged interface). After that,
+loaded resources are assigned tags:
+
+ If resource identifier matches identifier in cache,
+ corresponding tags are assigned to GimpTagged object.
+ Else, if the identifier is not found in tag cache,
+ attempt is made to check if resource file has been
+ moved/renamed. In such case checksum is used to match the
+ GimpTagged object with all of the records in tag cache.
+ If match is found,
+ identifier is updated in tag cache.
+ Otherwise,
+ loaded GimpTagged object is considered to be a newly
+ added resource.
+
+
+GimpFilteredContainer
+
+GimpFilteredContainer is a "view" (representation) of
+GimpContainer. What relates it to tagging, is that it can be used to
+filter GimpContainer to contain only GimpTagged objects which have
+certain tags assigned. It is automatically updated with any changes in
+GimpContainer it wraps. However, items should not be added or removed
+from this container manually as changes do not affect original
+container and would be lost when GimpFilteredContainer is
+updated. Instead, the contents should be changed by setting tag list
+which would be used to filter GimpTagged objects containing all of the
+given GimpTags.
+
+GimpFilteredContainer can use any GimpContainer as source
+container. Therefore, it is possible to use decorator design pattern
+to implement additional container views, such as view combining items
+from multiple containers.
+
+
+GimpTagEntry widget
+
+GimpTagEntry widget extends GtkEntry and is used to either assign or
+query tags depending on selected mode. The widget support various
+usability features:
+
+ * jellybeans. When tag is entered and confirmed by either separator,
+ pressing return or otherwise, it becomes a jellybean, i.e. a single
+ unit, not a bunch of characters. Navigating in GimpTagEntry,
+ deleting tags, etc can be performed much quicker. However, when tag
+ is just beeing entered (not yet confirmed), all actions operate on
+ characters as usual.
+
+ * custom auto completion is implemented in GimpTagEntry widget which
+ allows to complete tags in the middle of tag list, doesn't offer
+ already completed tags, tab cycles all possible completions, etc.
+
+ * when GimpTagEntry is empty and unused it displays description for
+ user regarding it's purpose.
+
+When operating in tag assignment mode, tags are assigned only when
+user hits return key.
+
+When operating in tag query mode, given GimpFilteredContainer is
+filtered as user types. GimpTagEntry also remembers recently used
+configurations, which can be cycled using up and down arrow keys.
+
+
+GimpComboTagEntry widget
+
+GimpComboTagEntry widget extends GimpTagEntry and adds ability to pick
+tags from a menu like list (GimpTagPopup widget).
+
+
+GimpTagPopup widget
+
+GimpTagPopup widget is used as a tag list menu from GimpComboTagEntry
+widget. It is not designed to be used with any other widget.
+
+GimpTagPopup has many visual and behavioral similarities to GtkMenu.
+In particular, it uses menu-like scrolling.
+
+GimpTagPopup implements various usability features, some of which are:
+
+ * tags which would result in empty selection of resource are made
+ insensitive.
+
+ * closing with either keyboard or pressing outside the popup area.
+
+ * underline highlighted (hovered) tags.
+
Modified: trunk/po/POTFILES.in
==============================================================================
--- trunk/po/POTFILES.in (original)
+++ trunk/po/POTFILES.in Sat Dec 20 14:46:54 2008
@@ -432,6 +432,7 @@
app/widgets/gimpsettingseditor.c
app/widgets/gimpsizebox.c
app/widgets/gimpstrokeeditor.c
+app/widgets/gimptagentry.c
app/widgets/gimptemplateeditor.c
app/widgets/gimptexteditor.c
app/widgets/gimpthumbbox.c
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]