/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* * Copyright 2013, 2015 Red Hat, Inc. * * This program 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 program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * Author: Alexander Larsson * Carlos Soriano */ #include "config.h" #include "gtkrevealer.h" #include "gtkintl.h" #include "gtkprivate.h" #include "gtkprogresstrackerprivate.h" #include "gtksettingsprivate.h" #include "gtksnapshot.h" #include "gtktypebuiltins.h" #include "gtkwidgetprivate.h" #include "fallback-c89.c" /** * SECTION:gtkrevealer * @Short_description: Hide and show with animation * @Title: GtkRevealer * @See_also: #GtkExpander * * The GtkRevealer widget is a container which animates * the transition of its child from invisible to visible. * * The style of transition can be controlled with * gtk_revealer_set_transition_type(). * * These animations respect the #GtkSettings:gtk-enable-animations * setting. * * # CSS nodes * * GtkRevealer has a single CSS node with name revealer. * When styling #GtkRevealer using CSS, remember that it only hides its contents, * not itself. That means applied margin, padding and borders will be * visible even when the #GtkRevealer:reveal-child property is set to %FALSE. * * The GtkRevealer widget was added in GTK+ 3.10. */ /** * GtkRevealerTransitionType: * @GTK_REVEALER_TRANSITION_TYPE_NONE: No transition * @GTK_REVEALER_TRANSITION_TYPE_CROSSFADE: Fade in * @GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT: Slide in from the left * @GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT: Slide in from the right * @GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP: Slide in from the bottom * @GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN: Slide in from the top * * These enumeration values describe the possible transitions * when the child of a #GtkRevealer widget is shown or hidden. */ enum { PROP_0, PROP_TRANSITION_TYPE, PROP_TRANSITION_DURATION, PROP_REVEAL_CHILD, PROP_CHILD_REVEALED, LAST_PROP }; typedef struct { GtkRevealerTransitionType transition_type; guint transition_duration; gdouble current_pos; gdouble source_pos; gdouble target_pos; guint tick_id; GtkProgressTracker tracker; } GtkRevealerPrivate; static GParamSpec *props[LAST_PROP] = { NULL, }; static void gtk_revealer_real_add (GtkContainer *widget, GtkWidget *child); static void gtk_revealer_real_size_allocate (GtkWidget *widget, const GtkAllocation *allocation, int baseline); static void gtk_revealer_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline); static void gtk_revealer_snapshot (GtkWidget *widget, GtkSnapshot *snapshot); G_DEFINE_TYPE_WITH_PRIVATE (GtkRevealer, gtk_revealer, GTK_TYPE_BIN) static void gtk_revealer_init (GtkRevealer *revealer) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); priv->transition_type = GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN; priv->transition_duration = 250; priv->current_pos = 0.0; priv->target_pos = 0.0; gtk_widget_set_has_surface ((GtkWidget*) revealer, FALSE); } static void gtk_revealer_finalize (GObject *obj) { GtkRevealer *revealer = GTK_REVEALER (obj); GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); if (priv->tick_id != 0) gtk_widget_remove_tick_callback (GTK_WIDGET (revealer), priv->tick_id); priv->tick_id = 0; G_OBJECT_CLASS (gtk_revealer_parent_class)->finalize (obj); } static void gtk_revealer_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GtkRevealer *revealer = GTK_REVEALER (object); switch (property_id) { case PROP_TRANSITION_TYPE: g_value_set_enum (value, gtk_revealer_get_transition_type (revealer)); break; case PROP_TRANSITION_DURATION: g_value_set_uint (value, gtk_revealer_get_transition_duration (revealer)); break; case PROP_REVEAL_CHILD: g_value_set_boolean (value, gtk_revealer_get_reveal_child (revealer)); break; case PROP_CHILD_REVEALED: g_value_set_boolean (value, gtk_revealer_get_child_revealed (revealer)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gtk_revealer_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GtkRevealer *revealer = GTK_REVEALER (object); switch (property_id) { case PROP_TRANSITION_TYPE: gtk_revealer_set_transition_type (revealer, g_value_get_enum (value)); break; case PROP_TRANSITION_DURATION: gtk_revealer_set_transition_duration (revealer, g_value_get_uint (value)); break; case PROP_REVEAL_CHILD: gtk_revealer_set_reveal_child (revealer, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gtk_revealer_unmap (GtkWidget *widget) { GtkRevealer *revealer = GTK_REVEALER (widget); GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); GTK_WIDGET_CLASS (gtk_revealer_parent_class)->unmap (widget); /* Finish & stop the animation */ if (priv->current_pos != priv->target_pos) { priv->current_pos = priv->target_pos; g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_CHILD_REVEALED]); } if (priv->tick_id != 0) { gtk_widget_remove_tick_callback (GTK_WIDGET (revealer), priv->tick_id); priv->tick_id = 0; } } static void gtk_revealer_class_init (GtkRevealerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); object_class->get_property = gtk_revealer_get_property; object_class->set_property = gtk_revealer_set_property; object_class->finalize = gtk_revealer_finalize; widget_class->unmap = gtk_revealer_unmap; widget_class->size_allocate = gtk_revealer_real_size_allocate; widget_class->measure = gtk_revealer_measure; widget_class->snapshot = gtk_revealer_snapshot; container_class->add = gtk_revealer_real_add; props[PROP_TRANSITION_TYPE] = g_param_spec_enum ("transition-type", P_("Transition type"), P_("The type of animation used to transition"), GTK_TYPE_REVEALER_TRANSITION_TYPE, GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN, GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY); props[PROP_TRANSITION_DURATION] = g_param_spec_uint ("transition-duration", P_("Transition duration"), P_("The animation duration, in milliseconds"), 0, G_MAXUINT, 250, GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY); props[PROP_REVEAL_CHILD] = g_param_spec_boolean ("reveal-child", P_("Reveal Child"), P_("Whether the container should reveal the child"), FALSE, GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY); props[PROP_CHILD_REVEALED] = g_param_spec_boolean ("child-revealed", P_("Child Revealed"), P_("Whether the child is revealed and the animation target reached"), FALSE, GTK_PARAM_READABLE); g_object_class_install_properties (object_class, LAST_PROP, props); gtk_widget_class_set_css_name (widget_class, I_("revealer")); } /** * gtk_revealer_new: * * Creates a new #GtkRevealer. * * Returns: a newly created #GtkRevealer */ GtkWidget * gtk_revealer_new (void) { return g_object_new (GTK_TYPE_REVEALER, NULL); } static GtkRevealerTransitionType effective_transition (GtkRevealer *revealer) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); if (gtk_widget_get_direction (GTK_WIDGET (revealer)) == GTK_TEXT_DIR_RTL) { if (priv->transition_type == GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT) return GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT; else if (priv->transition_type == GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) return GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT; } return priv->transition_type; } static void gtk_revealer_get_child_allocation (GtkRevealer *revealer, const GtkAllocation *allocation, GtkAllocation *child_allocation) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); GtkWidget *child; GtkRevealerTransitionType transition; g_return_if_fail (revealer != NULL); g_return_if_fail (allocation != NULL); child_allocation->x = 0; child_allocation->y = 0; child_allocation->width = 0; child_allocation->height = 0; child = gtk_bin_get_child (GTK_BIN (revealer)); if (child != NULL && gtk_widget_get_visible (child)) { transition = effective_transition (revealer); if (transition == GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT || transition == GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) gtk_widget_measure (child, GTK_ORIENTATION_HORIZONTAL, MAX (0, allocation->height), NULL, &child_allocation->width, NULL, NULL); else gtk_widget_measure (child, GTK_ORIENTATION_VERTICAL, MAX (0, allocation->width), NULL, &child_allocation->height, NULL, NULL); child_allocation->width = MAX (child_allocation->width, allocation->width); child_allocation->height = MAX (child_allocation->height, allocation->height); switch (transition) { case GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT: child_allocation->x = - child_allocation->width * (1 - priv->current_pos); break; case GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN: child_allocation->y = - child_allocation->height * (1 - priv->current_pos); break; case GTK_REVEALER_TRANSITION_TYPE_NONE: case GTK_REVEALER_TRANSITION_TYPE_CROSSFADE: case GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT: case GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP: default: break; } } } static void gtk_revealer_real_add (GtkContainer *container, GtkWidget *child) { GtkRevealer *revealer = GTK_REVEALER (container); GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); g_return_if_fail (child != NULL); gtk_widget_set_child_visible (child, priv->current_pos != 0.0); GTK_CONTAINER_CLASS (gtk_revealer_parent_class)->add (container, child); } static void gtk_revealer_real_size_allocate (GtkWidget *widget, const GtkAllocation *allocation, int baseline) { GtkRevealer *revealer = GTK_REVEALER (widget); GtkWidget *child; child = gtk_bin_get_child (GTK_BIN (revealer)); if (child != NULL && gtk_widget_get_visible (child)) { GtkAllocation child_allocation; gtk_revealer_get_child_allocation (revealer, allocation, &child_allocation); gtk_widget_size_allocate (child, &child_allocation, -1); } } static void gtk_revealer_set_position (GtkRevealer *revealer, gdouble pos) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); gboolean new_visible; GtkWidget *child; GtkRevealerTransitionType transition; priv->current_pos = pos; /* We check target_pos here too, because we want to ensure we set * child_visible immediately when starting a reveal operation * otherwise the child widgets will not be properly realized * after the reveal returns. */ new_visible = priv->current_pos != 0.0 || priv->target_pos != 0.0; child = gtk_bin_get_child (GTK_BIN (revealer)); if (child != NULL && new_visible != gtk_widget_get_child_visible (child)) { gtk_widget_set_child_visible (child, new_visible); gtk_widget_queue_resize (GTK_WIDGET (revealer)); } transition = effective_transition (revealer); if (transition == GTK_REVEALER_TRANSITION_TYPE_NONE) { gtk_widget_queue_draw (GTK_WIDGET (revealer)); } else if (transition == GTK_REVEALER_TRANSITION_TYPE_CROSSFADE) { gtk_widget_set_opacity (GTK_WIDGET (revealer), priv->current_pos); gtk_widget_queue_draw (GTK_WIDGET (revealer)); } else { gtk_widget_queue_resize (GTK_WIDGET (revealer)); } if (priv->current_pos == priv->target_pos) g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_CHILD_REVEALED]); } static gboolean gtk_revealer_animate_cb (GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data) { GtkRevealer *revealer = GTK_REVEALER (widget); GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); gdouble ease; gtk_progress_tracker_advance_frame (&priv->tracker, gdk_frame_clock_get_frame_time (frame_clock)); ease = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE); gtk_revealer_set_position (revealer, priv->source_pos + (ease * (priv->target_pos - priv->source_pos))); if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) { priv->tick_id = 0; return FALSE; } return TRUE; } static void gtk_revealer_start_animation (GtkRevealer *revealer, gdouble target) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); GtkWidget *widget = GTK_WIDGET (revealer); GtkRevealerTransitionType transition; if (priv->target_pos == target) return; priv->target_pos = target; g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_REVEAL_CHILD]); transition = effective_transition (revealer); if (gtk_widget_get_mapped (widget) && priv->transition_duration != 0 && transition != GTK_REVEALER_TRANSITION_TYPE_NONE && gtk_settings_get_enable_animations (gtk_widget_get_settings (widget))) { priv->source_pos = priv->current_pos; if (priv->tick_id == 0) priv->tick_id = gtk_widget_add_tick_callback (widget, gtk_revealer_animate_cb, revealer, NULL); gtk_progress_tracker_start (&priv->tracker, priv->transition_duration * 1000, 0, 1.0); } else { gtk_revealer_set_position (revealer, target); } } /** * gtk_revealer_set_reveal_child: * @revealer: a #GtkRevealer * @reveal_child: %TRUE to reveal the child * * Tells the #GtkRevealer to reveal or conceal its child. * * The transition will be animated with the current * transition type of @revealer. */ void gtk_revealer_set_reveal_child (GtkRevealer *revealer, gboolean reveal_child) { g_return_if_fail (GTK_IS_REVEALER (revealer)); if (reveal_child) gtk_revealer_start_animation (revealer, 1.0); else gtk_revealer_start_animation (revealer, 0.0); } /** * gtk_revealer_get_reveal_child: * @revealer: a #GtkRevealer * * Returns whether the child is currently * revealed. See gtk_revealer_set_reveal_child(). * * This function returns %TRUE as soon as the transition * is to the revealed state is started. To learn whether * the child is fully revealed (ie the transition is completed), * use gtk_revealer_get_child_revealed(). * * Returns: %TRUE if the child is revealed. */ gboolean gtk_revealer_get_reveal_child (GtkRevealer *revealer) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); g_return_val_if_fail (GTK_IS_REVEALER (revealer), FALSE); return priv->target_pos != 0.0; } /** * gtk_revealer_get_child_revealed: * @revealer: a #GtkRevealer * * Returns whether the child is fully revealed, in other words whether * the transition to the revealed state is completed. * * Returns: %TRUE if the child is fully revealed */ gboolean gtk_revealer_get_child_revealed (GtkRevealer *revealer) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); gboolean animation_finished = (priv->target_pos == priv->current_pos); gboolean reveal_child = gtk_revealer_get_reveal_child (revealer); if (animation_finished) return reveal_child; else return !reveal_child; } /* These all report only the natural size, ignoring the minimal size, * because its not really possible to allocate the right size during * animation if the child size can change (without the child * re-arranging itself during the animation). */ static void set_height (GtkRevealer *revealer, gint *minimum_height, gint *natural_height) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); GtkRevealerTransitionType transition; transition = effective_transition (revealer); if (transition == GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP || transition == GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN) { *minimum_height = round (*minimum_height * priv->current_pos); *natural_height = round (*natural_height * priv->current_pos); } } static void set_width (GtkRevealer *revealer, gint *minimum_width, gint *natural_width) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); GtkRevealerTransitionType transition; transition = effective_transition (revealer); if (transition == GTK_REVEALER_TRANSITION_TYPE_SLIDE_LEFT || transition == GTK_REVEALER_TRANSITION_TYPE_SLIDE_RIGHT) { *minimum_width = round (*minimum_width * priv->current_pos); *natural_width = round (*natural_width * priv->current_pos); } } static void gtk_revealer_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { GTK_WIDGET_CLASS (gtk_revealer_parent_class)->measure (widget, orientation, for_size, minimum, natural, NULL, NULL); if (orientation == GTK_ORIENTATION_HORIZONTAL) set_width (GTK_REVEALER (widget), minimum, natural); else set_height (GTK_REVEALER (widget), minimum, natural); } static void gtk_revealer_snapshot (GtkWidget *widget, GtkSnapshot *snapshot) { GtkRevealer *revealer = GTK_REVEALER (widget); GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); GtkRevealerTransitionType transition; GtkWidget *child; gboolean clip_child; child = gtk_bin_get_child (GTK_BIN (revealer)); if (child == NULL || !gtk_widget_get_mapped (child)) return; transition = effective_transition (revealer); clip_child = transition != GTK_REVEALER_TRANSITION_TYPE_NONE && transition != GTK_REVEALER_TRANSITION_TYPE_CROSSFADE && gtk_progress_tracker_get_state (&priv->tracker) != GTK_PROGRESS_STATE_AFTER; if (clip_child) { gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT( 0, 0, gtk_widget_get_width (widget), gtk_widget_get_height (widget) )); gtk_widget_snapshot_child (widget, child, snapshot); gtk_snapshot_pop (snapshot); } else gtk_widget_snapshot_child (widget, child, snapshot); } /** * gtk_revealer_get_transition_duration: * @revealer: a #GtkRevealer * * Returns the amount of time (in milliseconds) that * transitions will take. * * Returns: the transition duration */ guint gtk_revealer_get_transition_duration (GtkRevealer *revealer) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); g_return_val_if_fail (GTK_IS_REVEALER (revealer), 0); return priv->transition_duration; } /** * gtk_revealer_set_transition_duration: * @revealer: a #GtkRevealer * @duration: the new duration, in milliseconds * * Sets the duration that transitions will take. */ void gtk_revealer_set_transition_duration (GtkRevealer *revealer, guint value) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); g_return_if_fail (GTK_IS_REVEALER (revealer)); if (priv->transition_duration == value) return; priv->transition_duration = value; g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_TRANSITION_DURATION]); } /** * gtk_revealer_get_transition_type: * @revealer: a #GtkRevealer * * Gets the type of animation that will be used * for transitions in @revealer. * * Returns: the current transition type of @revealer */ GtkRevealerTransitionType gtk_revealer_get_transition_type (GtkRevealer *revealer) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); g_return_val_if_fail (GTK_IS_REVEALER (revealer), GTK_REVEALER_TRANSITION_TYPE_NONE); return priv->transition_type; } /** * gtk_revealer_set_transition_type: * @revealer: a #GtkRevealer * @transition: the new transition type * * Sets the type of animation that will be used for * transitions in @revealer. Available types include * various kinds of fades and slides. */ void gtk_revealer_set_transition_type (GtkRevealer *revealer, GtkRevealerTransitionType transition) { GtkRevealerPrivate *priv = gtk_revealer_get_instance_private (revealer); g_return_if_fail (GTK_IS_REVEALER (revealer)); if (priv->transition_type == transition) return; priv->transition_type = transition; gtk_widget_queue_resize (GTK_WIDGET (revealer)); g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_TRANSITION_TYPE]); }