/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimptagentry.c
 * Copyright (C) 2008 Aurimas Juška <aurisj@svn.gnome.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>

#include <gegl.h>
#include <gtk/gtk.h>

#include "widgets-types.h"

#include "core/gimpcontainer.h"
#include "core/gimpcontext.h"
#include "core/gimptag.h"
#include "core/gimptagged.h"
#include "core/gimptaggedcontainer.h"
#include "core/gimpviewable.h"

#include "gimpcombotagentry.h"
#include "gimptagentry.h"
#include "gimptagpopup.h"

#include "gimp-intl.h"


#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
#define GIMP_TAG_POPUP_PADDING       2
#define GIMP_TAG_POPUP_LINE_SPACING  2

enum
{
  PROP_0,
  PROP_OWNER
};

struct _PopupTagData
{
  GimpTag      *tag;
  GdkRectangle  bounds;
  GtkStateType  state;
};


static void     gimp_tag_popup_constructed             (GObject        *object);
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   *popup);
static gboolean gimp_tag_popup_list_expose             (GtkWidget      *widget,
                                                        GdkEventExpose *event,
                                                        GimpTagPopup   *popup);
static gboolean gimp_tag_popup_border_event            (GtkWidget      *widget,
                                                        GdkEvent       *event);
static gboolean gimp_tag_popup_list_event              (GtkWidget      *widget,
                                                        GdkEvent       *event,
                                                        GimpTagPopup   *popup);
static gboolean gimp_tag_popup_is_in_tag               (PopupTagData   *tag_data,
                                                        gint            x,
                                                        gint            y);
static void     gimp_tag_popup_queue_draw_tag          (GimpTagPopup   *widget,
                                                        PopupTagData   *tag_data);
static void     gimp_tag_popup_toggle_tag              (GimpTagPopup   *popup,
                                                        PopupTagData   *tag_data);
static void     gimp_tag_popup_check_can_toggle        (GimpTagged     *tagged,
                                                        GimpTagPopup   *popup);
static gint     gimp_tag_popup_layout_tags             (GimpTagPopup   *popup,
                                                        gint            width);
static gboolean gimp_tag_popup_scroll_timeout          (gpointer        data);
static void     gimp_tag_popup_remove_scroll_timeout   (GimpTagPopup   *popup);
static gboolean gimp_tag_popup_scroll_timeout_initial  (gpointer        data);
static void     gimp_tag_popup_start_scrolling         (GimpTagPopup   *popup);
static void     gimp_tag_popup_stop_scrolling          (GimpTagPopup   *popup);
static void     gimp_tag_popup_scroll_by               (GimpTagPopup   *popup,
                                                        gint            step);
static void     gimp_tag_popup_handle_scrolling        (GimpTagPopup   *popup,
                                                        gint            x,
                                                        gint            y,
                                                        gboolean        enter,
                                                        gboolean        motion);

