diff options
Diffstat (limited to 'app/widgets/gimptagpopup.c')
-rw-r--r-- | app/widgets/gimptagpopup.c | 1518 |
1 files changed, 1518 insertions, 0 deletions
diff --git a/app/widgets/gimptagpopup.c b/app/widgets/gimptagpopup.c new file mode 100644 index 0000000..cc76670 --- /dev/null +++ b/app/widgets/gimptagpopup.c @@ -0,0 +1,1518 @@ +/* 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 <https://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_clear_pointer (&popup->tag_data, g_free); + } + + 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; +} |