/* GTK - The GIMP Toolkit * Copyright (C) 2019 Red Hat, Inc. * * Authors: * - Matthias Clasen * * 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 . */ /** * GtkPopover: * * `GtkPopover` is a bubble-like context popup. * * ![An example GtkPopover](popover.png) * * It is primarily meant to provide context-dependent information * or options. Popovers are attached to a parent widget. By default, * they point to the whole widget area, although this behavior can be * changed with [method@Gtk.Popover.set_pointing_to]. * * The position of a popover relative to the widget it is attached to * can also be changed with [method@Gtk.Popover.set_position] * * By default, `GtkPopover` performs a grab, in order to ensure input * events get redirected to it while it is shown, and also so the popover * is dismissed in the expected situations (clicks outside the popover, * or the Escape key being pressed). If no such modal behavior is desired * on a popover, [method@Gtk.Popover.set_autohide] may be called on it to * tweak its behavior. * * ## GtkPopover as menu replacement * * `GtkPopover` is often used to replace menus. The best was to do this * is to use the [class@Gtk.PopoverMenu] subclass which supports being * populated from a `GMenuModel` with [ctor@Gtk.PopoverMenu.new_from_model]. * * ```xml *
* horizontal-buttons * * Cut * app.cut * edit-cut-symbolic * * * Copy * app.copy * edit-copy-symbolic * * * Paste * app.paste * edit-paste-symbolic * *
* ``` * * # CSS nodes * * ``` * popover[.menu] * ├── arrow * ╰── contents.background * ╰── * ``` * * The contents child node always gets the .background style class * and the popover itself gets the .menu style class if the popover * is menu-like (i.e. `GtkPopoverMenu`). * * Particular uses of `GtkPopover`, such as touch selection popups or * magnifiers in `GtkEntry` or `GtkTextView` get style classes like * .touch-selection or .magnifier to differentiate from plain popovers. * * When styling a popover directly, the popover node should usually * not have any background. The visible part of the popover can have * a shadow. To specify it in CSS, set the box-shadow of the contents node. * * Note that, in order to accomplish appropriate arrow visuals, `GtkPopover` * uses custom drawing for the arrow node. This makes it possible for the * arrow to change its shape dynamically, but it also limits the possibilities * of styling it using CSS. In particular, the arrow gets drawn over the * content node's border and shadow, so they look like one shape, which * means that the border width of the content node and the arrow node should * be the same. The arrow also does not support any border shape other than * solid, no border-radius, only one border width (border-bottom-width is * used) and no box-shadow. */ #include "config.h" #include "gtkpopoverprivate.h" #include "gtkpopovermenuprivate.h" #include "gtknative.h" #include "gtkwidgetprivate.h" #include "gtkeventcontrollerkey.h" #include "gtkeventcontrollerfocus.h" #include "gtkbinlayout.h" #include "gtkenums.h" #include "gtktypebuiltins.h" #include "gtkpopovercontentprivate.h" #include "gtkintl.h" #include "gtkprivate.h" #include "gtkmain.h" #include "gtkstack.h" #include "gtkmenusectionboxprivate.h" #include "gdk/gdkeventsprivate.h" #include "gtkpointerfocusprivate.h" #include "gtkcsscolorvalueprivate.h" #include "gtksnapshot.h" #include "gtkshortcutmanager.h" #include "gtkbuildable.h" #include "gtktooltipprivate.h" #include "gtkcssboxesimplprivate.h" #include "gtknativeprivate.h" #include "gtkrender.h" #include "gtkstylecontextprivate.h" #include "gtkroundedboxprivate.h" #include "gsk/gskroundedrectprivate.h" #include "gtkcssshadowvalueprivate.h" #include "gdk/gdksurfaceprivate.h" #define MNEMONICS_DELAY 300 /* ms */ #define TAIL_GAP_WIDTH 24 #define TAIL_HEIGHT 12 #define POS_IS_VERTICAL(p) ((p) == GTK_POS_TOP || (p) == GTK_POS_BOTTOM) typedef struct { GdkSurface *surface; GskRenderer *renderer; GtkWidget *default_widget; GdkRectangle pointing_to; gboolean has_pointing_to; guint surface_transform_changed_cb; GtkPositionType position; gboolean autohide; gboolean has_arrow; gboolean mnemonics_visible; gboolean disable_auto_mnemonics; gboolean cascade_popdown; int x_offset; int y_offset; guint mnemonics_display_timeout_id; GtkWidget *child; GtkWidget *contents_widget; GtkCssNode *arrow_node; GskRenderNode *arrow_render_node; GdkPopupLayout *layout; GdkRectangle final_rect; GtkPositionType final_position; } GtkPopoverPrivate; enum { CLOSED, ACTIVATE_DEFAULT, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; enum { PROP_POINTING_TO = 1, PROP_POSITION, PROP_AUTOHIDE, PROP_DEFAULT_WIDGET, PROP_HAS_ARROW, PROP_MNEMONICS_VISIBLE, PROP_CHILD, PROP_CASCADE_POPDOWN, NUM_PROPERTIES }; static GParamSpec *properties[NUM_PROPERTIES] = { NULL }; static void gtk_popover_buildable_init (GtkBuildableIface *iface); static void gtk_popover_shortcut_manager_interface_init (GtkShortcutManagerInterface *iface); static void gtk_popover_native_interface_init (GtkNativeInterface *iface); G_DEFINE_TYPE_WITH_CODE (GtkPopover, gtk_popover, GTK_TYPE_WIDGET, G_ADD_PRIVATE (GtkPopover) G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_popover_buildable_init) G_IMPLEMENT_INTERFACE (GTK_TYPE_SHORTCUT_MANAGER, gtk_popover_shortcut_manager_interface_init) G_IMPLEMENT_INTERFACE (GTK_TYPE_NATIVE, gtk_popover_native_interface_init)) static GdkSurface * gtk_popover_native_get_surface (GtkNative *native) { GtkPopover *popover = GTK_POPOVER (native); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); return priv->surface; } static GskRenderer * gtk_popover_native_get_renderer (GtkNative *native) { GtkPopover *popover = GTK_POPOVER (native); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); return priv->renderer; } static void gtk_popover_native_get_surface_transform (GtkNative *native, double *x, double *y) { GtkCssBoxes css_boxes; const graphene_rect_t *margin_rect; gtk_css_boxes_init (&css_boxes, GTK_WIDGET (native)); margin_rect = gtk_css_boxes_get_margin_rect (&css_boxes); *x = - margin_rect->origin.x; *y = - margin_rect->origin.y; } static gboolean is_gravity_facing_north (GdkGravity gravity) { switch (gravity) { case GDK_GRAVITY_NORTH_EAST: case GDK_GRAVITY_NORTH: case GDK_GRAVITY_NORTH_WEST: case GDK_GRAVITY_STATIC: return TRUE; case GDK_GRAVITY_SOUTH_WEST: case GDK_GRAVITY_WEST: case GDK_GRAVITY_SOUTH_EAST: case GDK_GRAVITY_EAST: case GDK_GRAVITY_CENTER: case GDK_GRAVITY_SOUTH: return FALSE; default: g_assert_not_reached (); } } static gboolean is_gravity_facing_south (GdkGravity gravity) { switch (gravity) { case GDK_GRAVITY_SOUTH_WEST: case GDK_GRAVITY_SOUTH_EAST: case GDK_GRAVITY_SOUTH: return TRUE; case GDK_GRAVITY_NORTH_EAST: case GDK_GRAVITY_NORTH: case GDK_GRAVITY_NORTH_WEST: case GDK_GRAVITY_STATIC: case GDK_GRAVITY_WEST: case GDK_GRAVITY_EAST: case GDK_GRAVITY_CENTER: return FALSE; default: g_assert_not_reached (); } } static gboolean is_gravity_facing_west (GdkGravity gravity) { switch (gravity) { case GDK_GRAVITY_NORTH_WEST: case GDK_GRAVITY_STATIC: case GDK_GRAVITY_SOUTH_WEST: case GDK_GRAVITY_WEST: return TRUE; case GDK_GRAVITY_NORTH_EAST: case GDK_GRAVITY_SOUTH_EAST: case GDK_GRAVITY_EAST: case GDK_GRAVITY_NORTH: case GDK_GRAVITY_CENTER: case GDK_GRAVITY_SOUTH: return FALSE; default: g_assert_not_reached (); } } static gboolean is_gravity_facing_east (GdkGravity gravity) { switch (gravity) { case GDK_GRAVITY_NORTH_EAST: case GDK_GRAVITY_SOUTH_EAST: case GDK_GRAVITY_EAST: return TRUE; case GDK_GRAVITY_NORTH_WEST: case GDK_GRAVITY_STATIC: case GDK_GRAVITY_SOUTH_WEST: case GDK_GRAVITY_WEST: case GDK_GRAVITY_NORTH: case GDK_GRAVITY_CENTER: case GDK_GRAVITY_SOUTH: return FALSE; default: g_assert_not_reached (); } } static gboolean did_flip_horizontally (GdkGravity original_gravity, GdkGravity final_gravity) { g_return_val_if_fail (original_gravity, FALSE); g_return_val_if_fail (final_gravity, FALSE); if (is_gravity_facing_east (original_gravity) && is_gravity_facing_west (final_gravity)) return TRUE; if (is_gravity_facing_west (original_gravity) && is_gravity_facing_east (final_gravity)) return TRUE; return FALSE; } static gboolean did_flip_vertically (GdkGravity original_gravity, GdkGravity final_gravity) { g_return_val_if_fail (original_gravity, FALSE); g_return_val_if_fail (final_gravity, FALSE); if (is_gravity_facing_north (original_gravity) && is_gravity_facing_south (final_gravity)) return TRUE; if (is_gravity_facing_south (original_gravity) && is_gravity_facing_north (final_gravity)) return TRUE; return FALSE; } static void update_popover_layout (GtkPopover *popover, GdkPopupLayout *layout, int width, int height) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GdkRectangle final_rect; gboolean flipped_x; gboolean flipped_y; GdkPopup *popup = GDK_POPUP (priv->surface); GtkPositionType position; g_clear_pointer (&priv->layout, gdk_popup_layout_unref); priv->layout = layout; final_rect = (GdkRectangle) { .x = gdk_popup_get_position_x (GDK_POPUP (priv->surface)), .y = gdk_popup_get_position_y (GDK_POPUP (priv->surface)), .width = gdk_surface_get_width (priv->surface), .height = gdk_surface_get_height (priv->surface), }; flipped_x = did_flip_horizontally (gdk_popup_layout_get_rect_anchor (layout), gdk_popup_get_rect_anchor (popup)) && did_flip_horizontally (gdk_popup_layout_get_surface_anchor (layout), gdk_popup_get_surface_anchor (popup)); flipped_y = did_flip_vertically (gdk_popup_layout_get_rect_anchor (layout), gdk_popup_get_rect_anchor (popup)) && did_flip_vertically (gdk_popup_layout_get_surface_anchor (layout), gdk_popup_get_surface_anchor (popup)); priv->final_rect = final_rect; position = priv->final_position; switch (priv->position) { case GTK_POS_LEFT: priv->final_position = flipped_x ? GTK_POS_RIGHT : GTK_POS_LEFT; break; case GTK_POS_RIGHT: priv->final_position = flipped_x ? GTK_POS_LEFT : GTK_POS_RIGHT; break; case GTK_POS_TOP: priv->final_position = flipped_y ? GTK_POS_BOTTOM : GTK_POS_TOP; break; case GTK_POS_BOTTOM: priv->final_position = flipped_y ? GTK_POS_TOP : GTK_POS_BOTTOM; break; default: g_assert_not_reached (); break; } if (priv->final_position != position || priv->final_rect.width != width || priv->final_rect.height != height) { gtk_widget_queue_allocate (GTK_WIDGET (popover)); g_clear_pointer (&priv->arrow_render_node, gsk_render_node_unref); } gtk_widget_queue_draw (GTK_WIDGET (popover)); } static GdkPopupLayout * create_popup_layout (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GdkRectangle rect; GdkGravity parent_anchor; GdkGravity surface_anchor; GdkAnchorHints anchor_hints; GdkPopupLayout *layout; GtkWidget *parent; GtkCssStyle *style; GtkBorder shadow_width; parent = gtk_widget_get_parent (GTK_WIDGET (popover)); gtk_widget_get_surface_allocation (parent, &rect); if (priv->has_pointing_to) { rect.x += priv->pointing_to.x; rect.y += priv->pointing_to.y; rect.width = priv->pointing_to.width; rect.height = priv->pointing_to.height; } style = gtk_css_node_get_style (gtk_widget_get_css_node (GTK_WIDGET (priv->contents_widget))); gtk_css_shadow_value_get_extents (style->background->box_shadow, &shadow_width); switch (priv->position) { case GTK_POS_LEFT: switch (gtk_widget_get_valign (GTK_WIDGET (popover))) { case GTK_ALIGN_START: parent_anchor = GDK_GRAVITY_NORTH_WEST; surface_anchor = GDK_GRAVITY_NORTH_EAST; break; case GTK_ALIGN_END: parent_anchor = GDK_GRAVITY_SOUTH_WEST; surface_anchor = GDK_GRAVITY_SOUTH_EAST; break; case GTK_ALIGN_FILL: case GTK_ALIGN_CENTER: case GTK_ALIGN_BASELINE: default: parent_anchor = GDK_GRAVITY_WEST; surface_anchor = GDK_GRAVITY_EAST; break; } anchor_hints = GDK_ANCHOR_FLIP_X | GDK_ANCHOR_SLIDE_Y; break; case GTK_POS_RIGHT: switch (gtk_widget_get_valign (GTK_WIDGET (popover))) { case GTK_ALIGN_START: parent_anchor = GDK_GRAVITY_NORTH_EAST; surface_anchor = GDK_GRAVITY_NORTH_WEST; break; case GTK_ALIGN_END: parent_anchor = GDK_GRAVITY_SOUTH_EAST; surface_anchor = GDK_GRAVITY_SOUTH_WEST; break; case GTK_ALIGN_FILL: case GTK_ALIGN_CENTER: case GTK_ALIGN_BASELINE: default: parent_anchor = GDK_GRAVITY_EAST; surface_anchor = GDK_GRAVITY_WEST; break; } anchor_hints = GDK_ANCHOR_FLIP_X | GDK_ANCHOR_SLIDE_Y; break; case GTK_POS_TOP: switch (gtk_widget_get_halign (GTK_WIDGET (popover))) { case GTK_ALIGN_START: parent_anchor = GDK_GRAVITY_NORTH_WEST; surface_anchor = GDK_GRAVITY_SOUTH_WEST; break; case GTK_ALIGN_END: parent_anchor = GDK_GRAVITY_NORTH_EAST; surface_anchor = GDK_GRAVITY_SOUTH_EAST; break; case GTK_ALIGN_FILL: case GTK_ALIGN_CENTER: case GTK_ALIGN_BASELINE: default: parent_anchor = GDK_GRAVITY_NORTH; surface_anchor = GDK_GRAVITY_SOUTH; break; } anchor_hints = GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE_X; break; case GTK_POS_BOTTOM: switch (gtk_widget_get_halign (GTK_WIDGET (popover))) { case GTK_ALIGN_START: parent_anchor = GDK_GRAVITY_SOUTH_WEST; surface_anchor = GDK_GRAVITY_NORTH_WEST; break; case GTK_ALIGN_END: parent_anchor = GDK_GRAVITY_SOUTH_EAST; surface_anchor = GDK_GRAVITY_NORTH_EAST; break; case GTK_ALIGN_FILL: case GTK_ALIGN_CENTER: case GTK_ALIGN_BASELINE: default: parent_anchor = GDK_GRAVITY_SOUTH; surface_anchor = GDK_GRAVITY_NORTH; break; } anchor_hints = GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE_X; break; default: g_assert_not_reached (); } anchor_hints |= GDK_ANCHOR_RESIZE; layout = gdk_popup_layout_new (&rect, parent_anchor, surface_anchor); gdk_popup_layout_set_anchor_hints (layout, anchor_hints); gdk_popup_layout_set_shadow_width (layout, shadow_width.left, shadow_width.right, shadow_width.top, shadow_width.bottom); if (priv->x_offset || priv->y_offset) gdk_popup_layout_set_offset (layout, priv->x_offset, priv->y_offset); return layout; } static gboolean present_popup (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkRequisition nat; GdkPopupLayout *layout; layout = create_popup_layout (popover); gtk_widget_get_preferred_size (GTK_WIDGET (popover), NULL, &nat); if (gdk_popup_present (GDK_POPUP (priv->surface), nat.width, nat.height, layout)) { update_popover_layout (popover, layout, nat.width, nat.height); return TRUE; } return FALSE; } /** * gtk_popover_present: * @popover: a `GtkPopover` * * Presents the popover to the user. */ void gtk_popover_present (GtkPopover *popover) { GtkWidget *widget = GTK_WIDGET (popover); if (!_gtk_widget_get_alloc_needed (widget)) gtk_widget_ensure_allocate (widget); else if (gtk_widget_get_visible (widget)) present_popup (popover); } static void maybe_request_motion_event (GtkPopover *popover) { GtkWidget *widget = GTK_WIDGET (popover); GtkRoot *root = gtk_widget_get_root (widget); GdkSeat *seat; GdkDevice *device; GtkWidget *focus; GdkSurface *focus_surface; seat = gdk_display_get_default_seat (gtk_widget_get_display (widget)); if (!seat) return; device = gdk_seat_get_pointer (seat); focus = gtk_window_lookup_pointer_focus_widget (GTK_WINDOW (root), device, NULL); if (!focus) return; if (!gtk_widget_is_ancestor (focus, GTK_WIDGET (popover))) return; focus_surface = gtk_native_get_surface (gtk_widget_get_native (focus)); gdk_surface_request_motion (focus_surface); } static void gtk_popover_native_layout (GtkNative *native, int width, int height) { GtkPopover *popover = GTK_POPOVER (native); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkWidget *widget = GTK_WIDGET (popover); update_popover_layout (popover, gdk_popup_layout_ref (priv->layout), width, height); if (gtk_widget_needs_allocate (widget)) { gtk_widget_allocate (widget, width, height, -1, NULL); /* This fake motion event is needed for getting up to date pointer focus * and coordinates when the pointer didn't move but the layout changed * within the popover. */ maybe_request_motion_event (popover); } else { gtk_widget_ensure_allocate (widget); } } static gboolean gtk_popover_has_mnemonic_modifier_pressed (GtkPopover *popover) { GList *seats, *s; gboolean retval = FALSE; seats = gdk_display_list_seats (gtk_widget_get_display (GTK_WIDGET (popover))); for (s = seats; s; s = s->next) { GdkDevice *dev = gdk_seat_get_keyboard (s->data); GdkModifierType mask; mask = gdk_device_get_modifier_state (dev); if ((mask & gtk_accelerator_get_default_mod_mask ()) == GDK_ALT_MASK) { retval = TRUE; break; } } g_list_free (seats); return retval; } static gboolean schedule_mnemonics_visible_cb (gpointer data) { GtkPopover *popover = data; GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); priv->mnemonics_display_timeout_id = 0; gtk_popover_set_mnemonics_visible (popover, TRUE); return G_SOURCE_REMOVE; } static void gtk_popover_schedule_mnemonics_visible (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->mnemonics_display_timeout_id) return; priv->mnemonics_display_timeout_id = g_timeout_add (MNEMONICS_DELAY, schedule_mnemonics_visible_cb, popover); gdk_source_set_static_name_by_id (priv->mnemonics_display_timeout_id, "[gtk] popover_schedule_mnemonics_visible_cb"); } static void gtk_popover_focus_in (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->disable_auto_mnemonics) return; if (gtk_widget_get_visible (widget)) { if (gtk_popover_has_mnemonic_modifier_pressed (popover)) gtk_popover_schedule_mnemonics_visible (popover); } } static void gtk_popover_focus_out (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->disable_auto_mnemonics) return; gtk_popover_set_mnemonics_visible (popover, FALSE); } static void update_mnemonics_visible (GtkPopover *popover, guint keyval, GdkModifierType state, gboolean visible) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->disable_auto_mnemonics) return; if ((keyval == GDK_KEY_Alt_L || keyval == GDK_KEY_Alt_R) && ((state & (gtk_accelerator_get_default_mod_mask ()) & ~(GDK_ALT_MASK)) == 0)) { if (visible) gtk_popover_schedule_mnemonics_visible (popover); else gtk_popover_set_mnemonics_visible (popover, FALSE); } } static gboolean gtk_popover_key_pressed (GtkWidget *widget, guint keyval, guint keycode, GdkModifierType state) { GtkPopover *popover = GTK_POPOVER (widget); GtkWindow *root; if (keyval == GDK_KEY_Escape) { gtk_popover_popdown (popover); return TRUE; } root = GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (popover))); _gtk_window_update_focus_visible (root, keyval, state, TRUE); update_mnemonics_visible (popover, keyval, state, TRUE); return FALSE; } static gboolean gtk_popover_key_released (GtkWidget *widget, guint keyval, guint keycode, GdkModifierType state) { GtkPopover *popover = GTK_POPOVER (widget); GtkWindow *root; root = GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (popover))); _gtk_window_update_focus_visible (root, keyval, state, FALSE); update_mnemonics_visible (popover, keyval, state, FALSE); return FALSE; } static void surface_mapped_changed (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); gtk_widget_set_visible (widget, gdk_surface_get_mapped (priv->surface)); } static gboolean surface_render (GdkSurface *surface, cairo_region_t *region, GtkWidget *widget) { gtk_widget_render (widget, surface, region); return TRUE; } static gboolean surface_event (GdkSurface *surface, GdkEvent *event, GtkWidget *widget) { gtk_main_do_event (event); return TRUE; } static void gtk_popover_activate_default (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkWidget *focus_widget; focus_widget = gtk_window_get_focus (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (popover)))); if (!gtk_widget_is_ancestor (focus_widget, GTK_WIDGET (popover))) focus_widget = NULL; if (priv->default_widget && gtk_widget_is_sensitive (priv->default_widget) && (!focus_widget || !gtk_widget_get_receives_default (focus_widget) )) gtk_widget_activate (priv->default_widget); else if (focus_widget && gtk_widget_is_sensitive (focus_widget)) gtk_widget_activate (focus_widget); } static void activate_default_cb (GSimpleAction *action, GVariant *parameter, gpointer data) { gtk_popover_activate_default (GTK_POPOVER (data)); } static void add_actions (GtkPopover *popover) { GActionEntry entries[] = { { "activate", activate_default_cb, NULL, NULL, NULL }, }; GActionGroup *actions; actions = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP (actions), entries, G_N_ELEMENTS (entries), popover); gtk_widget_insert_action_group (GTK_WIDGET (popover), "default", actions); g_object_unref (actions); } static void node_style_changed_cb (GtkCssNode *node, GtkCssStyleChange *change, GtkWidget *widget) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (GTK_POPOVER (widget)); g_clear_pointer (&priv->arrow_render_node, gsk_render_node_unref); if (gtk_css_style_change_affects (change, GTK_CSS_AFFECTS_SIZE)) gtk_widget_queue_resize (widget); else gtk_widget_queue_draw (widget); } static void gtk_popover_init (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkWidget *widget = GTK_WIDGET (popover); GtkEventController *controller; priv->position = GTK_POS_BOTTOM; priv->final_position = GTK_POS_BOTTOM; priv->autohide = TRUE; priv->has_arrow = TRUE; priv->cascade_popdown = FALSE; controller = gtk_event_controller_key_new (); g_signal_connect_swapped (controller, "key-pressed", G_CALLBACK (gtk_popover_key_pressed), popover); g_signal_connect_swapped (controller, "key-released", G_CALLBACK (gtk_popover_key_released), popover); gtk_widget_add_controller (GTK_WIDGET (popover), controller); controller = gtk_event_controller_focus_new (); g_signal_connect_swapped (controller, "enter", G_CALLBACK (gtk_popover_focus_in), popover); g_signal_connect_swapped (controller, "leave", G_CALLBACK (gtk_popover_focus_out), popover); gtk_widget_add_controller (widget, controller); priv->arrow_node = gtk_css_node_new (); gtk_css_node_set_name (priv->arrow_node, g_quark_from_static_string ("arrow")); gtk_css_node_set_parent (priv->arrow_node, gtk_widget_get_css_node (widget)); gtk_css_node_set_state (priv->arrow_node, gtk_css_node_get_state (gtk_widget_get_css_node (widget))); g_signal_connect_object (priv->arrow_node, "style-changed", G_CALLBACK (node_style_changed_cb), popover, 0); g_object_unref (priv->arrow_node); priv->contents_widget = gtk_popover_content_new (); gtk_widget_set_layout_manager (priv->contents_widget, gtk_bin_layout_new ()); gtk_widget_set_parent (priv->contents_widget, GTK_WIDGET (popover)); gtk_widget_set_overflow (priv->contents_widget, GTK_OVERFLOW_HIDDEN); gtk_widget_add_css_class (widget, "background"); add_actions (popover); } static void gtk_popover_realize (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GdkSurface *parent_surface; GtkWidget *parent; parent = gtk_widget_get_parent (widget); parent_surface = gtk_native_get_surface (gtk_widget_get_native (parent)); priv->surface = gdk_surface_new_popup (parent_surface, priv->autohide); gdk_surface_set_widget (priv->surface, widget); g_signal_connect_swapped (priv->surface, "notify::mapped", G_CALLBACK (surface_mapped_changed), widget); g_signal_connect (priv->surface, "render", G_CALLBACK (surface_render), widget); g_signal_connect (priv->surface, "event", G_CALLBACK (surface_event), widget); GTK_WIDGET_CLASS (gtk_popover_parent_class)->realize (widget); priv->renderer = gsk_renderer_new_for_surface (priv->surface); gtk_native_realize (GTK_NATIVE (popover)); gtk_native_update_opaque_region (GTK_NATIVE (popover), priv->contents_widget, TRUE, TRUE, 0); } static void gtk_popover_unrealize (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); gtk_native_unrealize (GTK_NATIVE (popover)); GTK_WIDGET_CLASS (gtk_popover_parent_class)->unrealize (widget); gsk_renderer_unrealize (priv->renderer); g_clear_object (&priv->renderer); g_signal_handlers_disconnect_by_func (priv->surface, surface_mapped_changed, widget); g_signal_handlers_disconnect_by_func (priv->surface, surface_render, widget); g_signal_handlers_disconnect_by_func (priv->surface, surface_event, widget); gdk_surface_set_widget (priv->surface, NULL); gdk_surface_destroy (priv->surface); g_clear_object (&priv->surface); } static gboolean gtk_popover_focus (GtkWidget *widget, GtkDirectionType direction) { if (!gtk_widget_get_visible (widget)) return FALSE; /* This code initially comes from gtkpopovermenu.c */ if (gtk_widget_get_first_child (widget) == NULL) { /* Empty popover, so nothing to Tab through. */ return FALSE; } else { /* Move focus normally, but when nothing can be focused in this direction then we cycle around. */ if (gtk_widget_focus_move (widget, direction)) return TRUE; if (gtk_popover_get_autohide (GTK_POPOVER (widget))) { GtkWidget *p = gtk_root_get_focus (gtk_widget_get_root (widget)); /* In the case where the popover doesn't have any focusable child (like * the GtkTreePopover for combo boxes) then the focus will end up out of * the popover, hence creating an infinite loop below. To avoid this, just * say we had focus and stop here. */ if (!gtk_widget_is_ancestor (p, widget) && p != widget) return TRUE; /* Cycle around with (Shift+)Tab */ if (direction == GTK_DIR_TAB_FORWARD || direction == GTK_DIR_TAB_BACKWARD) { for (; p != widget; p = gtk_widget_get_parent (p)) { /* Unfocus everything in the popover. */ gtk_widget_set_focus_child (p, NULL); } } /* Focus again from scratch */ gtk_widget_focus_move (widget, direction); return TRUE; } else { return FALSE; } } } static void gtk_popover_show (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); _gtk_widget_set_visible_flag (widget, TRUE); gtk_widget_realize (widget); if (!present_popup (popover)) return; gtk_widget_map (widget); if (priv->autohide) { if (!gtk_widget_get_focus_child (widget)) gtk_widget_child_focus (widget, GTK_DIR_TAB_FORWARD); gtk_grab_add (widget); } } static void gtk_popover_hide (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->autohide) gtk_grab_remove (widget); gtk_popover_set_mnemonics_visible (GTK_POPOVER (widget), FALSE); _gtk_widget_set_visible_flag (widget, FALSE); gtk_widget_unmap (widget); g_signal_emit (widget, signals[CLOSED], 0); } static void unset_surface_transform_changed_cb (gpointer data) { GtkPopover *popover = data; GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); priv->surface_transform_changed_cb = 0; } static gboolean surface_transform_changed_cb (GtkWidget *widget, const graphene_matrix_t *transform, gpointer user_data) { GtkPopover *popover = user_data; GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->surface && gdk_surface_get_mapped (priv->surface)) present_popup (popover); return G_SOURCE_CONTINUE; } static void gtk_popover_map (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkWidget *parent; present_popup (popover); parent = gtk_widget_get_parent (widget); priv->surface_transform_changed_cb = gtk_widget_add_surface_transform_changed_callback (parent, surface_transform_changed_cb, popover, unset_surface_transform_changed_cb); GTK_WIDGET_CLASS (gtk_popover_parent_class)->map (widget); } static void gtk_popover_unmap (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkWidget *parent; parent = gtk_widget_get_parent (widget); gtk_widget_remove_surface_transform_changed_callback (parent, priv->surface_transform_changed_cb); priv->surface_transform_changed_cb = 0; GTK_WIDGET_CLASS (gtk_popover_parent_class)->unmap (widget); gdk_surface_hide (priv->surface); } static void gtk_popover_dispose (GObject *object) { GtkPopover *popover = GTK_POPOVER (object); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_clear_object (&priv->default_widget); g_clear_pointer (&priv->contents_widget, gtk_widget_unparent); g_clear_pointer (&priv->arrow_render_node, gsk_render_node_unref); G_OBJECT_CLASS (gtk_popover_parent_class)->dispose (object); } static void gtk_popover_finalize (GObject *object) { GtkPopover *popover = GTK_POPOVER (object); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_clear_pointer (&priv->layout, gdk_popup_layout_unref); if (priv->mnemonics_display_timeout_id) { g_source_remove (priv->mnemonics_display_timeout_id); priv->mnemonics_display_timeout_id = 0; } G_OBJECT_CLASS (gtk_popover_parent_class)->finalize (object); } static void gtk_popover_get_gap_coords (GtkPopover *popover, int *initial_x_out, int *initial_y_out, int *tip_x_out, int *tip_y_out, int *final_x_out, int *final_y_out) { GtkWidget *widget = GTK_WIDGET (popover); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GdkRectangle rect = { 0 }; int base, tip, tip_pos; int initial_x, initial_y; int tip_x, tip_y; int final_x, final_y; GtkPositionType pos; int border_top, border_right, border_bottom; int border_radius; int popover_width, popover_height; GtkCssStyle *style; GtkWidget *parent; GtkBorder shadow_width; popover_width = gtk_widget_get_allocated_width (widget); popover_height = gtk_widget_get_allocated_height (widget); parent = gtk_widget_get_parent (widget); gtk_widget_get_surface_allocation (parent, &rect); if (priv->has_pointing_to) { rect.x += priv->pointing_to.x; rect.y += priv->pointing_to.y; rect.width = priv->pointing_to.width; rect.height = priv->pointing_to.height; } rect.x -= priv->final_rect.x; rect.y -= priv->final_rect.y; pos = priv->final_position; style = gtk_css_node_get_style (gtk_widget_get_css_node (priv->contents_widget)); border_radius = _gtk_css_number_value_get (style->border->border_top_left_radius, 100); border_top = _gtk_css_number_value_get (style->border->border_top_width, 100); border_right = _gtk_css_number_value_get (style->border->border_right_width, 100); border_bottom = _gtk_css_number_value_get (style->border->border_bottom_width, 100); gtk_css_shadow_value_get_extents (style->background->box_shadow, &shadow_width); if (pos == GTK_POS_BOTTOM) { tip = shadow_width.top; base = tip + TAIL_HEIGHT + border_top; } else if (pos == GTK_POS_RIGHT) { tip = shadow_width.left; base = tip + TAIL_HEIGHT + border_top; } else if (pos == GTK_POS_TOP) { tip = popover_height - shadow_width.bottom; base = tip - border_bottom - TAIL_HEIGHT; } else if (pos == GTK_POS_LEFT) { tip = popover_width - shadow_width.right; base = tip - border_right - TAIL_HEIGHT; } else g_assert_not_reached (); if (POS_IS_VERTICAL (pos)) { tip_pos = rect.x + (rect.width / 2); initial_x = CLAMP (tip_pos - TAIL_GAP_WIDTH / 2, border_radius, popover_width - TAIL_GAP_WIDTH - border_radius); initial_y = base; tip_x = CLAMP (tip_pos, 0, popover_width); tip_y = tip; final_x = CLAMP (tip_pos + TAIL_GAP_WIDTH / 2, border_radius + TAIL_GAP_WIDTH, popover_width - border_radius); final_y = base; } else { tip_pos = rect.y + (rect.height / 2); initial_x = base; initial_y = CLAMP (tip_pos - TAIL_GAP_WIDTH / 2, border_radius, popover_height - TAIL_GAP_WIDTH - border_radius); tip_x = tip; tip_y = CLAMP (tip_pos, 0, popover_height); final_x = base; final_y = CLAMP (tip_pos + TAIL_GAP_WIDTH / 2, border_radius + TAIL_GAP_WIDTH, popover_height - border_radius); } *initial_x_out = initial_x; *initial_y_out = initial_y; *tip_x_out = tip_x; *tip_y_out = tip_y; *final_x_out = final_x; *final_y_out = final_y; } static void get_border (GtkCssNode *node, GtkBorder *border) { GtkCssStyle *style; style = gtk_css_node_get_style (node); border->top = _gtk_css_number_value_get (style->border->border_top_width, 100); border->right = _gtk_css_number_value_get (style->border->border_right_width, 100); border->bottom = _gtk_css_number_value_get (style->border->border_bottom_width, 100); border->left = _gtk_css_number_value_get (style->border->border_left_width, 100); } static void gtk_popover_apply_tail_path (GtkPopover *popover, cairo_t *cr) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); int initial_x, initial_y; int tip_x, tip_y; int final_x, final_y; GtkBorder border; GtkWidget *parent; parent = gtk_widget_get_parent (GTK_WIDGET (popover)); if (!parent) return; get_border (priv->arrow_node, &border); cairo_set_line_width (cr, 1); gtk_popover_get_gap_coords (popover, &initial_x, &initial_y, &tip_x, &tip_y, &final_x, &final_y); cairo_move_to (cr, initial_x, initial_y); cairo_line_to (cr, tip_x, tip_y); cairo_line_to (cr, final_x, final_y); } static void gtk_popover_update_shape (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->has_arrow) { GtkCssBoxes content_css_boxes; const GskRoundedRect *box; cairo_surface_t *cairo_surface; cairo_region_t *region; cairo_t *cr; double x, y; double native_x, native_y; gtk_native_get_surface_transform (GTK_NATIVE (popover), &native_x, &native_y); gtk_css_boxes_init (&content_css_boxes, priv->contents_widget); cairo_surface = gdk_surface_create_similar_surface (priv->surface, CAIRO_CONTENT_COLOR_ALPHA, gdk_surface_get_width (priv->surface), gdk_surface_get_height (priv->surface)); cr = cairo_create (cairo_surface); cairo_translate (cr, native_x, native_y); cairo_set_source_rgba (cr, 0, 0, 0, 1); gtk_popover_apply_tail_path (popover, cr); cairo_close_path (cr); cairo_fill (cr); box = gtk_css_boxes_get_border_box (&content_css_boxes); gtk_widget_translate_coordinates (priv->contents_widget, GTK_WIDGET (popover), 0, 0, &x, &y); cairo_translate (cr, x, y); gsk_rounded_rect_path (box, cr); cairo_fill (cr); cairo_destroy (cr); region = gdk_cairo_region_create_from_surface (cairo_surface); cairo_surface_destroy (cairo_surface); gdk_surface_set_input_region (priv->surface, region); cairo_region_destroy (region); } else { GtkCssNode *content_css_node; GtkCssStyle *style; GtkBorder shadow_width; cairo_rectangle_int_t input_rect; cairo_region_t *region; content_css_node = gtk_widget_get_css_node (GTK_WIDGET (priv->contents_widget)); style = gtk_css_node_get_style (content_css_node); gtk_css_shadow_value_get_extents (style->background->box_shadow, &shadow_width); input_rect.x = shadow_width.left; input_rect.y = shadow_width.top; input_rect.width = gdk_surface_get_width (priv->surface) - (shadow_width.left + shadow_width.right); input_rect.height = gdk_surface_get_height (priv->surface) - (shadow_width.top + shadow_width.bottom); region = cairo_region_create_rectangle (&input_rect); gdk_surface_set_input_region (priv->surface, region); cairo_region_destroy (region); } if (_gtk_widget_get_realized (GTK_WIDGET (popover))) gtk_native_update_opaque_region (GTK_NATIVE (popover), priv->contents_widget, TRUE, TRUE, 0); } static int get_border_radius (GtkWidget *widget) { GtkCssStyle *style; style = gtk_css_node_get_style (gtk_widget_get_css_node (widget)); return round (_gtk_css_number_value_get (style->border->border_top_left_radius, 100)); } static int get_minimal_size (GtkPopover *popover, GtkOrientation orientation) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkPositionType pos; int minimal_size; int tail_gap_width = priv->has_arrow ? TAIL_GAP_WIDTH : 0; int min_width, min_height; minimal_size = 2 * get_border_radius (GTK_WIDGET (priv->contents_widget)); pos = priv->position; if ((orientation == GTK_ORIENTATION_HORIZONTAL && POS_IS_VERTICAL (pos)) || (orientation == GTK_ORIENTATION_VERTICAL && !POS_IS_VERTICAL (pos))) minimal_size += tail_gap_width; gtk_widget_get_size_request (GTK_WIDGET (popover), &min_width, &min_height); if (orientation == GTK_ORIENTATION_HORIZONTAL) minimal_size = MAX (minimal_size, min_width); else minimal_size = MAX (minimal_size, min_height); return minimal_size; } static void gtk_popover_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); int minimal_size; int tail_height = priv->has_arrow ? TAIL_HEIGHT : 0; GtkCssStyle *style; GtkBorder shadow_width; if (for_size >= 0) for_size -= tail_height; style = gtk_css_node_get_style (gtk_widget_get_css_node (GTK_WIDGET (priv->contents_widget))); gtk_css_shadow_value_get_extents (style->background->box_shadow, &shadow_width); gtk_widget_measure (priv->contents_widget, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline); minimal_size = get_minimal_size (popover, orientation); *minimum = MAX (*minimum, minimal_size); *natural = MAX (*natural, minimal_size); if (orientation == GTK_ORIENTATION_HORIZONTAL) { *minimum += shadow_width.left + shadow_width.right; *natural += shadow_width.left + shadow_width.right; } else { *minimum += shadow_width.top + shadow_width.bottom; *natural += shadow_width.top + shadow_width.bottom; } if (POS_IS_VERTICAL (priv->position) == (orientation == GTK_ORIENTATION_VERTICAL)) { *minimum += tail_height; *natural += tail_height; } } static void gtk_popover_size_allocate (GtkWidget *widget, int width, int height, int baseline) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkAllocation child_alloc; int tail_height = priv->has_arrow ? TAIL_HEIGHT : 0; GtkCssStyle *style; GtkBorder shadow_width; style = gtk_css_node_get_style (gtk_widget_get_css_node (GTK_WIDGET (priv->contents_widget))); gtk_css_shadow_value_get_extents (style->background->box_shadow, &shadow_width); switch (priv->final_position) { case GTK_POS_TOP: child_alloc.x = shadow_width.left; child_alloc.y = shadow_width.top; child_alloc.width = width - (shadow_width.left + shadow_width.right); child_alloc.height = height - (shadow_width.top + shadow_width.bottom + tail_height); break; case GTK_POS_BOTTOM: child_alloc.x = shadow_width.left; child_alloc.y = shadow_width.top + tail_height; child_alloc.width = width - (shadow_width.left + shadow_width.right); child_alloc.height = height - (shadow_width.top + shadow_width.bottom + tail_height); break; case GTK_POS_LEFT: child_alloc.x = shadow_width.left; child_alloc.y = shadow_width.top; child_alloc.width = width - (shadow_width.left + shadow_width.right + tail_height); child_alloc.height = height - (shadow_width.top + shadow_width.bottom); break; case GTK_POS_RIGHT: child_alloc.x = shadow_width.left + tail_height; child_alloc.y = shadow_width.top; child_alloc.width = width - (shadow_width.left + shadow_width.right + tail_height); child_alloc.height = height - (shadow_width.top + shadow_width.bottom); break; default: break; } gtk_widget_size_allocate (priv->contents_widget, &child_alloc, baseline); if (priv->surface) { gtk_popover_update_shape (popover); g_clear_pointer (&priv->arrow_render_node, gsk_render_node_unref); } gtk_tooltip_maybe_allocate (GTK_NATIVE (popover)); } static void create_arrow_render_node (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); GtkWidget *widget = GTK_WIDGET (popover); GtkStyleContext *context; GtkBorder border; cairo_t *cr; GtkSnapshot *snapshot; snapshot = gtk_snapshot_new (); cr = gtk_snapshot_append_cairo (snapshot, &GRAPHENE_RECT_INIT ( 0, 0, gtk_widget_get_width (widget), gtk_widget_get_height (widget) )); /* Clip to the arrow shape */ cairo_save (cr); gtk_popover_apply_tail_path (popover, cr); cairo_clip (cr); get_border (priv->arrow_node, &border); context = gtk_widget_get_style_context (widget); gtk_style_context_save_to_node (context, priv->arrow_node); /* Render the arrow background */ gtk_render_background (context, cr, 0, 0, gtk_widget_get_width (widget), gtk_widget_get_height (widget)); /* Render the border of the arrow tip */ if (border.bottom > 0) { GtkCssStyle *style; const GdkRGBA *border_color; style = gtk_css_node_get_style (priv->arrow_node); border_color = gtk_css_color_value_get_rgba (style->border->border_left_color ? style->border->border_left_color : style->core->color); gtk_popover_apply_tail_path (popover, cr); gdk_cairo_set_source_rgba (cr, border_color); cairo_set_line_width (cr, border.bottom + 1); cairo_stroke (cr); } cairo_restore (cr); cairo_destroy (cr); gtk_style_context_restore (context); priv->arrow_render_node = gtk_snapshot_free_to_node (snapshot); } static void gtk_popover_snapshot (GtkWidget *widget, GtkSnapshot *snapshot) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); gtk_widget_snapshot_child (widget, priv->contents_widget, snapshot); if (priv->has_arrow) { if (!priv->arrow_render_node) create_arrow_render_node (popover); gtk_snapshot_append_node (snapshot, priv->arrow_render_node); } } static void gtk_popover_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkPopover *popover = GTK_POPOVER (object); switch (prop_id) { case PROP_POINTING_TO: gtk_popover_set_pointing_to (popover, g_value_get_boxed (value)); break; case PROP_POSITION: gtk_popover_set_position (popover, g_value_get_enum (value)); break; case PROP_AUTOHIDE: gtk_popover_set_autohide (popover, g_value_get_boolean (value)); break; case PROP_DEFAULT_WIDGET: gtk_popover_set_default_widget (popover, g_value_get_object (value)); break; case PROP_HAS_ARROW: gtk_popover_set_has_arrow (popover, g_value_get_boolean (value)); break; case PROP_MNEMONICS_VISIBLE: gtk_popover_set_mnemonics_visible (popover, g_value_get_boolean (value)); break; case PROP_CHILD: gtk_popover_set_child (popover, g_value_get_object (value)); break; case PROP_CASCADE_POPDOWN: gtk_popover_set_cascade_popdown (popover, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_popover_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkPopover *popover = GTK_POPOVER (object); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); switch (prop_id) { case PROP_POINTING_TO: g_value_set_boxed (value, &priv->pointing_to); break; case PROP_POSITION: g_value_set_enum (value, priv->position); break; case PROP_AUTOHIDE: g_value_set_boolean (value, priv->autohide); break; case PROP_DEFAULT_WIDGET: g_value_set_object (value, priv->default_widget); break; case PROP_HAS_ARROW: g_value_set_boolean (value, priv->has_arrow); break; case PROP_MNEMONICS_VISIBLE: g_value_set_boolean (value, priv->mnemonics_visible); break; case PROP_CHILD: g_value_set_object (value, gtk_popover_get_child (popover)); break; case PROP_CASCADE_POPDOWN: g_value_set_boolean (value, priv->cascade_popdown); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void add_tab_bindings (GtkWidgetClass *widget_class, GdkModifierType modifiers, GtkDirectionType direction) { gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Tab, modifiers, "move-focus", "(i)", direction); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Tab, modifiers, "move-focus", "(i)", direction); } static void add_arrow_bindings (GtkWidgetClass *widget_class, guint keysym, GtkDirectionType direction) { guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left; gtk_widget_class_add_binding_signal (widget_class, keysym, 0, "move-focus", "(i)", direction); gtk_widget_class_add_binding_signal (widget_class, keysym, GDK_CONTROL_MASK, "move-focus", "(i)", direction); gtk_widget_class_add_binding_signal (widget_class, keypad_keysym, 0, "move-focus", "(i)", direction); gtk_widget_class_add_binding_signal (widget_class, keypad_keysym, GDK_CONTROL_MASK, "move-focus", "(i)", direction); } static void gtk_popover_compute_expand (GtkWidget *widget, gboolean *hexpand, gboolean *vexpand) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->child) { *hexpand = gtk_widget_compute_expand (priv->child, GTK_ORIENTATION_HORIZONTAL); *vexpand = gtk_widget_compute_expand (priv->child, GTK_ORIENTATION_VERTICAL); } else { *hexpand = FALSE; *vexpand = FALSE; } } static GtkSizeRequestMode gtk_popover_get_request_mode (GtkWidget *widget) { GtkPopover *popover = GTK_POPOVER (widget); GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->child) return gtk_widget_get_request_mode (priv->child); else return GTK_SIZE_REQUEST_CONSTANT_SIZE; } static void gtk_popover_class_init (GtkPopoverClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = gtk_popover_dispose; object_class->finalize = gtk_popover_finalize; object_class->set_property = gtk_popover_set_property; object_class->get_property = gtk_popover_get_property; widget_class->realize = gtk_popover_realize; widget_class->unrealize = gtk_popover_unrealize; widget_class->map = gtk_popover_map; widget_class->unmap = gtk_popover_unmap; widget_class->focus = gtk_popover_focus; widget_class->show = gtk_popover_show; widget_class->hide = gtk_popover_hide; widget_class->measure = gtk_popover_measure; widget_class->size_allocate = gtk_popover_size_allocate; widget_class->snapshot = gtk_popover_snapshot; widget_class->compute_expand = gtk_popover_compute_expand; widget_class->get_request_mode = gtk_popover_get_request_mode; klass->activate_default = gtk_popover_activate_default; /** * GtkPopover:pointing-to: (attributes org.gtk.Property.get=gtk_popover_get_pointing_to org.gtk.Property.set=gtk_popover_set_pointing_to) * * Rectangle in the parent widget that the popover points to. */ properties[PROP_POINTING_TO] = g_param_spec_boxed ("pointing-to", NULL, NULL, GDK_TYPE_RECTANGLE, GTK_PARAM_READWRITE); /** * GtkPopover:position: (attributes org.gtk.Property.get=gtk_popover_get_position org.gtk.Property.set=gtk_popover_set_position) * * How to place the popover, relative to its parent. */ properties[PROP_POSITION] = g_param_spec_enum ("position", NULL, NULL, GTK_TYPE_POSITION_TYPE, GTK_POS_BOTTOM, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); /** * GtkPopover:autohide: (attributes org.gtk.Property.get=gtk_popover_get_autohide org.gtk.Property.set=gtk_popover_set_autohide) * * Whether to dismiss the popover on outside clicks. */ properties[PROP_AUTOHIDE] = g_param_spec_boolean ("autohide", NULL, NULL, TRUE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); /** * GtkPopover:default-widget: (attributes org.gtk.Popover.set=gtk_popover_set_default_widget) * * The default widget inside the popover. */ properties[PROP_DEFAULT_WIDGET] = g_param_spec_object ("default-widget", NULL, NULL, GTK_TYPE_WIDGET, GTK_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); /** * GtkPopover:has-arrow: (attributes org.gtk.Popover.get=gtk_popover_get_has_arrow org.gtk.Property.set=gtk_popover_set_has_arrow) * * Whether to draw an arrow. */ properties[PROP_HAS_ARROW] = g_param_spec_boolean ("has-arrow", NULL, NULL, TRUE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); /** * GtkPopover:mnemonics-visible: (attributes org.gtk.Property.get=gtk_popover_get_mnemonics_visible org.gtk.Property.set=gtk_popover_set_mnemonics_visible) * * Whether mnemonics are currently visible in this popover. */ properties[PROP_MNEMONICS_VISIBLE] = g_param_spec_boolean ("mnemonics-visible", NULL, NULL, FALSE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); /** * GtkPopover:child: (attributes org.gtk.Property.get=gtk_popover_get_child org.gtk.Property.set=gtk_popover_set_child) * * The child widget. */ properties[PROP_CHILD] = g_param_spec_object ("child", NULL, NULL, GTK_TYPE_WIDGET, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); /** * GtkPopover:cascade-popdown: (attributes org.gtk.Property.get=gtk_popover_get_cascade_popdown org.gtk.Property.set=gtk_popover_set_cascade_popdown) * * Whether the popover pops down after a child popover. * * This is used to implement the expected behavior of submenus. */ properties[PROP_CASCADE_POPDOWN] = g_param_spec_boolean ("cascade-popdown", NULL, NULL, FALSE, GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, NUM_PROPERTIES, properties); /** * GtkPopover::closed: * @self: the `GtkPopover` which received the signal * * Emitted when the popover is closed. */ signals[CLOSED] = g_signal_new (I_("closed"), G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkPopoverClass, closed), NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkPopover::activate-default: * @self: the `GtkPopover` which received the signal * * Emitted whend the user activates the default widget. * * This is a [keybinding signal](class.SignalAction.html). */ signals[ACTIVATE_DEFAULT] = g_signal_new (I_("activate-default"), G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (GtkPopoverClass, activate_default), NULL, NULL, NULL, G_TYPE_NONE, 0); add_arrow_bindings (widget_class, GDK_KEY_Up, GTK_DIR_UP); add_arrow_bindings (widget_class, GDK_KEY_Down, GTK_DIR_DOWN); add_arrow_bindings (widget_class, GDK_KEY_Left, GTK_DIR_LEFT); add_arrow_bindings (widget_class, GDK_KEY_Right, GTK_DIR_RIGHT); add_tab_bindings (widget_class, 0, GTK_DIR_TAB_FORWARD); add_tab_bindings (widget_class, GDK_CONTROL_MASK, GTK_DIR_TAB_FORWARD); add_tab_bindings (widget_class, GDK_SHIFT_MASK, GTK_DIR_TAB_BACKWARD); add_tab_bindings (widget_class, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_DIR_TAB_BACKWARD); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Return, 0, "activate-default", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_ISO_Enter, 0, "activate-default", NULL); gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Enter, 0, "activate-default", NULL); gtk_widget_class_set_css_name (widget_class, "popover"); } /** * gtk_popover_new: * * Creates a new `GtkPopover`. * * Returns: the new `GtkPopover` */ GtkWidget * gtk_popover_new (void) { return g_object_new (GTK_TYPE_POPOVER, NULL); } /** * gtk_popover_set_child: (attributes org.gtk.Method.set_property=child) * @popover: a `GtkPopover` * @child: (nullable): the child widget * * Sets the child widget of @popover. */ void gtk_popover_set_child (GtkPopover *popover, GtkWidget *child) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); g_return_if_fail (child == NULL || GTK_IS_WIDGET (child)); if (priv->child == child) return; g_clear_pointer (&priv->child, gtk_widget_unparent); if (child) { priv->child = child; gtk_widget_set_parent (child, priv->contents_widget); } g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_CHILD]); } /** * gtk_popover_get_child: (attributes org.gtk.Method.get_property=child) * @popover: a `GtkPopover` * * Gets the child widget of @popover. * * Returns: (nullable) (transfer none): the child widget of @popover */ GtkWidget * gtk_popover_get_child (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_val_if_fail (GTK_IS_POPOVER (popover), NULL); return priv->child; } /** * gtk_popover_set_default_widget: (attributes org.gtk.Method.set_property=default-widget) * @popover: a `GtkPopover` * @widget: (nullable): a child widget of @popover to set as * the default, or %NULL to unset the default widget for the popover * * Sets the default widget of a `GtkPopover`. * * The default widget is the widget that’s activated when the user * presses Enter in a dialog (for example). This function sets or * unsets the default widget for a `GtkPopover`. */ void gtk_popover_set_default_widget (GtkPopover *popover, GtkWidget *widget) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (priv->default_widget == widget) return; if (priv->default_widget) { _gtk_widget_set_has_default (priv->default_widget, FALSE); gtk_widget_queue_draw (priv->default_widget); g_object_notify (G_OBJECT (priv->default_widget), "has-default"); } g_set_object (&priv->default_widget, widget); if (priv->default_widget) { _gtk_widget_set_has_default (priv->default_widget, TRUE); gtk_widget_queue_draw (priv->default_widget); g_object_notify (G_OBJECT (priv->default_widget), "has-default"); } g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_DEFAULT_WIDGET]); } static void gtk_popover_shortcut_manager_interface_init (GtkShortcutManagerInterface *iface) { } static void gtk_popover_native_interface_init (GtkNativeInterface *iface) { iface->get_surface = gtk_popover_native_get_surface; iface->get_renderer = gtk_popover_native_get_renderer; iface->get_surface_transform = gtk_popover_native_get_surface_transform; iface->layout = gtk_popover_native_layout; } static GtkBuildableIface *parent_buildable_iface; static void gtk_popover_buildable_add_child (GtkBuildable *buildable, GtkBuilder *builder, GObject *child, const char *type) { if (GTK_IS_WIDGET (child)) gtk_popover_set_child (GTK_POPOVER (buildable), GTK_WIDGET (child)); else parent_buildable_iface->add_child (buildable, builder, child, type); } static void gtk_popover_buildable_init (GtkBuildableIface *iface) { parent_buildable_iface = g_type_interface_peek_parent (iface); iface->add_child = gtk_popover_buildable_add_child; } /** * gtk_popover_set_pointing_to: (attributes org.gtk.Method.set_property=pointing-to) * @popover: a `GtkPopover` * @rect: (nullable): rectangle to point to * * Sets the rectangle that @popover points to. * * This is in the coordinate space of the @popover parent. */ void gtk_popover_set_pointing_to (GtkPopover *popover, const GdkRectangle *rect) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (rect) { priv->pointing_to = *rect; priv->has_pointing_to = TRUE; priv->pointing_to.width = MAX (priv->pointing_to.width, 1); priv->pointing_to.height = MAX (priv->pointing_to.height, 1); } else priv->has_pointing_to = FALSE; g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_POINTING_TO]); if (gtk_widget_is_visible (GTK_WIDGET (popover))) present_popup (popover); } /** * gtk_popover_get_pointing_to: (attributes org.gtk.Method.get_property=pointing-to) * @popover: a `GtkPopover` * @rect: (out): location to store the rectangle * * Gets the rectangle that the popover points to. * * If a rectangle to point to has been set, this function will * return %TRUE and fill in @rect with such rectangle, otherwise * it will return %FALSE and fill in @rect with the parent * widget coordinates. * * Returns: %TRUE if a rectangle to point to was set. */ gboolean gtk_popover_get_pointing_to (GtkPopover *popover, GdkRectangle *rect) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_val_if_fail (GTK_IS_POPOVER (popover), FALSE); g_return_val_if_fail (rect != NULL, FALSE); if (priv->has_pointing_to) *rect = priv->pointing_to; else { graphene_rect_t r; GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (popover)); if (!gtk_widget_compute_bounds (parent, parent, &r)) return FALSE; rect->x = floorf (r.origin.x); rect->y = floorf (r.origin.y); rect->width = ceilf (r.size.width); rect->height = ceilf (r.size.height); } return priv->has_pointing_to; } /** * gtk_popover_set_position: (attributes org.gtk.Method.set_property=position) * @popover: a `GtkPopover` * @position: preferred popover position * * Sets the preferred position for @popover to appear. * * If the @popover is currently visible, it will be immediately * updated. * * This preference will be respected where possible, although * on lack of space (eg. if close to the window edges), the * `GtkPopover` may choose to appear on the opposite side. */ void gtk_popover_set_position (GtkPopover *popover, GtkPositionType position) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (priv->position == position) return; priv->position = position; priv->final_position = position; g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_POSITION]); gtk_widget_queue_resize (GTK_WIDGET (popover)); if (gtk_widget_is_visible (GTK_WIDGET (popover))) present_popup (popover); } /** * gtk_popover_get_position: (attributes org.gtk.Method.get_property=position) * @popover: a `GtkPopover` * * Returns the preferred position of @popover. * * Returns: The preferred position. */ GtkPositionType gtk_popover_get_position (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_val_if_fail (GTK_IS_POPOVER (popover), GTK_POS_TOP); return priv->position; } /** * gtk_popover_set_autohide: (attributes org.gtk.Method.set_property=autohide) * @popover: a `GtkPopover` * @autohide: %TRUE to dismiss the popover on outside clicks * * Sets whether @popover is modal. * * A modal popover will grab the keyboard focus on it when being * displayed. Focus will wrap around within the popover. Clicking * outside the popover area or pressing Esc will dismiss the popover. * * Called this function on an already showing popup with a new * autohide value different from the current one, will cause the * popup to be hidden. */ void gtk_popover_set_autohide (GtkPopover *popover, gboolean autohide) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); autohide = autohide != FALSE; if (priv->autohide == autohide) return; priv->autohide = autohide; gtk_widget_unrealize (GTK_WIDGET (popover)); g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_AUTOHIDE]); } /** * gtk_popover_get_autohide: (attributes org.gtk.Method.get_property=autohide) * @popover: a `GtkPopover` * * Returns whether the popover is modal. * * See [method@Gtk.Popover.set_autohide] for the * implications of this. * * Returns: %TRUE if @popover is modal */ gboolean gtk_popover_get_autohide (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_val_if_fail (GTK_IS_POPOVER (popover), FALSE); return priv->autohide; } /** * gtk_popover_popup: * @popover: a `GtkPopover` * * Pops @popover up. */ void gtk_popover_popup (GtkPopover *popover) { g_return_if_fail (GTK_IS_POPOVER (popover)); gtk_widget_show (GTK_WIDGET (popover)); } static void cascade_popdown (GtkPopover *popover) { GtkWidget *parent; /* Do not trigger cascade close from non-modal popovers */ if (!gtk_popover_get_autohide (popover)) return; parent = gtk_widget_get_parent (GTK_WIDGET (popover)); while (parent) { if (GTK_IS_POPOVER (parent)) { if (gtk_popover_get_cascade_popdown (GTK_POPOVER (parent))) gtk_widget_hide (parent); else break; } parent = gtk_widget_get_parent (parent); } } /** * gtk_popover_popdown: * @popover: a `GtkPopover` * * Pops @popover down. * * This may have the side-effect of closing a parent popover * as well. See [property@Gtk.Popover:cascade-popdown]. */ void gtk_popover_popdown (GtkPopover *popover) { g_return_if_fail (GTK_IS_POPOVER (popover)); gtk_widget_hide (GTK_WIDGET (popover)); cascade_popdown (popover); } GtkWidget * gtk_popover_get_contents_widget (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); return priv->contents_widget; } /** * gtk_popover_set_has_arrow: (attributes org.gtk.Method.set_property=has-arrow) * @popover: a `GtkPopover` * @has_arrow: %TRUE to draw an arrow * * Sets whether this popover should draw an arrow * pointing at the widget it is relative to. */ void gtk_popover_set_has_arrow (GtkPopover *popover, gboolean has_arrow) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (priv->has_arrow == has_arrow) return; priv->has_arrow = has_arrow; g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_HAS_ARROW]); gtk_widget_queue_resize (GTK_WIDGET (popover)); } /** * gtk_popover_get_has_arrow: (attributes org.gtk.Method.get_property=has-arrow) * @popover: a `GtkPopover` * * Gets whether this popover is showing an arrow * pointing at the widget that it is relative to. * * Returns: whether the popover has an arrow */ gboolean gtk_popover_get_has_arrow (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_val_if_fail (GTK_IS_POPOVER (popover), TRUE); return priv->has_arrow; } /** * gtk_popover_set_mnemonics_visible: (attributes org.gtk.Method.set_property=mnemonics-visible) * @popover: a `GtkPopover` * @mnemonics_visible: the new value * * Sets whether mnemonics should be visible. */ void gtk_popover_set_mnemonics_visible (GtkPopover *popover, gboolean mnemonics_visible) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (priv->mnemonics_visible == mnemonics_visible) return; priv->mnemonics_visible = mnemonics_visible; g_object_notify_by_pspec (G_OBJECT (popover), properties[PROP_MNEMONICS_VISIBLE]); gtk_widget_queue_resize (GTK_WIDGET (popover)); if (priv->mnemonics_display_timeout_id) { g_source_remove (priv->mnemonics_display_timeout_id); priv->mnemonics_display_timeout_id = 0; } } /** * gtk_popover_get_mnemonics_visible: (attributes org.gtk.Method.get_property=mnemonics-visible) * @popover: a `GtkPopover` * * Gets whether mnemonics are visible. * * Returns: %TRUE if mnemonics are supposed to be visible * in this popover */ gboolean gtk_popover_get_mnemonics_visible (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_val_if_fail (GTK_IS_POPOVER (popover), FALSE); return priv->mnemonics_visible; } void gtk_popover_disable_auto_mnemonics (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); priv->disable_auto_mnemonics = TRUE; } /** * gtk_popover_set_offset: * @popover: a `GtkPopover` * @x_offset: the x offset to adjust the position by * @y_offset: the y offset to adjust the position by * * Sets the offset to use when calculating the position * of the popover. * * These values are used when preparing the [struct@Gdk.PopupLayout] * for positioning the popover. */ void gtk_popover_set_offset (GtkPopover *popover, int x_offset, int y_offset) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (priv->x_offset != x_offset || priv->y_offset != y_offset) { priv->x_offset = x_offset; priv->y_offset = y_offset; gtk_widget_queue_resize (GTK_WIDGET (popover)); } } /** * gtk_popover_get_offset: * @popover: a `GtkPopover` * @x_offset: (out) (nullable): a location for the x_offset * @y_offset: (out) (nullable): a location for the y_offset * * Gets the offset previous set with gtk_popover_set_offset(). */ void gtk_popover_get_offset (GtkPopover *popover, int *x_offset, int *y_offset) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); g_return_if_fail (GTK_IS_POPOVER (popover)); if (x_offset) *x_offset = priv->x_offset; if (y_offset) *y_offset = priv->y_offset; } /** * gtk_popover_set_cascade_popdown: (attributes org.gtk.Method.set_property=cascade-popdown) * @popover: A `GtkPopover` * @cascade_popdown: %TRUE if the popover should follow a child closing * * If @cascade_popdown is %TRUE, the popover will be * closed when a child modal popover is closed. * * If %FALSE, @popover will stay visible. */ void gtk_popover_set_cascade_popdown (GtkPopover *popover, gboolean cascade_popdown) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); if (priv->cascade_popdown != !!cascade_popdown) { priv->cascade_popdown = !!cascade_popdown; g_object_notify (G_OBJECT (popover), "cascade-popdown"); } } /** * gtk_popover_get_cascade_popdown: (attributes org.gtk.Method.get_property=cascade-popdown) * @popover: a `GtkPopover` * * Returns whether the popover will close after a modal child is closed. * * Returns: %TRUE if @popover will close after a modal child. */ gboolean gtk_popover_get_cascade_popdown (GtkPopover *popover) { GtkPopoverPrivate *priv = gtk_popover_get_instance_private (popover); return priv->cascade_popdown; }