gimp r27816 - in trunk: . app/core app/widgets devel-docs devel-docs/app po



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]