static gboolean gimp_tag_popup_button_scroll           (GimpTagPopup   *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   *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->constructed  = gimp_tag_popup_constructed;
  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 *popup)
{
  GtkWidget *widget = GTK_WIDGET (popup);

  popup->upper_arrow_state = GTK_STATE_NORMAL;
  popup->lower_arrow_state = GTK_STATE_NORMAL;

  gtk_widget_add_events (widget,
                         GDK_BUTTON_PRESS_MASK   |
                         GDK_BUTTON_RELEASE_MASK |
                         GDK_POINTER_MOTION_MASK |
                         GDK_KEY_RELEASE_MASK    |
                         GDK_SCROLL_MASK);

  popup->frame = gtk_frame_new (NULL);
  gtk_frame_set_shadow_type (GTK_FRAME (popup->frame), GTK_SHADOW_OUT);
  gtk_container_add (GTK_CONTAINER (popup), popup->frame);
  gtk_widget_show (popup->frame);

  popup->alignment = gtk_alignment_new (0.5, 0.5, 1.0, 1.0);
  gtk_container_add (GTK_CONTAINER (popup->frame), popup->alignment);
  gtk_widget_show (popup->alignment);

  popup->tag_area = gtk_drawing_area_new ();
  gtk_widget_add_events (popup->tag_area,
                         GDK_BUTTON_PRESS_MASK   |
                         GDK_BUTTON_RELEASE_MASK |
                         GDK_POINTER_MOTION_MASK);
  gtk_container_add (GTK_CONTAINER (popup->alignment), popup->tag_area);
  gtk_widget_show (popup->tag_area);

  g_signal_connect (popup->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 (popup->tag_area, "expose-event",
                    G_CALLBACK (gimp_tag_popup_list_expose),
                    popup);
  g_signal_connect (popup->tag_area, "event",
                    G_CALLBACK (gimp_tag_popup_list_event),
                    popup);
}

static void
gimp_tag_popup_constructed (GObject *object)
{
  GimpTagPopup        *popup = GIMP_TAG_POPUP (object);
  GimpTaggedContainer *container;
  GtkWidget           *entry;
  GtkAllocation        entry_allocation;
  GtkStyle            *frame_style;
  gint                 x;
  gint                 y;
  gint                 width;
  gint                 height;
  gint                 popup_height;
  GHashTable          *tag_hash;
  GList               *tag_list;
  GList               *tag_iterator;
  gint                 i;
  gint                 max_height;
  gint                 screen_height;
  gchar              **current_tags;
  gint                 current_count;
  GdkRectangle         popup_rects[2]; /* variants of popup placement */
  GdkRectangle         popup_rect; /* best popup rect in screen coordinates */

  G_OBJECT_CLASS (parent_class)->constructed (object);

  entry = GTK_WIDGET (popup->combo_entry);

  gtk_window_set_screen (GTK_WINDOW (popup), gtk_widget_get_screen (entry));

  popup->context = gtk_widget_create_pango_context (GTK_WIDGET (popup));
  popup->layout  = pango_layout_new (popup->context);

  gtk_widget_get_allocation (entry, &entry_allocation);

  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);

  container = GIMP_TAG_ENTRY (popup->combo_entry)->container;

  tag_hash = 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_new0 (PopupTagData, popup->tag_count);

  for (i = 0, tag_iterator = tag_list;
       i < popup->tag_count;
       i++, tag_iterator = g_list_next (tag_iterator))
    {
      PopupTagData *tag_data = &popup->tag_data[i];
      gint          j;

      tag_data->tag   = tag_iterator->data;
      tag_data->state = GTK_STATE_NORMAL;

      g_object_ref (tag_data->tag);

      for (j = 0; j < current_count; j++)
        {
          if (! gimp_tag_compare_with_string (tag_data->tag, current_tags[j]))
            {
              tag_data->state = GTK_STATE_SELECTED;
              break;
            }
        }
    }

  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 (container),
                              (GFunc) gimp_tag_popup_check_can_toggle,
                              popup);
    }

  frame_style = gtk_widget_get_style (popup->frame);

  width  = (entry_allocation.width -
            2 * frame_style->xthickness);
  height = (gimp_tag_popup_layout_tags (popup, width) +
            2 * frame_style->ythickness);

  gdk_window_get_origin (gtk_widget_get_window (entry), &x, &y);

  max_height = entry_allocation.height * 10;

  screen_height = gdk_screen_get_height (gtk_widget_get_screen (entry));

  popup_height = MIN (height, max_height);

  popup_rects[0].x      = x;
  popup_rects[0].y      = 0;
  popup_rects[0].width  = entry_allocation.width;
  popup_rects[0].height = y + entry_allocation.height;

  popup_rects[1].x      = 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_height = popup_rect.height;
    }

  if (popup_height < height)
    {
      popup->arrows_visible    = TRUE;
      popup->upper_arrow_state = GTK_STATE_INSENSITIVE;

      gtk_alignment_set_padding (GTK_ALIGNMENT (popup->alignment),
                                 popup->scroll_arrow_height + 2,
                                 popup->scroll_arrow_height + 2, 0, 0);

      popup_height -= 2 * popup->scroll_arrow_height + 4;

      popup->scroll_height = height - popup_height;
      popup->scroll_y      = 0;
      popup->scroll_step   = 0;
    }

  gtk_widget_set_size_request (popup->tag_area, width, 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);
}

