/* * Copyright (C) 2019 Zander Brown * Copyright (C) 2019 Purism SPC * * Based on gtkstackswitcher.c, Copyright (c) 2013 Red Hat, Inc. * https://gitlab.gnome.org/GNOME/gtk/blob/a0129f556b1fd655215165739d0277d7f7a2c1a8/gtk/gtkstackswitcher.c * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include "hdy-css-private.h" #include "hdy-enums.h" #include "hdy-view-switcher.h" #include "hdy-view-switcher-button-private.h" /** * SECTION:hdy-view-switcher * @short_description: An adaptive view switcher. * @title: HdyViewSwitcher * * An adaptive view switcher, designed to switch between multiple views in a * similar fashion than a #GtkStackSwitcher. * * Depending on the available width, the view switcher can adapt from a wide * mode showing the view's icon and title side by side, to a narrow mode showing * the view's icon and title one on top of the other, in a more compact way. * This can be controlled via the policy property. * * To look good in a header bar, an #HdyViewSwitcher requires to fill its full * height. Contrary to #GtkHeaderBar, #HdyHeaderBar doesn't force a vertical * alignment on its title widget, so we recommend it over #GtkHeaderBar. * * # CSS nodes * * #HdyViewSwitcher has a single CSS node with name viewswitcher. * * Since: 0.0.10 */ /** * HdyViewSwitcherPolicy: * @HDY_VIEW_SWITCHER_POLICY_AUTO: Automatically adapt to the best fitting mode * @HDY_VIEW_SWITCHER_POLICY_NARROW: Force the narrow mode * @HDY_VIEW_SWITCHER_POLICY_WIDE: Force the wide mode */ #define MIN_NAT_BUTTON_WIDTH 100 #define TIMEOUT_EXPAND 500 enum { PROP_0, PROP_POLICY, PROP_NARROW_ELLIPSIZE, PROP_STACK, LAST_PROP, }; struct _HdyViewSwitcher { GtkBin parent_instance; GtkWidget *box; GHashTable *buttons; gboolean in_child_changed; GtkWidget *switch_button; guint switch_timer; HdyViewSwitcherPolicy policy; PangoEllipsizeMode narrow_ellipsize; GtkStack *stack; }; static GParamSpec *props[LAST_PROP]; G_DEFINE_TYPE (HdyViewSwitcher, hdy_view_switcher, GTK_TYPE_BIN) static void set_visible_stack_child_for_button (HdyViewSwitcher *self, HdyViewSwitcherButton *button) { if (self->in_child_changed) return; gtk_stack_set_visible_child (self->stack, GTK_WIDGET (g_object_get_data (G_OBJECT (button), "stack-child"))); } static void update_button (HdyViewSwitcher *self, GtkWidget *widget, HdyViewSwitcherButton *button) { g_autofree gchar *title = NULL; g_autofree gchar *icon_name = NULL; gboolean needs_attention; gtk_container_child_get (GTK_CONTAINER (self->stack), widget, "title", &title, "icon-name", &icon_name, "needs-attention", &needs_attention, NULL); g_object_set (G_OBJECT (button), "icon-name", icon_name, "icon-size", GTK_ICON_SIZE_BUTTON, "label", title, "needs-attention", needs_attention, NULL); gtk_widget_set_visible (GTK_WIDGET (button), gtk_widget_get_visible (widget) && (title != NULL || icon_name != NULL)); } static void on_stack_child_updated (GtkWidget *widget, GParamSpec *pspec, HdyViewSwitcher *self) { update_button (self, widget, g_hash_table_lookup (self->buttons, widget)); } static void on_position_updated (GtkWidget *widget, GParamSpec *pspec, HdyViewSwitcher *self) { GtkWidget *button = g_hash_table_lookup (self->buttons, widget); gint position; gtk_container_child_get (GTK_CONTAINER (self->stack), widget, "position", &position, NULL); gtk_box_reorder_child (GTK_BOX (self->box), button, position); } static void remove_switch_timer (HdyViewSwitcher *self) { if (!self->switch_timer) return; g_source_remove (self->switch_timer); self->switch_timer = 0; } static gboolean hdy_view_switcher_switch_timeout (gpointer data) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (data); GtkWidget *button = self->switch_button; self->switch_timer = 0; self->switch_button = NULL; if (button) gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); return G_SOURCE_REMOVE; } static gboolean hdy_view_switcher_drag_motion (GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget); GtkAllocation allocation; GtkWidget *button; GHashTableIter iter; gpointer value; gboolean retval = FALSE; gtk_widget_get_allocation (widget, &allocation); x += allocation.x; y += allocation.y; button = NULL; g_hash_table_iter_init (&iter, self->buttons); while (g_hash_table_iter_next (&iter, NULL, &value)) { gtk_widget_get_allocation (GTK_WIDGET (value), &allocation); if (x >= allocation.x && x <= allocation.x + allocation.width && y >= allocation.y && y <= allocation.y + allocation.height) { button = GTK_WIDGET (value); retval = TRUE; break; } } if (button != self->switch_button) remove_switch_timer (self); self->switch_button = button; if (button && !self->switch_timer) { self->switch_timer = gdk_threads_add_timeout (TIMEOUT_EXPAND, hdy_view_switcher_switch_timeout, self); g_source_set_name_by_id (self->switch_timer, "[gtk+] hdy_view_switcher_switch_timeout"); } return retval; } static void hdy_view_switcher_drag_leave (GtkWidget *widget, GdkDragContext *context, guint time) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget); remove_switch_timer (self); } static void add_button_for_stack_child (HdyViewSwitcher *self, GtkWidget *stack_child) { g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box)); HdyViewSwitcherButton *button = HDY_VIEW_SWITCHER_BUTTON (hdy_view_switcher_button_new ()); g_object_set_data (G_OBJECT (button), "stack-child", stack_child); hdy_view_switcher_button_set_narrow_ellipsize (button, self->narrow_ellipsize); update_button (self, stack_child, button); if (children != NULL) gtk_radio_button_join_group (GTK_RADIO_BUTTON (button), GTK_RADIO_BUTTON (children->data)); gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (button)); g_signal_connect_swapped (button, "clicked", G_CALLBACK (set_visible_stack_child_for_button), self); g_signal_connect (stack_child, "notify::visible", G_CALLBACK (on_stack_child_updated), self); g_signal_connect (stack_child, "child-notify::title", G_CALLBACK (on_stack_child_updated), self); g_signal_connect (stack_child, "child-notify::icon-name", G_CALLBACK (on_stack_child_updated), self); g_signal_connect (stack_child, "child-notify::needs-attention", G_CALLBACK (on_stack_child_updated), self); g_signal_connect (stack_child, "child-notify::position", G_CALLBACK (on_position_updated), self); g_hash_table_insert (self->buttons, stack_child, button); } static void add_button_for_stack_child_cb (GtkWidget *stack_child, HdyViewSwitcher *self) { g_return_if_fail (HDY_IS_VIEW_SWITCHER (self)); g_return_if_fail (GTK_IS_WIDGET (stack_child)); add_button_for_stack_child (self, stack_child); } static void remove_button_for_stack_child (HdyViewSwitcher *self, GtkWidget *stack_child) { g_signal_handlers_disconnect_by_func (stack_child, on_stack_child_updated, self); g_signal_handlers_disconnect_by_func (stack_child, on_position_updated, self); gtk_container_remove (GTK_CONTAINER (self->box), g_hash_table_lookup (self->buttons, stack_child)); g_hash_table_remove (self->buttons, stack_child); } static void remove_button_for_stack_child_cb (GtkWidget *stack_child, HdyViewSwitcher *self) { g_return_if_fail (HDY_IS_VIEW_SWITCHER (self)); g_return_if_fail (GTK_IS_WIDGET (stack_child)); remove_button_for_stack_child (self, stack_child); } static void update_active_button_for_visible_stack_child (HdyViewSwitcher *self) { GtkWidget *visible_stack_child = gtk_stack_get_visible_child (self->stack); GtkWidget *button = g_hash_table_lookup (self->buttons, visible_stack_child); if (button == NULL) return; self->in_child_changed = TRUE; gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); self->in_child_changed = FALSE; } static void disconnect_stack_signals (HdyViewSwitcher *self) { g_signal_handlers_disconnect_by_func (self->stack, add_button_for_stack_child, self); g_signal_handlers_disconnect_by_func (self->stack, remove_button_for_stack_child, self); g_signal_handlers_disconnect_by_func (self->stack, update_active_button_for_visible_stack_child, self); g_signal_handlers_disconnect_by_func (self->stack, disconnect_stack_signals, self); } static void connect_stack_signals (HdyViewSwitcher *self) { g_signal_connect_object (self->stack, "add", G_CALLBACK (add_button_for_stack_child), self, G_CONNECT_AFTER | G_CONNECT_SWAPPED); g_signal_connect_object (self->stack, "remove", G_CALLBACK (remove_button_for_stack_child), self, G_CONNECT_AFTER | G_CONNECT_SWAPPED); g_signal_connect_object (self->stack, "notify::visible-child", G_CALLBACK (update_active_button_for_visible_stack_child), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->stack, "destroy", G_CALLBACK (disconnect_stack_signals), self, G_CONNECT_SWAPPED); } static void hdy_view_switcher_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object); switch (prop_id) { case PROP_POLICY: g_value_set_enum (value, hdy_view_switcher_get_policy (self)); break; case PROP_NARROW_ELLIPSIZE: g_value_set_enum (value, hdy_view_switcher_get_narrow_ellipsize (self)); break; case PROP_STACK: g_value_set_object (value, hdy_view_switcher_get_stack (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void hdy_view_switcher_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object); switch (prop_id) { case PROP_POLICY: hdy_view_switcher_set_policy (self, g_value_get_enum (value)); break; case PROP_NARROW_ELLIPSIZE: hdy_view_switcher_set_narrow_ellipsize (self, g_value_get_enum (value)); break; case PROP_STACK: hdy_view_switcher_set_stack (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void hdy_view_switcher_dispose (GObject *object) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object); remove_switch_timer (self); hdy_view_switcher_set_stack (self, NULL); G_OBJECT_CLASS (hdy_view_switcher_parent_class)->dispose (object); } static void hdy_view_switcher_finalize (GObject *object) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object); g_hash_table_destroy (self->buttons); G_OBJECT_CLASS (hdy_view_switcher_parent_class)->finalize (object); } static void hdy_view_switcher_get_preferred_width (GtkWidget *widget, gint *min, gint *nat) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget); g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box)); gint max_h_min = 0, max_h_nat = 0, max_v_min = 0, max_v_nat = 0; gint n_children = 0; for (GList *l = children; l != NULL; l = g_list_next (l)) { gint h_min = 0, h_nat = 0, v_min = 0, v_nat = 0; if (!gtk_widget_get_visible (l->data)) continue; hdy_view_switcher_button_get_size (HDY_VIEW_SWITCHER_BUTTON (l->data), &h_min, &h_nat, &v_min, &v_nat); max_h_min = MAX (h_min, max_h_min); max_h_nat = MAX (h_nat, max_h_nat); max_v_min = MAX (v_min, max_v_min); max_v_nat = MAX (v_nat, max_v_nat); n_children++; } /* Make the buttons ask at least a minimum arbitrary size for their natural * width. This prevents them from looking terribly narrow in a very wide bar. */ max_h_nat = MAX (max_h_nat, MIN_NAT_BUTTON_WIDTH); max_v_nat = MAX (max_v_nat, MIN_NAT_BUTTON_WIDTH); switch (self->policy) { case HDY_VIEW_SWITCHER_POLICY_NARROW: *min = max_v_min * n_children; *nat = max_v_nat * n_children; break; case HDY_VIEW_SWITCHER_POLICY_WIDE: *min = max_h_min * n_children; *nat = max_h_nat * n_children; break; case HDY_VIEW_SWITCHER_POLICY_AUTO: default: *min = max_v_min * n_children; *nat = max_h_nat * n_children; break; } hdy_css_measure (widget, GTK_ORIENTATION_HORIZONTAL, min, nat); } static gint is_narrow (HdyViewSwitcher *self, gint width) { g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box)); gint max_h_min = 0; gint n_children = 0; if (self->policy == HDY_VIEW_SWITCHER_POLICY_NARROW) return TRUE; if (self->policy == HDY_VIEW_SWITCHER_POLICY_WIDE) return FALSE; for (GList *l = children; l != NULL; l = g_list_next (l)) { gint h_min = 0; hdy_view_switcher_button_get_size (HDY_VIEW_SWITCHER_BUTTON (l->data), &h_min, NULL, NULL, NULL); max_h_min = MAX (max_h_min, h_min); n_children++; } return (max_h_min * n_children) > width; } static void hdy_view_switcher_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget); g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box)); GtkOrientation orientation; hdy_css_size_allocate (widget, allocation); orientation = is_narrow (HDY_VIEW_SWITCHER (widget), allocation->width) ? GTK_ORIENTATION_VERTICAL : GTK_ORIENTATION_HORIZONTAL; for (GList *l = children; l != NULL; l = g_list_next (l)) gtk_orientable_set_orientation (GTK_ORIENTABLE (l->data), orientation); GTK_WIDGET_CLASS (hdy_view_switcher_parent_class)->size_allocate (widget, allocation); } static void hdy_view_switcher_class_init (HdyViewSwitcherClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->get_property = hdy_view_switcher_get_property; object_class->set_property = hdy_view_switcher_set_property; object_class->dispose = hdy_view_switcher_dispose; object_class->finalize = hdy_view_switcher_finalize; widget_class->size_allocate = hdy_view_switcher_size_allocate; widget_class->get_preferred_width = hdy_view_switcher_get_preferred_width; widget_class->drag_motion = hdy_view_switcher_drag_motion; widget_class->drag_leave = hdy_view_switcher_drag_leave; /** * HdyViewSwitcher:policy: * * The #HdyViewSwitcherPolicy the view switcher should use to determine which * mode to use. * * Since: 0.0.10 */ props[PROP_POLICY] = g_param_spec_enum ("policy", _("Policy"), _("The policy to determine the mode to use"), HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_AUTO, G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * HdyViewSwitcher:narrow-ellipsize: * * The preferred place to ellipsize the string, if the narrow mode label does * not have enough room to display the entire string, specified as a * #PangoEllipsizeMode. * * Note that setting this property to a value other than %PANGO_ELLIPSIZE_NONE * has the side-effect that the label requests only enough space to display * the ellipsis. * * Since: 0.0.10 */ props[PROP_NARROW_ELLIPSIZE] = g_param_spec_enum ("narrow-ellipsize", _("Narrow ellipsize"), _("The preferred place to ellipsize the string, if the narrow mode label does not have enough room to display the entire string"), PANGO_TYPE_ELLIPSIZE_MODE, PANGO_ELLIPSIZE_NONE, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * HdyViewSwitcher:stack: * * The #GtkStack the view switcher controls. * * Since: 0.0.10 */ props[PROP_STACK] = g_param_spec_object ("stack", _("Stack"), _("Stack"), GTK_TYPE_STACK, G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, LAST_PROP, props); gtk_widget_class_set_css_name (widget_class, "viewswitcher"); } static void hdy_view_switcher_init (HdyViewSwitcher *self) { self->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_show (self->box); gtk_box_set_homogeneous (GTK_BOX (self->box), TRUE); gtk_container_add (GTK_CONTAINER (self), self->box); self->buttons = g_hash_table_new (g_direct_hash, g_direct_equal); gtk_widget_set_valign (GTK_WIDGET (self), GTK_ALIGN_FILL); gtk_drag_dest_set (GTK_WIDGET (self), 0, NULL, 0, 0); gtk_drag_dest_set_track_motion (GTK_WIDGET (self), TRUE); } /** * hdy_view_switcher_new: * * Creates a new #HdyViewSwitcher widget. * * Returns: a new #HdyViewSwitcher * * Since: 0.0.10 */ GtkWidget * hdy_view_switcher_new (void) { return g_object_new (HDY_TYPE_VIEW_SWITCHER, NULL); } /** * hdy_view_switcher_get_policy: * @self: a #HdyViewSwitcher * * Gets the policy of @self. * * Returns: the policy of @self * * Since: 0.0.10 */ HdyViewSwitcherPolicy hdy_view_switcher_get_policy (HdyViewSwitcher *self) { g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), HDY_VIEW_SWITCHER_POLICY_AUTO); return self->policy; } /** * hdy_view_switcher_set_policy: * @self: a #HdyViewSwitcher * @policy: the new policy * * Sets the policy of @self. * * Since: 0.0.10 */ void hdy_view_switcher_set_policy (HdyViewSwitcher *self, HdyViewSwitcherPolicy policy) { g_return_if_fail (HDY_IS_VIEW_SWITCHER (self)); if (self->policy == policy) return; self->policy = policy; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]); gtk_widget_queue_resize (GTK_WIDGET (self)); } /** * hdy_view_switcher_get_narrow_ellipsize: * @self: a #HdyViewSwitcher * * Get the ellipsizing position of the narrow mode label. See * hdy_view_switcher_set_narrow_ellipsize(). * * Returns: #PangoEllipsizeMode * * Since: 0.0.10 **/ PangoEllipsizeMode hdy_view_switcher_get_narrow_ellipsize (HdyViewSwitcher *self) { g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), PANGO_ELLIPSIZE_NONE); return self->narrow_ellipsize; } /** * hdy_view_switcher_set_narrow_ellipsize: * @self: a #HdyViewSwitcher * @mode: a #PangoEllipsizeMode * * Set the mode used to ellipsize the text in narrow mode if there is not * enough space to render the entire string. * * Since: 0.0.10 **/ void hdy_view_switcher_set_narrow_ellipsize (HdyViewSwitcher *self, PangoEllipsizeMode mode) { GHashTableIter iter; gpointer button; g_return_if_fail (HDY_IS_VIEW_SWITCHER (self)); g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END); if ((PangoEllipsizeMode) self->narrow_ellipsize == mode) return; self->narrow_ellipsize = mode; g_hash_table_iter_init (&iter, self->buttons); while (g_hash_table_iter_next (&iter, NULL, &button)) hdy_view_switcher_button_set_narrow_ellipsize (HDY_VIEW_SWITCHER_BUTTON (button), mode); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NARROW_ELLIPSIZE]); } /** * hdy_view_switcher_get_stack: * @self: a #HdyViewSwitcher * * Get the #GtkStack being controlled by the #HdyViewSwitcher. * * See: hdy_view_switcher_set_stack() * * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set * * Since: 0.0.10 */ GtkStack * hdy_view_switcher_get_stack (HdyViewSwitcher *self) { g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), NULL); return self->stack; } /** * hdy_view_switcher_set_stack: * @self: a #HdyViewSwitcher * @stack: (nullable): a #GtkStack * * Sets the #GtkStack to control. * * Since: 0.0.10 */ void hdy_view_switcher_set_stack (HdyViewSwitcher *self, GtkStack *stack) { g_return_if_fail (HDY_IS_VIEW_SWITCHER (self)); g_return_if_fail (stack == NULL || GTK_IS_STACK (stack)); if (self->stack == stack) return; if (self->stack) { disconnect_stack_signals (self); gtk_container_foreach (GTK_CONTAINER (self->stack), (GtkCallback) remove_button_for_stack_child_cb, self); } g_set_object (&self->stack, stack); if (self->stack) { gtk_container_foreach (GTK_CONTAINER (self->stack), (GtkCallback) add_button_for_stack_child_cb, self); update_active_button_for_visible_stack_child (self); connect_stack_signals (self); } gtk_widget_queue_resize (GTK_WIDGET (self)); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]); }