From 51be6b88dbff3ff6ed83d1247508ad58c0278df6 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Fri, 11 Jan 2013 17:32:08 +0100 Subject: [PATCH] Add GtkBubbleWindow This popup window widget can be used for touch friendly context menus that point to a concrete area. --- docs/reference/gtk/gtk-docs.sgml | 1 + docs/reference/gtk/gtk3-sections.txt | 28 + docs/reference/gtk/gtk3.types.in | 1 + gtk/Makefile.am | 2 + gtk/gtkbubblewindow.c | 1016 ++++++++++++++++++++++++++ gtk/gtkbubblewindow.h | 81 ++ 6 files changed, 1129 insertions(+) create mode 100644 gtk/gtkbubblewindow.c create mode 100644 gtk/gtkbubblewindow.h diff --git a/docs/reference/gtk/gtk-docs.sgml b/docs/reference/gtk/gtk-docs.sgml index 6260fb8cfc..c82e8f731c 100644 --- a/docs/reference/gtk/gtk-docs.sgml +++ b/docs/reference/gtk/gtk-docs.sgml @@ -89,6 +89,7 @@ + diff --git a/docs/reference/gtk/gtk3-sections.txt b/docs/reference/gtk/gtk3-sections.txt index 5db1c72c3b..b881cbf1fd 100644 --- a/docs/reference/gtk/gtk3-sections.txt +++ b/docs/reference/gtk/gtk3-sections.txt @@ -7494,3 +7494,31 @@ GTK_COLOR_CHOOSER_DIALOG_GET_CLASS GtkColorChooserDialogPrivate gtk_color_chooser_dialog_get_type + +
+gtkbubblewindow +GtkBubbleWindow +GtkBubbleWindow +gtk_bubble_window_new +gtk_bubble_window_set_relative_to +gtk_bubble_window_get_relative_to +gtk_bubble_window_set_pointing_to +gtk_bubble_window_get_pointing_to +gtk_bubble_window_set_position +gtk_bubble_window_get_position +gtk_bubble_window_popup +gtk_bubble_window_popdown +gtk_bubble_window_grab +gtk_bubble_window_ungrab + + +GTK_TYPE_BUBBLE_WINDOW +GTK_BUBBLE_WINDOW +GTK_BUBBLE_WINDOW_CLASS +GTK_IS_BUBBLE_WINDOW +GTK_IS_BUBBLE_WINDOW_CLASS +GTK_BUBBLE_WINDOW_GET_CLASS + + +gtk_bubble_window_get_type +
diff --git a/docs/reference/gtk/gtk3.types.in b/docs/reference/gtk/gtk3.types.in index 3f378b3b22..9cc637d575 100644 --- a/docs/reference/gtk/gtk3.types.in +++ b/docs/reference/gtk/gtk3.types.in @@ -23,6 +23,7 @@ gtk_aspect_frame_get_type gtk_assistant_get_type gtk_bin_get_type gtk_box_get_type +gtk_bubble_window_get_type gtk_builder_get_type gtk_buildable_get_type gtk_button_box_get_type diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 973f3c8994..ef7a7dc56a 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -202,6 +202,7 @@ gtk_public_h_sources = \ gtkbindings.h \ gtkborder.h \ gtkbox.h \ + gtkbubblewindow.h \ gtkbuilder.h \ gtkbuildable.h \ gtkbutton.h \ @@ -621,6 +622,7 @@ gtk_base_c_sources = \ gtkborder.c \ gtkborderimage.c \ gtkbox.c \ + gtkbubblewindow.c \ gtkbuildable.c \ gtkbuilder.c \ gtkbuilderparser.c \ diff --git a/gtk/gtkbubblewindow.c b/gtk/gtkbubblewindow.c new file mode 100644 index 0000000000..4198a95814 --- /dev/null +++ b/gtk/gtkbubblewindow.c @@ -0,0 +1,1016 @@ +/* GTK - The GIMP Toolkit + * Copyright © 2013 Carlos Garnacho + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/** + * SECTION:gtkbubblewindow + * @Short_description: Bubble-like context window + * @Title: GtkBubbleWindow + * + * GtkBubbleWindow is a bubble-like context window, primarily mean for + * context-dependent helpers on touch interfaces. + * + * In order to place a GtkBubbleWindow to point to some other area, + * use gtk_bubble_window_set_relative_to(), gtk_bubble_window_set_pointing_to() + * and gtk_bubble_window_set_position(). Although it is usually more + * convenient to use gtk_bubble_window_popup() which handles all of those + * at once. + * + * By default, no grabs are performed on the GtkBubbleWindow, leaving + * the popup/popdown semantics up to the caller, gtk_bubble_window_grab() + * can be used to grab the window for a device pair, bringing #GtkMenu alike + * popdown behavior by default on keyboard/pointer interaction. Grabs need + * to be undone through gtk_bubble_window_ungrab(). + */ + +#include "config.h" +#include +#include +#include "gtkbubblewindow.h" +#include "gtkprivate.h" +#include "gtkintl.h" + +#define TAIL_GAP_WIDTH 24 +#define TAIL_HEIGHT 12 + +#define POS_IS_VERTICAL(p) ((p) == GTK_POS_TOP || (p) == GTK_POS_BOTTOM) + +#define GRAB_EVENT_MASK \ + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | \ + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | \ + GDK_POINTER_MOTION_MASK + +typedef struct _GtkBubbleWindowPrivate GtkBubbleWindowPrivate; + +enum { + PROP_RELATIVE_TO = 1, + PROP_POINTING_TO, + PROP_POSITION +}; + +struct _GtkBubbleWindowPrivate +{ + GdkDevice *device; + GdkWindow *relative_to; + cairo_rectangle_int_t pointing_to; + gint win_x; + gint win_y; + guint has_pointing_to : 1; + guint grabbed : 1; + guint preferred_position : 2; + guint final_position : 2; +}; + +G_DEFINE_TYPE (GtkBubbleWindow, gtk_bubble_window, GTK_TYPE_WINDOW) + +static void +gtk_bubble_window_init (GtkBubbleWindow *window) +{ + GtkWidget *widget; + GdkScreen *screen; + GdkVisual *visual; + + widget = GTK_WIDGET (window); + window->_priv = G_TYPE_INSTANCE_GET_PRIVATE (window, + GTK_TYPE_BUBBLE_WINDOW, + GtkBubbleWindowPrivate); + gtk_window_set_default_size (GTK_WINDOW (window), + TAIL_GAP_WIDTH, TAIL_GAP_WIDTH); + gtk_widget_set_app_paintable (widget, TRUE); + + screen = gtk_widget_get_screen (widget); + visual = gdk_screen_get_rgba_visual (screen); + + if (visual) + gtk_widget_set_visual (widget, visual); +} + +static GObject * +gtk_bubble_window_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties) +{ + GObject *object; + + object = + G_OBJECT_CLASS (gtk_bubble_window_parent_class)->constructor (type, + n_construct_properties, + construct_properties); + g_object_set (object, "type", GTK_WINDOW_POPUP, NULL); + + return object; +} + +static void +gtk_bubble_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_RELATIVE_TO: + gtk_bubble_window_set_relative_to (GTK_BUBBLE_WINDOW (object), + g_value_get_object (value)); + break; + case PROP_POINTING_TO: + gtk_bubble_window_set_pointing_to (GTK_BUBBLE_WINDOW (object), + g_value_get_boxed (value)); + break; + case PROP_POSITION: + gtk_bubble_window_set_position (GTK_BUBBLE_WINDOW (object), + g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtk_bubble_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkBubbleWindowPrivate *priv = GTK_BUBBLE_WINDOW (object)->_priv; + + switch (prop_id) + { + case PROP_RELATIVE_TO: + g_value_set_object (value, priv->relative_to); + break; + case PROP_POINTING_TO: + g_value_set_boxed (value, &priv->pointing_to); + break; + case PROP_POSITION: + g_value_set_enum (value, priv->preferred_position); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtk_bubble_window_finalize (GObject *object) +{ + GtkBubbleWindow *window = GTK_BUBBLE_WINDOW (object); + GtkBubbleWindowPrivate *priv = window->_priv;; + + gtk_bubble_window_popdown (window); + + if (priv->relative_to) + g_object_unref (priv->relative_to); + + G_OBJECT_CLASS (gtk_bubble_window_parent_class)->finalize (object); +} + +static void +_gtk_bubble_window_get_pointed_to_coords (GtkBubbleWindow *window, + gint *x, + gint *y, + cairo_rectangle_int_t *root_rect) +{ + GtkBubbleWindowPrivate *priv = window->_priv; + cairo_rectangle_int_t rect; + GdkScreen *screen; + + rect = priv->pointing_to; + screen = gtk_widget_get_screen (GTK_WIDGET (window)); + + if (priv->relative_to) + gdk_window_get_root_coords (priv->relative_to, + rect.x, rect.y, &rect.x, &rect.y); + + if (POS_IS_VERTICAL (priv->final_position)) + { + *x = CLAMP (rect.x + (rect.width / 2), + 0, gdk_screen_get_width (screen)); + *y = rect.y; + + if (priv->final_position == GTK_POS_BOTTOM) + (*y) += rect.height; + } + else + { + *y = CLAMP (rect.y + (rect.height / 2), + 0, gdk_screen_get_height (screen)); + *x = rect.x; + + if (priv->final_position == GTK_POS_RIGHT) + (*x) += rect.width; + } + + if (root_rect) + *root_rect = rect; +} + +static void +_gtk_bubble_window_apply_tail_path (GtkBubbleWindow *window, + cairo_t *cr, + GtkAllocation *allocation) +{ + GtkBubbleWindowPrivate *priv = window->_priv; + gint base, tip, x, y; + + _gtk_bubble_window_get_pointed_to_coords (window, &x, &y, NULL); + + if (priv->final_position == GTK_POS_BOTTOM || + priv->final_position == GTK_POS_RIGHT) + { + base = TAIL_HEIGHT; + tip = 0; + } + else if (priv->final_position == GTK_POS_TOP) + { + base = allocation->height - TAIL_HEIGHT; + tip = allocation->height; + } + else if (priv->final_position == GTK_POS_LEFT) + { + base = allocation->width - TAIL_HEIGHT; + tip = allocation->width; + } + + if (POS_IS_VERTICAL (priv->final_position)) + { + cairo_move_to (cr, CLAMP (x - priv->win_x - TAIL_GAP_WIDTH / 2, + 0, allocation->width - TAIL_GAP_WIDTH), base); + cairo_line_to (cr, CLAMP (x - priv->win_x + TAIL_GAP_WIDTH / 2, + TAIL_GAP_WIDTH, allocation->width), base); + cairo_line_to (cr, CLAMP (x - priv->win_x, 0, allocation->width), tip); + } + else + { + cairo_move_to (cr, base, + CLAMP (y - priv->win_y - TAIL_GAP_WIDTH / 2, + 0, allocation->height - TAIL_GAP_WIDTH)); + cairo_line_to (cr, base, + CLAMP (y - priv->win_y + TAIL_GAP_WIDTH / 2, + TAIL_GAP_WIDTH, allocation->height)); + cairo_line_to (cr, tip, CLAMP (y - priv->win_y, 0, allocation->height)); + } + + cairo_close_path (cr); +} + +static void +_gtk_bubble_window_apply_border_path (GtkBubbleWindow *window, + cairo_t *cr) +{ + GtkBubbleWindowPrivate *priv; + GtkAllocation allocation; + + priv = window->_priv; + gtk_widget_get_allocation (GTK_WIDGET (window), &allocation); + + if (priv->final_position == GTK_POS_TOP) + cairo_rectangle (cr, 0, 0, allocation.width, allocation.height - TAIL_HEIGHT); + else if (priv->final_position == GTK_POS_BOTTOM) + cairo_rectangle (cr, 0, TAIL_HEIGHT , allocation.width, + allocation.height - TAIL_HEIGHT); + else if (priv->final_position == GTK_POS_LEFT) + cairo_rectangle (cr, 0, 0, allocation.width - TAIL_HEIGHT, allocation.height); + else if (priv->final_position == GTK_POS_RIGHT) + cairo_rectangle (cr, TAIL_HEIGHT, 0, + allocation.width - TAIL_HEIGHT, allocation.height); + + _gtk_bubble_window_apply_tail_path (window, cr, &allocation); +} + +static void +_gtk_bubble_window_update_shape (GtkBubbleWindow *window) +{ + cairo_surface_t *surface; + cairo_region_t *region; + GdkWindow *win; + cairo_t *cr; + + win = gtk_widget_get_window (GTK_WIDGET (window)); + surface = + gdk_window_create_similar_surface (win, + CAIRO_CONTENT_COLOR_ALPHA, + gdk_window_get_width (win), + gdk_window_get_height (win)); + + cr = cairo_create (surface); + _gtk_bubble_window_apply_border_path (window, cr); + cairo_fill (cr); + cairo_destroy (cr); + + region = gdk_cairo_region_create_from_surface (surface); + cairo_surface_destroy (surface); + + if (!gtk_widget_is_composited (GTK_WIDGET (window))) + gtk_widget_shape_combine_region (GTK_WIDGET (window), region); + + gtk_widget_input_shape_combine_region (GTK_WIDGET (window), region); + cairo_region_destroy (region); +} + +static void +_gtk_bubble_window_update_position (GtkBubbleWindow *window) +{ + GtkBubbleWindowPrivate *priv; + cairo_rectangle_int_t rect; + GtkAllocation allocation; + gint win_x, win_y, x, y; + GdkScreen *screen; + + priv = window->_priv; + screen = gtk_widget_get_screen (GTK_WIDGET (window)); + gtk_widget_get_allocation (GTK_WIDGET (window), &allocation); + priv->final_position = priv->preferred_position; + rect = priv->pointing_to; + + _gtk_bubble_window_get_pointed_to_coords (window, &x, &y, &rect); + + /* Check whether there's enough room on the + * preferred side, move to the opposite one if not. + */ + if (priv->preferred_position == GTK_POS_TOP && rect.y < allocation.height) + priv->final_position = GTK_POS_BOTTOM; + else if (priv->preferred_position == GTK_POS_BOTTOM && + rect.y > gdk_screen_get_height (screen) - allocation.height) + priv->final_position = GTK_POS_TOP; + else if (priv->preferred_position == GTK_POS_LEFT && rect.x < allocation.width) + priv->final_position = GTK_POS_RIGHT; + else if (priv->preferred_position == GTK_POS_RIGHT && + rect.x > gdk_screen_get_width (screen) - allocation.width) + priv->final_position = GTK_POS_LEFT; + + if (POS_IS_VERTICAL (priv->final_position)) + { + win_x = CLAMP (x - allocation.width / 2, + 0, gdk_screen_get_width (screen) - allocation.width); + win_y = y; + + if (priv->final_position == GTK_POS_TOP) + win_y -= allocation.height; + } + else + { + win_y = CLAMP (y - allocation.height / 2, + 0, gdk_screen_get_height (screen) - allocation.height); + win_x = x; + + if (priv->final_position == GTK_POS_LEFT) + win_x -= allocation.width; + + } + + priv->win_x = win_x; + priv->win_y = win_y; + gtk_window_move (GTK_WINDOW (window), win_x, win_y); + + gtk_widget_queue_resize (GTK_WIDGET (window)); +} + +static gboolean +gtk_bubble_window_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkStyleContext *context; + GtkAllocation allocation; + GtkWidget *child; + + cairo_save (cr); + context = gtk_widget_get_style_context (widget); + gtk_widget_get_allocation (widget, &allocation); + + if (gtk_widget_is_composited (widget)) + { + cairo_save (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + cairo_set_source_rgba (cr, 0, 0, 0, 0); + cairo_paint (cr); + cairo_restore (cr); + + _gtk_bubble_window_apply_border_path (GTK_BUBBLE_WINDOW (widget), cr); + cairo_clip (cr); + } + + gtk_render_background (context, cr, 0, 0, + allocation.width, allocation.height); + child = gtk_bin_get_child (GTK_BIN (widget)); + + if (child) + gtk_container_propagate_draw (GTK_CONTAINER (widget), child, cr); + + cairo_restore (cr); + + return TRUE; +} + +static void +gtk_bubble_window_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + GtkBubbleWindowPrivate *priv; + GtkWidget *child; + gint min, nat; + + priv = GTK_BUBBLE_WINDOW (widget)->_priv; + child = gtk_bin_get_child (GTK_BIN (widget)); + min = nat = 0; + + if (child) + gtk_widget_get_preferred_width (child, &min, &nat); + + if (!POS_IS_VERTICAL (priv->final_position)) + { + min += TAIL_HEIGHT; + nat += TAIL_HEIGHT; + } + + if (minimum_width) + *minimum_width = MAX (min, TAIL_GAP_WIDTH); + + if (natural_width) + *natural_width = MAX (nat, TAIL_GAP_WIDTH); +} + +static void +gtk_bubble_window_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + GtkBubbleWindowPrivate *priv; + GtkWidget *child; + gint min, nat; + + priv = GTK_BUBBLE_WINDOW (widget)->_priv; + child = gtk_bin_get_child (GTK_BIN (widget)); + min = nat = 0; + + if (child) + gtk_widget_get_preferred_height (child, &min, &nat); + + if (POS_IS_VERTICAL (priv->final_position)) + { + min += TAIL_HEIGHT; + nat += TAIL_HEIGHT; + } + + if (minimum_height) + *minimum_height = MAX (min, TAIL_GAP_WIDTH); + + if (natural_height) + *natural_height = MAX (nat, TAIL_GAP_WIDTH); +} + +static void +gtk_bubble_window_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkBubbleWindowPrivate *priv; + GtkWidget *child; + + priv = GTK_BUBBLE_WINDOW (widget)->_priv; + gtk_widget_set_allocation (widget, allocation); + child = gtk_bin_get_child (GTK_BIN (widget)); + + if (child) + { + GtkAllocation child_alloc; + + child_alloc.x = child_alloc.y = 0; + child_alloc.width = allocation->width; + child_alloc.height = allocation->height; + + if (POS_IS_VERTICAL (priv->final_position)) + child_alloc.height -= TAIL_HEIGHT; + else + child_alloc.width -= TAIL_HEIGHT; + + if (priv->final_position == GTK_POS_BOTTOM) + child_alloc.y += TAIL_HEIGHT; + else if (priv->final_position == GTK_POS_RIGHT) + child_alloc.x += TAIL_HEIGHT; + + gtk_widget_size_allocate (child, &child_alloc); + } + + if (gtk_widget_get_realized (widget)) + _gtk_bubble_window_update_shape (GTK_BUBBLE_WINDOW (widget)); + + if (gtk_widget_get_visible (widget)) + _gtk_bubble_window_update_position (GTK_BUBBLE_WINDOW (widget)); +} + +static gboolean +gtk_bubble_window_button_press (GtkWidget *widget, + GdkEventButton *event) +{ + GtkWidget *child; + + child = gtk_bin_get_child (GTK_BIN (widget)); + + if (child && event->window == gtk_widget_get_window (widget)) + { + GtkAllocation child_alloc; + + gtk_widget_get_allocation (child, &child_alloc); + + if (event->x < child_alloc.x || + event->x > child_alloc.x + child_alloc.width || + event->y < child_alloc.y || + event->y > child_alloc.y + child_alloc.height) + gtk_bubble_window_popdown (GTK_BUBBLE_WINDOW (widget)); + } + else + gtk_bubble_window_popdown (GTK_BUBBLE_WINDOW (widget)); + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +gtk_bubble_window_key_press (GtkWidget *widget, + GdkEventKey *event) +{ + if (event->keyval == GDK_KEY_Escape) + { + gtk_bubble_window_popdown (GTK_BUBBLE_WINDOW (widget)); + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +gtk_bubble_window_grab_broken (GtkWidget *widget, + GdkEventGrabBroken *grab_broken) +{ + GtkBubbleWindow *window = GTK_BUBBLE_WINDOW (widget); + GtkBubbleWindowPrivate *priv; + GdkDevice *event_device; + + priv = window->_priv; + event_device = gdk_event_get_device ((GdkEvent *) grab_broken); + + if (event_device == priv->device || + event_device == gdk_device_get_associated_device (priv->device)) + gtk_bubble_window_ungrab (window); + + return FALSE; +} + +static void +gtk_bubble_window_grab_notify (GtkWidget *widget, + gboolean was_grabbed) +{ + GtkBubbleWindow *window = GTK_BUBBLE_WINDOW (widget); + GtkBubbleWindowPrivate *priv; + + priv = window->_priv; + + if (priv->device && gtk_widget_device_is_shadowed (widget, priv->device)) + gtk_bubble_window_ungrab (window); +} + +static void +gtk_bubble_window_screen_changed (GtkWidget *widget, + GdkScreen *previous_screen) +{ + GdkScreen *screen; + GdkVisual *visual; + + screen = gtk_widget_get_screen (widget); + visual = gdk_screen_get_rgba_visual (screen); + + if (visual) + gtk_widget_set_visual (widget, visual); +} + +static void +gtk_bubble_window_class_init (GtkBubbleWindowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = gtk_bubble_window_constructor; + object_class->set_property = gtk_bubble_window_set_property; + object_class->get_property = gtk_bubble_window_get_property; + object_class->finalize = gtk_bubble_window_finalize; + + widget_class->get_preferred_width = gtk_bubble_window_get_preferred_width; + widget_class->get_preferred_height = gtk_bubble_window_get_preferred_height; + widget_class->size_allocate = gtk_bubble_window_size_allocate; + widget_class->draw = gtk_bubble_window_draw; + widget_class->button_press_event = gtk_bubble_window_button_press; + widget_class->key_press_event = gtk_bubble_window_key_press; + widget_class->grab_broken_event = gtk_bubble_window_grab_broken; + widget_class->grab_notify = gtk_bubble_window_grab_notify; + widget_class->screen_changed = gtk_bubble_window_screen_changed; + + g_object_class_install_property (object_class, + PROP_RELATIVE_TO, + g_param_spec_object ("relative-to", + P_("Relative to"), + P_("Window the bubble window points to"), + GDK_TYPE_WINDOW, + GTK_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_POINTING_TO, + g_param_spec_boxed ("pointing-to", + P_("Pointing to"), + P_("Rectangle the bubble window points to"), + CAIRO_GOBJECT_TYPE_RECTANGLE_INT, + GTK_PARAM_READWRITE)); + g_object_class_install_property (object_class, + PROP_POSITION, + g_param_spec_enum ("position", + P_("Position"), + P_("Position to place the bubble window"), + GTK_TYPE_POSITION_TYPE, GTK_POS_TOP, + GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT)); + + g_type_class_add_private (klass, sizeof (GtkBubbleWindowPrivate)); +} + +static void +_gtk_bubble_window_update_relative_to (GtkBubbleWindow *window, + GdkWindow *relative_to) +{ + GtkBubbleWindowPrivate *priv; + + priv = window->_priv; + + if (priv->relative_to == relative_to) + return; + + if (priv->relative_to) + g_object_unref (priv->relative_to); + + priv->relative_to = (relative_to) ? g_object_ref (relative_to) : NULL; + g_object_notify (G_OBJECT (window), "relative-to"); +} + +static void +_gtk_bubble_window_update_pointing_to (GtkBubbleWindow *window, + cairo_rectangle_int_t *pointing_to) +{ + GtkBubbleWindowPrivate *priv; + + priv = window->_priv; + priv->pointing_to = *pointing_to; + priv->has_pointing_to = TRUE; + g_object_notify (G_OBJECT (window), "pointing-to"); +} + +static void +_gtk_bubble_window_update_preferred_position (GtkBubbleWindow *window, + GtkPositionType position) +{ + GtkBubbleWindowPrivate *priv; + + priv = window->_priv; + priv->preferred_position = position; + g_object_notify (G_OBJECT (window), "position"); +} + +/** + * gtk_bubble_window_new: + * + * Returns a newly created #GtkBubblewindow + * + * Returns: a new #GtkBubbleWindow + * + * Since: 3.8 + **/ +GtkWidget * +gtk_bubble_window_new (void) +{ + return g_object_new (GTK_TYPE_BUBBLE_WINDOW, NULL); +} + +/** + * gtk_bubble_window_set_relative_to: + * @window: a #GtkBubbleWindow + * @relative_to: a #GdkWindow + * + * Sets the #GdkWindow to act as the origin of coordinates of + * @window, or %NULL to use the root window. See + * gtk_bubble_window_set_pointing_to() + * + * If @window is currently visible, it will be moved to reflect + * this change. + * + * Since: 3.8 + **/ +void +gtk_bubble_window_set_relative_to (GtkBubbleWindow *window, + GdkWindow *relative_to) +{ + g_return_if_fail (GTK_IS_BUBBLE_WINDOW (window)); + g_return_if_fail (!relative_to || GDK_IS_WINDOW (relative_to)); + + _gtk_bubble_window_update_relative_to (window, relative_to); + + if (gtk_widget_get_visible (GTK_WIDGET (window))) + _gtk_bubble_window_update_position (window); +} + +/** + * gtk_bubble_window_get_relative_to: + * @window: a #GtkBubbleWindow + * + * Returns the #GdkWindow used as the origin of coordinates. + * If @window is currently visible, it will be moved to reflect + * this change. + * + * Returns: the #GdkWindow @window is placed upon + * + * Since: 3.8 + **/ +GdkWindow * +gtk_bubble_window_get_relative_to (GtkBubbleWindow *window) +{ + GtkBubbleWindowPrivate *priv; + + g_return_val_if_fail (GTK_IS_BUBBLE_WINDOW (window), NULL); + + priv = window->_priv; + + return priv->relative_to; +} + +/** + * gtk_bubble_window_set_pointing_to: + * @window: a #GtkBubbleWindow + * @rect: rectangle to point to + * + * Sets the rectangle that @window will point to, the coordinates + * of this rectangle are relative to the #GdkWindow set through + * gtk_bubble_window_set_relative_to(). + * + * Since: 3.8 + **/ +void +gtk_bubble_window_set_pointing_to (GtkBubbleWindow *window, + cairo_rectangle_int_t *rect) +{ + g_return_if_fail (GTK_IS_BUBBLE_WINDOW (window)); + g_return_if_fail (rect != NULL); + + _gtk_bubble_window_update_pointing_to (window, rect); + + if (gtk_widget_get_visible (GTK_WIDGET (window))) + _gtk_bubble_window_update_position (window); +} + +/** + * gtk_bubble_window_get_pointing_to: + * @window: a #GtkBubbleWindow + * @rect: (out): location to store the rectangle + * + * If a rectangle to point to is set, this function will return + * %TRUE and fill in @rect with the rectangle @window is currently + * pointing to. + * + * Returns: %TRUE if a rectangle is set + * + * Since: 3.8 + **/ +gboolean +gtk_bubble_window_get_pointing_to (GtkBubbleWindow *window, + cairo_rectangle_int_t *rect) +{ + GtkBubbleWindowPrivate *priv; + + g_return_val_if_fail (GTK_IS_BUBBLE_WINDOW (window), FALSE); + + priv = window->_priv; + + if (rect) + *rect = priv->pointing_to; + + return priv->has_pointing_to; +} + +/** + * gtk_bubble_window_set_position: + * @window: a #GtkBubbleWindow + * @position: preferred bubble position + * + * Sets the preferred position for @window to appear. + * If @window is currently visible, it will be moved to reflect + * this change. + * + * + * This preference will be respected where possible, although + * on lack of space (eg. if close to the screen edges), the + * #GtkBubbleWindow may choose to appear on the opposite side + * + * + * Since: 3.8 + **/ +void +gtk_bubble_window_set_position (GtkBubbleWindow *window, + GtkPositionType position) +{ + g_return_if_fail (GTK_IS_BUBBLE_WINDOW (window)); + g_return_if_fail (position >= GTK_POS_LEFT && position <= GTK_POS_BOTTOM); + + _gtk_bubble_window_update_preferred_position (window, position); + + if (gtk_widget_get_visible (GTK_WIDGET (window))) + _gtk_bubble_window_update_position (window); +} + +/** + * gtk_bubble_window_get_position: + * @window: a #GtkBubbleWindow + * + * Returns the preferred position to place @window + * + * Returns: The preferred position + * + * Since: 3.8 + **/ +GtkPositionType +gtk_bubble_window_get_position (GtkBubbleWindow *window) +{ + GtkBubbleWindowPrivate *priv; + + g_return_val_if_fail (GTK_IS_BUBBLE_WINDOW (window), GTK_POS_TOP); + + priv = window->_priv; + + return priv->preferred_position; +} + +/** + * gtk_bubble_window_popup: + * @window: a #GtkBubbleWindow + * @relative_to: #GdkWindow to position upon + * @pointing_to: rectangle to point to, in @relative_to coordinates + * @position: preferred position for @window + * + * This function sets atomically all #GtkBubbleWindow position + * parameters, and shows/updates @window + * + * Since: 3.8 + **/ +void +gtk_bubble_window_popup (GtkBubbleWindow *window, + GdkWindow *relative_to, + cairo_rectangle_int_t *pointing_to, + GtkPositionType position) +{ + g_return_if_fail (GTK_IS_BUBBLE_WINDOW (window)); + g_return_if_fail (!relative_to || GDK_IS_WINDOW (relative_to)); + g_return_if_fail (position >= GTK_POS_LEFT && position <= GTK_POS_BOTTOM); + g_return_if_fail (pointing_to != NULL); + + _gtk_bubble_window_update_preferred_position (window, position); + _gtk_bubble_window_update_relative_to (window, relative_to); + _gtk_bubble_window_update_pointing_to (window, pointing_to); + + if (!gtk_widget_get_visible (GTK_WIDGET (window))) + gtk_widget_show (GTK_WIDGET (window)); + + _gtk_bubble_window_update_position (window); +} + +/** + * gtk_bubble_window_popdown: + * @window: a #GtkBubbleWindow + * + * Removes the window from the screen + * + * + * If a grab was previously added through gtk_bubble_window_grab(), + * the grab will be removed by this function. + * + * + * Since: 3.8 + **/ +void +gtk_bubble_window_popdown (GtkBubbleWindow *window) +{ + GtkBubbleWindowPrivate *priv = window->_priv; + + g_return_if_fail (GTK_IS_BUBBLE_WINDOW (window)); + + if (priv->grabbed) + gtk_bubble_window_ungrab (window); + + if (gtk_widget_get_visible (GTK_WIDGET (window))) + gtk_widget_hide (GTK_WIDGET (window)); +} + +/** + * gtk_bubble_window_grab: + * @window: a #GtkBubbleWindow + * @device: a master #GdkDevice + * @activate_time: timestamp to perform the grab + * + * This function performs GDK and GTK+ grabs on @device and + * its paired #GdkDevice. After this call all pointer/keyboard + * events will be handled by @window. + * + * Calling this also brings in a #GtkMenu alike behavior, clicking + * outside the #GtkBubbleWindow or pressing the Escape key will + * popdown the menu by default. + * + * + * If there was a previous grab, it will be undone before doing + * the requested grab. + * + * + * Returns: %TRUE if the grab was successful + * + * Since: 3.8 + **/ +gboolean +gtk_bubble_window_grab (GtkBubbleWindow *window, + GdkDevice *device, + guint32 activate_time) +{ + GtkBubbleWindowPrivate *priv; + GdkDevice *other_device; + GdkWindow *grab_window; + GdkGrabStatus status; + + g_return_val_if_fail (GTK_IS_BUBBLE_WINDOW (window), FALSE); + g_return_val_if_fail (GDK_IS_DEVICE (device), FALSE); + g_return_val_if_fail (gdk_device_get_device_type (device) == GDK_DEVICE_TYPE_MASTER, FALSE); + + priv = window->_priv; + + if (!priv->has_pointing_to || + gdk_window_is_destroyed (priv->relative_to)) + return FALSE; + + if (priv->device) + gtk_bubble_window_ungrab (window); + + gtk_widget_realize (GTK_WIDGET (window)); + grab_window = gtk_widget_get_window (GTK_WIDGET (window)); + other_device = gdk_device_get_associated_device (device); + + status = gdk_device_grab (device, grab_window, + GDK_OWNERSHIP_WINDOW, TRUE, GRAB_EVENT_MASK, + NULL, activate_time); + + if (status == GDK_GRAB_SUCCESS) + { + status = gdk_device_grab (other_device, grab_window, + GDK_OWNERSHIP_WINDOW, TRUE, GRAB_EVENT_MASK, + NULL, activate_time); + + /* Ungrab the first device on error */ + if (status != GDK_GRAB_SUCCESS) + gdk_device_ungrab (device, activate_time); + } + + if (status == GDK_GRAB_SUCCESS) + { + gtk_device_grab_add (GTK_WIDGET (window), device, TRUE); + priv->device = device; + } + + return status == GDK_GRAB_SUCCESS; +} + +/** + * gtk_bubble_window_ungrab: + * @window: a #GtkBubbleWindow + * + * This functions undoes a grab added through gtk_bubble_window_grab() + * in this @window, + * + * Since: 3.8 + **/ +void +gtk_bubble_window_ungrab (GtkBubbleWindow *window) +{ + GtkBubbleWindowPrivate *priv; + + g_return_if_fail (GTK_IS_BUBBLE_WINDOW (window)); + + priv = window->_priv; + + if (!priv->device) + return; + + gdk_device_ungrab (priv->device, GDK_CURRENT_TIME); + gdk_device_ungrab (gdk_device_get_associated_device (priv->device), + GDK_CURRENT_TIME); + gtk_device_grab_remove (GTK_WIDGET (window), priv->device); + priv->device = NULL; +} diff --git a/gtk/gtkbubblewindow.h b/gtk/gtkbubblewindow.h new file mode 100644 index 0000000000..5c0a9fd79d --- /dev/null +++ b/gtk/gtkbubblewindow.h @@ -0,0 +1,81 @@ +/* GTK - The GIMP Toolkit + * Copyright © 2013 Carlos Garnacho + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef __GTK_BUBBLE_WINDOW_H__ +#define __GTK_BUBBLE_WINDOW_H__ + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_BUBBLE_WINDOW (gtk_bubble_window_get_type ()) +#define GTK_BUBBLE_WINDOW(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GTK_TYPE_BUBBLE_WINDOW, GtkBubbleWindow)) +#define GTK_BUBBLE_WINDOW_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), GTK_TYPE_BUBBLE_WINDOW, GtkBubbleWindowClass)) +#define GTK_IS_BUBBLE_WINDOW(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GTK_TYPE_BUBBLE_WINDOW)) +#define GTK_IS_BUBBLE_WINDOW_CLASS(o) (G_TYPE_CHECK_CLASS_TYPE ((o), GTK_TYPE_BUBBLE_WINDOW)) +#define GTK_BUBBLE_WINDOW_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GTK_TYPE_BUBBLE_WINDOW, GtkBubbleWindowClass)) + +typedef struct _GtkBubbleWindow GtkBubbleWindow; +typedef struct _GtkBubbleWindowClass GtkBubbleWindowClass; + +struct _GtkBubbleWindow +{ + GtkWindow parent_instance; + + /*< private >*/ + gpointer _priv; +}; + +struct _GtkBubbleWindowClass +{ + GtkWindowClass parent_class; +}; + +GType gtk_bubble_window_get_type (void) G_GNUC_CONST; + +GtkWidget * gtk_bubble_window_new (void); + +void gtk_bubble_window_set_relative_to (GtkBubbleWindow *window, + GdkWindow *relative_to); +GdkWindow * gtk_bubble_window_get_relative_to (GtkBubbleWindow *window); + +void gtk_bubble_window_set_pointing_to (GtkBubbleWindow *window, + cairo_rectangle_int_t *rect); +gboolean gtk_bubble_window_get_pointing_to (GtkBubbleWindow *window, + cairo_rectangle_int_t *rect); +void gtk_bubble_window_set_position (GtkBubbleWindow *window, + GtkPositionType position); + +GtkPositionType + gtk_bubble_window_get_position (GtkBubbleWindow *window); + +void gtk_bubble_window_popup (GtkBubbleWindow *window, + GdkWindow *relative_to, + cairo_rectangle_int_t *pointing_to, + GtkPositionType position); + +void gtk_bubble_window_popdown (GtkBubbleWindow *window); + +gboolean gtk_bubble_window_grab (GtkBubbleWindow *window, + GdkDevice *device, + guint32 activate_time); + +void gtk_bubble_window_ungrab (GtkBubbleWindow *window); + +G_END_DECLS + +#endif /* __GTK_BUBBLE_POPUP_H__ */