static void
gimp_tag_popup_dispose (GObject *object)
{
  GimpTagPopup *popup = GIMP_TAG_POPUP (object);

  gimp_tag_popup_remove_scroll_timeout (popup);

  g_clear_object (&popup->combo_entry);
  g_clear_object (&popup->layout);
  g_clear_object (&popup->context);

  if (popup->tag_data)
    {
      gint i;

      for (i = 0; i < popup->tag_count; i++)
        {
          g_object_unref (popup->tag_data[i].tag);
        }

      g_free (popup->tag_data);
      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 *popup = GIMP_TAG_POPUP (object);

  switch (property_id)
    {
    case PROP_OWNER:
      popup->combo_entry = g_value_dup_object (value);
      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 *popup = GIMP_TAG_POPUP (object);

  switch (property_id)
    {
    case PROP_OWNER:
      g_value_set_object (value, 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)
{
  GtkWidget *widget;

  g_return_if_fail (GIMP_IS_TAG_POPUP (popup));

  widget = GTK_WIDGET (popup);

  gtk_widget_show (widget);

  gtk_grab_add (widget);
  gtk_widget_grab_focus (widget);

  if (gdk_pointer_grab (gtk_widget_get_window (widget), TRUE,
                        GDK_BUTTON_PRESS_MASK   |
                        GDK_BUTTON_RELEASE_MASK |
                        GDK_POINTER_MOTION_MASK,
                        NULL, NULL,
                        GDK_CURRENT_TIME) != GDK_GRAB_SUCCESS)
    {
      /* pointer grab must be attained otherwise user would have
       * problems closing the popup window.
       */
      gtk_grab_remove (widget);
      gtk_widget_destroy (widget);
    }
}

static gint
gimp_tag_popup_layout_tags (GimpTagPopup *popup,
                            gint          width)
{
  PangoFontMetrics *font_metrics;
  gint              x;
  gint              y;
  gint              height = 0;
  gint              i;
  gint              line_height;
  gint              space_width;

  x = GIMP_TAG_POPUP_MARGIN;
  y = GIMP_TAG_POPUP_MARGIN;

  font_metrics = pango_context_get_metrics (popup->context,
                                            pango_context_get_font_description (popup->context),
                                            NULL);

  line_height = PANGO_PIXELS ((pango_font_metrics_get_ascent (font_metrics) +
                               pango_font_metrics_get_descent (font_metrics)));
  space_width = PANGO_PIXELS (pango_font_metrics_get_approximate_char_width (font_metrics));

  pango_font_metrics_unref (font_metrics);

  for (i = 0; i < popup->tag_count; i++)
    {
      PopupTagData *tag_data = &popup->tag_data[i];
      gint          w, h;

      pango_layout_set_text (popup->layout,
                             gimp_tag_get_name (tag_data->tag), -1);
      pango_layout_get_pixel_size (popup->layout, &w, &h);

      tag_data->bounds.width  = w + 2 * GIMP_TAG_POPUP_PADDING;
      tag_data->bounds.height = h + 2 * GIMP_TAG_POPUP_PADDING;

      if (x + space_width + tag_data->bounds.width +
          GIMP_TAG_POPUP_MARGIN - 1 > width)
        {
          x = GIMP_TAG_POPUP_MARGIN;
          y += line_height + 2 * GIMP_TAG_POPUP_PADDING + GIMP_TAG_POPUP_LINE_SPACING;
        }

      tag_data->bounds.x = x;
      tag_data->bounds.y = y;

      x += tag_data->bounds.width + space_width;
    }

  if (gtk_widget_get_direction (GTK_WIDGET (popup)) == GTK_TEXT_DIR_RTL)
    {
      for (i = 0; i < popup->tag_count; i++)
        {
          PopupTagData *tag_data = &popup->tag_data[i];

          tag_data->bounds.x = (width -
                                tag_data->bounds.x -
                                tag_data->bounds.width);
        }
    }

  height = y + line_height + GIMP_TAG_POPUP_MARGIN;

  return height;
}

static gboolean
gimp_tag_popup_border_expose (GtkWidget      *widget,
                              GdkEventExpose *event,
                              GimpTagPopup   *popup)
{
  GdkWindow    *window = gtk_widget_get_window (widget);
  GtkStyle     *style  = gtk_widget_get_style (widget);
  GdkRectangle  border;
  GdkRectangle  upper;
  GdkRectangle  lower;
  gint          arrow_space;
  gint          arrow_size;

  if (event->window != gtk_widget_get_window (widget))
    return FALSE;

  get_arrows_visible_area (popup, &border, &upper, &lower, &arrow_space);

  arrow_size = 0.7 * arrow_space;

  gtk_paint_box (style, window,
                 GTK_STATE_NORMAL,
                 GTK_SHADOW_OUT,
                 &event->area, widget, "menu",
                 0, 0, -1, -1);

  if (popup->arrows_visible)
    {
      /*  upper arrow  */

      gtk_paint_box (style, window,
                     popup->upper_arrow_state,
                     GTK_SHADOW_OUT,
                     &event->area, widget, "menu",
                     upper.x,
                     upper.y,
                     upper.width,
                     upper.height);

      gtk_paint_arrow (style, window,
                       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 + style->ythickness + (arrow_space - arrow_size) / 2,
                       arrow_size, arrow_size);

      /*  lower arrow  */

      gtk_paint_box (style, window,
                     popup->lower_arrow_state,
                     GTK_SHADOW_OUT,
                     &event->area, widget, "menu",
                     lower.x,
                     lower.y,
                     lower.width,
                     lower.height);

      gtk_paint_arrow (style, window,
                       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 + style->ythickness + (arrow_space - arrow_size) / 2,
                       arrow_size, arrow_size);
    }

  return FALSE;
}

static gboolean
gimp_tag_popup_border_event (GtkWidget *widget,
                             GdkEvent  *event)
{
  GimpTagPopup *popup = GIMP_TAG_POPUP (widget);

  if (event->type == GDK_BUTTON_PRESS)
    {
      GdkEventButton *button_event = (GdkEventButton *) event;
      GtkAllocation   allocation;
      gint            x;
      gint            y;

      if (button_event->window == gtk_widget_get_window (widget) &&
          gimp_tag_popup_button_scroll (popup, button_event))
        {
          return TRUE;
        }

      gtk_widget_get_allocation (widget, &allocation);

      gdk_window_get_pointer (gtk_widget_get_window (widget), &x, &y, NULL);

      if (button_event->window != gtk_widget_get_window (popup->tag_area) &&
          (x < allocation.y                    ||
           y < allocation.x                    ||
           x > allocation.x + allocation.width ||
           y > allocation.y + 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)
    {
      GdkEventMotion *motion_event = (GdkEventMotion *) event;
      gint            x, y;

      gdk_window_get_pointer (gtk_widget_get_window (widget), &x, &y, NULL);

      gimp_tag_popup_handle_scrolling (popup, x, y,
                                       motion_event->window ==
                                       gtk_widget_get_window (widget),
                                       TRUE);
    }
  else if (event->type == GDK_BUTTON_RELEASE)
    {
      GdkEventButton *button_event = (GdkEventButton *) event;

      popup->single_select_disabled = TRUE;

      if (button_event->window == gtk_widget_get_window (widget) &&
          gimp_tag_popup_button_scroll (popup, button_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_grab_remove (widget);
      gdk_display_pointer_ungrab (gtk_widget_get_display (widget),
                                  GDK_CURRENT_TIME);
      gtk_widget_destroy (widget);
    }
  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 (popup, MENU_SCROLL_STEP2);
          return TRUE;

        case GDK_SCROLL_LEFT:
        case GDK_SCROLL_UP:
          gimp_tag_popup_scroll_by (popup, - MENU_SCROLL_STEP2);
          return TRUE;
        }
    }

  return FALSE;
}

static gboolean
gimp_tag_popup_list_expose (GtkWidget      *widget,
                            GdkEventExpose *event,
                            GimpTagPopup   *popup)
{
  GdkWindow      *window = gtk_widget_get_window (widget);
  GtkStyle       *style  = gtk_widget_get_style (widget);
  cairo_t        *cr;
  PangoAttribute *attribute;
  PangoAttrList  *attributes;
  gint            i;

  cr = gdk_cairo_create (event->window);

  gdk_cairo_region (cr, event->region);
  cairo_clip (cr);

  cairo_set_line_width (cr, 1.0);
  cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);

  for (i = 0; i < popup->tag_count; i++)
    {
      PopupTagData *tag_data = &popup->tag_data[i];

      pango_layout_set_text (popup->layout,
                             gimp_tag_get_name (tag_data->tag), -1);

      switch (tag_data->state)
        {
        case GTK_STATE_SELECTED:
          attributes = pango_attr_list_copy (popup->combo_entry->selected_item_attr);
          break;

        case GTK_STATE_INSENSITIVE:
          attributes = pango_attr_list_copy (popup->combo_entry->insensitive_item_attr);
          break;

        default:
          attributes = pango_attr_list_copy (popup->combo_entry->normal_item_attr);
          break;
        }

      if (tag_data == popup->prelight &&
          tag_data->state != GTK_STATE_INSENSITIVE)
        {
          attribute = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
          pango_attr_list_insert (attributes, attribute);
        }

      pango_layout_set_attributes (popup->layout, attributes);
      pango_attr_list_unref (attributes);

      if (tag_data->state == GTK_STATE_SELECTED)
        {
          gdk_cairo_set_source_color (cr,
                                      &popup->combo_entry->selected_item_color);

          cairo_rectangle (cr,
                           tag_data->bounds.x - 1,
                           tag_data->bounds.y - popup->scroll_y,
                           tag_data->bounds.width + 2,
                           tag_data->bounds.height);
          cairo_fill (cr);

          cairo_translate (cr, 0.5, 0.5);

          cairo_move_to (cr,
                         tag_data->bounds.x,
                         tag_data->bounds.y - popup->scroll_y - 1);
          cairo_line_to (cr,
                         tag_data->bounds.x + tag_data->bounds.width - 1,
                         tag_data->bounds.y - popup->scroll_y - 1);

          cairo_move_to (cr,
                         tag_data->bounds.x,
                         tag_data->bounds.y - popup->scroll_y + tag_data->bounds.height);
          cairo_line_to (cr,
                         tag_data->bounds.x + tag_data->bounds.width - 1,
                         tag_data->bounds.y - popup->scroll_y + tag_data->bounds.height);

          cairo_stroke (cr);

          cairo_translate (cr, -0.5, -0.5);
        }

      cairo_move_to (cr,
                     (tag_data->bounds.x +
                      GIMP_TAG_POPUP_PADDING),
                     (tag_data->bounds.y -
                      popup->scroll_y +
                      GIMP_TAG_POPUP_PADDING));

      pango_cairo_show_layout (cr, popup->layout);

      if (tag_data == popup->prelight              &&
          tag_data->state != GTK_STATE_INSENSITIVE &&
          ! popup->single_select_disabled)
        {
          gtk_paint_focus (style, window,
                           tag_data->state,
                           &event->area, widget, NULL,
                           tag_data->bounds.x,
                           tag_data->bounds.y - popup->scroll_y,
                           tag_data->bounds.width,
                           tag_data->bounds.height);
        }
    }

  cairo_destroy (cr);

  return FALSE;
}

static gboolean
gimp_tag_popup_list_event (GtkWidget    *widget,
                           GdkEvent     *event,
                           GimpTagPopup *popup)
{
  if (event->type == GDK_BUTTON_PRESS)
    {
      GdkEventButton *button_event = (GdkEventButton *) event;
      gint            x;
      gint            y;
      gint            i;

      popup->single_select_disabled = TRUE;

      x = button_event->x;
      y = button_event->y + popup->scroll_y;

      for (i = 0; i < popup->tag_count; i++)
        {
          PopupTagData *tag_data = &popup->tag_data[i];

          if (gimp_tag_popup_is_in_tag (tag_data, x, y))
            {
              gimp_tag_popup_toggle_tag (popup, tag_data);
              gtk_widget_queue_draw (widget);
              break;
            }
        }
    }
  else if (event->type == GDK_MOTION_NOTIFY)
    {
      GdkEventMotion *motion_event = (GdkEventMotion *) event;
      PopupTagData   *prelight     = NULL;
      gint            x;
      gint            y;
      gint            i;

      x = motion_event->x;
      y = motion_event->y + popup->scroll_y;

      for (i = 0; i < popup->tag_count; i++)
        {
          PopupTagData *tag_data = &popup->tag_data[i];

          if (gimp_tag_popup_is_in_tag (tag_data, x, y))
            {
              prelight = tag_data;
              break;
            }
        }

      if (prelight != popup->prelight)
        {
          if (popup->prelight)
            gimp_tag_popup_queue_draw_tag (popup, popup->prelight);

          popup->prelight = prelight;

          if (popup->prelight)
            gimp_tag_popup_queue_draw_tag (popup, popup->prelight);
        }
    }
  else if (event->type == GDK_BUTTON_RELEASE &&
           ! popup->single_select_disabled)
    {
      GdkEventButton *button_event = (GdkEventButton *) event;
      gint            x;
      gint            y;
      gint            i;

      popup->single_select_disabled = TRUE;

      x = button_event->x;
      y = button_event->y + popup->scroll_y;

      for (i = 0; i < popup->tag_count; i++)
        {
          PopupTagData *tag_data = &popup->tag_data[i];

          if (gimp_tag_popup_is_in_tag (tag_data, x, y))
            {
              gimp_tag_popup_toggle_tag (popup, tag_data);
              gtk_widget_destroy (GTK_WIDGET (popup));
              break;
            }
        }
    }

  return FALSE;
}

static gboolean
gimp_tag_popup_is_in_tag (PopupTagData *tag_data,
                          gint          x,
                          gint          y)
{
  if (x >= tag_data->bounds.x                          &&
      y >= tag_data->bounds.y                          &&
      x <  tag_data->bounds.x + tag_data->bounds.width &&
      y <  tag_data->bounds.y + tag_data->bounds.height)
    {
      return TRUE;
    }

  return FALSE;
}

static void
gimp_tag_popup_queue_draw_tag (GimpTagPopup *popup,
                               PopupTagData *tag_data)
{
  gtk_widget_queue_draw_area (popup->tag_area,
                              tag_data->bounds.x,
                              tag_data->bounds.y - popup->scroll_y,
                              tag_data->bounds.width,
                              tag_data->bounds.height);
}

static void
gimp_tag_popup_toggle_tag (GimpTagPopup *popup,
                           PopupTagData *tag_data)
{
  gchar    **current_tags;
  GString   *tag_str;
  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;
    }

  current_tags = gimp_tag_entry_parse_tags (GIMP_TAG_ENTRY (popup->combo_entry));
  tag_str = g_string_new ("");
  length = g_strv_length (current_tags);
  for (i = 0; i < length; i++)
    {
      if (! gimp_tag_compare_with_string (tag_data->tag, current_tags[i]))
        {
          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, gimp_tag_get_name (tag_data->tag));
    }

  gimp_tag_entry_set_tag_string (GIMP_TAG_ENTRY (popup->combo_entry),
                                 tag_str->str);

  g_string_free (tag_str, TRUE);
  g_strfreev (current_tags);

  if (GIMP_TAG_ENTRY (popup->combo_entry)->mode == GIMP_TAG_ENTRY_MODE_QUERY)
    {
      GimpTaggedContainer *container;

      container = GIMP_TAG_ENTRY (popup->combo_entry)->container;

      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 (container),
                              (GFunc) gimp_tag_popup_check_can_toggle,
                              popup);
    }
}

static int
gimp_tag_popup_data_compare (const void *a,
                             const void *b)
{
  return gimp_tag_compare_func (((PopupTagData *) a)->tag,
                                ((PopupTagData *) b)->tag);
}

static void
gimp_tag_popup_check_can_toggle (GimpTagged   *tagged,
                                 GimpTagPopup *popup)
{
  GList *iterator;

  for (iterator = gimp_tagged_get_tags (tagged);
       iterator;
       iterator = g_list_next (iterator))
    {
      PopupTagData  search_key;
      PopupTagData *search_result;

      search_key.tag = iterator->data;

      search_result =
        (PopupTagData *) bsearch (&search_key,
                                  popup->tag_data, 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 *popup = data;
  gboolean      touchscreen_mode;

  g_object_get (gtk_widget_get_settings (GTK_WIDGET (popup)),
                "gtk-touchscreen-mode", &touchscreen_mode,
                NULL);

  gimp_tag_popup_scroll_by (popup, popup->scroll_step);

  return TRUE;
}

static void
gimp_tag_popup_remove_scroll_timeout (GimpTagPopup *popup)
{
  if (popup->scroll_timeout_id)
    {
      g_source_remove (popup->scroll_timeout_id);
      popup->scroll_timeout_id = 0;
    }
}

static gboolean
gimp_tag_popup_scroll_timeout_initial (gpointer data)
{
  GimpTagPopup *popup = data;
  guint         timeout;
  gboolean      touchscreen_mode;

  g_object_get (gtk_widget_get_settings (GTK_WIDGET (popup)),
                "gtk-timeout-repeat",   &timeout,
                "gtk-touchscreen-mode", &touchscreen_mode,
                NULL);

  gimp_tag_popup_scroll_by (popup, popup->scroll_step);

  gimp_tag_popup_remove_scroll_timeout (popup);

  popup->scroll_timeout_id =
    gdk_threads_add_timeout (timeout,
                             gimp_tag_popup_scroll_timeout,
                             popup);

  return FALSE;
}

static void
gimp_tag_popup_start_scrolling (GimpTagPopup *popup)
{
  guint    timeout;
  gboolean touchscreen_mode;

  g_object_get (gtk_widget_get_settings (GTK_WIDGET (popup)),
                "gtk-timeout-repeat",   &timeout,
                "gtk-touchscreen-mode", &touchscreen_mode,
                NULL);

  gimp_tag_popup_scroll_by (popup, popup->scroll_step);

  popup->scroll_timeout_id =
    gdk_threads_add_timeout (timeout,
                             gimp_tag_popup_scroll_timeout_initial,
                             popup);
}

static void
gimp_tag_popup_stop_scrolling (GimpTagPopup *popup)
{
  gboolean touchscreen_mode;

  gimp_tag_popup_remove_scroll_timeout (popup);

  g_object_get (gtk_widget_get_settings (GTK_WIDGET (popup)),
                "gtk-touchscreen-mode", &touchscreen_mode,
                NULL);

  if (! touchscreen_mode)
    {
      popup->upper_arrow_prelight = FALSE;
      popup->lower_arrow_prelight = FALSE;
    }
}

static void
gimp_tag_popup_scroll_by (GimpTagPopup *popup,
                          gint          step)
{
  GtkStateType arrow_state;
  gint         new_scroll_y = popup->scroll_y + step;

  arrow_state = popup->upper_arrow_state;

  if (new_scroll_y < 0)
    {
      new_scroll_y = 0;

      if (arrow_state != GTK_STATE_INSENSITIVE)
        gimp_tag_popup_stop_scrolling (popup);

      arrow_state = GTK_STATE_INSENSITIVE;
    }
  else
    {
      arrow_state = (popup->upper_arrow_prelight ?
                     GTK_STATE_PRELIGHT : GTK_STATE_NORMAL);
    }

  if (arrow_state != popup->upper_arrow_state)
    {
      popup->upper_arrow_state = arrow_state;
      gtk_widget_queue_draw (GTK_WIDGET (popup));
    }

  arrow_state = popup->lower_arrow_state;

  if (new_scroll_y >= popup->scroll_height)
    {
      new_scroll_y = popup->scroll_height - 1;

      if (arrow_state != GTK_STATE_INSENSITIVE)
        gimp_tag_popup_stop_scrolling (popup);

      arrow_state = GTK_STATE_INSENSITIVE;
    }
  else
    {
      arrow_state = (popup->lower_arrow_prelight ?
                     GTK_STATE_PRELIGHT : GTK_STATE_NORMAL);
    }

  if (arrow_state != popup->lower_arrow_state)
    {
      popup->lower_arrow_state = arrow_state;
      gtk_widget_queue_draw (GTK_WIDGET (popup));
    }

  if (new_scroll_y != popup->scroll_y)
    {
      popup->scroll_y = new_scroll_y;

      gdk_window_scroll (gtk_widget_get_window (popup->tag_area), 0, -step);
    }
}

static void
gimp_tag_popup_handle_scrolling (GimpTagPopup *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 (popup)),
                "gtk-touchscreen-mode", &touchscreen_mode,
                NULL);

  /*  upper arrow handling  */

  get_arrows_sensitive_area (popup, &rect, NULL);

  in_arrow = FALSE;
  if (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)
    popup->upper_arrow_prelight = in_arrow;

  if (popup->upper_arrow_state != GTK_STATE_INSENSITIVE)
    {
      gboolean arrow_pressed = FALSE;

      if (popup->arrows_visible)
        {
          if (touchscreen_mode)
            {
              if (enter && popup->upper_arrow_prelight)
                {
                  if (popup->scroll_timeout_id == 0)
                    {
                      gimp_tag_popup_remove_scroll_timeout (popup);
                      popup->scroll_step = -MENU_SCROLL_STEP2; /* always fast */

                      if (! motion)
                        {
                          /* Only do stuff on click. */
                          gimp_tag_popup_start_scrolling (popup);
                          arrow_pressed = TRUE;
                        }
                    }
                  else
                    {
                      arrow_pressed = TRUE;
                    }
                }
              else if (! enter)
                {
                  gimp_tag_popup_stop_scrolling (popup);
                }
            }
          else /* !touchscreen_mode */
            {
              scroll_fast = (y < rect.y + MENU_SCROLL_FAST_ZONE);

              if (enter && in_arrow &&
                  (! popup->upper_arrow_prelight ||
                   popup->scroll_fast != scroll_fast))
                {
                  popup->upper_arrow_prelight = TRUE;
                  popup->scroll_fast          = scroll_fast;

                  gimp_tag_popup_remove_scroll_timeout (popup);
                  popup->scroll_step = (scroll_fast ?
                                        -MENU_SCROLL_STEP2 : -MENU_SCROLL_STEP1);

                  popup->scroll_timeout_id =
                    gdk_threads_add_timeout (scroll_fast ?
                                             MENU_SCROLL_TIMEOUT2 :
                                             MENU_SCROLL_TIMEOUT1,
                                             gimp_tag_popup_scroll_timeout,
                                             popup);
                }
              else if (! enter && ! in_arrow && popup->upper_arrow_prelight)
                {
                  gimp_tag_popup_stop_scrolling (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 (popup->upper_arrow_state != GTK_STATE_INSENSITIVE)
        {
          GtkStateType arrow_state = GTK_STATE_NORMAL;

          if (arrow_pressed)
            arrow_state = GTK_STATE_ACTIVE;
          else if (popup->upper_arrow_prelight)
            arrow_state = GTK_STATE_PRELIGHT;

          if (arrow_state != popup->upper_arrow_state)
            {
              popup->upper_arrow_state = arrow_state;

              gdk_window_invalidate_rect (gtk_widget_get_window (GTK_WIDGET (popup)),
                                          &rect, FALSE);
            }
        }
    }

  /*  lower arrow handling  */

  get_arrows_sensitive_area (popup, NULL, &rect);

  in_arrow = FALSE;
  if (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)
    popup->lower_arrow_prelight = in_arrow;

  if (popup->lower_arrow_state != GTK_STATE_INSENSITIVE)
    {
      gboolean arrow_pressed = FALSE;

      if (popup->arrows_visible)
        {
          if (touchscreen_mode)
            {
              if (enter && popup->lower_arrow_prelight)
                {
                  if (popup->scroll_timeout_id == 0)
                    {
                      gimp_tag_popup_remove_scroll_timeout (popup);
                      popup->scroll_step = MENU_SCROLL_STEP2; /* always fast */

                      if (! motion)
                        {
                          /* Only do stuff on click. */
                          gimp_tag_popup_start_scrolling (popup);
                          arrow_pressed = TRUE;
                        }
                    }
                  else
                    {
                      arrow_pressed = TRUE;
                    }
                }
              else if (! enter)
                {
                  gimp_tag_popup_stop_scrolling (popup);
                }
            }
          else /* !touchscreen_mode */
            {
              scroll_fast = (y > rect.y + rect.height - MENU_SCROLL_FAST_ZONE);

              if (enter && in_arrow &&
                  (! popup->lower_arrow_prelight ||
                   popup->scroll_fast != scroll_fast))
                {
                  popup->lower_arrow_prelight = TRUE;
                  popup->scroll_fast          = scroll_fast;

                  gimp_tag_popup_remove_scroll_timeout (popup);
                  popup->scroll_step = (scroll_fast ?
                                        MENU_SCROLL_STEP2 : MENU_SCROLL_STEP1);

                  popup->scroll_timeout_id =
                    gdk_threads_add_timeout (scroll_fast ?
                                             MENU_SCROLL_TIMEOUT2 :
                                             MENU_SCROLL_TIMEOUT1,
                                             gimp_tag_popup_scroll_timeout,
                                             popup);
                }
              else if (! enter && ! in_arrow && popup->lower_arrow_prelight)
                {
                  gimp_tag_popup_stop_scrolling (popup);
                }
            }
        }

      /*  gimp_tag_popup_start_scrolling() might have hit the bottom of the
       *  popup, so check if the button isn't insensitive before
       *  changing it to something else.
       */
      if (popup->lower_arrow_state != GTK_STATE_INSENSITIVE)
        {
          GtkStateType arrow_state = GTK_STATE_NORMAL;

          if (arrow_pressed)
            arrow_state = GTK_STATE_ACTIVE;
          else if (popup->lower_arrow_prelight)
            arrow_state = GTK_STATE_PRELIGHT;

          if (arrow_state != popup->lower_arrow_state)
            {
              popup->lower_arrow_state = arrow_state;

              gdk_window_invalidate_rect (gtk_widget_get_window (GTK_WIDGET (popup)),
                                          &rect, FALSE);
            }
        }
    }
}

static gboolean
gimp_tag_popup_button_scroll (GimpTagPopup   *popup,
                              GdkEventButton *event)
{
  if (popup->upper_arrow_prelight || popup->lower_arrow_prelight)
    {
      gboolean touchscreen_mode;

      g_object_get (gtk_widget_get_settings (GTK_WIDGET (popup)),
                    "gtk-touchscreen-mode", &touchscreen_mode,
                    NULL);

      if (touchscreen_mode)
        gimp_tag_popup_handle_scrolling (popup,
                                         event->x_root,
                                         event->y_root,
                                         event->type == GDK_BUTTON_PRESS,
                                         FALSE);

      return TRUE;
    }

  return FALSE;
}

static void
get_arrows_visible_area (GimpTagPopup *popup,
                         GdkRectangle *border,
                         GdkRectangle *upper,
                         GdkRectangle *lower,
                         gint         *arrow_space)
{
  GtkWidget *widget = GTK_WIDGET (popup->alignment);
  guint      padding_top;
  guint      padding_bottom;
  guint      padding_left;
  guint      padding_right;

  gtk_alignment_get_padding (GTK_ALIGNMENT (popup->alignment),
                             &padding_top, &padding_bottom,
                             &padding_left, &padding_right);

  gtk_widget_get_allocation (widget, border);

  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 = popup->scroll_arrow_height;
}

static void
get_arrows_sensitive_area (GimpTagPopup *popup,
                           GdkRectangle *upper,
                           GdkRectangle *lower)
{
  GdkRectangle tmp_border;
  GdkRectangle tmp_upper;
  GdkRectangle tmp_lower;
  gint         tmp_arrow_space;

  get_arrows_visible_area (popup,
                           &tmp_border, &tmp_upper, &tmp_lower, &tmp_arrow_space);

  if (upper)
    *upper = tmp_upper;

  if (lower)
    *lower = tmp_lower;
}
