/* dzl-stack-list.c * * Copyright (C) 2015-2017 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define G_LOG_DOMAIN "dzl-stack-list" #include "config.h" #include #include "animation/dzl-animation.h" #include "util/dzl-util-private.h" #include "widgets/dzl-rect-helper.h" #include "widgets/dzl-stack-list.h" #include "util/dzl-gtk.h" #define FADE_DURATION 250 #define SLIDE_DURATION_MAX 300 typedef struct { GtkOverlay *overlay; GtkScrolledWindow *scroller; GtkBox *box; GtkListBox *headers; GtkListBox *content; GtkListBox *fake_list; GtkStack *flip_stack; GPtrArray *models; GtkListBoxRow *activated; GtkListBoxRow *animating; DzlAnimation *animation; DzlRectHelper *animating_rect; } DzlStackListPrivate; typedef struct { GListModel *model; GtkWidget *header; DzlStackListCreateWidgetFunc create_widget_func; gpointer user_data; GDestroyNotify user_data_free_func; } ModelInfo; G_DEFINE_TYPE_WITH_PRIVATE (DzlStackList, dzl_stack_list, GTK_TYPE_BIN) enum { PROP_0, PROP_MODEL, LAST_PROP }; enum { HEADER_ACTIVATED, ROW_ACTIVATED, LAST_SIGNAL }; static GParamSpec *properties [LAST_PROP]; static guint signals [LAST_SIGNAL]; static void model_info_free (gpointer data) { ModelInfo *info = data; g_object_unref (info->model); if (info->user_data_free_func) info->user_data_free_func (info->user_data); g_slice_free (ModelInfo, info); } static void enable_activatable (GtkWidget *widget, gpointer user_data) { GtkWidget **last = user_data; g_assert (GTK_IS_LIST_BOX_ROW (widget)); g_assert (*last == NULL || GTK_IS_WIDGET (*last)); gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (widget), TRUE); *last = widget; } static void dzl_stack_list_update_activatables (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); GtkWidget *last = NULL; g_assert (DZL_IS_STACK_LIST (self)); gtk_container_foreach (GTK_CONTAINER (priv->headers), enable_activatable, &last); if (GTK_IS_LIST_BOX_ROW (last)) gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (last), FALSE); } static GtkWidget * dzl_stack_list_create_widget_func (gpointer item, gpointer user_data) { ModelInfo *info = user_data; return info->create_widget_func (item, info->user_data); } static void dzl_stack_list_content_row_activated (DzlStackList *self, GtkListBoxRow *row, GtkListBox *box) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); g_return_if_fail (DZL_IS_STACK_LIST (self)); g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); g_return_if_fail (GTK_IS_LIST_BOX (box)); priv->activated = row; g_signal_emit (self, signals [ROW_ACTIVATED], 0, row); priv->activated = NULL; } static void dzl_stack_list_header_row_activated (DzlStackList *self, GtkListBoxRow *row, GtkListBox *box) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); g_return_if_fail (DZL_IS_STACK_LIST (self)); g_return_if_fail (GTK_IS_LIST_BOX_ROW (row)); g_return_if_fail (GTK_IS_LIST_BOX (box)); priv->activated = row; g_signal_emit (self, signals [HEADER_ACTIVATED], 0, row); priv->activated = NULL; } static gboolean dzl_stack_list__overlay__get_child_position (DzlStackList *self, GtkWidget *widget, GdkRectangle *rect, GtkOverlay *overlay) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); GtkRequisition min, nat; g_assert (DZL_IS_STACK_LIST (self)); g_assert (GTK_IS_WIDGET (widget)); g_assert (rect != NULL); g_assert (GTK_IS_OVERLAY (overlay)); gtk_widget_get_preferred_size (widget, &min, &nat); dzl_rect_helper_get_rect (priv->animating_rect, rect); if (rect->width < min.width) rect->width = min.width; if (rect->height < min.height) rect->height = min.height; return TRUE; } static void dzl_stack_list_scroll_to_top (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); GtkAdjustment *vadj; g_assert (DZL_IS_STACK_LIST (self)); vadj = gtk_scrolled_window_get_vadjustment (priv->scroller); gtk_adjustment_set_value (vadj, 0.0); } static void dzl_stack_list_end_anim (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); GtkListBoxRow *header; ModelInfo *info; g_assert (DZL_IS_STACK_LIST (self)); g_assert (priv->animating != NULL); g_assert (priv->models->len > 0); info = g_ptr_array_index (priv->models, priv->models->len - 1); header = g_object_ref (priv->animating); dzl_gtk_widget_remove_style_class (GTK_WIDGET (header), "animating"); priv->animating = NULL; if (priv->animation != NULL) { dzl_animation_stop (priv->animation); g_clear_object (&priv->animation); } g_assert (header != NULL); g_assert (GTK_IS_LIST_BOX_ROW (header)); g_assert (gtk_widget_get_parent (GTK_WIDGET (header)) == GTK_WIDGET (priv->overlay)); gtk_container_remove (GTK_CONTAINER (priv->overlay), GTK_WIDGET (header)); gtk_container_add (GTK_CONTAINER (priv->headers), GTK_WIDGET (header)); gtk_list_box_bind_model (priv->content, info->model, dzl_stack_list_create_widget_func, info, NULL); dzl_stack_list_scroll_to_top (self); gtk_stack_set_transition_type (GTK_STACK (priv->flip_stack), GTK_STACK_TRANSITION_TYPE_SLIDE_DOWN); gtk_stack_set_visible_child (GTK_STACK (priv->flip_stack), GTK_WIDGET (priv->scroller)); dzl_stack_list_update_activatables (self); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); g_object_unref (header); } static void animation_finished (gpointer data) { DzlStackListPrivate *priv; DzlStackList *self; GtkListBoxRow *row; gpointer *closure = data; g_assert (closure != NULL); g_assert (DZL_IS_STACK_LIST (closure [0])); g_assert (GTK_IS_LIST_BOX_ROW (closure [1])); self = closure [0]; row = closure [1]; priv = dzl_stack_list_get_instance_private (self); if (row == priv->animating) dzl_stack_list_end_anim (self); g_object_unref (closure[0]); g_object_unref (closure[1]); g_free (closure); } static void dzl_stack_list_begin_anim (DzlStackList *self, GtkListBoxRow *row, const GdkRectangle *begin_area, const GdkRectangle *end_area) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); GdkFrameClock *frame_clock; gpointer *closure; guint pos; guint duration = 0; g_assert (DZL_IS_STACK_LIST (self)); g_assert (row != NULL); g_assert (begin_area != NULL); g_assert (end_area != NULL); priv->animating = row; dzl_gtk_widget_add_style_class (GTK_WIDGET (row), "animating"); g_object_set (priv->animating_rect, "x", begin_area->x, "y", begin_area->y, "width", begin_area->width, "height", begin_area->height, NULL); frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); closure = g_new0 (gpointer, 2); closure [0] = g_object_ref (self); closure [1] = g_object_ref_sink (row); gtk_overlay_add_overlay (GTK_OVERLAY (priv->overlay), GTK_WIDGET (row)); pos = gtk_list_box_row_get_index (row); if (pos != 0) { GdkMonitor *monitor; GdkDisplay *display; GdkWindow *window; guint distance = ABS (end_area->y - begin_area->y); display = gtk_widget_get_display (GTK_WIDGET (self)); window = gtk_widget_get_window (GTK_WIDGET (self)); monitor = gdk_display_get_monitor_at_window (display, window); duration = dzl_animation_calculate_duration (monitor, 0, distance); duration = MIN (duration, SLIDE_DURATION_MAX); } priv->animation = dzl_object_animate_full (priv->animating_rect, DZL_ANIMATION_EASE_IN_OUT_CUBIC, duration, frame_clock, animation_finished, closure, "x", end_area->x, "y", end_area->y, "width", end_area->width, "height", end_area->height, NULL); g_object_ref (priv->animation); g_signal_connect_object (priv->animating_rect, "notify", G_CALLBACK (gtk_widget_queue_resize), priv->animating, G_CONNECT_SWAPPED); gtk_stack_set_transition_type (GTK_STACK (priv->flip_stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); gtk_stack_set_visible_child (GTK_STACK (priv->flip_stack), GTK_WIDGET (priv->fake_list)); } static void dzl_stack_list_real_header_activated (DzlStackList *self, GtkListBoxRow *header) { gint pos; g_assert (DZL_IS_STACK_LIST (self)); g_assert (GTK_IS_LIST_BOX_ROW (header)); pos = gtk_list_box_row_get_index (header) + 1; while (dzl_stack_list_get_depth (self) > (guint)pos) dzl_stack_list_pop (self); } static void dzl_stack_list_finalize (GObject *object) { DzlStackList *self = (DzlStackList *)object; DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); g_clear_pointer (&priv->models, g_ptr_array_unref); g_clear_object (&priv->animating_rect); g_clear_object (&priv->animation); G_OBJECT_CLASS (dzl_stack_list_parent_class)->finalize (object); } static void dzl_stack_list_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { DzlStackList *self = DZL_STACK_LIST (object); switch (prop_id) { case PROP_MODEL: g_value_set_object (value, dzl_stack_list_get_model (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void dzl_stack_list_class_init (DzlStackListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->finalize = dzl_stack_list_finalize; object_class->get_property = dzl_stack_list_get_property; klass->header_activated = dzl_stack_list_real_header_activated; properties [PROP_MODEL] = g_param_spec_object ("model", _("Model"), _("Model"), G_TYPE_LIST_MODEL, (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, LAST_PROP, properties); signals [HEADER_ACTIVATED] = g_signal_new ("header-activated", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (DzlStackListClass, header_activated), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_LIST_BOX_ROW); signals [ROW_ACTIVATED] = g_signal_new ("row-activated", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (DzlStackListClass, row_activated), NULL, NULL, NULL, G_TYPE_NONE, 1, GTK_TYPE_LIST_BOX_ROW); gtk_widget_class_set_css_name (widget_class, "dzlstacklist"); } static void dzl_stack_list_init (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); priv->animating_rect = g_object_new (DZL_TYPE_RECT_HELPER, NULL); priv->models = g_ptr_array_new_with_free_func (model_info_free); priv->overlay = g_object_new (GTK_TYPE_OVERLAY, "visible", TRUE, NULL); g_signal_connect_swapped (priv->overlay, "get-child-position", G_CALLBACK (dzl_stack_list__overlay__get_child_position), self); gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (priv->overlay)); priv->box = g_object_new (GTK_TYPE_BOX, "orientation", GTK_ORIENTATION_VERTICAL, "vexpand", TRUE, "visible", TRUE, NULL); gtk_container_add (GTK_CONTAINER (priv->overlay), GTK_WIDGET (priv->box)); priv->headers = g_object_new (GTK_TYPE_LIST_BOX, "selection-mode", GTK_SELECTION_NONE, "visible", TRUE, NULL); g_signal_connect_swapped (priv->headers, "row-activated", G_CALLBACK (dzl_stack_list_header_row_activated), self); gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->headers)), "stack-header"); gtk_container_add (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->headers)); priv->flip_stack = g_object_new (GTK_TYPE_STACK, "transition-duration", FADE_DURATION, "transition-type", GTK_STACK_TRANSITION_TYPE_NONE, "visible", TRUE, "vexpand", TRUE, NULL); gtk_container_add (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->flip_stack)); priv->scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW, "shadow-type", GTK_SHADOW_NONE, "vexpand", TRUE, "visible", TRUE, NULL); gtk_container_add (GTK_CONTAINER (priv->flip_stack), GTK_WIDGET (priv->scroller)); priv->content = g_object_new (GTK_TYPE_LIST_BOX, "visible", TRUE, NULL); gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->content)), "stack-children"); g_signal_connect_object (priv->content, "row-activated", G_CALLBACK (dzl_stack_list_content_row_activated), self, G_CONNECT_SWAPPED); gtk_container_add (GTK_CONTAINER (priv->scroller), GTK_WIDGET (priv->content)); priv->fake_list = g_object_new (GTK_TYPE_LIST_BOX, "visible", TRUE, NULL); gtk_container_add (GTK_CONTAINER (priv->flip_stack), GTK_WIDGET (priv->fake_list)); } GtkWidget * dzl_stack_list_new (void) { return g_object_new (DZL_TYPE_STACK_LIST, NULL); } void dzl_stack_list_push (DzlStackList *self, GtkWidget *header, GListModel *model, DzlStackListCreateWidgetFunc create_widget_func, gpointer user_data, GDestroyNotify user_data_free_func) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); ModelInfo *info; GdkRectangle current_area; GdkRectangle target_area; g_return_if_fail (DZL_IS_STACK_LIST (self)); g_return_if_fail (GTK_IS_WIDGET (header)); g_return_if_fail (G_IS_LIST_MODEL (model)); g_return_if_fail (create_widget_func != NULL); if (priv->animating != NULL) dzl_stack_list_end_anim (self); if (!GTK_IS_LIST_BOX_ROW (header)) header = g_object_new (GTK_TYPE_LIST_BOX_ROW, "child", header, "visible", TRUE, NULL); info = g_slice_new0 (ModelInfo); info->header = header; info->model = g_object_ref (model); info->create_widget_func = create_widget_func; info->user_data = user_data; info->user_data_free_func = user_data_free_func; g_ptr_array_add (priv->models, info); /* * Nothing to animate, make everything happen immediately. */ if (priv->activated == NULL) { gtk_container_add (GTK_CONTAINER (priv->headers), GTK_WIDGET (header)); dzl_stack_list_update_activatables (self); gtk_list_box_bind_model (priv->content, model, dzl_stack_list_create_widget_func, info, NULL); dzl_stack_list_scroll_to_top (self); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); return; } /* * Get the location to begin the animation. */ gtk_widget_get_allocation (GTK_WIDGET (priv->activated), ¤t_area); gtk_widget_translate_coordinates (GTK_WIDGET (priv->activated), GTK_WIDGET (priv->overlay), 0, 0, ¤t_area.x, ¤t_area.y); /* * Get the location to end the animation. */ gtk_widget_get_allocation (GTK_WIDGET (priv->headers), &target_area); target_area.x = current_area.x; target_area.y = target_area.height; target_area.width = current_area.width; target_area.height = current_area.height; dzl_stack_list_begin_anim (self, GTK_LIST_BOX_ROW (header), ¤t_area, &target_area); } void dzl_stack_list_pop (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); ModelInfo *info; g_return_if_fail (DZL_IS_STACK_LIST (self)); if (priv->models->len == 0) return; if (priv->animating != NULL) dzl_stack_list_end_anim (self); info = g_ptr_array_index (priv->models, priv->models->len - 1); gtk_container_remove (GTK_CONTAINER (priv->headers), GTK_WIDGET (info->header)); gtk_list_box_bind_model (priv->content, NULL, NULL, NULL, NULL); g_ptr_array_remove_index (priv->models, priv->models->len - 1); if (priv->models->len > 0) { info = g_ptr_array_index (priv->models, priv->models->len - 1); gtk_list_box_bind_model (priv->content, info->model, dzl_stack_list_create_widget_func, info, NULL); } dzl_stack_list_update_activatables (self); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]); } /** * dzl_stack_list_get_model: * * Returns: (transfer none): An #DzlStackList. */ GListModel * dzl_stack_list_get_model (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); ModelInfo *info; g_return_val_if_fail (DZL_IS_STACK_LIST (self), NULL); if (priv->models->len == 0) return NULL; info = g_ptr_array_index (priv->models, priv->models->len - 1); return info->model; } guint dzl_stack_list_get_depth (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); g_return_val_if_fail (DZL_IS_STACK_LIST (self), 0); return priv->models->len; } void dzl_stack_list_clear (DzlStackList *self) { DzlStackListPrivate *priv = dzl_stack_list_get_instance_private (self); g_return_if_fail (DZL_IS_STACK_LIST (self)); while (priv->models->len > 0) dzl_stack_list_pop (self); }