diff options
Diffstat (limited to '')
139 files changed, 35802 insertions, 0 deletions
diff --git a/subprojects/libhandy/src/gen-public-types.sh b/subprojects/libhandy/src/gen-public-types.sh new file mode 100644 index 0000000..036c336 --- /dev/null +++ b/subprojects/libhandy/src/gen-public-types.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +echo '/* This file was generated by gen-plublic-types.sh, do not edit it. */ +' + +for var in "$@" +do + echo "#include \"$var\"" +done + +echo '#include "hdy-main-private.h" + +void +hdy_init_public_types (void) +{' + +sed -ne 's/^#define \{1,\}\(HDY_TYPE_[A-Z0-9_]\{1,\}\) \{1,\}.*/ g_type_ensure (\1);/p' "$@" | sort + +echo '} +' diff --git a/subprojects/libhandy/src/gtk-window-private.h b/subprojects/libhandy/src/gtk-window-private.h new file mode 100644 index 0000000..e2f183e --- /dev/null +++ b/subprojects/libhandy/src/gtk-window-private.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void hdy_gtk_window_toggle_maximized (GtkWindow *window); +GdkPixbuf *hdy_gtk_window_get_icon_for_size (GtkWindow *window, + gint size); +GdkWindowState hdy_gtk_window_get_state (GtkWindow *window); + +G_END_DECLS diff --git a/subprojects/libhandy/src/gtk-window.c b/subprojects/libhandy/src/gtk-window.c new file mode 100644 index 0000000..154acdb --- /dev/null +++ b/subprojects/libhandy/src/gtk-window.c @@ -0,0 +1,169 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +/* Bits taken from GTK 3.24 and tweaked to be used by libhandy. */ + +#include "gtk-window-private.h" + +typedef struct +{ + GList *icon_list; + gchar *icon_name; + guint realized : 1; + guint using_default_icon : 1; + guint using_parent_icon : 1; + guint using_themed_icon : 1; +} GtkWindowIconInfo; + +static GQuark quark_gtk_window_icon_info = 0; + +static void +ensure_quarks (void) +{ + if (!quark_gtk_window_icon_info) + quark_gtk_window_icon_info = g_quark_from_static_string ("gtk-window-icon-info"); +} + +void +hdy_gtk_window_toggle_maximized (GtkWindow *window) +{ + if (gtk_window_is_maximized (window)) + gtk_window_unmaximize (window); + else + gtk_window_maximize (window); +} + +static GtkWindowIconInfo* +get_icon_info (GtkWindow *window) +{ + ensure_quarks (); + + return g_object_get_qdata (G_OBJECT (window), quark_gtk_window_icon_info); +} + +static void +free_icon_info (GtkWindowIconInfo *info) +{ + g_free (info->icon_name); + g_slice_free (GtkWindowIconInfo, info); +} + +static GtkWindowIconInfo* +ensure_icon_info (GtkWindow *window) +{ + GtkWindowIconInfo *info; + + ensure_quarks (); + + info = get_icon_info (window); + + if (info == NULL) + { + info = g_slice_new0 (GtkWindowIconInfo); + g_object_set_qdata_full (G_OBJECT (window), + quark_gtk_window_icon_info, + info, + (GDestroyNotify)free_icon_info); + } + + return info; +} + +static GdkPixbuf * +icon_from_list (GList *list, + gint size) +{ + GdkPixbuf *best; + GdkPixbuf *pixbuf; + GList *l; + + best = NULL; + for (l = list; l; l = l->next) + { + pixbuf = list->data; + if (gdk_pixbuf_get_width (pixbuf) <= size && + gdk_pixbuf_get_height (pixbuf) <= size) + { + best = g_object_ref (pixbuf); + break; + } + } + + if (best == NULL) + best = gdk_pixbuf_scale_simple (GDK_PIXBUF (list->data), size, size, GDK_INTERP_BILINEAR); + + return best; +} + +static GdkPixbuf * +icon_from_name (const gchar *name, + gint size) +{ + return gtk_icon_theme_load_icon (gtk_icon_theme_get_default (), + name, size, + GTK_ICON_LOOKUP_FORCE_SIZE, NULL); +} + +GdkPixbuf * +hdy_gtk_window_get_icon_for_size (GtkWindow *window, + gint size) +{ + GtkWindowIconInfo *info; + const gchar *name; + g_autoptr (GList) default_icon_list = gtk_window_get_default_icon_list (); + + info = ensure_icon_info (window); + + if (info->icon_list != NULL) + return icon_from_list (info->icon_list, size); + + name = gtk_window_get_icon_name (window); + if (name != NULL) + return icon_from_name (name, size); + + if (gtk_window_get_transient_for (window) != NULL) + { + info = ensure_icon_info (gtk_window_get_transient_for (window)); + if (info->icon_list) + return icon_from_list (info->icon_list, size); + } + + if (default_icon_list != NULL) + return icon_from_list (default_icon_list, size); + + if (gtk_window_get_default_icon_name () != NULL) + return icon_from_name (gtk_window_get_default_icon_name (), size); + + return NULL; +} + +GdkWindowState +hdy_gtk_window_get_state (GtkWindow *window) +{ + GdkWindow *gdk_window = gtk_widget_get_window (GTK_WIDGET (window)); + + return gdk_window ? gdk_window_get_state (gdk_window) : 0; +} diff --git a/subprojects/libhandy/src/gtkprogresstracker.c b/subprojects/libhandy/src/gtkprogresstracker.c new file mode 100644 index 0000000..72d2013 --- /dev/null +++ b/subprojects/libhandy/src/gtkprogresstracker.c @@ -0,0 +1,248 @@ +/* + * Copyright © 2016 Endless Mobile Inc. + * + * 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.1 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 <http://www.gnu.org/licenses/>. + * + * Authors: Matthew Watson <mattdangerw@gmail.com> + */ + +#include "gtkprogresstrackerprivate.h" +/* #include "gtkprivate.h" */ +/* #include "gtkcsseasevalueprivate.h" */ + +#include <math.h> +#include <string.h> + +#include "hdy-animation-private.h" + +/* + * Progress tracker is small helper for tracking progress through gtk + * animations. It's a simple zero-initable struct, meant to be thrown in a + * widget's private data without the need for setup or teardown. + * + * Progress tracker will handle translating frame clock timestamps to a + * fractional progress value for interpolating between animation targets. + * + * Progress tracker will use the GTK_SLOWDOWN environment variable to control + * the speed of animations. This can be useful for debugging. + */ + +static gdouble gtk_slowdown = 1.0; + +/** + * gtk_progress_tracker_init_copy: + * @source: The source progress tracker + * @dest: The destination progress tracker + * + * Copy all progress tracker state from the source tracker to dest tracker. + **/ +void +gtk_progress_tracker_init_copy (GtkProgressTracker *source, + GtkProgressTracker *dest) +{ + memcpy (dest, source, sizeof (GtkProgressTracker)); +} + +/** + * gtk_progress_tracker_start: + * @tracker: The progress tracker + * @duration: Animation duration in us + * @delay: Animation delay in us + * @iteration_count: Number of iterations to run the animation, must be >= 0 + * + * Begins tracking progress for a new animation. Clears all previous state. + **/ +void +gtk_progress_tracker_start (GtkProgressTracker *tracker, + guint64 duration, + gint64 delay, + gdouble iteration_count) +{ + tracker->is_running = TRUE; + tracker->last_frame_time = 0; + tracker->duration = duration; + tracker->iteration = - delay / (gdouble) duration; + tracker->iteration_count = iteration_count; +} + +/** + * gtk_progress_tracker_finish: + * @tracker: The progress tracker + * + * Stops running the current animation. + **/ +void +gtk_progress_tracker_finish (GtkProgressTracker *tracker) +{ + tracker->is_running = FALSE; +} + +/** + * gtk_progress_tracker_advance_frame: + * @tracker: The progress tracker + * @frame_time: The current frame time, usually from the frame clock. + * + * Increments the progress of the animation forward a frame. If no animation has + * been started, does nothing. + **/ +void +gtk_progress_tracker_advance_frame (GtkProgressTracker *tracker, + guint64 frame_time) +{ + gdouble delta; + + if (!tracker->is_running) + return; + + if (tracker->last_frame_time == 0) + { + tracker->last_frame_time = frame_time; + return; + } + + if (frame_time < tracker->last_frame_time) + { + g_warning ("Progress tracker frame set backwards, ignoring."); + return; + } + + delta = (frame_time - tracker->last_frame_time) / gtk_slowdown / tracker->duration; + tracker->last_frame_time = frame_time; + tracker->iteration += delta; +} + +/** + * gtk_progress_tracker_skip_frame: + * @tracker: The progress tracker + * @frame_time: The current frame time, usually from the frame clock. + * + * Does not update the progress of the animation forward, but records the frame + * to calculate future deltas. Calling this each frame will effectively pause + * the animation. + **/ +void +gtk_progress_tracker_skip_frame (GtkProgressTracker *tracker, + guint64 frame_time) +{ + if (!tracker->is_running) + return; + + tracker->last_frame_time = frame_time; +} + +/** + * gtk_progress_tracker_get_state: + * @tracker: The progress tracker + * + * Returns whether the tracker is before, during or after the currently started + * animation. The tracker will only ever be in the before state if the animation + * was started with a delay. If no animation has been started, returns + * %GTK_PROGRESS_STATE_AFTER. + * + * Returns: A GtkProgressState + **/ +GtkProgressState +gtk_progress_tracker_get_state (GtkProgressTracker *tracker) +{ + if (!tracker->is_running || tracker->iteration > tracker->iteration_count) + return GTK_PROGRESS_STATE_AFTER; + if (tracker->iteration < 0) + return GTK_PROGRESS_STATE_BEFORE; + return GTK_PROGRESS_STATE_DURING; +} + +/** + * gtk_progress_tracker_get_iteration: + * @tracker: The progress tracker + * + * Returns the fractional number of cycles the animation has completed. For + * example, it you started an animation with iteration-count of 2 and are half + * way through the second animation, this returns 1.5. + * + * Returns: The current iteration. + **/ +gdouble +gtk_progress_tracker_get_iteration (GtkProgressTracker *tracker) +{ + return tracker->is_running ? CLAMP (tracker->iteration, 0.0, tracker->iteration_count) : 1.0; +} + +/** + * gtk_progress_tracker_get_iteration_cycle: + * @tracker: The progress tracker + * + * Returns an integer index of the current iteration cycle tracker is + * progressing through. Handles edge cases, such as an iteration value of 2.0 + * which could be considered the end of the second iteration of the beginning of + * the third, in the same way as gtk_progress_tracker_get_progress(). + * + * Returns: The integer count of the current animation cycle. + **/ +guint64 +gtk_progress_tracker_get_iteration_cycle (GtkProgressTracker *tracker) +{ + gdouble iteration = gtk_progress_tracker_get_iteration (tracker); + + /* Some complexity here. We want an iteration of 0.0 to always map to 0 (start + * of the first iteration), but an iteration of 1.0 to also map to 0 (end of + * first iteration) and 2.0 to 1 (end of the second iteration). + */ + if (iteration == 0.0) + return 0; + + return (guint64) ceil (iteration) - 1; +} + +/** + * gtk_progress_tracker_get_progress: + * @tracker: The progress tracker + * @reversed: If progress should be reversed. + * + * Gets the progress through the current animation iteration, from [0, 1]. Use + * to interpolate between animation targets. If reverse is true each iteration + * will begin at 1 and end at 0. + * + * Returns: The progress value. + **/ +gdouble +gtk_progress_tracker_get_progress (GtkProgressTracker *tracker, + gboolean reversed) +{ + gdouble progress, iteration; + guint64 iteration_cycle; + + iteration = gtk_progress_tracker_get_iteration (tracker); + iteration_cycle = gtk_progress_tracker_get_iteration_cycle (tracker); + + progress = iteration - iteration_cycle; + return reversed ? 1.0 - progress : progress; +} + +/** + * gtk_progress_tracker_get_ease_out_cubic: + * @tracker: The progress tracker + * @reversed: If progress should be reversed before applying the ease function. + * + * Applies a simple ease out cubic function to the result of + * gtk_progress_tracker_get_progress(). + * + * Returns: The eased progress value. + **/ +gdouble +gtk_progress_tracker_get_ease_out_cubic (GtkProgressTracker *tracker, + gboolean reversed) +{ + gdouble progress = gtk_progress_tracker_get_progress (tracker, reversed); + return hdy_ease_out_cubic (progress); +} diff --git a/subprojects/libhandy/src/gtkprogresstrackerprivate.h b/subprojects/libhandy/src/gtkprogresstrackerprivate.h new file mode 100644 index 0000000..fcce609 --- /dev/null +++ b/subprojects/libhandy/src/gtkprogresstrackerprivate.h @@ -0,0 +1,74 @@ +/* + * Copyright © 2016 Endless Mobile Inc. + * + * 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.1 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 <http://www.gnu.org/licenses/>. + * + * Authors: Matthew Watson <mattdangerw@gmail.com> + */ + +#ifndef __GTK_PROGRESS_TRACKER_PRIVATE_H__ +#define __GTK_PROGRESS_TRACKER_PRIVATE_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef enum { + GTK_PROGRESS_STATE_BEFORE, + GTK_PROGRESS_STATE_DURING, + GTK_PROGRESS_STATE_AFTER, +} GtkProgressState; + +typedef struct _GtkProgressTracker GtkProgressTracker; + +struct _GtkProgressTracker +{ + gboolean is_running; + guint64 last_frame_time; + guint64 duration; + gdouble iteration; + gdouble iteration_count; +}; + +void gtk_progress_tracker_init_copy (GtkProgressTracker *source, + GtkProgressTracker *dest); + +void gtk_progress_tracker_start (GtkProgressTracker *tracker, + guint64 duration, + gint64 delay, + gdouble iteration_count); + +void gtk_progress_tracker_finish (GtkProgressTracker *tracker); + +void gtk_progress_tracker_advance_frame (GtkProgressTracker *tracker, + guint64 frame_time); + +void gtk_progress_tracker_skip_frame (GtkProgressTracker *tracker, + guint64 frame_time); + +GtkProgressState gtk_progress_tracker_get_state (GtkProgressTracker *tracker); + +gdouble gtk_progress_tracker_get_iteration (GtkProgressTracker *tracker); + +guint64 gtk_progress_tracker_get_iteration_cycle (GtkProgressTracker *tracker); + +gdouble gtk_progress_tracker_get_progress (GtkProgressTracker *tracker, + gboolean reverse); + +gdouble gtk_progress_tracker_get_ease_out_cubic (GtkProgressTracker *tracker, + gboolean reverse); + +G_END_DECLS + +#endif /* __GTK_PROGRESS_TRACKER_PRIVATE_H__ */ diff --git a/subprojects/libhandy/src/handy.gresources.xml b/subprojects/libhandy/src/handy.gresources.xml new file mode 100644 index 0000000..b96444b --- /dev/null +++ b/subprojects/libhandy/src/handy.gresources.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/sm/puri/handy"> + <file preprocess="xml-stripblanks">icons/avatar-default-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/hdy-expander-arrow-symbolic.svg</file> + <file compressed="true">themes/Adwaita.css</file> + <file compressed="true">themes/Adwaita-dark.css</file> + <file compressed="true">themes/fallback.css</file> + <file compressed="true">themes/HighContrast.css</file> + <file compressed="true">themes/HighContrastInverse.css</file> + <file compressed="true">themes/shared.css</file> + </gresource> + <gresource prefix="/sm/puri/handy/ui"> + <file preprocess="xml-stripblanks">hdy-action-row.ui</file> + <file preprocess="xml-stripblanks">hdy-carousel.ui</file> + <file preprocess="xml-stripblanks">hdy-combo-row.ui</file> + <file preprocess="xml-stripblanks">hdy-expander-row.ui</file> + <file preprocess="xml-stripblanks">hdy-keypad.ui</file> + <file preprocess="xml-stripblanks">hdy-keypad-button.ui</file> + <file preprocess="xml-stripblanks">hdy-preferences-group.ui</file> + <file preprocess="xml-stripblanks">hdy-preferences-page.ui</file> + <file preprocess="xml-stripblanks">hdy-preferences-window.ui</file> + <file preprocess="xml-stripblanks">hdy-search-bar.ui</file> + <file preprocess="xml-stripblanks">hdy-view-switcher-bar.ui</file> + <file preprocess="xml-stripblanks">hdy-view-switcher-button.ui</file> + <file preprocess="xml-stripblanks">hdy-view-switcher-title.ui</file> + </gresource> +</gresources> diff --git a/subprojects/libhandy/src/handy.h b/subprojects/libhandy/src/handy.h new file mode 100644 index 0000000..1ea48a7 --- /dev/null +++ b/subprojects/libhandy/src/handy.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2017 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#if !GTK_CHECK_VERSION(3, 22, 0) +# error "libhandy requires gtk+-3.0 >= 3.22.0" +#endif + +#if !GLIB_CHECK_VERSION(2, 50, 0) +# error "libhandy requires glib-2.0 >= 2.50.0" +#endif + +#define _HANDY_INSIDE + +#include "hdy-version.h" +#include "hdy-action-row.h" +#include "hdy-animation.h" +#include "hdy-application-window.h" +#include "hdy-avatar.h" +#include "hdy-carousel.h" +#include "hdy-carousel-indicator-dots.h" +#include "hdy-carousel-indicator-lines.h" +#include "hdy-clamp.h" +#include "hdy-combo-row.h" +#include "hdy-deck.h" +#include "hdy-deprecation-macros.h" +#include "hdy-enum-value-object.h" +#include "hdy-expander-row.h" +#include "hdy-header-bar.h" +#include "hdy-header-group.h" +#include "hdy-keypad.h" +#include "hdy-leaflet.h" +#include "hdy-main.h" +#include "hdy-navigation-direction.h" +#include "hdy-preferences-group.h" +#include "hdy-preferences-page.h" +#include "hdy-preferences-row.h" +#include "hdy-preferences-window.h" +#include "hdy-search-bar.h" +#include "hdy-squeezer.h" +#include "hdy-swipe-group.h" +#include "hdy-swipe-tracker.h" +#include "hdy-swipeable.h" +#include "hdy-title-bar.h" +#include "hdy-types.h" +#include "hdy-value-object.h" +#include "hdy-view-switcher.h" +#include "hdy-view-switcher-bar.h" +#include "hdy-view-switcher-title.h" +#include "hdy-window.h" +#include "hdy-window-handle.h" + +#undef _HANDY_INSIDE + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-action-row.c b/subprojects/libhandy/src/hdy-action-row.c new file mode 100644 index 0000000..95108ae --- /dev/null +++ b/subprojects/libhandy/src/hdy-action-row.c @@ -0,0 +1,774 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include "hdy-action-row.h" + +#include <glib/gi18n-lib.h> + +/** + * SECTION:hdy-action-row + * @short_description: A #GtkListBox row used to present actions. + * @Title: HdyActionRow + * + * The #HdyActionRow widget can have a title, a subtitle and an icon. The row + * can receive additional widgets at its end, or prefix widgets at its start. + * + * It is convenient to present a preference and its related actions. + * + * #HdyActionRow is unactivatable by default, giving it an activatable widget + * will automatically make it activatable, but unsetting it won't change the + * row's activatability. + * + * # HdyActionRow as GtkBuildable + * + * The GtkWindow implementation of the GtkBuildable interface supports setting a + * child at its end by omitting the “type” attribute of a <child> element. + * + * It also supports setting a child as a prefix widget by specifying “prefix” as + * the “type” attribute of a <child> element. + * + * # CSS nodes + * + * #HdyActionRow has a main CSS node with name row. + * + * It contains the subnode box.header for its main horizontal box, and box.title + * for the vertical box containing the title and subtitle labels. + * + * It contains subnodes label.title and label.subtitle representing respectively + * the title label and subtitle label. + * + * Since: 0.0.6 + */ + +typedef struct +{ + GtkBox *header; + GtkImage *image; + GtkBox *prefixes; + GtkLabel *subtitle; + GtkBox *suffixes; + GtkLabel *title; + GtkBox *title_box; + + GtkWidget *previous_parent; + + gboolean use_underline; + GtkWidget *activatable_widget; +} HdyActionRowPrivate; + +static void hdy_action_row_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyActionRow, hdy_action_row, HDY_TYPE_PREFERENCES_ROW, + G_ADD_PRIVATE (HdyActionRow) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + hdy_action_row_buildable_init)) + +static GtkBuildableIface *parent_buildable_iface; + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_ACTIVATABLE_WIDGET, + PROP_SUBTITLE, + PROP_USE_UNDERLINE, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_ACTIVATED, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +static void +row_activated_cb (HdyActionRow *self, + GtkListBoxRow *row) +{ + /* No need to use GTK_LIST_BOX_ROW() for a pointer comparison. */ + if ((GtkListBoxRow *) self == row) + hdy_action_row_activate (self); +} + +static void +parent_cb (HdyActionRow *self) +{ + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (self)); + + if (priv->previous_parent != NULL) { + g_signal_handlers_disconnect_by_func (priv->previous_parent, G_CALLBACK (row_activated_cb), self); + priv->previous_parent = NULL; + } + + if (parent == NULL || !GTK_IS_LIST_BOX (parent)) + return; + + priv->previous_parent = parent; + g_signal_connect_swapped (parent, "row-activated", G_CALLBACK (row_activated_cb), self); +} + +static void +update_subtitle_visibility (HdyActionRow *self) +{ + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + gtk_widget_set_visible (GTK_WIDGET (priv->subtitle), + gtk_label_get_text (priv->subtitle) != NULL && + g_strcmp0 (gtk_label_get_text (priv->subtitle), "") != 0); +} + +static void +hdy_action_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyActionRow *self = HDY_ACTION_ROW (object); + + switch (prop_id) { + case PROP_ICON_NAME: + g_value_set_string (value, hdy_action_row_get_icon_name (self)); + break; + case PROP_ACTIVATABLE_WIDGET: + g_value_set_object (value, (GObject *) hdy_action_row_get_activatable_widget (self)); + break; + case PROP_SUBTITLE: + g_value_set_string (value, hdy_action_row_get_subtitle (self)); + break; + case PROP_USE_UNDERLINE: + g_value_set_boolean (value, hdy_action_row_get_use_underline (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_action_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyActionRow *self = HDY_ACTION_ROW (object); + + switch (prop_id) { + case PROP_ICON_NAME: + hdy_action_row_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_ACTIVATABLE_WIDGET: + hdy_action_row_set_activatable_widget (self, (GtkWidget*) g_value_get_object (value)); + break; + case PROP_SUBTITLE: + hdy_action_row_set_subtitle (self, g_value_get_string (value)); + break; + case PROP_USE_UNDERLINE: + hdy_action_row_set_use_underline (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_action_row_dispose (GObject *object) +{ + HdyActionRow *self = HDY_ACTION_ROW (object); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + if (priv->previous_parent != NULL) { + g_signal_handlers_disconnect_by_func (priv->previous_parent, G_CALLBACK (row_activated_cb), self); + priv->previous_parent = NULL; + } + + G_OBJECT_CLASS (hdy_action_row_parent_class)->dispose (object); +} + +static void +hdy_action_row_show_all (GtkWidget *widget) +{ + HdyActionRow *self = HDY_ACTION_ROW (widget); + HdyActionRowPrivate *priv; + + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + + priv = hdy_action_row_get_instance_private (self); + + gtk_container_foreach (GTK_CONTAINER (priv->prefixes), + (GtkCallback) gtk_widget_show_all, + NULL); + + gtk_container_foreach (GTK_CONTAINER (priv->suffixes), + (GtkCallback) gtk_widget_show_all, + NULL); + + GTK_WIDGET_CLASS (hdy_action_row_parent_class)->show_all (widget); +} + +static void +hdy_action_row_destroy (GtkWidget *widget) +{ + HdyActionRow *self = HDY_ACTION_ROW (widget); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + if (priv->header) { + gtk_widget_destroy (GTK_WIDGET (priv->header)); + priv->header = NULL; + } + + hdy_action_row_set_activatable_widget (self, NULL); + + priv->prefixes = NULL; + priv->suffixes = NULL; + + GTK_WIDGET_CLASS (hdy_action_row_parent_class)->destroy (widget); +} + +static void +hdy_action_row_add (GtkContainer *container, + GtkWidget *child) +{ + HdyActionRow *self = HDY_ACTION_ROW (container); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + /* When constructing the widget, we want the box to be added as the child of + * the GtkListBoxRow, as an implementation detail. + */ + if (priv->header == NULL) + GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->add (container, child); + else { + gtk_container_add (GTK_CONTAINER (priv->suffixes), child); + gtk_widget_show (GTK_WIDGET (priv->suffixes)); + } +} + +static void +hdy_action_row_remove (GtkContainer *container, + GtkWidget *child) +{ + HdyActionRow *self = HDY_ACTION_ROW (container); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + if (child == GTK_WIDGET (priv->header)) + GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->remove (container, child); + else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->prefixes)) + gtk_container_remove (GTK_CONTAINER (priv->prefixes), child); + else + gtk_container_remove (GTK_CONTAINER (priv->suffixes), child); +} + +typedef struct { + HdyActionRow *row; + GtkCallback callback; + gpointer callback_data; +} ForallData; + +static void +for_non_internal_child (GtkWidget *widget, + gpointer callback_data) +{ + ForallData *data = callback_data; + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (data->row); + + if (widget != (GtkWidget *) priv->image && + widget != (GtkWidget *) priv->prefixes && + widget != (GtkWidget *) priv->suffixes && + widget != (GtkWidget *) priv->title_box) + data->callback (widget, data->callback_data); +} + +static void +hdy_action_row_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyActionRow *self = HDY_ACTION_ROW (container); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + ForallData data; + + if (include_internals) { + GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data); + + return; + } + + data.row = self; + data.callback = callback; + data.callback_data = callback_data; + + if (priv->prefixes) + GTK_CONTAINER_GET_CLASS (priv->prefixes)->forall (GTK_CONTAINER (priv->prefixes), include_internals, for_non_internal_child, &data); + if (priv->suffixes) + GTK_CONTAINER_GET_CLASS (priv->suffixes)->forall (GTK_CONTAINER (priv->suffixes), include_internals, for_non_internal_child, &data); + if (priv->header) + GTK_CONTAINER_GET_CLASS (priv->header)->forall (GTK_CONTAINER (priv->header), include_internals, for_non_internal_child, &data); +} + +static void +hdy_action_row_activate_real (HdyActionRow *self) +{ + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + if (priv->activatable_widget) + gtk_widget_mnemonic_activate (priv->activatable_widget, FALSE); + + g_signal_emit (self, signals[SIGNAL_ACTIVATED], 0); +} + +static void +hdy_action_row_class_init (HdyActionRowClass *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 = hdy_action_row_get_property; + object_class->set_property = hdy_action_row_set_property; + object_class->dispose = hdy_action_row_dispose; + + widget_class->destroy = hdy_action_row_destroy; + widget_class->show_all = hdy_action_row_show_all; + + container_class->add = hdy_action_row_add; + container_class->remove = hdy_action_row_remove; + container_class->forall = hdy_action_row_forall; + + klass->activate = hdy_action_row_activate_real; + + /** + * HdyActionRow:icon-name: + * + * The icon name for this row. + * + * Since: 0.0.6 + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + _("Icon name"), + _("Icon name"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyActionRow:activatable-widget: + * + * The activatable widget for this row. + * + * Since: 0.0.7 + */ + props[PROP_ACTIVATABLE_WIDGET] = + g_param_spec_object ("activatable-widget", + _("Activatable widget"), + _("The widget to be activated when the row is activated"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE); + + /** + * HdyActionRow:subtitle: + * + * The subtitle for this row. + * + * Since: 0.0.6 + */ + props[PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + _("Subtitle"), + _("Subtitle"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyActionRow:use-underline: + * + * Whether an embedded underline in the text of the title and subtitle labels + * indicates a mnemonic. + * + * Since: 0.0.6 + */ + props[PROP_USE_UNDERLINE] = + g_param_spec_boolean ("use-underline", + _("Use underline"), + _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * HdyActionRow::activated: + * @self: The #HdyActionRow instance + * + * This signal is emitted after the row has been activated. + * + * Since: 1.0 + */ + signals[SIGNAL_ACTIVATED] = + g_signal_new ("activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-action-row.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, header); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, image); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, prefixes); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, subtitle); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, suffixes); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, title); + gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, title_box); +} + +static gboolean +string_is_not_empty (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + const gchar *string = g_value_get_string (from_value); + + g_value_set_boolean (to_value, string != NULL && g_strcmp0 (string, "") != 0); + + return TRUE; +} + +static void +hdy_action_row_init (HdyActionRow *self) +{ + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_bind_property_full (self, "title", priv->title, "visible", G_BINDING_SYNC_CREATE, + string_is_not_empty, NULL, NULL, NULL); + + update_subtitle_visibility (self); + + g_signal_connect (self, "notify::parent", G_CALLBACK (parent_cb), NULL); + +} + +static void +hdy_action_row_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + HdyActionRow *self = HDY_ACTION_ROW (buildable); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + if (priv->header == NULL || !type) + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child)); + else if (type && strcmp (type, "prefix") == 0) + hdy_action_row_add_prefix (self, GTK_WIDGET (child)); + else + GTK_BUILDER_WARN_INVALID_CHILD_TYPE (self, type); +} + +static void +hdy_action_row_buildable_init (GtkBuildableIface *iface) +{ + parent_buildable_iface = g_type_interface_peek_parent (iface); + iface->add_child = hdy_action_row_buildable_add_child; +} + +/** + * hdy_action_row_new: + * + * Creates a new #HdyActionRow. + * + * Returns: a new #HdyActionRow + * + * Since: 0.0.6 + */ +GtkWidget * +hdy_action_row_new (void) +{ + return g_object_new (HDY_TYPE_ACTION_ROW, NULL); +} + +/** + * hdy_action_row_get_subtitle: + * @self: a #HdyActionRow + * + * Gets the subtitle for @self. + * + * Returns: (transfer none) (nullable): the subtitle for @self, or %NULL. + * + * Since: 0.0.6 + */ +const gchar * +hdy_action_row_get_subtitle (HdyActionRow *self) +{ + HdyActionRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL); + + priv = hdy_action_row_get_instance_private (self); + + return gtk_label_get_text (priv->subtitle); +} + +/** + * hdy_action_row_set_subtitle: + * @self: a #HdyActionRow + * @subtitle: (nullable): the subtitle + * + * Sets the subtitle for @self. + * + * Since: 0.0.6 + */ +void +hdy_action_row_set_subtitle (HdyActionRow *self, + const gchar *subtitle) +{ + HdyActionRowPrivate *priv; + + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + + priv = hdy_action_row_get_instance_private (self); + + if (g_strcmp0 (gtk_label_get_text (priv->subtitle), subtitle) == 0) + return; + + gtk_label_set_text (priv->subtitle, subtitle); + gtk_widget_set_visible (GTK_WIDGET (priv->subtitle), + subtitle != NULL && g_strcmp0 (subtitle, "") != 0); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]); +} + +/** + * hdy_action_row_get_icon_name: + * @self: a #HdyActionRow + * + * Gets the icon name for @self. + * + * Returns: the icon name for @self. + * + * Since: 0.0.6 + */ +const gchar * +hdy_action_row_get_icon_name (HdyActionRow *self) +{ + HdyActionRowPrivate *priv; + const gchar *icon_name; + + g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL); + + priv = hdy_action_row_get_instance_private (self); + + gtk_image_get_icon_name (priv->image, &icon_name, NULL); + + return icon_name; +} + +/** + * hdy_action_row_set_icon_name: + * @self: a #HdyActionRow + * @icon_name: the icon name + * + * Sets the icon name for @self. + * + * Since: 0.0.6 + */ +void +hdy_action_row_set_icon_name (HdyActionRow *self, + const gchar *icon_name) +{ + HdyActionRowPrivate *priv; + const gchar *old_icon_name; + + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + + priv = hdy_action_row_get_instance_private (self); + + gtk_image_get_icon_name (priv->image, &old_icon_name, NULL); + if (g_strcmp0 (old_icon_name, icon_name) == 0) + return; + + gtk_image_set_from_icon_name (priv->image, icon_name, GTK_ICON_SIZE_INVALID); + gtk_widget_set_visible (GTK_WIDGET (priv->image), + icon_name != NULL && g_strcmp0 (icon_name, "") != 0); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + +/** + * hdy_action_row_get_activatable_widget: + * @self: a #HdyActionRow + * + * Gets the widget activated when @self is activated. + * + * Returns: (nullable) (transfer none): the widget activated when @self is + * activated, or %NULL if none has been set. + * + * Since: 0.0.7 + */ +GtkWidget * +hdy_action_row_get_activatable_widget (HdyActionRow *self) +{ + HdyActionRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL); + + priv = hdy_action_row_get_instance_private (self); + + return priv->activatable_widget; +} + +static void +activatable_widget_weak_notify (gpointer data, + GObject *where_the_object_was) +{ + HdyActionRow *self = HDY_ACTION_ROW (data); + HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self); + + priv->activatable_widget = NULL; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVATABLE_WIDGET]); +} + +/** + * hdy_action_row_set_activatable_widget: + * @self: a #HdyActionRow + * @widget: (nullable): the target #GtkWidget, or %NULL to unset + * + * Sets the widget to activate when @self is activated, either by clicking + * on it, by calling hdy_action_row_activate(), or via mnemonics in the title or + * the subtitle. See the “use_underline” property to enable mnemonics. + * + * The target widget will be activated by emitting the + * GtkWidget::mnemonic-activate signal on it. + * + * Since: 0.0.7 + */ +void +hdy_action_row_set_activatable_widget (HdyActionRow *self, + GtkWidget *widget) +{ + HdyActionRowPrivate *priv; + + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget)); + + priv = hdy_action_row_get_instance_private (self); + + if (priv->activatable_widget == widget) + return; + + if (priv->activatable_widget) + g_object_weak_unref (G_OBJECT (priv->activatable_widget), + activatable_widget_weak_notify, + self); + + priv->activatable_widget = widget; + + if (priv->activatable_widget != NULL) { + g_object_weak_ref (G_OBJECT (priv->activatable_widget), + activatable_widget_weak_notify, + self); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), TRUE); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVATABLE_WIDGET]); +} + +/** + * hdy_action_row_get_use_underline: + * @self: a #HdyActionRow + * + * Gets whether an embedded underline in the text of the title and subtitle + * labels indicates a mnemonic. See hdy_action_row_set_use_underline(). + * + * Returns: %TRUE if an embedded underline in the title and subtitle labels + * indicates the mnemonic accelerator keys. + * + * Since: 0.0.6 + */ +gboolean +hdy_action_row_get_use_underline (HdyActionRow *self) +{ + HdyActionRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_ACTION_ROW (self), FALSE); + + priv = hdy_action_row_get_instance_private (self); + + return priv->use_underline; +} + +/** + * hdy_action_row_set_use_underline: + * @self: a #HdyActionRow + * @use_underline: %TRUE if underlines in the text indicate mnemonics + * + * If true, an underline in the text of the title and subtitle labels indicates + * the next character should be used for the mnemonic accelerator key. + * + * Since: 0.0.6 + */ +void +hdy_action_row_set_use_underline (HdyActionRow *self, + gboolean use_underline) +{ + HdyActionRowPrivate *priv; + + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + + priv = hdy_action_row_get_instance_private (self); + + if (priv->use_underline == !!use_underline) + return; + + priv->use_underline = !!use_underline; + hdy_preferences_row_set_use_underline (HDY_PREFERENCES_ROW (self), priv->use_underline); + gtk_label_set_use_underline (priv->title, priv->use_underline); + gtk_label_set_use_underline (priv->subtitle, priv->use_underline); + gtk_label_set_mnemonic_widget (priv->title, GTK_WIDGET (self)); + gtk_label_set_mnemonic_widget (priv->subtitle, GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_UNDERLINE]); +} + +/** + * hdy_action_row_add_prefix: + * @self: a #HdyActionRow + * @widget: the prefix widget + * + * Adds a prefix widget to @self. + * + * Since: 0.0.6 + */ +void +hdy_action_row_add_prefix (HdyActionRow *self, + GtkWidget *widget) +{ + HdyActionRowPrivate *priv; + + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + g_return_if_fail (GTK_IS_WIDGET (self)); + + priv = hdy_action_row_get_instance_private (self); + + gtk_box_pack_start (priv->prefixes, widget, FALSE, TRUE, 0); + gtk_widget_show (GTK_WIDGET (priv->prefixes)); +} + +void +hdy_action_row_activate (HdyActionRow *self) +{ + g_return_if_fail (HDY_IS_ACTION_ROW (self)); + + HDY_ACTION_ROW_GET_CLASS (self)->activate (self); +} diff --git a/subprojects/libhandy/src/hdy-action-row.h b/subprojects/libhandy/src/hdy-action-row.h new file mode 100644 index 0000000..7b5dd53 --- /dev/null +++ b/subprojects/libhandy/src/hdy-action-row.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include "hdy-preferences-row.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_ACTION_ROW (hdy_action_row_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyActionRow, hdy_action_row, HDY, ACTION_ROW, HdyPreferencesRow) + +/** + * HdyActionRowClass + * @parent_class: The parent class + * @activate: Activates the row to trigger its main action. + */ +struct _HdyActionRowClass +{ + GtkListBoxRowClass parent_class; + + void (*activate) (HdyActionRow *self); + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_action_row_new (void); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_action_row_get_subtitle (HdyActionRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_action_row_set_subtitle (HdyActionRow *self, + const gchar *subtitle); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_action_row_get_icon_name (HdyActionRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_action_row_set_icon_name (HdyActionRow *self, + const gchar *icon_name); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_action_row_get_activatable_widget (HdyActionRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_action_row_set_activatable_widget (HdyActionRow *self, + GtkWidget *widget); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_action_row_get_use_underline (HdyActionRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_action_row_set_use_underline (HdyActionRow *self, + gboolean use_underline); + +HDY_AVAILABLE_IN_ALL +void hdy_action_row_add_prefix (HdyActionRow *self, + GtkWidget *widget); + +HDY_AVAILABLE_IN_ALL +void hdy_action_row_activate (HdyActionRow *self); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-action-row.ui b/subprojects/libhandy/src/hdy-action-row.ui new file mode 100644 index 0000000..ff54c15 --- /dev/null +++ b/subprojects/libhandy/src/hdy-action-row.ui @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="HdyActionRow" parent="HdyPreferencesRow"> + <property name="activatable">False</property> + <child> + <object class="GtkBox" id="header"> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <property name="valign">center</property> + <property name="visible">True</property> + <style> + <class name="header"/> + </style> + <child> + <object class="GtkBox" id="prefixes"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="spacing">12</property> + <property name="visible">False</property> + </object> + </child> + <child> + <object class="GtkImage" id="image"> + <property name="no_show_all">True</property> + <property name="pixel_size">32</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkBox" id="title_box"> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="no_show_all">True</property> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <property name="visible">True</property> + <style> + <class name="title"/> + </style> + <child> + <object class="GtkLabel" id="title"> + <property name="can_focus">False</property> + <property name="ellipsize">end</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="label" bind-source="HdyActionRow" bind-property="title" bind-flags="sync-create"/> + <property name="visible">True</property> + <property name="xalign">0</property> + <style> + <class name="title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="subtitle"> + <property name="can_focus">False</property> + <property name="ellipsize">end</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="xalign">0</property> + <style> + <class name="subtitle"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="suffixes"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="spacing">12</property> + <property name="visible">False</property> + </object> + <packing> + <property name="pack-type">end</property> + </packing> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-animation-private.h b/subprojects/libhandy/src/hdy-animation-private.h new file mode 100644 index 0000000..f31002a --- /dev/null +++ b/subprojects/libhandy/src/hdy-animation-private.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-animation.h" + +G_BEGIN_DECLS + +gdouble hdy_lerp (gdouble a, gdouble b, gdouble t); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-animation.c b/subprojects/libhandy/src/hdy-animation.c new file mode 100644 index 0000000..ce5bf64 --- /dev/null +++ b/subprojects/libhandy/src/hdy-animation.c @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-animation-private.h" + +/** + * SECTION:hdy-animation + * @short_description: Animation helpers + * @title: Animation Helpers + * + * Animation helpers. + * + * Since: 0.0.11 + */ + +/** + * hdy_get_enable_animations: + * @widget: a #GtkWidget + * + * Returns whether animations are enabled for that widget. This should be used + * when implementing an animated widget to know whether to animate it or not. + * + * Returns: %TRUE if animations are enabled for @widget. + * + * Since: 0.0.11 + */ +gboolean +hdy_get_enable_animations (GtkWidget *widget) +{ + gboolean enable_animations = TRUE; + + g_assert (GTK_IS_WIDGET (widget)); + + g_object_get (gtk_widget_get_settings (widget), + "gtk-enable-animations", &enable_animations, + NULL); + + return enable_animations; +} + +/** + * hdy_lerp: (skip) + * @a: the start + * @b: the end + * @t: the interpolation rate + * + * Computes the linear interpolation between @a and @b for @t. + * + * Returns: the linear interpolation between @a and @b for @t. + * + * Since: 0.0.11 + */ +gdouble +hdy_lerp (gdouble a, gdouble b, gdouble t) +{ + return a * (1.0 - t) + b * t; +} + +/* From clutter-easing.c, based on Robert Penner's + * infamous easing equations, MIT license. + */ + +/** + * hdy_ease_out_cubic: + * @t: the term + * + * Computes the ease out for @t. + * + * Returns: the ease out for @t. + * + * Since: 0.0.11 + */ +gdouble +hdy_ease_out_cubic (gdouble t) +{ + gdouble p = t - 1; + return p * p * p + 1; +} diff --git a/subprojects/libhandy/src/hdy-animation.h b/subprojects/libhandy/src/hdy-animation.h new file mode 100644 index 0000000..5af34c0 --- /dev/null +++ b/subprojects/libhandy/src/hdy-animation.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +HDY_AVAILABLE_IN_ALL +gboolean hdy_get_enable_animations (GtkWidget *widget); + +HDY_AVAILABLE_IN_ALL +gdouble hdy_ease_out_cubic (gdouble t); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-application-window.c b/subprojects/libhandy/src/hdy-application-window.c new file mode 100644 index 0000000..d3979cf --- /dev/null +++ b/subprojects/libhandy/src/hdy-application-window.c @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-application-window.h" +#include "hdy-window-mixin-private.h" + +/** + * SECTION:hdy-application-window + * @short_description: A freeform application window. + * @title: HdyApplicationWindow + * @See_also: #HdyHeaderBar, #HdyWindow, #HdyWindowHandle + * + * HdyApplicationWindow is a #GtkApplicationWindow subclass providing the same + * features as #HdyWindow. + * + * See #HdyWindow for details. + * + * Using gtk_application_set_app_menu() and gtk_application_set_menubar() is + * not supported and may result in visual glitches. + * + * Since: 1.0 + */ + +typedef struct +{ + HdyWindowMixin *mixin; +} HdyApplicationWindowPrivate; + +static void hdy_application_window_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyApplicationWindow, hdy_application_window, GTK_TYPE_APPLICATION_WINDOW, + G_ADD_PRIVATE (HdyApplicationWindow) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, hdy_application_window_buildable_init)) + +#define HDY_GET_WINDOW_MIXIN(obj) (((HdyApplicationWindowPrivate *) hdy_application_window_get_instance_private (HDY_APPLICATION_WINDOW (obj)))->mixin) + +static void +hdy_application_window_add (GtkContainer *container, + GtkWidget *widget) +{ + hdy_window_mixin_add (HDY_GET_WINDOW_MIXIN (container), widget); +} + +static void +hdy_application_window_remove (GtkContainer *container, + GtkWidget *widget) +{ + hdy_window_mixin_remove (HDY_GET_WINDOW_MIXIN (container), widget); +} + +static void +hdy_application_window_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + hdy_window_mixin_forall (HDY_GET_WINDOW_MIXIN (container), + include_internals, + callback, + callback_data); +} + +static gboolean +hdy_application_window_draw (GtkWidget *widget, + cairo_t *cr) +{ + return hdy_window_mixin_draw (HDY_GET_WINDOW_MIXIN (widget), cr); +} + +static void +hdy_application_window_destroy (GtkWidget *widget) +{ + hdy_window_mixin_destroy (HDY_GET_WINDOW_MIXIN (widget)); +} + +static void +hdy_application_window_finalize (GObject *object) +{ + HdyApplicationWindow *self = (HdyApplicationWindow *)object; + HdyApplicationWindowPrivate *priv = hdy_application_window_get_instance_private (self); + + g_clear_object (&priv->mixin); + + G_OBJECT_CLASS (hdy_application_window_parent_class)->finalize (object); +} + +static void +hdy_application_window_class_init (HdyApplicationWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = hdy_application_window_finalize; + widget_class->draw = hdy_application_window_draw; + widget_class->destroy = hdy_application_window_destroy; + container_class->add = hdy_application_window_add; + container_class->remove = hdy_application_window_remove; + container_class->forall = hdy_application_window_forall; +} + +static void +hdy_application_window_init (HdyApplicationWindow *self) +{ + HdyApplicationWindowPrivate *priv = hdy_application_window_get_instance_private (self); + + priv->mixin = hdy_window_mixin_new (GTK_WINDOW (self), + GTK_WINDOW_CLASS (hdy_application_window_parent_class)); + + gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (self), FALSE); +} + +static void +hdy_application_window_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + hdy_window_mixin_buildable_add_child (HDY_GET_WINDOW_MIXIN (buildable), + builder, + child, + type); +} + +static void +hdy_application_window_buildable_init (GtkBuildableIface *iface) +{ + iface->add_child = hdy_application_window_buildable_add_child; +} + +/** + * hdy_application_window_new: + * + * Creates a new #HdyApplicationWindow. + * + * Returns: (transfer full): a newly created #HdyApplicationWindow + * + * Since: 1.0 + */ +GtkWidget * +hdy_application_window_new (void) +{ + return g_object_new (HDY_TYPE_APPLICATION_WINDOW, + NULL); +} diff --git a/subprojects/libhandy/src/hdy-application-window.h b/subprojects/libhandy/src/hdy-application-window.h new file mode 100644 index 0000000..ed01eb1 --- /dev/null +++ b/subprojects/libhandy/src/hdy-application-window.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_APPLICATION_WINDOW (hdy_application_window_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyApplicationWindow, hdy_application_window, HDY, APPLICATION_WINDOW, GtkApplicationWindow) + +struct _HdyApplicationWindowClass +{ + GtkApplicationWindowClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_application_window_new (void); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-avatar.c b/subprojects/libhandy/src/hdy-avatar.c new file mode 100644 index 0000000..9dcdcdf --- /dev/null +++ b/subprojects/libhandy/src/hdy-avatar.c @@ -0,0 +1,811 @@ +/* + * Copyright (C) 2020 Purism SPC + * Copyright (C) 2020 Felipe Borges + * + * Authors: + * Felipe Borges <felipeborges@gnome.org> + * Julian Sparber <julian@sparber.net> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + */ + +#include "config.h" +#include <math.h> + +#include "hdy-avatar.h" +#include "hdy-cairo-private.h" + +#define NUMBER_OF_COLORS 14 +/** + * SECTION:hdy-avatar + * @short_description: A widget displaying an image, with a generated fallback. + * @Title: HdyAvatar + * + * #HdyAvatar is a widget to display a round avatar. + * A provided image is made round before displaying, if no image is given this + * widget generates a round fallback with the initials of the #HdyAvatar:text + * on top of a colord background. + * The color is picked based on the hash of the #HdyAvatar:text. + * If #HdyAvatar:show-initials is set to %FALSE, `avatar-default-symbolic` is + * shown in place of the initials. + * Use hdy_avatar_set_image_load_func () to set a custom image. + * Create a #HdyAvatarImageLoadFunc similar to this example: + * + * |[<!-- language="C" --> + * static GdkPixbuf * + * image_load_func (gint size, gpointer user_data) + * { + * g_autoptr (GError) error = NULL; + * g_autoptr (GdkPixbuf) pixbuf = NULL; + * g_autofree gchar *file = gtk_file_chooser_get_filename ("avatar.png"); + * gint width, height; + * + * gdk_pixbuf_get_file_info (file, &width, &height); + * + * pixbuf = gdk_pixbuf_new_from_file_at_scale (file, + * (width <= height) ? size : -1, + * (width >= height) ? size : -1, + * TRUE, + * error); + * if (error != NULL) { + * g_critical ("Failed to create pixbuf from file: %s", error->message); + * + * return NULL; + * } + * + * return pixbuf; + * } + * ]| + * + * # CSS nodes + * + * #HdyAvatar has a single CSS node with name avatar. + * + */ + +struct _HdyAvatar +{ + GtkDrawingArea parent_instance; + + gchar *icon_name; + gchar *text; + PangoLayout *layout; + gboolean show_initials; + guint color_class; + gint size; + cairo_surface_t *round_image; + + HdyAvatarImageLoadFunc load_image_func; + gpointer load_image_func_target; + GDestroyNotify load_image_func_target_destroy_notify; +}; + +G_DEFINE_TYPE (HdyAvatar, hdy_avatar, GTK_TYPE_DRAWING_AREA); + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_TEXT, + PROP_SHOW_INITIALS, + PROP_SIZE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +static cairo_surface_t * +round_image (GdkPixbuf *pixbuf, + gdouble size) +{ + g_autoptr (cairo_surface_t) surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + g_autoptr (cairo_t) cr = cairo_create (surface); + + /* Clip a circle */ + cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0, 0, 2 * G_PI); + cairo_clip (cr); + cairo_new_path (cr); + + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_paint (cr); + + return g_steal_pointer (&surface); +} + +static gchar * +extract_initials_from_text (const gchar *text) +{ + GString *initials; + g_autofree gchar *p = g_utf8_strup (text, -1); + g_autofree gchar *normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE); + gunichar unichar; + gchar *q = NULL; + + if (normalized == NULL) + return NULL; + + initials = g_string_new (""); + + unichar = g_utf8_get_char (normalized); + g_string_append_unichar (initials, unichar); + + q = g_utf8_strrchr (normalized, -1, ' '); + if (q != NULL && g_utf8_next_char (q) != NULL) { + q = g_utf8_next_char (q); + + unichar = g_utf8_get_char (q); + g_string_append_unichar (initials, unichar); + } + + return g_string_free (initials, FALSE); +} + +static void +update_custom_image (HdyAvatar *self) +{ + g_autoptr (GdkPixbuf) pixbuf = NULL; + gint scale_factor; + gint size; + gboolean was_custom = FALSE; + + if (self->round_image != NULL) { + g_clear_pointer (&self->round_image, cairo_surface_destroy); + was_custom = TRUE; + } + + if (self->load_image_func != NULL) { + scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self)); + size = MIN (gtk_widget_get_allocated_width (GTK_WIDGET (self)), + gtk_widget_get_allocated_height (GTK_WIDGET (self))); + pixbuf = self->load_image_func (size * scale_factor, self->load_image_func_target); + if (pixbuf != NULL) { + self->round_image = round_image (pixbuf, (gdouble) size * scale_factor); + cairo_surface_set_device_scale (self->round_image, scale_factor, scale_factor); + } + } + + if (was_custom || self->round_image != NULL) + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + +static void +set_class_color (HdyAvatar *self) +{ + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self)); + g_autofree GRand *rand = NULL; + g_autofree gchar *new_class = NULL; + g_autofree gchar *old_class = g_strdup_printf ("color%d", self->color_class); + + gtk_style_context_remove_class (context, old_class); + + if (self->text == NULL || strlen (self->text) == 0) { + /* Use a random color if we don't have a text */ + rand = g_rand_new (); + self->color_class = g_rand_int_range (rand, 1, NUMBER_OF_COLORS); + } else { + self->color_class = (g_str_hash (self->text) % NUMBER_OF_COLORS) + 1; + } + + new_class = g_strdup_printf ("color%d", self->color_class); + gtk_style_context_add_class (context, new_class); +} + +static void +set_class_contrasted (HdyAvatar *self, gint size) +{ + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + if (size < 25) + gtk_style_context_add_class (context, "contrasted"); + else + gtk_style_context_remove_class (context, "contrasted"); +} + +static void +clear_pango_layout (HdyAvatar *self) +{ + g_clear_object (&self->layout); +} + +static void +ensure_pango_layout (HdyAvatar *self) +{ + g_autofree gchar *initials = NULL; + + if (self->layout != NULL || self->text == NULL || strlen (self->text) == 0) + return; + + initials = extract_initials_from_text (self->text); + self->layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), initials); +} + +static void +set_font_size (HdyAvatar *self, + gint size) +{ + GtkStyleContext *context; + PangoFontDescription *font_desc; + gint width, height; + gdouble padding; + gdouble sqr_size; + gdouble max_size; + gdouble new_font_size; + + if (self->round_image != NULL || self->layout == NULL) + return; + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + gtk_style_context_get (context, gtk_style_context_get_state (context), + "font", &font_desc, NULL); + + pango_layout_set_font_description (self->layout, font_desc); + pango_layout_get_pixel_size (self->layout, &width, &height); + + /* This is the size of the biggest square fitting inside the circle */ + sqr_size = (gdouble)size / 1.4142; + /* The padding has to be a function of the overall size. + * The 0.4 is how steep the linear function grows and the -5 is just + * an adjustment for smaller sizes which doesn't have a big impact on bigger sizes. + * Make also sure we don't have a negative padding */ + padding = MAX (size * 0.4 - 5, 0); + max_size = sqr_size - padding; + new_font_size = (gdouble)height * (max_size / (gdouble)width); + + font_desc = pango_font_description_copy (font_desc); + pango_font_description_set_absolute_size (font_desc, + CLAMP (new_font_size, 0, max_size) * PANGO_SCALE); + pango_layout_set_font_description (self->layout, font_desc); + pango_font_description_free (font_desc); +} + +static void +hdy_avatar_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + HdyAvatar *self = HDY_AVATAR (object); + + switch (property_id) { + case PROP_ICON_NAME: + g_value_set_string (value, hdy_avatar_get_icon_name (self)); + break; + + case PROP_TEXT: + g_value_set_string (value, hdy_avatar_get_text (self)); + break; + + case PROP_SHOW_INITIALS: + g_value_set_boolean (value, hdy_avatar_get_show_initials (self)); + break; + + case PROP_SIZE: + g_value_set_int (value, hdy_avatar_get_size (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +hdy_avatar_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyAvatar *self = HDY_AVATAR (object); + + switch (property_id) { + case PROP_ICON_NAME: + hdy_avatar_set_icon_name (self, g_value_get_string (value)); + break; + + case PROP_TEXT: + hdy_avatar_set_text (self, g_value_get_string (value)); + break; + + case PROP_SHOW_INITIALS: + hdy_avatar_set_show_initials (self, g_value_get_boolean (value)); + break; + + case PROP_SIZE: + hdy_avatar_set_size (self, g_value_get_int (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +hdy_avatar_finalize (GObject *object) +{ + HdyAvatar *self = HDY_AVATAR (object); + + g_clear_pointer (&self->icon_name, g_free); + g_clear_pointer (&self->text, g_free); + g_clear_pointer (&self->round_image, cairo_surface_destroy); + g_clear_object (&self->layout); + + if (self->load_image_func_target_destroy_notify != NULL) + self->load_image_func_target_destroy_notify (self->load_image_func_target); + + G_OBJECT_CLASS (hdy_avatar_parent_class)->finalize (object); +} + +static gboolean +hdy_avatar_draw (GtkWidget *widget, + cairo_t *cr) +{ + HdyAvatar *self = HDY_AVATAR (widget); + GtkStyleContext *context = gtk_widget_get_style_context (widget); + gint width = gtk_widget_get_allocated_width (widget); + gint height = gtk_widget_get_allocated_height (widget); + gint size = MIN (width, height); + gdouble x = (gdouble)(width - size) / 2.0; + gdouble y = (gdouble)(height - size) / 2.0; + const gchar *icon_name; + gint scale; + GdkRGBA color; + g_autoptr (GtkIconInfo) icon = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (cairo_surface_t) surface = NULL; + + set_class_contrasted (HDY_AVATAR (widget), size); + + gtk_render_frame (context, cr, x, y, size, size); + + if (self->round_image) { + cairo_set_source_surface (cr, self->round_image, x, y); + cairo_paint (cr); + + return FALSE; + } + + gtk_render_background (context, cr, x, y, size, size); + ensure_pango_layout (HDY_AVATAR (widget)); + + if (self->show_initials && self->layout != NULL) { + set_font_size (HDY_AVATAR (widget), size); + pango_layout_get_pixel_size (self->layout, &width, &height); + + gtk_render_layout (context, cr, + ((gdouble)(size - width) / 2.0) + x, + ((gdouble)(size - height) / 2.0) + y, + self->layout); + + return FALSE; + } + + icon_name = self->icon_name && *self->icon_name != '\0' ? + self->icon_name : "avatar-default-symbolic"; + scale = gtk_widget_get_scale_factor (widget); + icon = gtk_icon_theme_lookup_icon_for_scale (gtk_icon_theme_get_default (), + icon_name, + size / 2, scale, + GTK_ICON_LOOKUP_FORCE_SYMBOLIC); + if (icon == NULL) { + g_critical ("Failed to load icon `%s'", icon_name); + + return FALSE; + } + + gtk_style_context_get_color (context, gtk_style_context_get_state (context), &color); + pixbuf = gtk_icon_info_load_symbolic (icon, &color, NULL, NULL, NULL, NULL, &error); + if (error != NULL) { + g_critical ("Failed to load icon `%s': %s", icon_name, error->message); + + return FALSE; + } + + surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, + gtk_widget_get_window (widget)); + + width = cairo_image_surface_get_width (surface); + height = cairo_image_surface_get_height (surface); + gtk_render_icon_surface (context, cr, surface, + (((gdouble)size - ((gdouble)width / (gdouble)scale)) / 2.0) + x, + (((gdouble)size - ((gdouble)height / (gdouble)scale)) / 2.0) + y); + + return FALSE; +} + +/* This private method is prefixed by the class name because it will be a + * virtual method in GTK 4. + */ +static void +hdy_avatar_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + HdyAvatar *self = HDY_AVATAR (widget); + + if (minimum) + *minimum = self->size; + if (natural) + *natural = self->size; +} + +static void +hdy_avatar_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_avatar_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum, natural, NULL, NULL); +} + +static void +hdy_avatar_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_avatar_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, NULL, NULL); +} + +static GtkSizeRequestMode +hdy_avatar_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +static void +hdy_avatar_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkAllocation clip; + + gtk_render_background_get_clip (gtk_widget_get_style_context (widget), + allocation->x, + allocation->y, + allocation->width, + allocation->height, + &clip); + + GTK_WIDGET_CLASS (hdy_avatar_parent_class)->size_allocate (widget, allocation); + gtk_widget_set_clip (widget, &clip); +} + +static void +hdy_avatar_class_init (HdyAvatarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = hdy_avatar_finalize; + + object_class->set_property = hdy_avatar_set_property; + object_class->get_property = hdy_avatar_get_property; + + widget_class->draw = hdy_avatar_draw; + widget_class->get_request_mode = hdy_avatar_get_request_mode; + widget_class->get_preferred_width = hdy_avatar_get_preferred_width; + widget_class->get_preferred_height = hdy_avatar_get_preferred_height; + widget_class->get_preferred_width_for_height = hdy_avatar_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = hdy_avatar_get_preferred_height_for_width; + widget_class->size_allocate = hdy_avatar_size_allocate; + + /** + * HdyAvatar:size: + * + * The avatar size of the avatar. + */ + props[PROP_SIZE] = + g_param_spec_int ("size", + "Size", + "The size of the avatar", + -1, INT_MAX, -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyAvatar:icon-name: + * + * The name of the icon in the icon theme to use when the icon should be + * displayed. + * If no name is set, the avatar-default-symbolic icon will be used. + * If the name doesn't match a valid icon, it is an error and no icon will be + * displayed. + * If the icon theme is changed, the image will be updated automatically. + * + * Since: 1.0 + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon name", + "The name of the icon from the icon theme", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyAvatar:text: + * + * The text used for the initials and for generating the color. + * If #HdyAvatar:show-initials is %FALSE it's only used to generate the color. + */ + props[PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "The text used to generate the color and the initials", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyAvatar:show_initials: + * + * Whether to show the initials or the fallback icon on the generated avatar. + */ + props[PROP_SHOW_INITIALS] = + g_param_spec_boolean ("show-initials", + "Show initials", + "Whether to show the initials", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "avatar"); +} + +static void +hdy_avatar_init (HdyAvatar *self) +{ + set_class_color (self); + g_signal_connect (self, "notify::scale-factor", G_CALLBACK (update_custom_image), NULL); + g_signal_connect (self, "size-allocate", G_CALLBACK (update_custom_image), NULL); + g_signal_connect (self, "screen-changed", G_CALLBACK (clear_pango_layout), NULL); +} + +/** + * hdy_avatar_new: + * @size: The size of the avatar + * @text: (nullable): The text used to generate the color and initials if + * @show_initials is %TRUE. The color is selected at random if @text is empty. + * @show_initials: whether to show the initials or the fallback icon on + * top of the color generated based on @text. + * + * Creates a new #HdyAvatar. + * + * Returns: the newly created #HdyAvatar + */ +GtkWidget * +hdy_avatar_new (gint size, + const gchar *text, + gboolean show_initials) +{ + return g_object_new (HDY_TYPE_AVATAR, + "size", size, + "text", text, + "show-initials", show_initials, + NULL); +} + +/** + * hdy_avatar_get_icon_name: + * @self: a #HdyAvatar + * + * Gets the name of the icon in the icon theme to use when the icon should be + * displayed. + * + * Returns: (nullable) (transfer none): the name of the icon from the icon theme. + * + * Since: 1.0 + */ +const gchar * +hdy_avatar_get_icon_name (HdyAvatar *self) +{ + g_return_val_if_fail (HDY_IS_AVATAR (self), NULL); + + return self->icon_name; +} + +/** + * hdy_avatar_set_icon_name: + * @self: a #HdyAvatar + * @icon_name: (nullable): the name of the icon from the icon theme + * + * Sets the name of the icon in the icon theme to use when the icon should be + * displayed. + * If no name is set, the avatar-default-symbolic icon will be used. + * If the name doesn't match a valid icon, it is an error and no icon will be + * displayed. + * If the icon theme is changed, the image will be updated automatically. + * + * Since: 1.0 + */ +void +hdy_avatar_set_icon_name (HdyAvatar *self, + const gchar *icon_name) +{ + g_return_if_fail (HDY_IS_AVATAR (self)); + + if (g_strcmp0 (self->icon_name, icon_name) == 0) + return; + + g_clear_pointer (&self->icon_name, g_free); + self->icon_name = g_strdup (icon_name); + + if (!self->round_image && + (!self->show_initials || self->layout == NULL)) + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + +/** + * hdy_avatar_get_text: + * @self: a #HdyAvatar + * + * Get the text used to generate the fallback initials and color + * + * Returns: (nullable) (transfer none): returns the text used to generate + * the fallback initials. This is the internal string used by + * the #HdyAvatar, and must not be modified. + */ +const gchar * +hdy_avatar_get_text (HdyAvatar *self) +{ + g_return_val_if_fail (HDY_IS_AVATAR (self), NULL); + + return self->text; +} + +/** + * hdy_avatar_set_text: + * @self: a #HdyAvatar + * @text: (nullable): the text used to get the initials and color + * + * Set the text used to generate the fallback initials color + */ +void +hdy_avatar_set_text (HdyAvatar *self, + const gchar *text) +{ + g_return_if_fail (HDY_IS_AVATAR (self)); + + if (g_strcmp0 (self->text, text) == 0) + return; + + g_clear_pointer (&self->text, g_free); + self->text = g_strdup (text); + + clear_pango_layout (self); + set_class_color (self); + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TEXT]); +} + +/** + * hdy_avatar_get_show_initials: + * @self: a #HdyAvatar + * + * Returns whether initials are used for the fallback or the icon. + * + * Returns: %TRUE if the initials are used for the fallback. + */ +gboolean +hdy_avatar_get_show_initials (HdyAvatar *self) +{ + g_return_val_if_fail (HDY_IS_AVATAR (self), FALSE); + + return self->show_initials; +} + +/** + * hdy_avatar_set_show_initials: + * @self: a #HdyAvatar + * @show_initials: whether the initials should be shown on the fallback avatar + * or the icon. + * + * Sets whether the initials should be shown on the fallback avatar or the icon. + */ +void +hdy_avatar_set_show_initials (HdyAvatar *self, + gboolean show_initials) +{ + g_return_if_fail (HDY_IS_AVATAR (self)); + + if (self->show_initials == show_initials) + return; + + self->show_initials = show_initials; + + gtk_widget_queue_draw (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_INITIALS]); +} + +/** + * hdy_avatar_set_image_load_func: + * @self: a #HdyAvatar + * @load_image: (closure user_data) (nullable): callback to set a custom image + * @user_data: (nullable): user data passed to @load_image + * @destroy: (nullable): destroy notifier for @user_data + * + * A callback which is called when the custom image need to be reloaded for some + * reason (e.g. scale-factor changes). + */ +void +hdy_avatar_set_image_load_func (HdyAvatar *self, + HdyAvatarImageLoadFunc load_image, + gpointer user_data, + GDestroyNotify destroy) +{ + g_return_if_fail (HDY_IS_AVATAR (self)); + g_return_if_fail (user_data != NULL || (user_data == NULL && destroy == NULL)); + + if (self->load_image_func_target_destroy_notify != NULL) + self->load_image_func_target_destroy_notify (self->load_image_func_target); + + self->load_image_func = load_image; + self->load_image_func_target = user_data; + self->load_image_func_target_destroy_notify = destroy; + + update_custom_image (self); +} + +/** + * hdy_avatar_get_size: + * @self: a #HdyAvatar + * + * Returns the size of the avatar. + * + * Returns: the size of the avatar. + */ +gint +hdy_avatar_get_size (HdyAvatar *self) +{ + g_return_val_if_fail (HDY_IS_AVATAR (self), 0); + + return self->size; +} + +/** + * hdy_avatar_set_size: + * @self: a #HdyAvatar + * @size: The size to be used for the avatar + * + * Sets the size of the avatar. + */ +void +hdy_avatar_set_size (HdyAvatar *self, + gint size) +{ + g_return_if_fail (HDY_IS_AVATAR (self)); + g_return_if_fail (size >= -1); + + if (self->size == size) + return; + + self->size = size; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SIZE]); +} diff --git a/subprojects/libhandy/src/hdy-avatar.h b/subprojects/libhandy/src/hdy-avatar.h new file mode 100644 index 0000000..54f3787 --- /dev/null +++ b/subprojects/libhandy/src/hdy-avatar.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_AVATAR (hdy_avatar_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyAvatar, hdy_avatar, HDY, AVATAR, GtkDrawingArea) + +/** + * HdyAvatarImageLoadFunc: + * @size: the required size of the avatar + * @user_data: (closure): user data + * + * The returned #GdkPixbuf is expected to be square with width and height set + * to @size. The image is cropped to a circle without any scaling or transformation. + * + * Returns: (nullable) (transfer full): the #GdkPixbuf to use as a custom avatar + * or %NULL to fallback to the generated avatar. + */ +typedef GdkPixbuf *(*HdyAvatarImageLoadFunc) (gint size, + gpointer user_data); + + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_avatar_new (gint size, + const gchar *text, + gboolean show_initials); +HDY_AVAILABLE_IN_ALL +const gchar *hdy_avatar_get_icon_name (HdyAvatar *self); +HDY_AVAILABLE_IN_ALL +void hdy_avatar_set_icon_name (HdyAvatar *self, + const gchar *icon_name); +HDY_AVAILABLE_IN_ALL +const gchar *hdy_avatar_get_text (HdyAvatar *self); +HDY_AVAILABLE_IN_ALL +void hdy_avatar_set_text (HdyAvatar *self, + const gchar *text); +HDY_AVAILABLE_IN_ALL +gboolean hdy_avatar_get_show_initials (HdyAvatar *self); +HDY_AVAILABLE_IN_ALL +void hdy_avatar_set_show_initials (HdyAvatar *self, + gboolean show_initials); +HDY_AVAILABLE_IN_ALL +void hdy_avatar_set_image_load_func (HdyAvatar *self, + HdyAvatarImageLoadFunc load_image, + gpointer user_data, + GDestroyNotify destroy); +HDY_AVAILABLE_IN_ALL +gint hdy_avatar_get_size (HdyAvatar *self); +HDY_AVAILABLE_IN_ALL +void hdy_avatar_set_size (HdyAvatar *self, + gint size); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-cairo-private.h b/subprojects/libhandy/src/hdy-cairo-private.h new file mode 100644 index 0000000..d064f04 --- /dev/null +++ b/subprojects/libhandy/src/hdy-cairo-private.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <glib.h> +#include <cairo/cairo.h> + +G_BEGIN_DECLS + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_t, cairo_destroy) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_surface_t, cairo_surface_destroy) + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-carousel-box-private.h b/subprojects/libhandy/src/hdy-carousel-box-private.h new file mode 100644 index 0000000..98d3435 --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-box-private.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_CAROUSEL_BOX (hdy_carousel_box_get_type()) + +G_DECLARE_FINAL_TYPE (HdyCarouselBox, hdy_carousel_box, HDY, CAROUSEL_BOX, GtkContainer) + +GtkWidget *hdy_carousel_box_new (void); + +void hdy_carousel_box_insert (HdyCarouselBox *self, + GtkWidget *widget, + gint position); +void hdy_carousel_box_reorder (HdyCarouselBox *self, + GtkWidget *widget, + gint position); + +gboolean hdy_carousel_box_is_animating (HdyCarouselBox *self); +void hdy_carousel_box_stop_animation (HdyCarouselBox *self); + +void hdy_carousel_box_scroll_to (HdyCarouselBox *self, + GtkWidget *widget, + gint64 duration); + +guint hdy_carousel_box_get_n_pages (HdyCarouselBox *self); +gdouble hdy_carousel_box_get_distance (HdyCarouselBox *self); + +gdouble hdy_carousel_box_get_position (HdyCarouselBox *self); +void hdy_carousel_box_set_position (HdyCarouselBox *self, + gdouble position); + +guint hdy_carousel_box_get_spacing (HdyCarouselBox *self); +void hdy_carousel_box_set_spacing (HdyCarouselBox *self, + guint spacing); + +guint hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self); +void hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self, + guint reveal_duration); + +GtkWidget *hdy_carousel_box_get_nth_child (HdyCarouselBox *self, + guint n); + +gdouble *hdy_carousel_box_get_snap_points (HdyCarouselBox *self, + gint *n_snap_points); +void hdy_carousel_box_get_range (HdyCarouselBox *self, + gdouble *lower, + gdouble *upper); +gdouble hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self); +GtkWidget *hdy_carousel_box_get_page_at_position (HdyCarouselBox *self, + gdouble position); +gint hdy_carousel_box_get_current_page_index (HdyCarouselBox *self); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-carousel-box.c b/subprojects/libhandy/src/hdy-carousel-box.c new file mode 100644 index 0000000..1e0355f --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-box.c @@ -0,0 +1,1768 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-animation-private.h" +#include "hdy-cairo-private.h" +#include "hdy-carousel-box-private.h" + +#include <math.h> + +/** + * PRIVATE:hdy-carousel-box + * @short_description: Scrolling box used in #HdyCarousel + * @title: HdyCarouselBox + * @See_also: #HdyCarousel + * @stability: Private + * + * The #HdyCarouselBox object is meant to be used exclusively as part of the + * #HdyCarousel implementation. + * + * Since: 1.0 + */ + +typedef struct _HdyCarouselBoxAnimation HdyCarouselBoxAnimation; + +struct _HdyCarouselBoxAnimation +{ + gint64 start_time; + gint64 end_time; + gdouble start_value; + gdouble end_value; +}; + +typedef struct _HdyCarouselBoxChildInfo HdyCarouselBoxChildInfo; + +struct _HdyCarouselBoxChildInfo +{ + GtkWidget *widget; + GdkWindow *window; + gint position; + gboolean visible; + gdouble size; + gdouble snap_point; + gboolean adding; + gboolean removing; + + gboolean shift_position; + HdyCarouselBoxAnimation resize_animation; + + cairo_surface_t *surface; + cairo_region_t *dirty_region; +}; + +struct _HdyCarouselBox +{ + GtkContainer parent_instance; + + HdyCarouselBoxAnimation animation; + HdyCarouselBoxChildInfo *destination_child; + GList *children; + + gint child_width; + gint child_height; + + gdouble distance; + gdouble position; + guint spacing; + GtkOrientation orientation; + guint reveal_duration; + + guint tick_cb_id; +}; + +G_DEFINE_TYPE_WITH_CODE (HdyCarouselBox, hdy_carousel_box, GTK_TYPE_CONTAINER, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)); + +enum { + PROP_0, + PROP_N_PAGES, + PROP_POSITION, + PROP_SPACING, + PROP_REVEAL_DURATION, + + /* GtkOrientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_REVEAL_DURATION + 1, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_ANIMATION_STOPPED, + SIGNAL_POSITION_SHIFTED, + SIGNAL_LAST_SIGNAL, +}; +static guint signals[SIGNAL_LAST_SIGNAL]; + +static HdyCarouselBoxChildInfo * +find_child_info (HdyCarouselBox *self, + GtkWidget *widget) +{ + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (widget == info->widget) + return info; + } + + return NULL; +} + +static gint +find_child_index (HdyCarouselBox *self, + GtkWidget *widget, + gboolean count_removing) +{ + GList *l; + gint i; + + i = 0; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (info->removing && !count_removing) + continue; + + if (widget == info->widget) + return i; + + i++; + } + + return -1; +} + +static GList * +get_nth_link (HdyCarouselBox *self, + gint n) +{ + + GList *l; + gint i; + + i = n; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (info->removing) + continue; + + if (i-- == 0) + return l; + } + + return NULL; +} + +static HdyCarouselBoxChildInfo * +find_child_info_by_window (HdyCarouselBox *self, + GdkWindow *window) +{ + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (window == info->window) + return info; + } + + return NULL; +} + +static HdyCarouselBoxChildInfo * +get_closest_child_at (HdyCarouselBox *self, + gdouble position, + gboolean count_adding, + gboolean count_removing) +{ + GList *l; + HdyCarouselBoxChildInfo *closest_child = NULL; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (child->adding && !count_adding) + continue; + + if (child->removing && !count_removing) + continue; + + if (!closest_child || + ABS (closest_child->snap_point - position) > + ABS (child->snap_point - position)) + closest_child = child; + } + + return closest_child; +} + +static void +free_child_info (HdyCarouselBoxChildInfo *info) +{ + if (info->surface) + cairo_surface_destroy (info->surface); + if (info->dirty_region) + cairo_region_destroy (info->dirty_region); + g_free (info); +} + +static void +invalidate_handler_cb (GdkWindow *window, + cairo_region_t *region) +{ + gpointer user_data; + HdyCarouselBox *self; + HdyCarouselBoxChildInfo *info; + + gdk_window_get_user_data (window, &user_data); + g_assert (HDY_IS_CAROUSEL_BOX (user_data)); + self = HDY_CAROUSEL_BOX (user_data); + + info = find_child_info_by_window (self, window); + + if (!info->dirty_region) + info->dirty_region = cairo_region_create (); + + cairo_region_union (info->dirty_region, region); +} + +static void +register_window (HdyCarouselBoxChildInfo *info, + HdyCarouselBox *self) +{ + GtkWidget *widget; + GdkWindow *window; + GdkWindowAttr attributes; + GtkAllocation allocation; + gint attributes_mask; + + if (info->removing) + return; + + widget = GTK_WIDGET (self); + gtk_widget_get_allocation (info->widget, &allocation); + + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (widget); + attributes.event_mask = gtk_widget_get_events (widget); + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + window = gdk_window_new (gtk_widget_get_parent_window (widget), + &attributes, attributes_mask); + gtk_widget_register_window (widget, window); + gtk_widget_set_parent_window (info->widget, window); + + gdk_window_set_user_data (window, self); + + gdk_window_show (window); + + info->window = window; + + gdk_window_set_invalidate_handler (window, invalidate_handler_cb); +} + +static void +unregister_window (HdyCarouselBoxChildInfo *info, + HdyCarouselBox *self) +{ + if (!info->widget) + return; + + gtk_widget_set_parent_window (info->widget, NULL); + gtk_widget_unregister_window (GTK_WIDGET (self), info->window); + gdk_window_destroy (info->window); + info->window = NULL; +} + +static gdouble +get_animation_value (HdyCarouselBoxAnimation *animation, + GdkFrameClock *frame_clock) +{ + gint64 frame_time, duration; + gdouble t; + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + frame_time = MIN (frame_time, animation->end_time); + + duration = animation->end_time - animation->start_time; + t = (gdouble) (frame_time - animation->start_time) / duration; + t = hdy_ease_out_cubic (t); + + return hdy_lerp (animation->start_value, animation->end_value, t); +} + +static gboolean +animate_position (HdyCarouselBox *self, + GdkFrameClock *frame_clock) +{ + gint64 frame_time; + gdouble value; + + if (!hdy_carousel_box_is_animating (self)) + return G_SOURCE_REMOVE; + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + + self->animation.end_value = self->destination_child->snap_point; + value = get_animation_value (&self->animation, frame_clock); + hdy_carousel_box_set_position (self, value); + + if (frame_time >= self->animation.end_time) { + self->animation.start_time = 0; + self->animation.end_time = 0; + g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void update_windows (HdyCarouselBox *self); + +static void +complete_child_animation (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child) +{ + update_windows (self); + + if (child->adding) + child->adding = FALSE; + + if (child->removing) { + self->children = g_list_remove (self->children, child); + + free_child_info (child); + } +} + +static gboolean +animate_child_size (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child, + GdkFrameClock *frame_clock, + gdouble *delta) +{ + gint64 frame_time; + gdouble d, new_value; + + if (child->resize_animation.start_time == 0) + return G_SOURCE_REMOVE; + + new_value = get_animation_value (&child->resize_animation, frame_clock); + d = new_value - child->size; + + child->size += d; + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + + if (delta) + *delta = d; + + if (frame_time >= child->resize_animation.end_time) { + child->resize_animation.start_time = 0; + child->resize_animation.end_time = 0; + complete_child_animation (self, child); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +set_position (HdyCarouselBox *self, + gdouble position) +{ + gdouble lower, upper; + + hdy_carousel_box_get_range (self, &lower, &upper); + + position = CLAMP (position, lower, upper); + + self->position = position; + update_windows (self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]); +} + +static gboolean +animation_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + g_autoptr (GList) children = NULL; + GList *l; + gboolean should_continue; + gdouble position_shift; + + should_continue = G_SOURCE_REMOVE; + + position_shift = 0; + + children = g_list_copy (self->children); + for (l = children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + gdouble delta; + gboolean shift; + + delta = 0; + shift = child->shift_position; + + should_continue |= animate_child_size (self, child, frame_clock, &delta); + + if (shift) + position_shift += delta; + } + + update_windows (self); + + if (position_shift != 0) { + set_position (self, self->position + position_shift); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, position_shift); + } + + should_continue |= animate_position (self, frame_clock); + + update_windows (self); + + if (!should_continue) + self->tick_cb_id = 0; + + return should_continue; +} + +static void +update_shift_position_flag (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child) +{ + HdyCarouselBoxChildInfo *closest_child; + gint animating_index, closest_index; + + /* We want to still shift position when the active child is being removed */ + closest_child = get_closest_child_at (self, self->position, FALSE, TRUE); + + if (!closest_child) + return; + + animating_index = g_list_index (self->children, child); + closest_index = g_list_index (self->children, closest_child); + + child->shift_position = (closest_index >= animating_index); +} + +static void +animate_child (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child, + gdouble value, + gint64 duration) +{ + GdkFrameClock *frame_clock; + gint64 frame_time; + + if (child->resize_animation.start_time > 0) { + child->resize_animation.start_time = 0; + child->resize_animation.end_time = 0; + } + + update_shift_position_flag (self, child); + + if (!gtk_widget_get_realized (GTK_WIDGET (self)) || + duration <= 0 || + !hdy_get_enable_animations (GTK_WIDGET (self))) { + gdouble delta = value - child->size; + + child->size = value; + + if (child->shift_position) { + set_position (self, self->position + delta); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta); + } + + complete_child_animation (self, child); + return; + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + if (!frame_clock) { + gdouble delta = value - child->size; + + child->size = value; + + if (child->shift_position) { + set_position (self, self->position + delta); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta); + } + + complete_child_animation (self, child); + return; + } + + frame_time = gdk_frame_clock_get_frame_time (frame_clock); + + child->resize_animation.start_value = child->size; + child->resize_animation.end_value = value; + + child->resize_animation.start_time = frame_time / 1000; + child->resize_animation.end_time = child->resize_animation.start_time + duration; + if (self->tick_cb_id == 0) + self->tick_cb_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL); +} + +static gboolean +hdy_carousel_box_draw (GtkWidget *widget, + cairo_t *cr) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + if (info->adding || info->removing) + continue; + + if (!info->visible) + continue; + + if (info->dirty_region && !info->removing) { + g_autoptr (cairo_t) surface_cr = NULL; + GtkAllocation child_alloc; + + if (!info->surface) { + gint width, height; + + width = gdk_window_get_width (info->window); + height = gdk_window_get_height (info->window); + + info->surface = gdk_window_create_similar_surface (info->window, + CAIRO_CONTENT_COLOR_ALPHA, + width, height); + } + + gtk_widget_get_allocation (info->widget, &child_alloc); + + surface_cr = cairo_create (info->surface); + + gdk_cairo_region (surface_cr, info->dirty_region); + cairo_clip (surface_cr); + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + cairo_translate (surface_cr, 0, -info->position); + else + cairo_translate (surface_cr, -info->position, 0); + + cairo_save (surface_cr); + cairo_set_source_rgba (surface_cr, 0, 0, 0, 0); + cairo_set_operator (surface_cr, CAIRO_OPERATOR_SOURCE); + cairo_paint (surface_cr); + cairo_restore (surface_cr); + + gtk_container_propagate_draw (GTK_CONTAINER (self), info->widget, surface_cr); + + cairo_region_destroy (info->dirty_region); + info->dirty_region = NULL; + } + + if (!info->surface) + continue; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + cairo_set_source_surface (cr, info->surface, 0, info->position); + else + cairo_set_source_surface (cr, info->surface, info->position, 0); + cairo_paint (cr); + } + + return GDK_EVENT_PROPAGATE; +} + +static void +measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + GList *children; + + if (minimum) + *minimum = 0; + if (natural) + *natural = 0; + + if (minimum_baseline) + *minimum_baseline = -1; + if (natural_baseline) + *natural_baseline = -1; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + GtkWidget *child = child_info->widget; + gint child_min, child_nat; + + if (child_info->removing) + continue; + + if (!gtk_widget_get_visible (child)) + continue; + + if (orientation == GTK_ORIENTATION_VERTICAL) { + if (for_size < 0) + gtk_widget_get_preferred_height (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat); + } else { + if (for_size < 0) + gtk_widget_get_preferred_width (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat); + } + + if (minimum) + *minimum = MAX (*minimum, child_min); + if (natural) + *natural = MAX (*natural, child_nat); + } +} + +static void +hdy_carousel_box_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_carousel_box_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_carousel_box_get_preferred_width_for_height (GtkWidget *widget, + gint for_height, + gint *minimum_width, + gint *natural_width) +{ + measure (widget, GTK_ORIENTATION_HORIZONTAL, for_height, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_carousel_box_get_preferred_height_for_width (GtkWidget *widget, + gint for_width, + gint *minimum_height, + gint *natural_height) +{ + measure (widget, GTK_ORIENTATION_VERTICAL, for_width, + minimum_height, natural_height, NULL, NULL); +} + +static void +invalidate_cache_for_child (HdyCarouselBox *self, + HdyCarouselBoxChildInfo *child) +{ + cairo_rectangle_int_t rect; + + rect.x = 0; + rect.y = 0; + rect.width = self->child_width; + rect.height = self->child_height; + + if (child->surface) + g_clear_pointer (&child->surface, cairo_surface_destroy); + + if (child->dirty_region) + cairo_region_destroy (child->dirty_region); + child->dirty_region = cairo_region_create_rectangle (&rect); +} + +static void +invalidate_drawing_cache (HdyCarouselBox *self) +{ + GList *l; + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child_info = l->data; + + invalidate_cache_for_child (self, child_info); + } +} + +static void +update_windows (HdyCarouselBox *self) +{ + GList *children; + GtkAllocation alloc; + gdouble x, y, offset; + gboolean is_rtl; + gdouble snap_point; + + snap_point = 0; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + + child_info->snap_point = snap_point + child_info->size - 1; + + snap_point += child_info->size; + } + + if (!gtk_widget_get_realized (GTK_WIDGET (self))) + return; + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + x = alloc.x; + y = alloc.y; + + is_rtl = (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL); + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + offset = (self->distance * self->position) - (alloc.height - self->child_height) / 2.0; + else if (is_rtl) + offset = -(self->distance * self->position) + (alloc.width - self->child_width) / 2.0; + else + offset = (self->distance * self->position) - (alloc.width - self->child_width) / 2.0; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + y -= offset; + else + x -= offset; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + + if (!child_info->removing) { + if (!gtk_widget_get_visible (child_info->widget)) + continue; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) { + child_info->position = y; + child_info->visible = child_info->position < alloc.height && + child_info->position + self->child_height > 0; + gdk_window_move (child_info->window, alloc.x, alloc.y + child_info->position); + } else { + child_info->position = x; + child_info->visible = child_info->position < alloc.width && + child_info->position + self->child_width > 0; + gdk_window_move (child_info->window, alloc.x + child_info->position, alloc.y); + } + + if (!child_info->visible) + invalidate_cache_for_child (self, child_info); + } + + if (self->orientation == GTK_ORIENTATION_VERTICAL) + y += self->distance * child_info->size; + else if (is_rtl) + x -= self->distance * child_info->size; + else + x += self->distance * child_info->size; + } +} + +static void +hdy_carousel_box_map (GtkWidget *widget) +{ + GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->map (widget); + + gtk_widget_queue_draw (GTK_WIDGET (widget)); +} + +static void +hdy_carousel_box_realize (GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + + GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->realize (widget); + + g_list_foreach (self->children, (GFunc) register_window, self); + + gtk_widget_queue_allocate (widget); +} + +static void +hdy_carousel_box_unrealize (GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + + g_list_foreach (self->children, (GFunc) unregister_window, self); + + GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->unrealize (widget); +} + +static void +hdy_carousel_box_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget); + gint size, width, height; + GList *children; + + gtk_widget_set_allocation (widget, allocation); + + size = 0; + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + GtkWidget *child = child_info->widget; + gint min, nat; + gint child_size; + + if (child_info->removing) + continue; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + gtk_widget_get_preferred_width_for_height (child, allocation->height, + &min, &nat); + if (gtk_widget_get_hexpand (child)) + child_size = MAX (min, allocation->width); + else + child_size = MAX (min, nat); + } else { + gtk_widget_get_preferred_height_for_width (child, allocation->width, + &min, &nat); + if (gtk_widget_get_vexpand (child)) + child_size = MAX (min, allocation->height); + else + child_size = MAX (min, nat); + } + + size = MAX (size, child_size); + } + + self->distance = size + self->spacing; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + width = size; + height = allocation->height; + } else { + width = allocation->width; + height = size; + } + + if (width != self->child_width || height != self->child_height) + invalidate_drawing_cache (self); + + self->child_width = width; + self->child_height = height; + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + + if (child_info->removing) + continue; + + if (!gtk_widget_get_visible (child_info->widget)) + continue; + + if (!gtk_widget_get_realized (GTK_WIDGET (self))) + continue; + + gdk_window_resize (child_info->window, width, height); + } + + update_windows (self); + + for (children = self->children; children; children = children->next) { + HdyCarouselBoxChildInfo *child_info = children->data; + GtkWidget *child = child_info->widget; + GtkAllocation alloc; + + if (child_info->removing) + continue; + + if (!gtk_widget_get_visible (child)) + continue; + + alloc.x = 0; + alloc.y = 0; + alloc.width = width; + alloc.height = height; + gtk_widget_size_allocate (child, &alloc); + } + + invalidate_drawing_cache (self); + gtk_widget_set_clip (widget, allocation); +} + +static void +hdy_carousel_box_add (GtkContainer *container, + GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (container); + + hdy_carousel_box_insert (self, widget, -1); +} + +static void +shift_position (HdyCarouselBox *self, + gdouble delta) +{ + hdy_carousel_box_set_position (self, self->position + delta); + g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta); +} + +static void +hdy_carousel_box_remove (GtkContainer *container, + GtkWidget *widget) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (container); + HdyCarouselBoxChildInfo *info; + + info = find_child_info (self, widget); + if (!info) + return; + + info->removing = TRUE; + + gtk_widget_unparent (widget); + + if (gtk_widget_get_realized (GTK_WIDGET (container))) + unregister_window (info, self); + + info->widget = NULL; + + if (!gtk_widget_in_destruction (GTK_WIDGET (container))) + animate_child (self, info, 0, self->reveal_duration); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]); +} + +static void +hdy_carousel_box_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (container); + g_autoptr (GList) children = NULL; + GList *l; + + children = g_list_copy (self->children); + for (l = children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (!child->removing) + (* callback) (child->widget, callback_data); + } +} + +static void +hdy_carousel_box_finalize (GObject *object) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (object); + + if (self->tick_cb_id > 0) + gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id); + + g_list_free_full (self->children, (GDestroyNotify) free_child_info); + + G_OBJECT_CLASS (hdy_carousel_box_parent_class)->finalize (object); +} + +static void +hdy_carousel_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (object); + + switch (prop_id) { + case PROP_N_PAGES: + g_value_set_uint (value, hdy_carousel_box_get_n_pages (self)); + break; + + case PROP_POSITION: + g_value_set_double (value, hdy_carousel_box_get_position (self)); + break; + + case PROP_SPACING: + g_value_set_uint (value, hdy_carousel_box_get_spacing (self)); + break; + + case PROP_REVEAL_DURATION: + g_value_set_uint (value, hdy_carousel_box_get_reveal_duration (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyCarouselBox *self = HDY_CAROUSEL_BOX (object); + + switch (prop_id) { + case PROP_POSITION: + hdy_carousel_box_set_position (self, g_value_get_double (value)); + break; + + case PROP_SPACING: + hdy_carousel_box_set_spacing (self, g_value_get_uint (value)); + break; + + case PROP_REVEAL_DURATION: + hdy_carousel_box_set_reveal_duration (self, g_value_get_uint (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_box_class_init (HdyCarouselBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = hdy_carousel_box_finalize; + object_class->get_property = hdy_carousel_box_get_property; + object_class->set_property = hdy_carousel_box_set_property; + widget_class->draw = hdy_carousel_box_draw; + widget_class->get_preferred_width = hdy_carousel_box_get_preferred_width; + widget_class->get_preferred_height = hdy_carousel_box_get_preferred_height; + widget_class->get_preferred_width_for_height = hdy_carousel_box_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = hdy_carousel_box_get_preferred_height_for_width; + widget_class->map = hdy_carousel_box_map; + widget_class->realize = hdy_carousel_box_realize; + widget_class->unrealize = hdy_carousel_box_unrealize; + widget_class->size_allocate = hdy_carousel_box_size_allocate; + container_class->add = hdy_carousel_box_add; + container_class->remove = hdy_carousel_box_remove; + container_class->forall = hdy_carousel_box_forall; + + /** + * HdyCarouselBox:n-pages: + * + * The number of pages in a #HdyCarouselBox + * + * Since: 1.0 + */ + props[PROP_N_PAGES] = + g_param_spec_uint ("n-pages", + _("Number of pages"), + _("Number of pages"), + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarouselBox:position: + * + * Current scrolling position, unitless. 1 matches 1 page. + * + * Since: 1.0 + */ + props[PROP_POSITION] = + g_param_spec_double ("position", + _("Position"), + _("Current scrolling position"), + 0, + G_MAXDOUBLE, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarouselBox:spacing: + * + * Spacing between pages in pixels. + * + * Since: 1.0 + */ + props[PROP_SPACING] = + g_param_spec_uint ("spacing", + _("Spacing"), + _("Spacing between pages"), + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarouselBox:reveal-duration: + * + * Duration of the animation used when adding or removing pages, in + * milliseconds. + * + * Since: 1.0 + */ + props[PROP_REVEAL_DURATION] = + g_param_spec_uint ("reveal-duration", + _("Reveal duration"), + _("Page reveal duration"), + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * HdyCarouselBox::animation-stopped: + * @self: The #HdyCarouselBox instance + * + * This signal is emitted after an animation has been stopped. If animations + * are disabled, the signal is emitted as well. + * + * Since: 1.0 + */ + signals[SIGNAL_ANIMATION_STOPPED] = + g_signal_new ("animation-stopped", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + /** + * HdyCarouselBox::position-shifted: + * @self: The #HdyCarouselBox instance + * @delta: The amount to shift the position by + * + * This signal is emitted when position has been programmatically shifted. + * + * Since: 1.0 + */ + signals[SIGNAL_POSITION_SHIFTED] = + g_signal_new ("position-shifted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_DOUBLE); +} + +static void +hdy_carousel_box_init (HdyCarouselBox *self) +{ + GtkWidget *widget = GTK_WIDGET (self); + + self->orientation = GTK_ORIENTATION_HORIZONTAL; + self->reveal_duration = 0; + + gtk_widget_set_has_window (widget, FALSE); +} + +/** + * hdy_carousel_box_new: + * + * Create a new #HdyCarouselBox widget. + * + * Returns: The newly created #HdyCarouselBox widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_box_new (void) +{ + return g_object_new (HDY_TYPE_CAROUSEL_BOX, NULL); +} + +/** + * hdy_carousel_box_insert: + * @self: a #HdyCarouselBox + * @widget: a widget to add + * @position: the position to insert @widget in. + * + * Inserts @widget into @self at position @position. + * + * If position is -1, or larger than the number of pages, @widget will be + * appended to the end. + * + * Since: 1.0 + */ +void +hdy_carousel_box_insert (HdyCarouselBox *self, + GtkWidget *widget, + gint position) +{ + HdyCarouselBoxChildInfo *info; + GList *prev_link; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + info = g_new0 (HdyCarouselBoxChildInfo, 1); + info->widget = widget; + info->size = 0; + info->adding = TRUE; + + if (gtk_widget_get_realized (GTK_WIDGET (self))) + register_window (info, self); + + if (position >= 0) + prev_link = get_nth_link (self, position); + else + prev_link = NULL; + + self->children = g_list_insert_before (self->children, prev_link, info); + + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + + update_windows (self); + + animate_child (self, info, 1, self->reveal_duration); + + invalidate_drawing_cache (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]); +} + +/** + * hdy_carousel_box_reorder: + * @self: a #HdyCarouselBox + * @widget: a widget to add + * @position: the position to move @widget to. + * + * Moves @widget into position @position. + * + * If position is -1, or larger than the number of pages, @widget will be moved + * to the end. + * + * Since: 1.0 + */ +void +hdy_carousel_box_reorder (HdyCarouselBox *self, + GtkWidget *widget, + gint position) +{ + HdyCarouselBoxChildInfo *info, *prev_info; + GList *link, *prev_link; + gint old_position; + gdouble closest_point, old_point, new_point; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + closest_point = hdy_carousel_box_get_closest_snap_point (self); + + info = find_child_info (self, widget); + link = g_list_find (self->children, info); + old_position = g_list_position (self->children, link); + + if (position == old_position) + return; + + old_point = ((HdyCarouselBoxChildInfo *) link->data)->snap_point; + + if (position < 0 || position >= hdy_carousel_box_get_n_pages (self)) + prev_link = g_list_last (self->children); + else + prev_link = get_nth_link (self, position); + + prev_info = prev_link->data; + new_point = prev_info->snap_point; + if (new_point > old_point) + new_point -= prev_info->size; + + self->children = g_list_remove_link (self->children, link); + self->children = g_list_insert_before (self->children, prev_link, link->data); + + if (closest_point == old_point) + shift_position (self, new_point - old_point); + else if (old_point > closest_point && closest_point >= new_point) + shift_position (self, info->size); + else if (new_point >= closest_point && closest_point > old_point) + shift_position (self, -info->size); +} + +/** + * hdy_carousel_box_is_animating: + * @self: a #HdyCarouselBox + * + * Get whether @self is animating position. + * + * Returns: %TRUE if an animation is running + * + * Since: 1.0 + */ +gboolean +hdy_carousel_box_is_animating (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), FALSE); + + return (self->animation.start_time != 0); +} + +/** + * hdy_carousel_box_stop_animation: + * @self: a #HdyCarouselBox + * + * Stops a running animation. If there's no animation running, does nothing. + * + * It does not reset position to a non-transient value automatically. + * + * Since: 1.0 + */ +void +hdy_carousel_box_stop_animation (HdyCarouselBox *self) +{ + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + if (self->animation.start_time == 0) + return; + + self->animation.start_time = 0; + self->animation.end_time = 0; +} + +/** + * hdy_carousel_box_scroll_to: + * @self: a #HdyCarouselBox + * @widget: a child of @self + * @duration: animation duration in milliseconds + * + * Scrolls to @widget position over the next @duration milliseconds using + * easeOutCubic interpolator. + * + * If an animation was already running, it will be cancelled automatically. + * + * @duration can be 0, in that case the position will be + * changed immediately. + * + * Since: 1.0 + */ +void +hdy_carousel_box_scroll_to (HdyCarouselBox *self, + GtkWidget *widget, + gint64 duration) +{ + GdkFrameClock *frame_clock; + gint64 frame_time; + gdouble position; + HdyCarouselBoxChildInfo *child; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + g_return_if_fail (duration >= 0); + + child = find_child_info (self, widget); + position = child->snap_point; + + hdy_carousel_box_stop_animation (self); + + if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) { + hdy_carousel_box_set_position (self, position); + g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0); + return; + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + if (!frame_clock) { + hdy_carousel_box_set_position (self, position); + g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0); + return; + } + + frame_time = gdk_frame_clock_get_frame_time (frame_clock); + + self->destination_child = child; + + self->animation.start_value = self->position; + self->animation.end_value = position; + + self->animation.start_time = frame_time / 1000; + self->animation.end_time = self->animation.start_time + duration; + if (self->tick_cb_id == 0) + self->tick_cb_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL); +} + +/** + * hdy_carousel_box_get_n_pages: + * @self: a #HdyCarouselBox + * + * Gets the number of pages in @self. + * + * Returns: The number of pages in @self + * + * Since: 1.0 + */ +guint +hdy_carousel_box_get_n_pages (HdyCarouselBox *self) +{ + GList *l; + guint n_pages; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + n_pages = 0; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (!child->removing) + n_pages++; + } + + return n_pages; +} + +/** + * hdy_carousel_box_get_distance: + * @self: a #HdyCarouselBox + * + * Gets swiping distance between two adjacent children in pixels. + * + * Returns: The swiping distance in pixels + * + * Since: 1.0 + */ +gdouble +hdy_carousel_box_get_distance (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->distance; +} + +/** + * hdy_carousel_box_get_position: + * @self: a #HdyCarouselBox + * + * Gets current scroll position in @self. It's unitless, 1 matches 1 page. + * + * Returns: The scroll position + * + * Since: 1.0 + */ +gdouble +hdy_carousel_box_get_position (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->position; +} + +/** + * hdy_carousel_box_set_position: + * @self: a #HdyCarouselBox + * @position: the new position value + * + * Sets current scroll position in @self, unitless, 1 matches 1 page. + * + * Since: 1.0 + */ +void +hdy_carousel_box_set_position (HdyCarouselBox *self, + gdouble position) +{ + GList *l; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + set_position (self, position); + + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *child = l->data; + + if (child->adding || child->removing) + update_shift_position_flag (self, child); + } +} + +/** + * hdy_carousel_box_get_spacing: + * @self: a #HdyCarouselBox + * + * Gets spacing between pages in pixels. + * + * Returns: Spacing between pages + * + * Since: 1.0 + */ +guint +hdy_carousel_box_get_spacing (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->spacing; +} + +/** + * hdy_carousel_box_set_spacing: + * @self: a #HdyCarouselBox + * @spacing: the new spacing value + * + * Sets spacing between pages in pixels. + * + * Since: 1.0 + */ +void +hdy_carousel_box_set_spacing (HdyCarouselBox *self, + guint spacing) +{ + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + if (self->spacing == spacing) + return; + + self->spacing = spacing; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]); +} + +/** + * hdy_carousel_box_get_reveal_duration: + * @self: a #HdyCarouselBox + * + * Gets duration of the animation used when adding or removing pages in + * milliseconds. + * + * Returns: Page reveal duration + * + * Since: 1.0 + */ +guint +hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + return self->reveal_duration; +} + +/** + * hdy_carousel_box_set_reveal_duration: + * @self: a #HdyCarouselBox + * @reveal_duration: the new reveal duration value + * + * Sets duration of the animation used when adding or removing pages in + * milliseconds. + * + * Since: 1.0 + */ +void +hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self, + guint reveal_duration) +{ + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + if (self->reveal_duration == reveal_duration) + return; + + self->reveal_duration = reveal_duration; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]); +} + +/** + * hdy_carousel_box_get_nth_child: + * @self: a #HdyCarouselBox + * @n: the child index + * + * Retrieves @n-th child widget of @self. + * + * Returns: The @n-th child widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_box_get_nth_child (HdyCarouselBox *self, + guint n) +{ + HdyCarouselBoxChildInfo *info; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL); + g_return_val_if_fail (n < hdy_carousel_box_get_n_pages (self), NULL); + + info = get_nth_link (self, n)->data; + + return info->widget; +} + +/** + * hdy_carousel_box_get_snap_points: + * @self: a #HdyCarouselBox + * @n_snap_points: (out) + * + * Gets the snap points of @self, representing the points between each page, + * before the first page and after the last page. + * + * Returns: (array length=n_snap_points) (transfer full): the snap points of @self + * + * Since: 1.0 + */ +gdouble * +hdy_carousel_box_get_snap_points (HdyCarouselBox *self, + gint *n_snap_points) +{ + guint i, n_pages; + gdouble *points; + GList *l; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL); + + n_pages = MAX (g_list_length (self->children), 1); + + points = g_new0 (gdouble, n_pages); + + i = 0; + for (l = self->children; l; l = l->next) { + HdyCarouselBoxChildInfo *info = l->data; + + points[i++] = info->snap_point; + } + + if (n_snap_points) + *n_snap_points = n_pages; + + return points; +} + +/** + * hdy_carousel_box_get_range: + * @self: a #HdyCarouselBox + * @lower: (out) (optional): location to store the lowest possible position, or %NULL + * @upper: (out) (optional): location to store the maximum possible position, or %NULL + * + * Gets the range of possible positions. + * + * Since: 1.0 + */ +void +hdy_carousel_box_get_range (HdyCarouselBox *self, + gdouble *lower, + gdouble *upper) +{ + GList *l; + HdyCarouselBoxChildInfo *child; + + g_return_if_fail (HDY_IS_CAROUSEL_BOX (self)); + + l = g_list_last (self->children); + child = l ? l->data : NULL; + + if (lower) + *lower = 0; + + if (upper) + *upper = child ? child->snap_point : 0; +} + +/** + * hdy_carousel_box_get_closest_snap_point: + * @self: a #HdyCarouselBox + * + * Gets the snap point closest to the current position. + * + * Returns: the closest snap point. + * + * Since: 1.0 + */ +gdouble +hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self) +{ + HdyCarouselBoxChildInfo *closest_child; + + closest_child = get_closest_child_at (self, self->position, TRUE, TRUE); + + if (!closest_child) + return 0; + + return closest_child->snap_point; +} + +/** + * hdy_carousel_box_get_page_at_position: + * @self: a #HdyCarouselBox + * @position: a scroll position + * + * Gets the page closest to @position. For example, if @position matches + * the current position, the returned widget will match the currently + * displayed page. + * + * Returns: the closest page. + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_box_get_page_at_position (HdyCarouselBox *self, + gdouble position) +{ + gdouble lower, upper; + HdyCarouselBoxChildInfo *child; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL); + + hdy_carousel_box_get_range (self, &lower, &upper); + + position = CLAMP (position, lower, upper); + + child = get_closest_child_at (self, position, TRUE, FALSE); + + return child->widget; +} + +/** + * hdy_carousel_box_get_current_page_index: + * @self: a #HdyCarouselBox + * + * Gets the index of the currently displayed page. + * + * Returns: the index of the current page. + * + * Since: 1.0 + */ +gint +hdy_carousel_box_get_current_page_index (HdyCarouselBox *self) +{ + GtkWidget *child; + + g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0); + + child = hdy_carousel_box_get_page_at_position (self, self->position); + + return find_child_index (self, child, FALSE); +} diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-dots.c b/subprojects/libhandy/src/hdy-carousel-indicator-dots.c new file mode 100644 index 0000000..5bbc541 --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-indicator-dots.c @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-carousel-indicator-dots.h" + +#include "hdy-animation-private.h" +#include "hdy-swipeable.h" + +#include <math.h> + +#define DOTS_RADIUS 3 +#define DOTS_RADIUS_SELECTED 4 +#define DOTS_OPACITY 0.3 +#define DOTS_OPACITY_SELECTED 0.9 +#define DOTS_SPACING 7 +#define DOTS_MARGIN 6 + +/** + * SECTION:hdy-carousel-indicator-dots + * @short_description: A dots indicator for #HdyCarousel + * @title: HdyCarouselIndicatorDots + * @See_also: #HdyCarousel, #HdyCarouselIndicatorLines + * + * The #HdyCarouselIndicatorDots widget can be used to show a set of dots for each + * page of a given #HdyCarousel. The dot representing the carousel's active page + * is larger and more opaque than the others, the transition to the active and + * inactive state is gradual to match the carousel's position. + * + * # CSS nodes + * + * #HdyCarouselIndicatorDots has a single CSS node with name carouselindicatordots. + * + * Since: 1.0 + */ + +struct _HdyCarouselIndicatorDots +{ + GtkDrawingArea parent_instance; + + HdyCarousel *carousel; + GtkOrientation orientation; + + guint tick_cb_id; + guint64 end_time; +}; + +G_DEFINE_TYPE_WITH_CODE (HdyCarouselIndicatorDots, hdy_carousel_indicator_dots, GTK_TYPE_DRAWING_AREA, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +enum { + PROP_0, + PROP_CAROUSEL, + + /* GtkOrientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_CAROUSEL + 1, +}; + +static GParamSpec *props[LAST_PROP]; + +static gboolean +animation_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget); + gint64 frame_time; + + g_assert (self->tick_cb_id > 0); + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + + if (frame_time >= self->end_time || + !hdy_get_enable_animations (GTK_WIDGET (self))) { + self->tick_cb_id = 0; + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +stop_animation (HdyCarouselIndicatorDots *self) +{ + if (self->tick_cb_id == 0) + return; + + gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id); + self->tick_cb_id = 0; +} + +static void +animate (HdyCarouselIndicatorDots *self, + gint64 duration) +{ + GdkFrameClock *frame_clock; + gint64 frame_time; + + if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) { + gtk_widget_queue_draw (GTK_WIDGET (self)); + return; + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + if (!frame_clock) { + gtk_widget_queue_draw (GTK_WIDGET (self)); + return; + } + + frame_time = gdk_frame_clock_get_frame_time (frame_clock); + + self->end_time = MAX (self->end_time, frame_time / 1000 + duration); + if (self->tick_cb_id == 0) + self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), + animation_cb, + NULL, NULL); +} + +static GdkRGBA +get_color (GtkWidget *widget) +{ + GtkStyleContext *context; + GtkStateFlags flags; + GdkRGBA color; + + context = gtk_widget_get_style_context (widget); + flags = gtk_widget_get_state_flags (widget); + gtk_style_context_get_color (context, flags, &color); + + return color; +} + +static void +draw_dots (GtkWidget *widget, + cairo_t *cr, + GtkOrientation orientation, + gdouble position, + gdouble *sizes, + guint n_pages) +{ + GdkRGBA color; + gint i, widget_length, widget_thickness; + gdouble x, y, indicator_length, dot_size, full_size; + gdouble current_position, remaining_progress; + + color = get_color (widget); + dot_size = 2 * DOTS_RADIUS_SELECTED + DOTS_SPACING; + + indicator_length = 0; + for (i = 0; i < n_pages; i++) + indicator_length += dot_size * sizes[i]; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + widget_length = gtk_widget_get_allocated_width (widget); + widget_thickness = gtk_widget_get_allocated_height (widget); + } else { + widget_length = gtk_widget_get_allocated_height (widget); + widget_thickness = gtk_widget_get_allocated_width (widget); + } + + /* Ensure the indicators are aligned to pixel grid when not animating */ + full_size = round (indicator_length / dot_size) * dot_size; + if ((widget_length - (gint) full_size) % 2 == 0) + widget_length--; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + cairo_translate (cr, (widget_length - indicator_length) / 2.0, widget_thickness / 2); + else + cairo_translate (cr, widget_thickness / 2, (widget_length - indicator_length) / 2.0); + + x = 0; + y = 0; + + current_position = 0; + remaining_progress = 1; + + for (i = 0; i < n_pages; i++) { + gdouble progress, radius, opacity; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + x += dot_size * sizes[i] / 2.0; + else + y += dot_size * sizes[i] / 2.0; + + current_position += sizes[i]; + + progress = CLAMP (current_position - position, 0, remaining_progress); + remaining_progress -= progress; + + radius = hdy_lerp (DOTS_RADIUS, DOTS_RADIUS_SELECTED, progress) * sizes[i]; + opacity = hdy_lerp (DOTS_OPACITY, DOTS_OPACITY_SELECTED, progress) * sizes[i]; + + cairo_set_source_rgba (cr, color.red, color.green, color.blue, + color.alpha * opacity); + cairo_arc (cr, x, y, radius, 0, 2 * G_PI); + cairo_fill (cr); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + x += dot_size * sizes[i] / 2.0; + else + y += dot_size * sizes[i] / 2.0; + } +} + +static void +n_pages_changed_cb (HdyCarouselIndicatorDots *self) +{ + animate (self, hdy_carousel_get_reveal_duration (self->carousel)); +} + +static void +hdy_carousel_indicator_dots_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget); + gint size = 0; + + if (orientation == self->orientation) { + gint n_pages = 0; + if (self->carousel) + n_pages = hdy_carousel_get_n_pages (self->carousel); + + size = MAX (0, (2 * DOTS_RADIUS_SELECTED + DOTS_SPACING) * n_pages - DOTS_SPACING); + } else { + size = 2 * DOTS_RADIUS_SELECTED; + } + + size += 2 * DOTS_MARGIN; + + if (minimum) + *minimum = size; + + if (natural) + *natural = size; + + if (minimum_baseline) + *minimum_baseline = -1; + + if (natural_baseline) + *natural_baseline = -1; +} + +static void +hdy_carousel_indicator_dots_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + hdy_carousel_indicator_dots_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_carousel_indicator_dots_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + hdy_carousel_indicator_dots_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static gboolean +hdy_carousel_indicator_dots_draw (GtkWidget *widget, + cairo_t *cr) +{ + HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget); + gint i, n_points; + gdouble position; + g_autofree gdouble *points = NULL; + g_autofree gdouble *sizes = NULL; + + if (!self->carousel) + return GDK_EVENT_PROPAGATE; + + points = hdy_swipeable_get_snap_points (HDY_SWIPEABLE (self->carousel), &n_points); + position = hdy_carousel_get_position (self->carousel); + + if (n_points < 2) + return GDK_EVENT_PROPAGATE; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL && + gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + position = points[n_points - 1] - position; + + sizes = g_new0 (gdouble, n_points); + + sizes[0] = points[0] + 1; + for (i = 1; i < n_points; i++) + sizes[i] = points[i] - points[i - 1]; + + draw_dots (widget, cr, self->orientation, position, sizes, n_points); + + return GDK_EVENT_PROPAGATE; +} + +static void +hdy_carousel_dispose (GObject *object) +{ + HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object); + + hdy_carousel_indicator_dots_set_carousel (self, NULL); + + G_OBJECT_CLASS (hdy_carousel_indicator_dots_parent_class)->dispose (object); +} + +static void +hdy_carousel_indicator_dots_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object); + + switch (prop_id) { + case PROP_CAROUSEL: + g_value_set_object (value, hdy_carousel_indicator_dots_get_carousel (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_indicator_dots_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object); + + switch (prop_id) { + case PROP_CAROUSEL: + hdy_carousel_indicator_dots_set_carousel (self, g_value_get_object (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_indicator_dots_class_init (HdyCarouselIndicatorDotsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = hdy_carousel_dispose; + object_class->get_property = hdy_carousel_indicator_dots_get_property; + object_class->set_property = hdy_carousel_indicator_dots_set_property; + + widget_class->get_preferred_width = hdy_carousel_indicator_dots_get_preferred_width; + widget_class->get_preferred_height = hdy_carousel_indicator_dots_get_preferred_height; + widget_class->draw = hdy_carousel_indicator_dots_draw; + + /** + * HdyCarouselIndicatorDots:carousel: + * + * The #HdyCarousel the indicator uses. + * + * Since: 1.0 + */ + props[PROP_CAROUSEL] = + g_param_spec_object ("carousel", + _("Carousel"), + _("Carousel"), + HDY_TYPE_CAROUSEL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "carouselindicatordots"); +} + +static void +hdy_carousel_indicator_dots_init (HdyCarouselIndicatorDots *self) +{ +} + +/** + * hdy_carousel_indicator_dots_new: + * + * Create a new #HdyCarouselIndicatorDots widget. + * + * Returns: (transfer full): The newly created #HdyCarouselIndicatorDots widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_indicator_dots_new (void) +{ + return g_object_new (HDY_TYPE_CAROUSEL_INDICATOR_DOTS, NULL); +} + +/** + * hdy_carousel_indicator_dots_get_carousel: + * @self: a #HdyCarouselIndicatorDots + * + * Get the #HdyCarousel the indicator uses. + * + * See: hdy_carousel_indicator_dots_set_carousel() + * + * Returns: (nullable) (transfer none): the #HdyCarousel, or %NULL if none has been set + * + * Since: 1.0 + */ + +HdyCarousel * +hdy_carousel_indicator_dots_get_carousel (HdyCarouselIndicatorDots *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_INDICATOR_DOTS (self), NULL); + + return self->carousel; +} + +/** + * hdy_carousel_indicator_dots_set_carousel: + * @self: a #HdyCarouselIndicatorDots + * @carousel: (nullable): a #HdyCarousel + * + * Sets the #HdyCarousel to use. + * + * Since: 1.0 + */ +void +hdy_carousel_indicator_dots_set_carousel (HdyCarouselIndicatorDots *self, + HdyCarousel *carousel) +{ + g_return_if_fail (HDY_IS_CAROUSEL_INDICATOR_DOTS (self)); + g_return_if_fail (HDY_IS_CAROUSEL (carousel) || carousel == NULL); + + if (self->carousel == carousel) + return; + + if (self->carousel) { + stop_animation (self); + g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self); + g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self); + } + + g_set_object (&self->carousel, carousel); + + if (self->carousel) { + g_signal_connect_object (self->carousel, "notify::position", + G_CALLBACK (gtk_widget_queue_draw), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->carousel, "notify::n-pages", + G_CALLBACK (n_pages_changed_cb), self, + G_CONNECT_SWAPPED); + } + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]); +} diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-dots.h b/subprojects/libhandy/src/hdy-carousel-indicator-dots.h new file mode 100644 index 0000000..032886e --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-indicator-dots.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-carousel.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_CAROUSEL_INDICATOR_DOTS (hdy_carousel_indicator_dots_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyCarouselIndicatorDots, hdy_carousel_indicator_dots, HDY, CAROUSEL_INDICATOR_DOTS, GtkDrawingArea) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_carousel_indicator_dots_new (void); + +HDY_AVAILABLE_IN_ALL +HdyCarousel *hdy_carousel_indicator_dots_get_carousel (HdyCarouselIndicatorDots *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_indicator_dots_set_carousel (HdyCarouselIndicatorDots *self, + HdyCarousel *carousel); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-lines.c b/subprojects/libhandy/src/hdy-carousel-indicator-lines.c new file mode 100644 index 0000000..fba9b38 --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-indicator-lines.c @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-carousel-indicator-lines.h" + +#include "hdy-animation-private.h" +#include "hdy-swipeable.h" + +#include <math.h> + +#define LINE_WIDTH 3 +#define LINE_LENGTH 35 +#define LINE_SPACING 5 +#define LINE_OPACITY 0.3 +#define LINE_OPACITY_ACTIVE 0.9 +#define LINE_MARGIN 2 + +/** + * SECTION:hdy-carousel-indicator-lines + * @short_description: A lines indicator for #HdyCarousel + * @title: HdyCarouselIndicatorLines + * @See_also: #HdyCarousel, #HdyCarouselIndicatorDots + * + * The #HdyCarouselIndicatorLines widget can be used to show a set of thin and long + * rectangles for each page of a given #HdyCarousel. The carousel's active page + * is shown with another rectangle that moves between them to match the + * carousel's position. + * + * # CSS nodes + * + * #HdyCarouselIndicatorLines has a single CSS node with name carouselindicatorlines. + * + * Since: 1.0 + */ + +struct _HdyCarouselIndicatorLines +{ + GtkDrawingArea parent_instance; + + HdyCarousel *carousel; + GtkOrientation orientation; + + guint tick_cb_id; + guint64 end_time; +}; + +G_DEFINE_TYPE_WITH_CODE (HdyCarouselIndicatorLines, hdy_carousel_indicator_lines, GTK_TYPE_DRAWING_AREA, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +enum { + PROP_0, + PROP_CAROUSEL, + + /* GtkOrientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_CAROUSEL + 1, +}; + +static GParamSpec *props[LAST_PROP]; + +static gboolean +animation_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget); + gint64 frame_time; + + g_assert (self->tick_cb_id > 0); + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + + if (frame_time >= self->end_time || + !hdy_get_enable_animations (GTK_WIDGET (self))) { + self->tick_cb_id = 0; + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +stop_animation (HdyCarouselIndicatorLines *self) +{ + if (self->tick_cb_id == 0) + return; + + gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id); + self->tick_cb_id = 0; +} + +static void +animate (HdyCarouselIndicatorLines *self, + gint64 duration) +{ + GdkFrameClock *frame_clock; + gint64 frame_time; + + if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) { + gtk_widget_queue_draw (GTK_WIDGET (self)); + return; + } + + frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self)); + if (!frame_clock) { + gtk_widget_queue_draw (GTK_WIDGET (self)); + return; + } + + frame_time = gdk_frame_clock_get_frame_time (frame_clock); + + self->end_time = MAX (self->end_time, frame_time / 1000 + duration); + if (self->tick_cb_id == 0) + self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), + animation_cb, + NULL, NULL); +} + +static GdkRGBA +get_color (GtkWidget *widget) +{ + GtkStyleContext *context; + GtkStateFlags flags; + GdkRGBA color; + + context = gtk_widget_get_style_context (widget); + flags = gtk_widget_get_state_flags (widget); + gtk_style_context_get_color (context, flags, &color); + + return color; +} + +static void +draw_lines (GtkWidget *widget, + cairo_t *cr, + GtkOrientation orientation, + gdouble position, + gdouble *sizes, + guint n_pages) +{ + GdkRGBA color; + gint i, widget_length, widget_thickness; + gdouble indicator_length, full_size, line_size, pos; + + color = get_color (widget); + + line_size = LINE_LENGTH + LINE_SPACING; + indicator_length = 0; + for (i = 0; i < n_pages; i++) + indicator_length += line_size * sizes[i]; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + widget_length = gtk_widget_get_allocated_width (widget); + widget_thickness = gtk_widget_get_allocated_height (widget); + } else { + widget_length = gtk_widget_get_allocated_height (widget); + widget_thickness = gtk_widget_get_allocated_width (widget); + } + + /* Ensure the indicators are aligned to pixel grid when not animating */ + full_size = round (indicator_length / line_size) * line_size; + if ((widget_length - (gint) full_size) % 2 == 0) + widget_length--; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + cairo_translate (cr, (widget_length - indicator_length) / 2.0, (widget_thickness - LINE_WIDTH) / 2); + cairo_scale (cr, 1, LINE_WIDTH); + } else { + cairo_translate (cr, (widget_thickness - LINE_WIDTH) / 2, (widget_length - indicator_length) / 2.0); + cairo_scale (cr, LINE_WIDTH, 1); + } + + pos = 0; + cairo_set_source_rgba (cr, color.red, color.green, color.blue, + color.alpha * LINE_OPACITY); + for (i = 0; i < n_pages; i++) { + gdouble length; + + length = (LINE_LENGTH + LINE_SPACING) * sizes[i] - LINE_SPACING; + + if (length > 0) { + if (orientation == GTK_ORIENTATION_HORIZONTAL) + cairo_rectangle (cr, LINE_SPACING / 2.0 + pos, 0, length, 1); + else + cairo_rectangle (cr, 0, LINE_SPACING / 2.0 + pos, 1, length); + } + + cairo_fill (cr); + + pos += (LINE_LENGTH + LINE_SPACING) * sizes[i]; + } + + cairo_set_source_rgba (cr, color.red, color.green, color.blue, + color.alpha * LINE_OPACITY_ACTIVE); + + pos = LINE_SPACING / 2.0 + position * (LINE_LENGTH + LINE_SPACING); + if (orientation == GTK_ORIENTATION_HORIZONTAL) + cairo_rectangle (cr, pos, 0, LINE_LENGTH, 1); + else + cairo_rectangle (cr, 0, pos, 1, LINE_LENGTH); + cairo_fill (cr); +} + +static void +n_pages_changed_cb (HdyCarouselIndicatorLines *self) +{ + animate (self, hdy_carousel_get_reveal_duration (self->carousel)); +} + +static void +hdy_carousel_indicator_lines_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget); + gint size = 0; + + if (orientation == self->orientation) { + gint n_pages = 0; + if (self->carousel) + n_pages = hdy_carousel_get_n_pages (self->carousel); + + size = MAX (0, (LINE_LENGTH + LINE_SPACING) * n_pages - LINE_SPACING); + } else { + size = LINE_WIDTH; + } + + size += 2 * LINE_MARGIN; + + if (minimum) + *minimum = size; + + if (natural) + *natural = size; + + if (minimum_baseline) + *minimum_baseline = -1; + + if (natural_baseline) + *natural_baseline = -1; +} + +static void +hdy_carousel_indicator_lines_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + hdy_carousel_indicator_lines_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_carousel_indicator_lines_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + hdy_carousel_indicator_lines_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static gboolean +hdy_carousel_indicator_lines_draw (GtkWidget *widget, + cairo_t *cr) +{ + HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget); + gint i, n_points; + gdouble position; + g_autofree gdouble *points = NULL; + g_autofree gdouble *sizes = NULL; + + if (!self->carousel) + return GDK_EVENT_PROPAGATE; + + points = hdy_swipeable_get_snap_points (HDY_SWIPEABLE (self->carousel), &n_points); + position = hdy_carousel_get_position (self->carousel); + + if (n_points < 2) + return GDK_EVENT_PROPAGATE; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL && + gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) + position = points[n_points - 1] - position; + + sizes = g_new0 (gdouble, n_points); + + sizes[0] = points[0] + 1; + for (i = 1; i < n_points; i++) + sizes[i] = points[i] - points[i - 1]; + + draw_lines (widget, cr, self->orientation, position, sizes, n_points); + + return GDK_EVENT_PROPAGATE; +} + +static void +hdy_carousel_dispose (GObject *object) +{ + HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object); + + hdy_carousel_indicator_lines_set_carousel (self, NULL); + + G_OBJECT_CLASS (hdy_carousel_indicator_lines_parent_class)->dispose (object); +} + +static void +hdy_carousel_indicator_lines_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object); + + switch (prop_id) { + case PROP_CAROUSEL: + g_value_set_object (value, hdy_carousel_indicator_lines_get_carousel (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_indicator_lines_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object); + + switch (prop_id) { + case PROP_CAROUSEL: + hdy_carousel_indicator_lines_set_carousel (self, g_value_get_object (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_indicator_lines_class_init (HdyCarouselIndicatorLinesClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = hdy_carousel_dispose; + object_class->get_property = hdy_carousel_indicator_lines_get_property; + object_class->set_property = hdy_carousel_indicator_lines_set_property; + + widget_class->get_preferred_width = hdy_carousel_indicator_lines_get_preferred_width; + widget_class->get_preferred_height = hdy_carousel_indicator_lines_get_preferred_height; + widget_class->draw = hdy_carousel_indicator_lines_draw; + + /** + * HdyCarouselIndicatorLines:carousel: + * + * The #HdyCarousel the indicator uses. + * + * Since: 1.0 + */ + props[PROP_CAROUSEL] = + g_param_spec_object ("carousel", + _("Carousel"), + _("Carousel"), + HDY_TYPE_CAROUSEL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "carouselindicatorlines"); +} + +static void +hdy_carousel_indicator_lines_init (HdyCarouselIndicatorLines *self) +{ +} + +/** + * hdy_carousel_indicator_lines_new: + * + * Create a new #HdyCarouselIndicatorLines widget. + * + * Returns: (transfer full): The newly created #HdyCarouselIndicatorLines widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_indicator_lines_new (void) +{ + return g_object_new (HDY_TYPE_CAROUSEL_INDICATOR_LINES, NULL); +} + +/** + * hdy_carousel_indicator_lines_get_carousel: + * @self: a #HdyCarouselIndicatorLines + * + * Get the #HdyCarousel the indicator uses. + * + * See: hdy_carousel_indicator_lines_set_carousel() + * + * Returns: (nullable) (transfer none): the #HdyCarousel, or %NULL if none has been set + * + * Since: 1.0 + */ + +HdyCarousel * +hdy_carousel_indicator_lines_get_carousel (HdyCarouselIndicatorLines *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL_INDICATOR_LINES (self), NULL); + + return self->carousel; +} + +/** + * hdy_carousel_indicator_lines_set_carousel: + * @self: a #HdyCarouselIndicatorLines + * @carousel: (nullable): a #HdyCarousel + * + * Sets the #HdyCarousel to use. + * + * Since: 1.0 + */ +void +hdy_carousel_indicator_lines_set_carousel (HdyCarouselIndicatorLines *self, + HdyCarousel *carousel) +{ + g_return_if_fail (HDY_IS_CAROUSEL_INDICATOR_LINES (self)); + g_return_if_fail (HDY_IS_CAROUSEL (carousel) || carousel == NULL); + + if (self->carousel == carousel) + return; + + if (self->carousel) { + stop_animation (self); + g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self); + g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self); + } + + g_set_object (&self->carousel, carousel); + + if (self->carousel) { + g_signal_connect_object (self->carousel, "notify::position", + G_CALLBACK (gtk_widget_queue_draw), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->carousel, "notify::n-pages", + G_CALLBACK (n_pages_changed_cb), self, + G_CONNECT_SWAPPED); + } + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]); +} diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-lines.h b/subprojects/libhandy/src/hdy-carousel-indicator-lines.h new file mode 100644 index 0000000..baae57d --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel-indicator-lines.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-carousel.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_CAROUSEL_INDICATOR_LINES (hdy_carousel_indicator_lines_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyCarouselIndicatorLines, hdy_carousel_indicator_lines, HDY, CAROUSEL_INDICATOR_LINES, GtkDrawingArea) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_carousel_indicator_lines_new (void); + +HDY_AVAILABLE_IN_ALL +HdyCarousel *hdy_carousel_indicator_lines_get_carousel (HdyCarouselIndicatorLines *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_indicator_lines_set_carousel (HdyCarouselIndicatorLines *self, + HdyCarousel *carousel); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-carousel.c b/subprojects/libhandy/src/hdy-carousel.c new file mode 100644 index 0000000..7d8db55 --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel.c @@ -0,0 +1,1099 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-carousel.h" + +#include "hdy-animation-private.h" +#include "hdy-carousel-box-private.h" +#include "hdy-navigation-direction.h" +#include "hdy-swipe-tracker.h" +#include "hdy-swipeable.h" + +#include <math.h> + +#define DEFAULT_DURATION 250 + +/** + * SECTION:hdy-carousel + * @short_description: A paginated scrolling widget. + * @title: HdyCarousel + * @See_also: #HdyCarouselIndicatorDots, #HdyCarouselIndicatorLines + * + * The #HdyCarousel widget can be used to display a set of pages with + * swipe-based navigation between them. + * + * # CSS nodes + * + * #HdyCarousel has a single CSS node with name carousel. + * + * Since: 1.0 + */ + +struct _HdyCarousel +{ + GtkEventBox parent_instance; + + HdyCarouselBox *scrolling_box; + + HdySwipeTracker *tracker; + + GtkOrientation orientation; + guint animation_duration; + + gulong scroll_timeout_id; + gboolean can_scroll; +}; + +static void hdy_carousel_swipeable_init (HdySwipeableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyCarousel, hdy_carousel, GTK_TYPE_EVENT_BOX, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL) + G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_carousel_swipeable_init)) + +enum { + PROP_0, + PROP_N_PAGES, + PROP_POSITION, + PROP_INTERACTIVE, + PROP_SPACING, + PROP_ANIMATION_DURATION, + PROP_ALLOW_MOUSE_DRAG, + PROP_REVEAL_DURATION, + + /* GtkOrientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_REVEAL_DURATION + 1, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_PAGE_CHANGED, + SIGNAL_LAST_SIGNAL, +}; +static guint signals[SIGNAL_LAST_SIGNAL]; + + +static void +hdy_carousel_switch_child (HdySwipeable *swipeable, + guint index, + gint64 duration) +{ + HdyCarousel *self = HDY_CAROUSEL (swipeable); + GtkWidget *child; + + child = hdy_carousel_box_get_nth_child (self->scrolling_box, index); + + hdy_carousel_box_scroll_to (self->scrolling_box, child, duration); +} + +static void +begin_swipe_cb (HdySwipeTracker *tracker, + HdyNavigationDirection direction, + gboolean direct, + HdyCarousel *self) +{ + hdy_carousel_box_stop_animation (self->scrolling_box); +} + +static void +update_swipe_cb (HdySwipeTracker *tracker, + gdouble progress, + HdyCarousel *self) +{ + hdy_carousel_box_set_position (self->scrolling_box, progress); +} + +static void +end_swipe_cb (HdySwipeTracker *tracker, + gint64 duration, + gdouble to, + HdyCarousel *self) +{ + GtkWidget *child; + + child = hdy_carousel_box_get_page_at_position (self->scrolling_box, to); + hdy_carousel_box_scroll_to (self->scrolling_box, child, duration); +} + +static HdySwipeTracker * +hdy_carousel_get_swipe_tracker (HdySwipeable *swipeable) +{ + HdyCarousel *self = HDY_CAROUSEL (swipeable); + + return self->tracker; +} + +static gdouble +hdy_carousel_get_distance (HdySwipeable *swipeable) +{ + HdyCarousel *self = HDY_CAROUSEL (swipeable); + + return hdy_carousel_box_get_distance (self->scrolling_box); +} + +static gdouble * +hdy_carousel_get_snap_points (HdySwipeable *swipeable, + gint *n_snap_points) +{ + HdyCarousel *self = HDY_CAROUSEL (swipeable); + + return hdy_carousel_box_get_snap_points (self->scrolling_box, + n_snap_points); +} + +static gdouble +hdy_carousel_get_progress (HdySwipeable *swipeable) +{ + HdyCarousel *self = HDY_CAROUSEL (swipeable); + + return hdy_carousel_get_position (self); +} + +static gdouble +hdy_carousel_get_cancel_progress (HdySwipeable *swipeable) +{ + HdyCarousel *self = HDY_CAROUSEL (swipeable); + + return hdy_carousel_box_get_closest_snap_point (self->scrolling_box); +} + +static void +notify_n_pages_cb (HdyCarousel *self, + GParamSpec *spec, + GObject *object) +{ + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]); +} + +static void +notify_position_cb (HdyCarousel *self, + GParamSpec *spec, + GObject *object) +{ + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]); +} + +static void +notify_spacing_cb (HdyCarousel *self, + GParamSpec *spec, + GObject *object) +{ + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]); +} + +static void +notify_reveal_duration_cb (HdyCarousel *self, + GParamSpec *spec, + GObject *object) +{ + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]); +} + +static void +animation_stopped_cb (HdyCarousel *self, + HdyCarouselBox *box) +{ + gint index; + + index = hdy_carousel_box_get_current_page_index (self->scrolling_box); + + g_signal_emit (self, signals[SIGNAL_PAGE_CHANGED], 0, index); +} + +static void +position_shifted_cb (HdyCarousel *self, + gdouble delta, + HdyCarouselBox *box) +{ + hdy_swipe_tracker_shift_position (self->tracker, delta); +} + +/* Copied from GtkOrientable. Orientable widgets are supposed + * to do this manually via a private GTK function. */ +static void +set_orientable_style_classes (GtkOrientable *orientable) +{ + GtkStyleContext *context; + GtkOrientation orientation; + + g_return_if_fail (GTK_IS_ORIENTABLE (orientable)); + g_return_if_fail (GTK_IS_WIDGET (orientable)); + + context = gtk_widget_get_style_context (GTK_WIDGET (orientable)); + orientation = gtk_orientable_get_orientation (orientable); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + { + gtk_style_context_add_class (context, GTK_STYLE_CLASS_HORIZONTAL); + gtk_style_context_remove_class (context, GTK_STYLE_CLASS_VERTICAL); + } + else + { + gtk_style_context_add_class (context, GTK_STYLE_CLASS_VERTICAL); + gtk_style_context_remove_class (context, GTK_STYLE_CLASS_HORIZONTAL); + } +} + +static void +update_orientation (HdyCarousel *self) +{ + gboolean reversed; + + if (!self->scrolling_box) + return; + + reversed = self->orientation == GTK_ORIENTATION_HORIZONTAL && + gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; + + g_object_set (self->scrolling_box, "orientation", self->orientation, NULL); + g_object_set (self->tracker, "orientation", self->orientation, + "reversed", reversed, NULL); + + set_orientable_style_classes (GTK_ORIENTABLE (self)); + set_orientable_style_classes (GTK_ORIENTABLE (self->scrolling_box)); +} + +static gboolean +scroll_timeout_cb (HdyCarousel *self) +{ + self->can_scroll = TRUE; + return G_SOURCE_REMOVE; +} + +static gboolean +scroll_event_cb (HdyCarousel *self, + GdkEvent *event) +{ + GdkDevice *source_device; + GdkInputSource input_source; + GdkScrollDirection direction; + gdouble dx, dy; + gint index; + gboolean allow_vertical; + GtkOrientation orientation; + guint duration; + + if (!self->can_scroll) + return GDK_EVENT_PROPAGATE; + + if (!hdy_carousel_get_interactive (self)) + return GDK_EVENT_PROPAGATE; + + if (event->type != GDK_SCROLL) + return GDK_EVENT_PROPAGATE; + + source_device = gdk_event_get_source_device (event); + input_source = gdk_device_get_source (source_device); + if (input_source == GDK_SOURCE_TOUCHPAD) + return GDK_EVENT_PROPAGATE; + + /* Mice often don't have easily accessible horizontal scrolling, + * hence allow vertical mouse scrolling regardless of orientation */ + allow_vertical = (input_source == GDK_SOURCE_MOUSE); + + if (gdk_event_get_scroll_direction (event, &direction)) { + dx = 0; + dy = 0; + + switch (direction) { + case GDK_SCROLL_UP: + dy = -1; + break; + case GDK_SCROLL_DOWN: + dy = 1; + break; + case GDK_SCROLL_LEFT: + dy = -1; + break; + case GDK_SCROLL_RIGHT: + dy = 1; + break; + case GDK_SCROLL_SMOOTH: + g_assert_not_reached (); + default: + return GDK_EVENT_PROPAGATE; + } + } else { + gdk_event_get_scroll_deltas (event, &dx, &dy); + } + + orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (self)); + index = 0; + + if (orientation == GTK_ORIENTATION_VERTICAL || allow_vertical) { + if (dy > 0) + index++; + else if (dy < 0) + index--; + } + + if (orientation == GTK_ORIENTATION_HORIZONTAL && index == 0) { + if (dx > 0) + index++; + else if (dx < 0) + index--; + } + + if (index == 0) + return GDK_EVENT_PROPAGATE; + + index += hdy_carousel_box_get_current_page_index (self->scrolling_box); + index = CLAMP (index, 0, (gint) hdy_carousel_get_n_pages (self) - 1); + + hdy_carousel_scroll_to (self, hdy_carousel_box_get_nth_child (self->scrolling_box, index)); + + /* Don't allow the delay to go lower than 250ms */ + duration = MIN (self->animation_duration, DEFAULT_DURATION); + + self->can_scroll = FALSE; + g_timeout_add (duration, (GSourceFunc) scroll_timeout_cb, self); + + return GDK_EVENT_STOP; +} + +static void +hdy_carousel_destroy (GtkWidget *widget) +{ + HdyCarousel *self = HDY_CAROUSEL (widget); + + if (self->scrolling_box) { + gtk_widget_destroy (GTK_WIDGET (self->scrolling_box)); + self->scrolling_box = NULL; + } + + GTK_WIDGET_CLASS (hdy_carousel_parent_class)->destroy (widget); +} + +static void +hdy_carousel_direction_changed (GtkWidget *widget, + GtkTextDirection previous_direction) +{ + HdyCarousel *self = HDY_CAROUSEL (widget); + + update_orientation (self); +} + +static void +hdy_carousel_add (GtkContainer *container, + GtkWidget *widget) +{ + HdyCarousel *self = HDY_CAROUSEL (container); + + if (self->scrolling_box) + gtk_container_add (GTK_CONTAINER (self->scrolling_box), widget); + else + GTK_CONTAINER_CLASS (hdy_carousel_parent_class)->add (container, widget); +} + +static void +hdy_carousel_remove (GtkContainer *container, + GtkWidget *widget) +{ + HdyCarousel *self = HDY_CAROUSEL (container); + + if (self->scrolling_box) + gtk_container_remove (GTK_CONTAINER (self->scrolling_box), widget); + else + GTK_CONTAINER_CLASS (hdy_carousel_parent_class)->remove (container, widget); +} + +static void +hdy_carousel_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyCarousel *self = HDY_CAROUSEL (container); + + if (include_internals) + (* callback) (GTK_WIDGET (self->scrolling_box), callback_data); + else if (self->scrolling_box) + gtk_container_foreach (GTK_CONTAINER (self->scrolling_box), + callback, callback_data); +} + +static void +hdy_carousel_constructed (GObject *object) +{ + HdyCarousel *self = (HdyCarousel *)object; + + update_orientation (self); + + G_OBJECT_CLASS (hdy_carousel_parent_class)->constructed (object); +} + +static void +hdy_carousel_dispose (GObject *object) +{ + HdyCarousel *self = (HdyCarousel *)object; + + g_clear_object (&self->tracker); + + if (self->scroll_timeout_id != 0) { + g_source_remove (self->scroll_timeout_id); + self->scroll_timeout_id = 0; + } + + G_OBJECT_CLASS (hdy_carousel_parent_class)->dispose (object); +} + +static void +hdy_carousel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyCarousel *self = HDY_CAROUSEL (object); + + switch (prop_id) { + case PROP_N_PAGES: + g_value_set_uint (value, hdy_carousel_get_n_pages (self)); + break; + + case PROP_POSITION: + g_value_set_double (value, hdy_carousel_get_position (self)); + break; + + case PROP_INTERACTIVE: + g_value_set_boolean (value, hdy_carousel_get_interactive (self)); + break; + + case PROP_SPACING: + g_value_set_uint (value, hdy_carousel_get_spacing (self)); + break; + + case PROP_ALLOW_MOUSE_DRAG: + g_value_set_boolean (value, hdy_carousel_get_allow_mouse_drag (self)); + break; + + case PROP_REVEAL_DURATION: + g_value_set_uint (value, hdy_carousel_get_reveal_duration (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + case PROP_ANIMATION_DURATION: + g_value_set_uint (value, hdy_carousel_get_animation_duration (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyCarousel *self = HDY_CAROUSEL (object); + + switch (prop_id) { + case PROP_INTERACTIVE: + hdy_carousel_set_interactive (self, g_value_get_boolean (value)); + break; + + case PROP_SPACING: + hdy_carousel_set_spacing (self, g_value_get_uint (value)); + break; + + case PROP_ANIMATION_DURATION: + hdy_carousel_set_animation_duration (self, g_value_get_uint (value)); + break; + + case PROP_REVEAL_DURATION: + hdy_carousel_set_reveal_duration (self, g_value_get_uint (value)); + break; + + case PROP_ALLOW_MOUSE_DRAG: + hdy_carousel_set_allow_mouse_drag (self, g_value_get_boolean (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = orientation; + update_orientation (self); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_carousel_swipeable_init (HdySwipeableInterface *iface) +{ + iface->switch_child = hdy_carousel_switch_child; + iface->get_swipe_tracker = hdy_carousel_get_swipe_tracker; + iface->get_distance = hdy_carousel_get_distance; + iface->get_snap_points = hdy_carousel_get_snap_points; + iface->get_progress = hdy_carousel_get_progress; + iface->get_cancel_progress = hdy_carousel_get_cancel_progress; +} + +static void +hdy_carousel_class_init (HdyCarouselClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->constructed = hdy_carousel_constructed; + object_class->dispose = hdy_carousel_dispose; + object_class->get_property = hdy_carousel_get_property; + object_class->set_property = hdy_carousel_set_property; + widget_class->destroy = hdy_carousel_destroy; + widget_class->direction_changed = hdy_carousel_direction_changed; + container_class->add = hdy_carousel_add; + container_class->remove = hdy_carousel_remove; + container_class->forall = hdy_carousel_forall; + + /** + * HdyCarousel:n-pages: + * + * The number of pages in a #HdyCarousel + * + * Since: 1.0 + */ + props[PROP_N_PAGES] = + g_param_spec_uint ("n-pages", + _("Number of pages"), + _("Number of pages"), + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarousel:position: + * + * Current scrolling position, unitless. 1 matches 1 page. Use + * hdy_carousel_scroll_to() for changing it. + * + * Since: 1.0 + */ + props[PROP_POSITION] = + g_param_spec_double ("position", + _("Position"), + _("Current scrolling position"), + 0, + G_MAXDOUBLE, + 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarousel:interactive: + * + * Whether the carousel can be navigated. This can be used to temporarily + * disable a #HdyCarousel to only allow navigating it in a certain state. + * + * Since: 1.0 + */ + props[PROP_INTERACTIVE] = + g_param_spec_boolean ("interactive", + _("Interactive"), + _("Whether the widget can be swiped"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarousel:spacing: + * + * Spacing between pages in pixels. + * + * Since: 1.0 + */ + props[PROP_SPACING] = + g_param_spec_uint ("spacing", + _("Spacing"), + _("Spacing between pages"), + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarousel:animation-duration: + * + * Animation duration in milliseconds, used by hdy_carousel_scroll_to(). + * + * Since: 1.0 + */ + props[PROP_ANIMATION_DURATION] = + g_param_spec_uint ("animation-duration", + _("Animation duration"), + _("Default animation duration"), + 0, G_MAXUINT, DEFAULT_DURATION, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarousel:allow-mouse-drag: + * + * Sets whether the #HdyCarousel can be dragged with mouse pointer. If the + * value is %FALSE, dragging is only available on touch. + * + * Since: 1.0 + */ + props[PROP_ALLOW_MOUSE_DRAG] = + g_param_spec_boolean ("allow-mouse-drag", + _("Allow mouse drag"), + _("Whether to allow dragging with mouse pointer"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyCarousel:reveal-duration: + * + * Page reveal duration in milliseconds. + * + * Since: 1.0 + */ + props[PROP_REVEAL_DURATION] = + g_param_spec_uint ("reveal-duration", + _("Reveal duration"), + _("Page reveal duration"), + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * HdyCarousel::page-changed: + * @self: The #HdyCarousel instance + * @index: Current page + * + * This signal is emitted after a page has been changed. This can be used to + * implement "infinite scrolling" by connecting to this signal and amending + * the pages. + * + * Since: 1.0 + */ + signals[SIGNAL_PAGE_CHANGED] = + g_signal_new ("page-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_UINT); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-carousel.ui"); + gtk_widget_class_bind_template_child (widget_class, HdyCarousel, scrolling_box); + gtk_widget_class_bind_template_callback (widget_class, scroll_event_cb); + gtk_widget_class_bind_template_callback (widget_class, notify_n_pages_cb); + gtk_widget_class_bind_template_callback (widget_class, notify_position_cb); + gtk_widget_class_bind_template_callback (widget_class, notify_spacing_cb); + gtk_widget_class_bind_template_callback (widget_class, notify_reveal_duration_cb); + gtk_widget_class_bind_template_callback (widget_class, animation_stopped_cb); + gtk_widget_class_bind_template_callback (widget_class, position_shifted_cb); + + gtk_widget_class_set_css_name (widget_class, "carousel"); +} + +static void +hdy_carousel_init (HdyCarousel *self) +{ + g_type_ensure (HDY_TYPE_CAROUSEL_BOX); + gtk_widget_init_template (GTK_WIDGET (self)); + + self->animation_duration = DEFAULT_DURATION; + + self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self)); + hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, TRUE); + + g_signal_connect_object (self->tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self, 0); + g_signal_connect_object (self->tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self, 0); + g_signal_connect_object (self->tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self, 0); + + self->can_scroll = TRUE; +} + +/** + * hdy_carousel_new: + * + * Create a new #HdyCarousel widget. + * + * Returns: The newly created #HdyCarousel widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_carousel_new (void) +{ + return g_object_new (HDY_TYPE_CAROUSEL, NULL); +} + +/** + * hdy_carousel_prepend: + * @self: a #HdyCarousel + * @child: a widget to add + * + * Prepends @child to @self + * + * Since: 1.0 + */ +void +hdy_carousel_prepend (HdyCarousel *self, + GtkWidget *widget) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + hdy_carousel_box_insert (self->scrolling_box, widget, 0); +} + +/** + * hdy_carousel_insert: + * @self: a #HdyCarousel + * @child: a widget to add + * @position: the position to insert @child in. + * + * Inserts @child into @self at position @position. + * + * If position is -1, or larger than the number of pages, + * @child will be appended to the end. + * + * Since: 1.0 + */ +void +hdy_carousel_insert (HdyCarousel *self, + GtkWidget *widget, + gint position) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + hdy_carousel_box_insert (self->scrolling_box, widget, position); +} +/** + * hdy_carousel_reorder: + * @self: a #HdyCarousel + * @child: a widget to add + * @position: the position to move @child to. + * + * Moves @child into position @position. + * + * If position is -1, or larger than the number of pages, @child will be moved + * to the end. + * + * Since: 1.0 + */ +void +hdy_carousel_reorder (HdyCarousel *self, + GtkWidget *child, + gint position) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + g_return_if_fail (GTK_IS_WIDGET (child)); + + hdy_carousel_box_reorder (self->scrolling_box, child, position); +} + +/** + * hdy_carousel_scroll_to: + * @self: a #HdyCarousel + * @widget: a child of @self + * + * Scrolls to @widget position with an animation. + * #HdyCarousel:animation-duration property can be used for controlling the + * duration. + * + * Since: 1.0 + */ +void +hdy_carousel_scroll_to (HdyCarousel *self, + GtkWidget *widget) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + hdy_carousel_scroll_to_full (self, widget, self->animation_duration); +} + +/** + * hdy_carousel_scroll_to_full: + * @self: a #HdyCarousel + * @widget: a child of @self + * @duration: animation duration in milliseconds + * + * Scrolls to @widget position with an animation. + * + * Since: 1.0 + */ +void +hdy_carousel_scroll_to_full (HdyCarousel *self, + GtkWidget *widget, + gint64 duration) +{ + GList *children; + gint n; + + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + children = gtk_container_get_children (GTK_CONTAINER (self->scrolling_box)); + n = g_list_index (children, widget); + g_list_free (children); + + hdy_carousel_box_scroll_to (self->scrolling_box, widget, + duration); + hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self), n, duration); +} + +/** + * hdy_carousel_get_n_pages: + * @self: a #HdyCarousel + * + * Gets the number of pages in @self. + * + * Returns: The number of pages in @self + * + * Since: 1.0 + */ +guint +hdy_carousel_get_n_pages (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0); + + return hdy_carousel_box_get_n_pages (self->scrolling_box); +} + +/** + * hdy_carousel_get_position: + * @self: a #HdyCarousel + * + * Gets current scroll position in @self. It's unitless, 1 matches 1 page. + * + * Returns: The scroll position + * + * Since: 1.0 + */ +gdouble +hdy_carousel_get_position (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0); + + return hdy_carousel_box_get_position (self->scrolling_box); +} + +/** + * hdy_carousel_get_interactive + * @self: a #HdyCarousel + * + * Gets whether @self can be navigated. + * + * Returns: %TRUE if @self can be swiped + * + * Since: 1.0 + */ +gboolean +hdy_carousel_get_interactive (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), FALSE); + + return hdy_swipe_tracker_get_enabled (self->tracker); +} + +/** + * hdy_carousel_set_interactive + * @self: a #HdyCarousel + * @interactive: whether @self can be swiped. + * + * Sets whether @self can be navigated. This can be used to temporarily disable + * a #HdyCarousel to only allow swiping in a certain state. + * + * Since: 1.0 + */ +void +hdy_carousel_set_interactive (HdyCarousel *self, + gboolean interactive) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + interactive = !!interactive; + + if (hdy_swipe_tracker_get_enabled (self->tracker) == interactive) + return; + + hdy_swipe_tracker_set_enabled (self->tracker, interactive); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERACTIVE]); +} + +/** + * hdy_carousel_get_spacing: + * @self: a #HdyCarousel + * + * Gets spacing between pages in pixels. + * + * Returns: Spacing between pages + * + * Since: 1.0 + */ +guint +hdy_carousel_get_spacing (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0); + + return hdy_carousel_box_get_spacing (self->scrolling_box); +} + +/** + * hdy_carousel_set_spacing: + * @self: a #HdyCarousel + * @spacing: the new spacing value + * + * Sets spacing between pages in pixels. + * + * Since: 1.0 + */ +void +hdy_carousel_set_spacing (HdyCarousel *self, + guint spacing) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + hdy_carousel_box_set_spacing (self->scrolling_box, spacing); +} + +/** + * hdy_carousel_get_animation_duration: + * @self: a #HdyCarousel + * + * Gets animation duration used by hdy_carousel_scroll_to(). + * + * Returns: Animation duration in milliseconds + * + * Since: 1.0 + */ +guint +hdy_carousel_get_animation_duration (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0); + + return self->animation_duration; +} + +/** + * hdy_carousel_set_animation_duration: + * @self: a #HdyCarousel + * @duration: animation duration in milliseconds + * + * Sets animation duration used by hdy_carousel_scroll_to(). + * + * Since: 1.0 + */ +void +hdy_carousel_set_animation_duration (HdyCarousel *self, + guint duration) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + if (self->animation_duration == duration) + return; + + self->animation_duration = duration; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ANIMATION_DURATION]); +} + +/** + * hdy_carousel_get_allow_mouse_drag: + * @self: a #HdyCarousel + * + * Sets whether @self can be dragged with mouse pointer + * + * Returns: %TRUE if @self can be dragged with mouse + * + * Since: 1.0 + */ +gboolean +hdy_carousel_get_allow_mouse_drag (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), FALSE); + + return hdy_swipe_tracker_get_allow_mouse_drag (self->tracker); +} + +/** + * hdy_carousel_set_allow_mouse_drag: + * @self: a #HdyCarousel + * @allow_mouse_drag: whether @self can be dragged with mouse pointer + * + * Sets whether @self can be dragged with mouse pointer. If @allow_mouse_drag + * is %FALSE, dragging is only available on touch. + * + * Since: 1.0 + */ +void +hdy_carousel_set_allow_mouse_drag (HdyCarousel *self, + gboolean allow_mouse_drag) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + allow_mouse_drag = !!allow_mouse_drag; + + if (hdy_carousel_get_allow_mouse_drag (self) == allow_mouse_drag) + return; + + hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, allow_mouse_drag); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]); +} + +/** + * hdy_carousel_get_reveal_duration: + * @self: a #HdyCarousel + * + * Gets duration of the animation used when adding or removing pages in + * milliseconds. + * + * Returns: Page reveal duration + * + * Since: 1.0 + */ +guint +hdy_carousel_get_reveal_duration (HdyCarousel *self) +{ + g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0); + + return hdy_carousel_box_get_reveal_duration (self->scrolling_box); +} + +/** + * hdy_carousel_set_reveal_duration: + * @self: a #HdyCarousel + * @reveal_duration: the new reveal duration value + * + * Sets duration of the animation used when adding or removing pages in + * milliseconds. + * + * Since: 1.0 + */ +void +hdy_carousel_set_reveal_duration (HdyCarousel *self, + guint reveal_duration) +{ + g_return_if_fail (HDY_IS_CAROUSEL (self)); + + hdy_carousel_box_set_reveal_duration (self->scrolling_box, reveal_duration); +} diff --git a/subprojects/libhandy/src/hdy-carousel.h b/subprojects/libhandy/src/hdy-carousel.h new file mode 100644 index 0000000..4318b65 --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_CAROUSEL (hdy_carousel_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyCarousel, hdy_carousel, HDY, CAROUSEL, GtkEventBox) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_carousel_new (void); + +HDY_AVAILABLE_IN_ALL +void hdy_carousel_prepend (HdyCarousel *self, + GtkWidget *child); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_insert (HdyCarousel *self, + GtkWidget *child, + gint position); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_reorder (HdyCarousel *self, + GtkWidget *child, + gint position); + +HDY_AVAILABLE_IN_ALL +void hdy_carousel_scroll_to (HdyCarousel *self, + GtkWidget *widget); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_scroll_to_full (HdyCarousel *self, + GtkWidget *widget, + gint64 duration); + +HDY_AVAILABLE_IN_ALL +guint hdy_carousel_get_n_pages (HdyCarousel *self); +HDY_AVAILABLE_IN_ALL +gdouble hdy_carousel_get_position (HdyCarousel *self); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_carousel_get_interactive (HdyCarousel *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_set_interactive (HdyCarousel *self, + gboolean interactive); + +HDY_AVAILABLE_IN_ALL +guint hdy_carousel_get_spacing (HdyCarousel *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_set_spacing (HdyCarousel *self, + guint spacing); + +HDY_AVAILABLE_IN_ALL +guint hdy_carousel_get_animation_duration (HdyCarousel *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_set_animation_duration (HdyCarousel *self, + guint duration); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_carousel_get_allow_mouse_drag (HdyCarousel *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_set_allow_mouse_drag (HdyCarousel *self, + gboolean allow_mouse_drag); + +HDY_AVAILABLE_IN_ALL +guint hdy_carousel_get_reveal_duration (HdyCarousel *self); +HDY_AVAILABLE_IN_ALL +void hdy_carousel_set_reveal_duration (HdyCarousel *self, + guint reveal_duration); +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-carousel.ui b/subprojects/libhandy/src/hdy-carousel.ui new file mode 100644 index 0000000..c9bf553 --- /dev/null +++ b/subprojects/libhandy/src/hdy-carousel.ui @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="HdyCarousel" parent="GtkEventBox"> + <property name="visible">True</property> + <property name="orientation">horizontal</property> + <signal name="scroll-event" handler="scroll_event_cb"/> + <child> + <object class="HdyCarouselBox" id="scrolling_box"> + <property name="visible">True</property> + <property name="expand">True</property> + <signal name="notify::n-pages" handler="notify_n_pages_cb" swapped="true"/> + <signal name="notify::position" handler="notify_position_cb" swapped="true"/> + <signal name="notify::spacing" handler="notify_spacing_cb" swapped="true"/> + <signal name="notify::reveal-duration" handler="notify_reveal_duration_cb" swapped="true"/> + <signal name="animation-stopped" handler="animation_stopped_cb" swapped="true"/> + <signal name="position-shifted" handler="position_shifted_cb" swapped="true"/> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-clamp.c b/subprojects/libhandy/src/hdy-clamp.c new file mode 100644 index 0000000..9cb9f23 --- /dev/null +++ b/subprojects/libhandy/src/hdy-clamp.c @@ -0,0 +1,563 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include "hdy-clamp.h" + +#include <glib/gi18n-lib.h> +#include <math.h> + +#include "hdy-animation-private.h" + +/** + * SECTION:hdy-clamp + * @short_description: A container constraining its child to a given size. + * @Title: HdyClamp + * + * The #HdyClamp widget constraints the size of the widget it contains to a + * given maximum size. It will constrain the width if it is horizontal, or the + * height if it is vertical. The expansion of the child from its minimum to its + * maximum size is eased out for a smooth transition. + * + * If the child requires more than the requested maximum size, it will be + * allocated the minimum size it can fit in instead. + * + * # CSS nodes + * + * #HdyClamp has a single CSS node with name clamp. The node will get the style + * classes .large when its child reached its maximum size, .small when the clamp + * allocates its full size to its child, .medium in-between, or none if it + * didn't compute its size yet. + * + * Since: 1.0 + */ + +#define HDY_EASE_OUT_TAN_CUBIC 3 + +enum { + PROP_0, + PROP_MAXIMUM_SIZE, + PROP_TIGHTENING_THRESHOLD, + + /* Overridden properties */ + PROP_ORIENTATION, + + LAST_PROP = PROP_TIGHTENING_THRESHOLD + 1, +}; + +struct _HdyClamp +{ + GtkBin parent_instance; + + gint maximum_size; + gint tightening_threshold; + + GtkOrientation orientation; +}; + +static GParamSpec *props[LAST_PROP]; + +G_DEFINE_TYPE_WITH_CODE (HdyClamp, hdy_clamp, GTK_TYPE_BIN, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +static void +set_orientation (HdyClamp *self, + GtkOrientation orientation) +{ + if (self->orientation == orientation) + return; + + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); +} + +static void +hdy_clamp_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyClamp *self = HDY_CLAMP (object); + + switch (prop_id) { + case PROP_MAXIMUM_SIZE: + g_value_set_int (value, hdy_clamp_get_maximum_size (self)); + break; + case PROP_TIGHTENING_THRESHOLD: + g_value_set_int (value, hdy_clamp_get_tightening_threshold (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_clamp_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyClamp *self = HDY_CLAMP (object); + + switch (prop_id) { + case PROP_MAXIMUM_SIZE: + hdy_clamp_set_maximum_size (self, g_value_get_int (value)); + break; + case PROP_TIGHTENING_THRESHOLD: + hdy_clamp_set_tightening_threshold (self, g_value_get_int (value)); + break; + case PROP_ORIENTATION: + set_orientation (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +/** + * get_child_size: + * @self: a #HdyClamp + * @for_size: the size of the clamp + * @child_minimum: the minimum size reachable by the child, and hence by @self + * @child_maximum: the maximum size @self will ever allocate its child + * @lower_threshold: the threshold below which @self will allocate its full size to its child + * @upper_threshold: the threshold up from which @self will allocate its maximum size to its child + * + * Measures the child's extremes, the clamp's thresholds, and returns size to + * allocate to the child. + * + * If the clamp is horizontal, all values are widths, otherwise they are + * heights. + */ +static gint +get_child_size (HdyClamp *self, + gint for_size, + gint *child_minimum, + gint *child_maximum, + gint *lower_threshold, + gint *upper_threshold) +{ + GtkBin *bin = GTK_BIN (self); + GtkWidget *child; + gint min = 0, max = 0, lower = 0, upper = 0; + gdouble amplitude, progress; + + child = gtk_bin_get_child (bin); + if (child == NULL) + return 0; + + if (gtk_widget_get_visible (child)) { + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width (child, &min, NULL); + else + gtk_widget_get_preferred_height (child, &min, NULL); + } + + lower = MAX (MIN (self->tightening_threshold, self->maximum_size), min); + max = MAX (lower, self->maximum_size); + amplitude = max - lower; + upper = HDY_EASE_OUT_TAN_CUBIC * amplitude + lower; + + if (child_minimum) + *child_minimum = min; + if (child_maximum) + *child_maximum = max; + if (lower_threshold) + *lower_threshold = lower; + if (upper_threshold) + *upper_threshold = upper; + + if (for_size < 0) + return 0; + + if (for_size <= lower) + return for_size; + + if (for_size >= upper) + return max; + + progress = (double) (for_size - lower) / (double) (upper - lower); + + return hdy_ease_out_cubic (progress) * amplitude + lower; +} + +/* This private method is prefixed by the call name because it will be a virtual + * method in GTK 4. + */ +static void +hdy_clamp_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + HdyClamp *self = HDY_CLAMP (widget); + GtkBin *bin = GTK_BIN (widget); + GtkWidget *child; + gint child_size; + + if (minimum) + *minimum = 0; + if (natural) + *natural = 0; + if (minimum_baseline) + *minimum_baseline = -1; + if (natural_baseline) + *natural_baseline = -1; + + child = gtk_bin_get_child (bin); + if (!(child && gtk_widget_get_visible (child))) + return; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + if (self->orientation == orientation) { + gtk_widget_get_preferred_width (child, minimum, natural); + + return; + } + + child_size = get_child_size (HDY_CLAMP (widget), for_size, NULL, NULL, NULL, NULL); + + gtk_widget_get_preferred_width_for_height (child, + child_size, + minimum, + natural); + } else { + if (self->orientation == orientation) { + gtk_widget_get_preferred_height (child, minimum, natural); + + return; + } + + child_size = get_child_size (HDY_CLAMP (widget), for_size, NULL, NULL, NULL, NULL); + + gtk_widget_get_preferred_height_and_baseline_for_width (child, + child_size, + minimum, + natural, + minimum_baseline, + natural_baseline); + } +} + +static GtkSizeRequestMode +hdy_clamp_get_request_mode (GtkWidget *widget) +{ + HdyClamp *self = HDY_CLAMP (widget); + + return self->orientation == GTK_ORIENTATION_HORIZONTAL ? + GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : + GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; +} + +static void +hdy_clamp_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + hdy_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum, natural, NULL, NULL); +} + +static void +hdy_clamp_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_clamp_get_preferred_height_and_baseline_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, minimum_baseline, natural_baseline); +} + +static void +hdy_clamp_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, NULL, NULL); +} + +static void +hdy_clamp_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_clamp_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + HdyClamp *self = HDY_CLAMP (widget); + GtkBin *bin = GTK_BIN (widget); + GtkAllocation child_allocation; + gint baseline; + GtkWidget *child; + GtkStyleContext *context = gtk_widget_get_style_context (widget); + gint child_maximum = 0, lower_threshold = 0; + gint child_clamped_size; + + gtk_widget_set_allocation (widget, allocation); + + child = gtk_bin_get_child (bin); + if (!(child && gtk_widget_get_visible (child))) { + gtk_style_context_remove_class (context, "small"); + gtk_style_context_remove_class (context, "medium"); + gtk_style_context_remove_class (context, "large"); + + return; + } + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + child_allocation.width = get_child_size (self, allocation->width, NULL, &child_maximum, &lower_threshold, NULL); + child_allocation.height = allocation->height; + + child_clamped_size = child_allocation.width; + } + else { + child_allocation.width = allocation->width; + child_allocation.height = get_child_size (self, allocation->height, NULL, &child_maximum, &lower_threshold, NULL); + + child_clamped_size = child_allocation.height; + } + + if (child_clamped_size >= child_maximum) { + gtk_style_context_remove_class (context, "small"); + gtk_style_context_remove_class (context, "medium"); + gtk_style_context_add_class (context, "large"); + } else if (child_clamped_size <= lower_threshold) { + gtk_style_context_add_class (context, "small"); + gtk_style_context_remove_class (context, "medium"); + gtk_style_context_remove_class (context, "large"); + } else { + gtk_style_context_remove_class (context, "small"); + gtk_style_context_add_class (context, "medium"); + gtk_style_context_remove_class (context, "large"); + } + + if (!gtk_widget_get_has_window (widget)) { + /* This always center the child on the side of the orientation. */ + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + child_allocation.x = allocation->x + (allocation->width - child_allocation.width) / 2; + child_allocation.y = allocation->y; + } else { + child_allocation.x = allocation->x; + child_allocation.y = allocation->y + (allocation->height - child_allocation.height) / 2; + } + } + else { + child_allocation.x = 0; + child_allocation.y = 0; + } + + baseline = gtk_widget_get_allocated_baseline (widget); + gtk_widget_size_allocate_with_baseline (child, &child_allocation, baseline); +} + +static void +hdy_clamp_class_init (HdyClampClass *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 = hdy_clamp_get_property; + object_class->set_property = hdy_clamp_set_property; + + widget_class->get_request_mode = hdy_clamp_get_request_mode; + widget_class->get_preferred_width = hdy_clamp_get_preferred_width; + widget_class->get_preferred_width_for_height = hdy_clamp_get_preferred_width_for_height; + widget_class->get_preferred_height = hdy_clamp_get_preferred_height; + widget_class->get_preferred_height_for_width = hdy_clamp_get_preferred_height_for_width; + widget_class->get_preferred_height_and_baseline_for_width = hdy_clamp_get_preferred_height_and_baseline_for_width; + widget_class->size_allocate = hdy_clamp_size_allocate; + + gtk_container_class_handle_border_width (container_class); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + /** + * HdyClamp:maximum-size: + * + * The maximum size to allocate to the child. It is the width if the clamp is + * horizontal, or the height if it is vertical. + * + * Since: 1.0 + */ + props[PROP_MAXIMUM_SIZE] = + g_param_spec_int ("maximum-size", + _("Maximum size"), + _("The maximum size allocated to the child"), + 0, G_MAXINT, 600, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyClamp:tightening-threshold: + * + * The size starting from which the clamp will tighten its grip on the child, + * slowly allocating less and less of the available size up to the maximum + * allocated size. Below that threshold and below the maximum width, the child + * will be allocated all the available size. + * + * If the threshold is greater than the maximum size to allocate to the child, + * the child will be allocated all the width up to the maximum. + * If the threshold is lower than the minimum size to allocate to the child, + * that size will be used as the tightening threshold. + * + * Effectively, tightening the grip on the child before it reaches its maximum + * size makes transitions to and from the maximum size smoother when resizing. + * + * Since: 1.0 + */ + props[PROP_TIGHTENING_THRESHOLD] = + g_param_spec_int ("tightening-threshold", + _("Tightening threshold"), + _("The size from which the clamp will tighten its grip on the child"), + 0, G_MAXINT, 400, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "clamp"); +} + +static void +hdy_clamp_init (HdyClamp *self) +{ + self->maximum_size = 600; + self->tightening_threshold = 400; +} + +/** + * hdy_clamp_new: + * + * Creates a new #HdyClamp. + * + * Returns: a new #HdyClamp + * + * Since: 1.0 + */ +GtkWidget * +hdy_clamp_new (void) +{ + return g_object_new (HDY_TYPE_CLAMP, NULL); +} + +/** + * hdy_clamp_get_maximum_size: + * @self: a #HdyClamp + * + * Gets the maximum size to allocate to the contained child. It is the width if + * @self is horizontal, or the height if it is vertical. + * + * Returns: the maximum width to allocate to the contained child. + * + * Since: 1.0 + */ +gint +hdy_clamp_get_maximum_size (HdyClamp *self) +{ + g_return_val_if_fail (HDY_IS_CLAMP (self), 0); + + return self->maximum_size; +} + +/** + * hdy_clamp_set_maximum_size: + * @self: a #HdyClamp + * @maximum_size: the maximum size + * + * Sets the maximum size to allocate to the contained child. It is the width if + * @self is horizontal, or the height if it is vertical. + * + * Since: 1.0 + */ +void +hdy_clamp_set_maximum_size (HdyClamp *self, + gint maximum_size) +{ + g_return_if_fail (HDY_IS_CLAMP (self)); + + if (self->maximum_size == maximum_size) + return; + + self->maximum_size = maximum_size; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MAXIMUM_SIZE]); +} + +/** + * hdy_clamp_get_tightening_threshold: + * @self: a #HdyClamp + * + * Gets the size starting from which the clamp will tighten its grip on the + * child. + * + * Returns: the size starting from which the clamp will tighten its grip on the + * child. + * + * Since: 1.0 + */ +gint +hdy_clamp_get_tightening_threshold (HdyClamp *self) +{ + g_return_val_if_fail (HDY_IS_CLAMP (self), 0); + + return self->tightening_threshold; +} + +/** + * hdy_clamp_set_tightening_threshold: + * @self: a #HdyClamp + * @tightening_threshold: the tightening threshold + * + * Sets the size starting from which the clamp will tighten its grip on the + * child. + * + * Since: 1.0 + */ +void +hdy_clamp_set_tightening_threshold (HdyClamp *self, + gint tightening_threshold) +{ + g_return_if_fail (HDY_IS_CLAMP (self)); + + if (self->tightening_threshold == tightening_threshold) + return; + + self->tightening_threshold = tightening_threshold; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIGHTENING_THRESHOLD]); +} diff --git a/subprojects/libhandy/src/hdy-clamp.h b/subprojects/libhandy/src/hdy-clamp.h new file mode 100644 index 0000000..46ad6dd --- /dev/null +++ b/subprojects/libhandy/src/hdy-clamp.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_CLAMP (hdy_clamp_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyClamp, hdy_clamp, HDY, CLAMP, GtkBin) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_clamp_new (void); +HDY_AVAILABLE_IN_ALL +gint hdy_clamp_get_maximum_size (HdyClamp *self); +HDY_AVAILABLE_IN_ALL +void hdy_clamp_set_maximum_size (HdyClamp *self, + gint maximum_size); +HDY_AVAILABLE_IN_ALL +gint hdy_clamp_get_tightening_threshold (HdyClamp *self); +HDY_AVAILABLE_IN_ALL +void hdy_clamp_set_tightening_threshold (HdyClamp *self, + gint tightening_threshold); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-combo-row.c b/subprojects/libhandy/src/hdy-combo-row.c new file mode 100644 index 0000000..e9cc6bf --- /dev/null +++ b/subprojects/libhandy/src/hdy-combo-row.c @@ -0,0 +1,829 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include "hdy-combo-row.h" + +#include <glib/gi18n-lib.h> + +/** + * SECTION:hdy-combo-row + * @short_description: A #GtkListBox row used to choose from a list of items. + * @Title: HdyComboRow + * + * The #HdyComboRow widget allows the user to choose from a list of valid + * choices. The row displays the selected choice. When activated, the row + * displays a popover which allows the user to make a new choice. + * + * The #HdyComboRow uses the model-view pattern; the list of valid choices + * is specified in the form of a #GListModel, and the display of the choices can + * be adapted to the data in the model via widget creation functions. + * + * #HdyComboRow is #GtkListBoxRow:activatable if a model is set. + * + * # CSS nodes + * + * #HdyComboRow has a main CSS node with name row. + * + * Its popover has the node name popover with the .combo style class, it + * contains a #GtkScrolledWindow, which in turn contains a #GtkListBox, both are + * accessible via their regular nodes. + * + * A checkmark of node and style class image.checkmark in the popover denotes + * the current item. + * + * Since: 0.0.6 + */ + +/* + * This was mostly inspired by code from the display panel from GNOME Settings. + */ + +typedef struct +{ + HdyComboRowGetNameFunc func; + gpointer func_data; + GDestroyNotify func_data_destroy; +} HdyComboRowGetName; + +typedef struct +{ + GtkBox *current; + GtkImage *image; + GtkListBox *list; + GtkPopover *popover; + gint selected_index; + gboolean use_subtitle; + HdyComboRowGetName *get_name; + + GListModel *bound_model; + GtkListBoxCreateWidgetFunc create_list_widget_func; + GtkListBoxCreateWidgetFunc create_current_widget_func; + gpointer create_widget_func_data; + GDestroyNotify create_widget_func_data_free_func; + /* This is owned by create_widget_func_data, which is ultimately owned by the + * list box, and hence should not be destroyed manually. + */ + HdyComboRowGetName *get_name_internal; +} HdyComboRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdyComboRow, hdy_combo_row, HDY_TYPE_ACTION_ROW) + +enum { + PROP_0, + PROP_SELECTED_INDEX, + PROP_USE_SUBTITLE, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +static GtkWidget * +create_list_label (gpointer item, + gpointer user_data) +{ + HdyComboRowGetName *get_name = (HdyComboRowGetName *) user_data; + g_autofree gchar *name = get_name->func (item, get_name->func_data); + + return g_object_new (GTK_TYPE_LABEL, + "ellipsize", PANGO_ELLIPSIZE_END, + "label", name, + "max-width-chars", 20, + "valign", GTK_ALIGN_CENTER, + "visible", TRUE, + "xalign", 0.0, + NULL); +} + +static GtkWidget * +create_current_label (gpointer item, + gpointer user_data) +{ + HdyComboRowGetName *get_name = (HdyComboRowGetName *) user_data; + g_autofree gchar *name = NULL; + + if (get_name->func) + name = get_name->func (item, get_name->func_data); + + return g_object_new (GTK_TYPE_LABEL, + "ellipsize", PANGO_ELLIPSIZE_END, + "halign", GTK_ALIGN_END, + "label", name, + "valign", GTK_ALIGN_CENTER, + "visible", TRUE, + "xalign", 0.0, + NULL); +} + +static void +create_list_widget_data_free (gpointer user_data) +{ + HdyComboRow *self = HDY_COMBO_ROW (user_data); + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + if (priv->create_widget_func_data_free_func) + priv->create_widget_func_data_free_func (priv->create_widget_func_data); +} + +static GtkWidget * +create_list_widget (gpointer item, + gpointer user_data) +{ + HdyComboRow *self = HDY_COMBO_ROW (user_data); + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + GtkWidget *checkmark = g_object_new (GTK_TYPE_IMAGE, + "halign", GTK_ALIGN_START, + "icon-name", "emblem-ok-symbolic", + "valign", GTK_ALIGN_CENTER, + NULL); + GtkWidget *box = g_object_new (GTK_TYPE_BOX, + "child", priv->create_list_widget_func (item, priv->create_widget_func_data), + "child", checkmark, + "halign", GTK_ALIGN_START, + "spacing", 6, + "valign", GTK_ALIGN_CENTER, + "visible", TRUE, + NULL); + GtkStyleContext *checkmark_context = gtk_widget_get_style_context (checkmark); + + gtk_style_context_add_class (checkmark_context, "checkmark"); + + g_object_set_data (G_OBJECT (box), "checkmark", checkmark); + + return box; +} + +static void +get_name_free (HdyComboRowGetName *get_name) +{ + if (get_name == NULL) + return; + + if (get_name->func_data_destroy) + get_name->func_data_destroy (get_name->func_data); + get_name->func = NULL; + get_name->func_data = NULL; + get_name->func_data_destroy = NULL; + + g_free (get_name); +} + +static void +update (HdyComboRow *self) +{ + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + g_autoptr(GObject) item = NULL; + g_autofree gchar *name = NULL; + GtkWidget *widget; + guint n_items = priv->bound_model ? g_list_model_get_n_items (priv->bound_model) : 0; + + gtk_widget_set_visible (GTK_WIDGET (priv->current), !priv->use_subtitle); + gtk_container_foreach (GTK_CONTAINER (priv->current), (GtkCallback) gtk_widget_destroy, NULL); + + gtk_widget_set_sensitive (GTK_WIDGET (self), n_items > 0); + gtk_widget_set_visible (GTK_WIDGET (priv->image), n_items > 1); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), n_items > 1); + + if (n_items == 0) { + g_assert (priv->selected_index == -1); + + return; + } + + g_assert (priv->selected_index >= 0 && priv->selected_index <= n_items); + + { + g_autoptr (GList) rows = gtk_container_get_children (GTK_CONTAINER (priv->list)); + GList *l; + int i = 0; + + for (l = rows; l; l = l->next) { + GtkWidget *row = GTK_WIDGET (l->data); + GtkWidget *box = gtk_bin_get_child (GTK_BIN (row)); + + gtk_widget_set_visible (GTK_WIDGET (g_object_get_data (G_OBJECT (box), "checkmark")), + priv->selected_index == i++); + } + } + + item = g_list_model_get_item (priv->bound_model, priv->selected_index); + if (priv->use_subtitle) { + if (priv->get_name != NULL && priv->get_name->func) + name = priv->get_name->func (item, priv->get_name->func_data); + else if (priv->get_name_internal != NULL && priv->get_name_internal->func) + name = priv->get_name_internal->func (item, priv->get_name_internal->func_data); + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), name); + } + else { + widget = priv->create_current_widget_func (item, priv->create_widget_func_data); + gtk_container_add (GTK_CONTAINER (priv->current), widget); + } +} + +static void +bound_model_changed (GListModel *list, + guint index, + guint removed, + guint added, + gpointer user_data) +{ + gint new_idx; + HdyComboRow *self = HDY_COMBO_ROW (user_data); + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + /* Selection is in front of insertion/removal point, nothing to do */ + if (priv->selected_index > 0 && priv->selected_index < index) + return; + + if (priv->selected_index < index + removed) { + /* The item selected item was removed (or none is selected) */ + new_idx = -1; + } else { + /* The item selected item was behind the insertion/removal */ + new_idx = priv->selected_index + added - removed; + } + + /* Select the first item if none is selected. */ + if (new_idx == -1 && g_list_model_get_n_items (list) > 0) + new_idx = 0; + + hdy_combo_row_set_selected_index (self, new_idx); +} + +static void +row_activated_cb (HdyComboRow *self, + GtkListBoxRow *row) +{ + hdy_combo_row_set_selected_index (self, gtk_list_box_row_get_index (row)); +} + +static void +hdy_combo_row_activate (HdyActionRow *row) +{ + HdyComboRow *self = HDY_COMBO_ROW (row); + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + if (gtk_widget_get_visible (GTK_WIDGET (priv->image))) + gtk_popover_popup (priv->popover); +} + +static void +destroy_model (HdyComboRow *self) +{ + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + if (!priv->bound_model) + return; + + /* Disconnect the bound model *before* releasing it. */ + g_signal_handlers_disconnect_by_func (priv->bound_model, bound_model_changed, self); + + /* Destroy the model and the user data. */ + if (priv->list) + gtk_list_box_bind_model (priv->list, NULL, NULL, NULL, NULL); + + priv->bound_model = NULL; + priv->create_list_widget_func = NULL; + priv->create_current_widget_func = NULL; + priv->create_widget_func_data = NULL; + priv->create_widget_func_data_free_func = NULL; +} + +static void +hdy_combo_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyComboRow *self = HDY_COMBO_ROW (object); + + switch (prop_id) { + case PROP_SELECTED_INDEX: + g_value_set_int (value, hdy_combo_row_get_selected_index (self)); + break; + case PROP_USE_SUBTITLE: + g_value_set_boolean (value, hdy_combo_row_get_use_subtitle (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_combo_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyComboRow *self = HDY_COMBO_ROW (object); + + switch (prop_id) { + case PROP_SELECTED_INDEX: + hdy_combo_row_set_selected_index (self, g_value_get_int (value)); + break; + case PROP_USE_SUBTITLE: + hdy_combo_row_set_use_subtitle (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_combo_row_dispose (GObject *object) +{ + HdyComboRow *self = HDY_COMBO_ROW (object); + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + destroy_model (self); + g_clear_pointer (&priv->get_name, get_name_free); + + G_OBJECT_CLASS (hdy_combo_row_parent_class)->dispose (object); +} + +typedef struct { + HdyComboRow *row; + GtkCallback callback; + gpointer callback_data; +} ForallData; + +static void +for_non_internal_child (GtkWidget *widget, + gpointer callback_data) +{ + ForallData *data = callback_data; + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (data->row); + + if (widget != (GtkWidget *) priv->current && + widget != (GtkWidget *) priv->image) + data->callback (widget, data->callback_data); +} + +static void +hdy_combo_row_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyComboRow *self = HDY_COMBO_ROW (container); + ForallData data; + + if (include_internals) { + GTK_CONTAINER_CLASS (hdy_combo_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data); + + return; + } + + data.row = self; + data.callback = callback; + data.callback_data = callback_data; + + GTK_CONTAINER_CLASS (hdy_combo_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, for_non_internal_child, &data); +} + +static void +hdy_combo_row_class_init (HdyComboRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + HdyActionRowClass *row_class = HDY_ACTION_ROW_CLASS (klass); + + object_class->get_property = hdy_combo_row_get_property; + object_class->set_property = hdy_combo_row_set_property; + object_class->dispose = hdy_combo_row_dispose; + + container_class->forall = hdy_combo_row_forall; + + row_class->activate = hdy_combo_row_activate; + + /** + * HdyComboRow:selected-index: + * + * The index of the selected item in its #GListModel. + * + * Since: 0.0.7 + */ + props[PROP_SELECTED_INDEX] = + g_param_spec_int ("selected-index", + _("Selected index"), + _("The index of the selected item"), + -1, G_MAXINT, -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyComboRow:use-subtitle: + * + * %TRUE to set the current value as the subtitle. + * + * If you use a custom widget creation function, you will need to give the row + * a name conversion closure with hdy_combo_row_set_get_name_func(). + * + * If %TRUE, you should not access HdyActionRow:subtitle. + * + * Since: 0.0.10 + */ + props[PROP_USE_SUBTITLE] = + g_param_spec_boolean ("use-subtitle", + _("Use subtitle"), + _("Set the current value as the subtitle"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-combo-row.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, current); + gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, image); + gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, list); + gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, popover); +} + +static void +hdy_combo_row_init (HdyComboRow *self) +{ + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->selected_index = -1; + + g_signal_connect_object (priv->list, "row-activated", G_CALLBACK (gtk_widget_hide), + priv->popover, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->list, "row-activated", G_CALLBACK (row_activated_cb), + self, G_CONNECT_SWAPPED); + + update (self); +} + +/** + * hdy_combo_row_new: + * + * Creates a new #HdyComboRow. + * + * Returns: a new #HdyComboRow + * + * Since: 0.0.6 + */ +GtkWidget * +hdy_combo_row_new (void) +{ + return g_object_new (HDY_TYPE_COMBO_ROW, NULL); +} + +/** + * hdy_combo_row_get_model: + * @self: a #HdyComboRow + * + * Gets the model bound to @self, or %NULL if none is bound. + * + * Returns: (transfer none) (nullable): the #GListModel bound to @self or %NULL + * + * Since: 0.0.6 + */ +GListModel * +hdy_combo_row_get_model (HdyComboRow *self) +{ + HdyComboRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_COMBO_ROW (self), NULL); + + priv = hdy_combo_row_get_instance_private (self); + + return priv->bound_model; +} + +/** + * hdy_combo_row_bind_model: + * @self: a #HdyComboRow + * @model: (nullable): the #GListModel to be bound to @self + * @create_list_widget_func: (nullable) (scope call): a function that creates + * widgets for items to display in the list, or %NULL in case you also passed + * %NULL as @model + * @create_current_widget_func: (nullable) (scope call): a function that creates + * widgets for items to display as the selected item, or %NULL in case you + * also passed %NULL as @model + * @user_data: user data passed to @create_list_widget_func and + * @create_current_widget_func + * @user_data_free_func: function for freeing @user_data + * + * Binds @model to @self. + * + * If @self was already bound to a model, that previous binding is destroyed. + * + * The contents of @self are cleared and then filled with widgets that represent + * items from @model. @self is updated whenever @model changes. If @model is + * %NULL, @self is left empty. + * + * Since: 0.0.6 + */ +void +hdy_combo_row_bind_model (HdyComboRow *self, + GListModel *model, + GtkListBoxCreateWidgetFunc create_list_widget_func, + GtkListBoxCreateWidgetFunc create_current_widget_func, + gpointer user_data, + GDestroyNotify user_data_free_func) +{ + HdyComboRowPrivate *priv; + + g_return_if_fail (HDY_IS_COMBO_ROW (self)); + g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); + g_return_if_fail (model == NULL || create_list_widget_func != NULL); + g_return_if_fail (model == NULL || create_current_widget_func != NULL); + + priv = hdy_combo_row_get_instance_private (self); + + destroy_model (self); + + gtk_container_foreach (GTK_CONTAINER (priv->current), (GtkCallback) gtk_widget_destroy, NULL); + priv->selected_index = -1; + + if (model == NULL) { + update (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]); + return; + } + + /* We don't need take a reference as the list box holds one for us. */ + priv->bound_model = model; + priv->create_list_widget_func = create_list_widget_func; + priv->create_current_widget_func = create_current_widget_func; + priv->create_widget_func_data = user_data; + priv->create_widget_func_data_free_func = user_data_free_func; + + g_signal_connect (priv->bound_model, "items-changed", G_CALLBACK (bound_model_changed), self); + + if (g_list_model_get_n_items (priv->bound_model) > 0) + priv->selected_index = 0; + + gtk_list_box_bind_model (priv->list, model, create_list_widget, self, create_list_widget_data_free); + + update (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]); +} + +/** + * hdy_combo_row_bind_name_model: + * @self: a #HdyComboRow + * @model: (nullable): the #GListModel to be bound to @self + * @get_name_func: (nullable): a function that creates names for items, or %NULL + * in case you also passed %NULL as @model + * @user_data: user data passed to @get_name_func + * @user_data_free_func: function for freeing @user_data + * + * Binds @model to @self. + * + * If @self was already bound to a model, that previous binding is destroyed. + * + * The contents of @self are cleared and then filled with widgets that represent + * items from @model. @self is updated whenever @model changes. If @model is + * %NULL, @self is left empty. + * + * This is more convenient to use than hdy_combo_row_bind_model() if you want to + * represent items of the model with names. + * + * Since: 0.0.6 + */ +void +hdy_combo_row_bind_name_model (HdyComboRow *self, + GListModel *model, + HdyComboRowGetNameFunc get_name_func, + gpointer user_data, + GDestroyNotify user_data_free_func) +{ + HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self); + + g_return_if_fail (HDY_IS_COMBO_ROW (self)); + g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); + g_return_if_fail (model == NULL || get_name_func != NULL); + + priv->get_name_internal = g_new0 (HdyComboRowGetName, 1); + priv->get_name_internal->func = get_name_func; + priv->get_name_internal->func_data = user_data; + priv->get_name_internal->func_data_destroy = user_data_free_func; + + hdy_combo_row_bind_model (self, model, create_list_label, create_current_label, priv->get_name_internal, (GDestroyNotify) get_name_free); +} + +/** + * hdy_combo_row_set_for_enum: + * @self: a #HdyComboRow + * @enum_type: the enumeration #GType to be bound to @self + * @get_name_func: (nullable): a function that creates names for items, or %NULL + * in case you also passed %NULL as @model + * @user_data: user data passed to @get_name_func + * @user_data_free_func: function for freeing @user_data + * + * Creates a model for @enum_type and binds it to @self. The items of the model + * will be #HdyEnumValueObject objects. + * + * If @self was already bound to a model, that previous binding is destroyed. + * + * The contents of @self are cleared and then filled with widgets that represent + * items from @model. @self is updated whenever @model changes. If @model is + * %NULL, @self is left empty. + * + * This is more convenient to use than hdy_combo_row_bind_name_model() if you + * want to represent values of an enumeration with names. + * + * See hdy_enum_value_row_name(). + * + * Since: 0.0.6 + */ +void +hdy_combo_row_set_for_enum (HdyComboRow *self, + GType enum_type, + HdyComboRowGetEnumValueNameFunc get_name_func, + gpointer user_data, + GDestroyNotify user_data_free_func) +{ + g_autoptr (GListStore) store = g_list_store_new (HDY_TYPE_ENUM_VALUE_OBJECT); + /* g_autoptr for GEnumClass would require glib > 2.56 */ + GEnumClass *enum_class = NULL; + gsize i; + + g_return_if_fail (HDY_IS_COMBO_ROW (self)); + + enum_class = g_type_class_ref (enum_type); + for (i = 0; i < enum_class->n_values; i++) + { + g_autoptr(HdyEnumValueObject) obj = hdy_enum_value_object_new (&enum_class->values[i]); + + g_list_store_append (store, obj); + } + + hdy_combo_row_bind_name_model (self, G_LIST_MODEL (store), (HdyComboRowGetNameFunc) get_name_func, user_data, user_data_free_func); + g_type_class_unref (enum_class); +} + +/** + * hdy_combo_row_get_selected_index: + * @self: a #GtkListBoxRow + * + * Gets the index of the selected item in its #GListModel. + * + * Returns: the index of the selected item, or -1 if no item is selected + * + * Since: 0.0.7 + */ +gint +hdy_combo_row_get_selected_index (HdyComboRow *self) +{ + HdyComboRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_COMBO_ROW (self), -1); + + priv = hdy_combo_row_get_instance_private (self); + + return priv->selected_index; +} + +/** + * hdy_combo_row_set_selected_index: + * @self: a #HdyComboRow + * @selected_index: the index of the selected item + * + * Sets the index of the selected item in its #GListModel. + * + * Since: 0.0.7 + */ +void +hdy_combo_row_set_selected_index (HdyComboRow *self, + gint selected_index) +{ + HdyComboRowPrivate *priv; + + g_return_if_fail (HDY_IS_COMBO_ROW (self)); + g_return_if_fail (selected_index >= -1); + + priv = hdy_combo_row_get_instance_private (self); + + g_return_if_fail (selected_index >= 0 || priv->bound_model == NULL || g_list_model_get_n_items (priv->bound_model) == 0); + g_return_if_fail (selected_index == -1 || (priv->bound_model != NULL && selected_index < g_list_model_get_n_items (priv->bound_model))); + + if (priv->selected_index == selected_index) + return; + + priv->selected_index = selected_index; + update (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]); +} + +/** + * hdy_combo_row_get_use_subtitle: + * @self: a #GtkListBoxRow + * + * Gets whether the current value of @self should be displayed as its subtitle. + * + * Returns: whether the current value of @self should be displayed as its subtitle + * + * Since: 0.0.10 + */ +gboolean +hdy_combo_row_get_use_subtitle (HdyComboRow *self) +{ + HdyComboRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_COMBO_ROW (self), FALSE); + + priv = hdy_combo_row_get_instance_private (self); + + return priv->use_subtitle; +} + +/** + * hdy_combo_row_set_use_subtitle: + * @self: a #HdyComboRow + * @use_subtitle: %TRUE to set the current value as the subtitle + * + * Sets whether the current value of @self should be displayed as its subtitle. + * + * If %TRUE, you should not access HdyActionRow:subtitle. + * + * Since: 0.0.10 + */ +void +hdy_combo_row_set_use_subtitle (HdyComboRow *self, + gboolean use_subtitle) +{ + HdyComboRowPrivate *priv; + + g_return_if_fail (HDY_IS_COMBO_ROW (self)); + + priv = hdy_combo_row_get_instance_private (self); + + use_subtitle = !!use_subtitle; + + if (priv->use_subtitle == use_subtitle) + return; + + priv->use_subtitle = use_subtitle; + update (self); + if (!use_subtitle) + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_SUBTITLE]); +} + +/** + * hdy_combo_row_set_get_name_func: + * @self: a #HdyComboRow + * @get_name_func: (nullable): a function that creates names for items, or %NULL + * in case you also passed %NULL as @model + * @user_data: user data passed to @get_name_func + * @user_data_free_func: function for freeing @user_data + * + * Sets a closure to convert items into names. See HdyComboRow:use-subtitle. + * + * Since: 0.0.10 + */ +void +hdy_combo_row_set_get_name_func (HdyComboRow *self, + HdyComboRowGetNameFunc get_name_func, + gpointer user_data, + GDestroyNotify user_data_free_func) +{ + HdyComboRowPrivate *priv; + + g_return_if_fail (HDY_IS_COMBO_ROW (self)); + + priv = hdy_combo_row_get_instance_private (self); + + get_name_free (priv->get_name); + priv->get_name = g_new0 (HdyComboRowGetName, 1); + priv->get_name->func = get_name_func; + priv->get_name->func_data = user_data; + priv->get_name->func_data_destroy = user_data_free_func; +} + +/** + * hdy_enum_value_row_name: + * @value: the value from the enum from which to get a name + * @user_data: (closure): unused user data + * + * This is a default implementation of #HdyComboRowGetEnumValueNameFunc to be + * used with hdy_combo_row_set_for_enum(). If the enumeration has a nickname, it + * will return it, otherwise it will return its name. + * + * Returns: (transfer full): a newly allocated displayable name that represents @value + * + * Since: 0.0.6 + */ +gchar * +hdy_enum_value_row_name (HdyEnumValueObject *value, + gpointer user_data) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL); + + return g_strdup (hdy_enum_value_object_get_nick (value) != NULL ? + hdy_enum_value_object_get_nick (value) : + hdy_enum_value_object_get_name (value)); +} diff --git a/subprojects/libhandy/src/hdy-combo-row.h b/subprojects/libhandy/src/hdy-combo-row.h new file mode 100644 index 0000000..68657a0 --- /dev/null +++ b/subprojects/libhandy/src/hdy-combo-row.h @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-enum-value-object.h" +#include "hdy-action-row.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_COMBO_ROW (hdy_combo_row_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyComboRow, hdy_combo_row, HDY, COMBO_ROW, HdyActionRow) + +/** + * HdyComboRowGetNameFunc: + * @item: (type GObject): the item from the model from which to get a name + * @user_data: (closure): user data + * + * Called for combo rows that are bound to a #GListModel with + * hdy_combo_row_bind_name_model() for each item that gets added to the model. + * + * Returns: (transfer full): a newly allocated displayable name that represents @item + */ +typedef gchar * (*HdyComboRowGetNameFunc) (gpointer item, + gpointer user_data); + +/** + * HdyComboRowGetEnumValueNameFunc: + * @value: the value from the enum from which to get a name + * @user_data: (closure): user data + * + * Called for combo rows that are bound to an enumeration with + * hdy_combo_row_set_for_enum() for each value from that enumeration. + * + * Returns: (transfer full): a newly allocated displayable name that represents @value + */ +typedef gchar * (*HdyComboRowGetEnumValueNameFunc) (HdyEnumValueObject *value, + gpointer user_data); + +/** + * HdyComboRowClass + * @parent_class: The parent class + */ +struct _HdyComboRowClass +{ + HdyActionRowClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_combo_row_new (void); + +HDY_AVAILABLE_IN_ALL +GListModel *hdy_combo_row_get_model (HdyComboRow *self); + +HDY_AVAILABLE_IN_ALL +void hdy_combo_row_bind_model (HdyComboRow *self, + GListModel *model, + GtkListBoxCreateWidgetFunc create_list_widget_func, + GtkListBoxCreateWidgetFunc create_current_widget_func, + gpointer user_data, + GDestroyNotify user_data_free_func); +HDY_AVAILABLE_IN_ALL +void hdy_combo_row_bind_name_model (HdyComboRow *self, + GListModel *model, + HdyComboRowGetNameFunc get_name_func, + gpointer user_data, + GDestroyNotify user_data_free_func); +HDY_AVAILABLE_IN_ALL +void hdy_combo_row_set_for_enum (HdyComboRow *self, + GType enum_type, + HdyComboRowGetEnumValueNameFunc get_name_func, + gpointer user_data, + GDestroyNotify user_data_free_func); + +HDY_AVAILABLE_IN_ALL +gint hdy_combo_row_get_selected_index (HdyComboRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_combo_row_set_selected_index (HdyComboRow *self, + gint selected_index); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_combo_row_get_use_subtitle (HdyComboRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_combo_row_set_use_subtitle (HdyComboRow *self, + gboolean use_subtitle); + +HDY_AVAILABLE_IN_ALL +void hdy_combo_row_set_get_name_func (HdyComboRow *self, + HdyComboRowGetNameFunc get_name_func, + gpointer user_data, + GDestroyNotify user_data_free_func); + +HDY_AVAILABLE_IN_ALL +gchar *hdy_enum_value_row_name (HdyEnumValueObject *value, + gpointer user_data); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-combo-row.ui b/subprojects/libhandy/src/hdy-combo-row.ui new file mode 100644 index 0000000..080a591 --- /dev/null +++ b/subprojects/libhandy/src/hdy-combo-row.ui @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="HdyComboRow" parent="HdyActionRow"> + <property name="activatable">False</property> + <child> + <object class="GtkBox" id="current"> + <property name="halign">end</property> + <property name="valign">center</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkImage" id="image"> + <property name="icon_name">pan-down-symbolic</property> + <property name="icon_size">1</property> + <property name="valign">center</property> + <property name="visible">True</property> + </object> + </child> + </template> + <object class="GtkPopover" id="popover"> + <property name="position">bottom</property> + <property name="relative_to">image</property> + <style> + <class name="combo"/> + </style> + <child> + <object class="GtkScrolledWindow"> + <property name="hscrollbar_policy">never</property> + <property name="max_content_height">400</property> + <property name="propagate_natural_width">True</property> + <property name="propagate_natural_height">True</property> + <property name="visible">True</property> + <child> + <object class="GtkListBox" id="list"> + <property name="selection_mode">none</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/subprojects/libhandy/src/hdy-css-private.h b/subprojects/libhandy/src/hdy-css-private.h new file mode 100644 index 0000000..d8190b5 --- /dev/null +++ b/subprojects/libhandy/src/hdy-css-private.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void hdy_css_measure (GtkWidget *widget, + GtkOrientation orientation, + gint *minimum, + gint *natural); + +void hdy_css_size_allocate (GtkWidget *widget, + GtkAllocation *allocation); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-css.c b/subprojects/libhandy/src/hdy-css.c new file mode 100644 index 0000000..7a056e2 --- /dev/null +++ b/subprojects/libhandy/src/hdy-css.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-css-private.h" + +void +hdy_css_measure (GtkWidget *widget, + GtkOrientation orientation, + gint *minimum, + gint *natural) +{ + GtkStyleContext *style_context = gtk_widget_get_style_context (widget); + GtkStateFlags state_flags = gtk_widget_get_state_flags (widget); + GtkBorder border, margin, padding; + gint css_width, css_height; + + /* Manually apply minimum sizes, the border, the padding and the margin as we + * can't use the private GtkGagdet. + */ + gtk_style_context_get (style_context, state_flags, + "min-width", &css_width, + "min-height", &css_height, + NULL); + gtk_style_context_get_border (style_context, state_flags, &border); + gtk_style_context_get_margin (style_context, state_flags, &margin); + gtk_style_context_get_padding (style_context, state_flags, &padding); + if (orientation == GTK_ORIENTATION_VERTICAL) { + *minimum = MAX (*minimum, css_height) + + border.top + margin.top + padding.top + + border.bottom + margin.bottom + padding.bottom; + *natural = MAX (*natural, css_height) + + border.top + margin.top + padding.top + + border.bottom + margin.bottom + padding.bottom; + } else { + *minimum = MAX (*minimum, css_width) + + border.left + margin.left + padding.left + + border.right + margin.right + padding.right; + *natural = MAX (*natural, css_width) + + border.left + margin.left + padding.left + + border.right + margin.right + padding.right; + } +} + +void +hdy_css_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkStyleContext *style_context; + GtkStateFlags state_flags; + GtkBorder border, margin, padding; + + /* Manually apply the border, the padding and the margin as we can't use the + * private GtkGagdet. + */ + style_context = gtk_widget_get_style_context (widget); + state_flags = gtk_widget_get_state_flags (widget); + gtk_style_context_get_border (style_context, state_flags, &border); + gtk_style_context_get_margin (style_context, state_flags, &margin); + gtk_style_context_get_padding (style_context, state_flags, &padding); + allocation->width -= border.left + border.right + + margin.left + margin.right + + padding.left + padding.right; + allocation->height -= border.top + border.bottom + + margin.top + margin.bottom + + padding.top + padding.bottom; + allocation->x += border.left + margin.left + padding.left; + allocation->y += border.top + margin.top + padding.top; +} diff --git a/subprojects/libhandy/src/hdy-deck.c b/subprojects/libhandy/src/hdy-deck.c new file mode 100644 index 0000000..01dc45e --- /dev/null +++ b/subprojects/libhandy/src/hdy-deck.c @@ -0,0 +1,1103 @@ +/* + * Copyright (C) 2018 Purism SPC + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-deck.h" +#include "hdy-stackable-box-private.h" +#include "hdy-swipeable.h" + +/** + * SECTION:hdy-deck + * @short_description: A swipeable widget showing one of the visible children at a time. + * @Title: HdyDeck + * + * The #HdyDeck widget displays one of the visible children, similar to a + * #GtkStack. The children are strictly ordered and can be navigated using + * swipe gestures. + * + * The “over” and “under” stack the children one on top of the other, while the + * “slide” transition puts the children side by side. While navigating to a + * child on the side or below can be performed by swiping the current child + * away, navigating to an upper child requires dragging it from the edge where + * it resides. This doesn't affect non-dragging swipes. + * + * The “over” and “under” transitions can draw their shadow on top of the + * window's transparent areas, like the rounded corners. This is a side-effect + * of allowing shadows to be drawn on top of OpenGL areas. It can be mitigated + * by using #HdyWindow or #HdyApplicationWindow as they will crop anything drawn + * beyond the rounded corners. + * + * # CSS nodes + * + * #HdyDeck has a single CSS node with name deck. + * + * Since: 1.0 + */ + +/** + * HdyDeckTransitionType: + * @HDY_DECK_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order + * @HDY_DECK_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order + * @HDY_DECK_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order + * + * This enumeration value describes the possible transitions between children + * in a #HdyDeck widget. + * + * New values may be added to this enumeration over time. + * + * Since: 1.0 + */ + +enum { + PROP_0, + PROP_HHOMOGENEOUS, + PROP_VHOMOGENEOUS, + PROP_VISIBLE_CHILD, + PROP_VISIBLE_CHILD_NAME, + PROP_TRANSITION_TYPE, + PROP_TRANSITION_DURATION, + PROP_TRANSITION_RUNNING, + PROP_INTERPOLATE_SIZE, + PROP_CAN_SWIPE_BACK, + PROP_CAN_SWIPE_FORWARD, + + /* orientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_ORIENTATION, +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_NAME, + LAST_CHILD_PROP, +}; + +typedef struct +{ + HdyStackableBox *box; +} HdyDeckPrivate; + +static GParamSpec *props[LAST_PROP]; +static GParamSpec *child_props[LAST_CHILD_PROP]; + +static void hdy_deck_swipeable_init (HdySwipeableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyDeck, hdy_deck, GTK_TYPE_CONTAINER, + G_ADD_PRIVATE (HdyDeck) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL) + G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_deck_swipeable_init)) + +#define HDY_GET_HELPER(obj) (((HdyDeckPrivate *) hdy_deck_get_instance_private (HDY_DECK (obj)))->box) + +/** + * hdy_deck_set_homogeneous: + * @self: a #HdyDeck + * @orientation: the orientation + * @homogeneous: %TRUE to make @self homogeneous + * + * Sets the #HdyDeck to be homogeneous or not for the given orientation. + * If it is homogeneous, the #HdyDeck will request the same + * width or height for all its children depending on the orientation. + * If it isn't, the deck may change width or height when a different child + * becomes visible. + * + * Since: 1.0 + */ +void +hdy_deck_set_homogeneous (HdyDeck *self, + GtkOrientation orientation, + gboolean homogeneous) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_homogeneous (HDY_GET_HELPER (self), TRUE, orientation, homogeneous); +} + +/** + * hdy_deck_get_homogeneous: + * @self: a #HdyDeck + * @orientation: the orientation + * + * Gets whether @self is homogeneous for the given orientation. + * See hdy_deck_set_homogeneous(). + * + * Returns: whether @self is homogeneous for the given orientation. + * + * Since: 1.0 + */ +gboolean +hdy_deck_get_homogeneous (HdyDeck *self, + GtkOrientation orientation) +{ + g_return_val_if_fail (HDY_IS_DECK (self), FALSE); + + return hdy_stackable_box_get_homogeneous (HDY_GET_HELPER (self), TRUE, orientation); +} + +/** + * hdy_deck_get_transition_type: + * @self: a #HdyDeck + * + * Gets the type of animation that will be used + * for transitions between children in @self. + * + * Returns: the current transition type of @self + * + * Since: 1.0 + */ +HdyDeckTransitionType +hdy_deck_get_transition_type (HdyDeck *self) +{ + HdyStackableBoxTransitionType type; + + g_return_val_if_fail (HDY_IS_DECK (self), HDY_DECK_TRANSITION_TYPE_OVER); + + type = hdy_stackable_box_get_transition_type (HDY_GET_HELPER (self)); + + switch (type) { + case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: + return HDY_DECK_TRANSITION_TYPE_OVER; + + case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: + return HDY_DECK_TRANSITION_TYPE_UNDER; + + case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: + return HDY_DECK_TRANSITION_TYPE_SLIDE; + + default: + g_assert_not_reached (); + } +} + +/** + * hdy_deck_set_transition_type: + * @self: a #HdyDeck + * @transition: the new transition type + * + * Sets the type of animation that will be used for transitions between children + * in @self. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the child that is about to become + * current. + * + * Since: 1.0 + */ +void +hdy_deck_set_transition_type (HdyDeck *self, + HdyDeckTransitionType transition) +{ + HdyStackableBoxTransitionType type; + + g_return_if_fail (HDY_IS_DECK (self)); + g_return_if_fail (transition <= HDY_DECK_TRANSITION_TYPE_SLIDE); + + switch (transition) { + case HDY_DECK_TRANSITION_TYPE_OVER: + type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER; + break; + + case HDY_DECK_TRANSITION_TYPE_UNDER: + type = HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER; + break; + + case HDY_DECK_TRANSITION_TYPE_SLIDE: + type = HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE; + break; + + default: + g_assert_not_reached (); + } + + hdy_stackable_box_set_transition_type (HDY_GET_HELPER (self), type); +} + +/** + * hdy_deck_get_transition_duration: + * @self: a #HdyDeck + * + * Returns the amount of time (in milliseconds) that + * transitions between children in @self will take. + * + * Returns: the child transition duration + * + * Since: 1.0 + */ +guint +hdy_deck_get_transition_duration (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), 0); + + return hdy_stackable_box_get_child_transition_duration (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_set_transition_duration: + * @self: a #HdyDeck + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between children in @self + * will take. + * + * Since: 1.0 + */ +void +hdy_deck_set_transition_duration (HdyDeck *self, + guint duration) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_child_transition_duration (HDY_GET_HELPER (self), duration); +} + +/** + * hdy_deck_get_visible_child: + * @self: a #HdyDeck + * + * Gets the visible child widget. + * + * Returns: (transfer none): the visible child widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_deck_get_visible_child (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), NULL); + + return hdy_stackable_box_get_visible_child (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_set_visible_child: + * @self: a #HdyDeck + * @visible_child: the new child + * + * Makes @visible_child visible using a transition determined by + * HdyDeck:transition-type and HdyDeck:transition-duration. The transition can + * be cancelled by the user, in which case visible child will change back to + * the previously visible child. + * + * Since: 1.0 + */ +void +hdy_deck_set_visible_child (HdyDeck *self, + GtkWidget *visible_child) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_visible_child (HDY_GET_HELPER (self), visible_child); +} + +/** + * hdy_deck_get_visible_child_name: + * @self: a #HdyDeck + * + * Gets the name of the currently visible child widget. + * + * Returns: (transfer none): the name of the visible child + * + * Since: 1.0 + */ +const gchar * +hdy_deck_get_visible_child_name (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), NULL); + + return hdy_stackable_box_get_visible_child_name (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_set_visible_child_name: + * @self: a #HdyDeck + * @name: the name of a child + * + * Makes the child with the name @name visible. + * + * See hdy_deck_set_visible_child() for more details. + * + * Since: 1.0 + */ +void +hdy_deck_set_visible_child_name (HdyDeck *self, + const gchar *name) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_visible_child_name (HDY_GET_HELPER (self), name); +} + +/** + * hdy_deck_get_transition_running: + * @self: a #HdyDeck + * + * Returns whether @self is currently in a transition from one page to + * another. + * + * Returns: %TRUE if the transition is currently running, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_deck_get_transition_running (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), FALSE); + + return hdy_stackable_box_get_child_transition_running (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_set_interpolate_size: + * @self: a #HdyDeck + * @interpolate_size: the new value + * + * Sets whether or not @self will interpolate its size when + * changing the visible child. If the #HdyDeck:interpolate-size + * property is set to %TRUE, @self will interpolate its size between + * the current one and the one it'll take after changing the + * visible child, according to the set transition duration. + * + * Since: 1.0 + */ +void +hdy_deck_set_interpolate_size (HdyDeck *self, + gboolean interpolate_size) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_interpolate_size (HDY_GET_HELPER (self), interpolate_size); +} + +/** + * hdy_deck_get_interpolate_size: + * @self: a #HdyDeck + * + * Returns whether the #HdyDeck is set up to interpolate between + * the sizes of children on page switch. + * + * Returns: %TRUE if child sizes are interpolated + * + * Since: 1.0 + */ +gboolean +hdy_deck_get_interpolate_size (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), FALSE); + + return hdy_stackable_box_get_interpolate_size (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_set_can_swipe_back: + * @self: a #HdyDeck + * @can_swipe_back: the new value + * + * Sets whether or not @self allows switching to the previous child via a swipe + * gesture. + * + * Since: 1.0 + */ +void +hdy_deck_set_can_swipe_back (HdyDeck *self, + gboolean can_swipe_back) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_can_swipe_back (HDY_GET_HELPER (self), can_swipe_back); +} + +/** + * hdy_deck_get_can_swipe_back + * @self: a #HdyDeck + * + * Returns whether the #HdyDeck allows swiping to the previous child. + * + * Returns: %TRUE if back swipe is enabled. + * + * Since: 1.0 + */ +gboolean +hdy_deck_get_can_swipe_back (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), FALSE); + + return hdy_stackable_box_get_can_swipe_back (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_set_can_swipe_forward: + * @self: a #HdyDeck + * @can_swipe_forward: the new value + * + * Sets whether or not @self allows switching to the next child via a swipe + * gesture. + * + * Since: 1.0 + */ +void +hdy_deck_set_can_swipe_forward (HdyDeck *self, + gboolean can_swipe_forward) +{ + g_return_if_fail (HDY_IS_DECK (self)); + + hdy_stackable_box_set_can_swipe_forward (HDY_GET_HELPER (self), can_swipe_forward); +} + +/** + * hdy_deck_get_can_swipe_forward + * @self: a #HdyDeck + * + * Returns whether the #HdyDeck allows swiping to the next child. + * + * Returns: %TRUE if forward swipe is enabled. + * + * Since: 1.0 + */ +gboolean +hdy_deck_get_can_swipe_forward (HdyDeck *self) +{ + g_return_val_if_fail (HDY_IS_DECK (self), FALSE); + + return hdy_stackable_box_get_can_swipe_forward (HDY_GET_HELPER (self)); +} + +/** + * hdy_deck_get_adjacent_child + * @self: a #HdyDeck + * @direction: the direction + * + * Gets the previous or next child, or %NULL if it doesn't exist. This will be + * the same widget hdy_deck_navigate() will navigate to. + * + * Returns: (nullable) (transfer none): the previous or next child, or + * %NULL if it doesn't exist. + * + * Since: 1.0 + */ +GtkWidget * +hdy_deck_get_adjacent_child (HdyDeck *self, + HdyNavigationDirection direction) +{ + g_return_val_if_fail (HDY_IS_DECK (self), NULL); + + return hdy_stackable_box_get_adjacent_child (HDY_GET_HELPER (self), direction); +} + +/** + * hdy_deck_navigate + * @self: a #HdyDeck + * @direction: the direction + * + * Switches to the previous or next child, similar to performing a swipe + * gesture to go in @direction. + * + * Returns: %TRUE if visible child was changed, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_deck_navigate (HdyDeck *self, + HdyNavigationDirection direction) +{ + g_return_val_if_fail (HDY_IS_DECK (self), FALSE); + + return hdy_stackable_box_navigate (HDY_GET_HELPER (self), direction); +} + +/** + * hdy_deck_get_child_by_name: + * @self: a #HdyDeck + * @name: the name of the child to find + * + * Finds the child of @self with the name given as the argument. Returns %NULL + * if there is no child with this name. + * + * Returns: (transfer none) (nullable): the requested child of @self + * + * Since: 1.0 + */ +GtkWidget * +hdy_deck_get_child_by_name (HdyDeck *self, + const gchar *name) +{ + g_return_val_if_fail (HDY_IS_DECK (self), NULL); + + return hdy_stackable_box_get_child_by_name (HDY_GET_HELPER (self), name); +} + +/* This private method is prefixed by the call name because it will be a virtual + * method in GTK 4. + */ +static void +hdy_deck_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + hdy_stackable_box_measure (HDY_GET_HELPER (widget), + orientation, for_size, + minimum, natural, + minimum_baseline, natural_baseline); +} + +static void +hdy_deck_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + hdy_deck_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_deck_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + hdy_deck_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_deck_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width) +{ + hdy_deck_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_deck_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum_height, + gint *natural_height) +{ + hdy_deck_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_deck_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + hdy_stackable_box_size_allocate (HDY_GET_HELPER (widget), allocation); +} + +static gboolean +hdy_deck_draw (GtkWidget *widget, + cairo_t *cr) +{ + return hdy_stackable_box_draw (HDY_GET_HELPER (widget), cr); +} + +static void +hdy_deck_direction_changed (GtkWidget *widget, + GtkTextDirection previous_direction) +{ + hdy_stackable_box_direction_changed (HDY_GET_HELPER (widget), previous_direction); +} + +static void +hdy_deck_add (GtkContainer *container, + GtkWidget *widget) +{ + hdy_stackable_box_add (HDY_GET_HELPER (container), widget); +} + +static void +hdy_deck_remove (GtkContainer *container, + GtkWidget *widget) +{ + hdy_stackable_box_remove (HDY_GET_HELPER (container), widget); +} + +static void +hdy_deck_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + hdy_stackable_box_forall (HDY_GET_HELPER (container), include_internals, callback, callback_data); +} + +static void +hdy_deck_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyDeck *self = HDY_DECK (object); + + switch (prop_id) { + case PROP_HHOMOGENEOUS: + g_value_set_boolean (value, hdy_deck_get_homogeneous (self, GTK_ORIENTATION_HORIZONTAL)); + break; + case PROP_VHOMOGENEOUS: + g_value_set_boolean (value, hdy_deck_get_homogeneous (self, GTK_ORIENTATION_VERTICAL)); + break; + case PROP_VISIBLE_CHILD: + g_value_set_object (value, hdy_deck_get_visible_child (self)); + break; + case PROP_VISIBLE_CHILD_NAME: + g_value_set_string (value, hdy_deck_get_visible_child_name (self)); + break; + case PROP_TRANSITION_TYPE: + g_value_set_enum (value, hdy_deck_get_transition_type (self)); + break; + case PROP_TRANSITION_DURATION: + g_value_set_uint (value, hdy_deck_get_transition_duration (self)); + break; + case PROP_TRANSITION_RUNNING: + g_value_set_boolean (value, hdy_deck_get_transition_running (self)); + break; + case PROP_INTERPOLATE_SIZE: + g_value_set_boolean (value, hdy_deck_get_interpolate_size (self)); + break; + case PROP_CAN_SWIPE_BACK: + g_value_set_boolean (value, hdy_deck_get_can_swipe_back (self)); + break; + case PROP_CAN_SWIPE_FORWARD: + g_value_set_boolean (value, hdy_deck_get_can_swipe_forward (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, hdy_stackable_box_get_orientation (HDY_GET_HELPER (self))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_deck_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyDeck *self = HDY_DECK (object); + + switch (prop_id) { + case PROP_HHOMOGENEOUS: + hdy_deck_set_homogeneous (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value)); + break; + case PROP_VHOMOGENEOUS: + hdy_deck_set_homogeneous (self, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value)); + break; + case PROP_VISIBLE_CHILD: + hdy_deck_set_visible_child (self, g_value_get_object (value)); + break; + case PROP_VISIBLE_CHILD_NAME: + hdy_deck_set_visible_child_name (self, g_value_get_string (value)); + break; + case PROP_TRANSITION_TYPE: + hdy_deck_set_transition_type (self, g_value_get_enum (value)); + break; + case PROP_TRANSITION_DURATION: + hdy_deck_set_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_INTERPOLATE_SIZE: + hdy_deck_set_interpolate_size (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_BACK: + hdy_deck_set_can_swipe_back (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_FORWARD: + hdy_deck_set_can_swipe_forward (self, g_value_get_boolean (value)); + break; + case PROP_ORIENTATION: + hdy_stackable_box_set_orientation (HDY_GET_HELPER (self), g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_deck_finalize (GObject *object) +{ + HdyDeck *self = HDY_DECK (object); + HdyDeckPrivate *priv = hdy_deck_get_instance_private (self); + + g_clear_object (&priv->box); + + G_OBJECT_CLASS (hdy_deck_parent_class)->finalize (object); +} + +static void +hdy_deck_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case CHILD_PROP_NAME: + g_value_set_string (value, hdy_stackable_box_get_child_name (HDY_GET_HELPER (container), widget)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_deck_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case CHILD_PROP_NAME: + hdy_stackable_box_set_child_name (HDY_GET_HELPER (container), widget, g_value_get_string (value)); + gtk_container_child_notify_by_pspec (container, widget, pspec); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_deck_realize (GtkWidget *widget) +{ + hdy_stackable_box_realize (HDY_GET_HELPER (widget)); +} + +static void +hdy_deck_unrealize (GtkWidget *widget) +{ + hdy_stackable_box_unrealize (HDY_GET_HELPER (widget)); +} + +static void +hdy_deck_map (GtkWidget *widget) +{ + hdy_stackable_box_map (HDY_GET_HELPER (widget)); +} + +static void +hdy_deck_unmap (GtkWidget *widget) +{ + hdy_stackable_box_unmap (HDY_GET_HELPER (widget)); +} + +static void +hdy_deck_switch_child (HdySwipeable *swipeable, + guint index, + gint64 duration) +{ + hdy_stackable_box_switch_child (HDY_GET_HELPER (swipeable), index, duration); +} + +static HdySwipeTracker * +hdy_deck_get_swipe_tracker (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_swipe_tracker (HDY_GET_HELPER (swipeable)); +} + +static gdouble +hdy_deck_get_distance (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_distance (HDY_GET_HELPER (swipeable)); +} + +static gdouble * +hdy_deck_get_snap_points (HdySwipeable *swipeable, + gint *n_snap_points) +{ + return hdy_stackable_box_get_snap_points (HDY_GET_HELPER (swipeable), n_snap_points); +} + +static gdouble +hdy_deck_get_progress (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_progress (HDY_GET_HELPER (swipeable)); +} + +static gdouble +hdy_deck_get_cancel_progress (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_cancel_progress (HDY_GET_HELPER (swipeable)); +} + +static void +hdy_deck_get_swipe_area (HdySwipeable *swipeable, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect) +{ + hdy_stackable_box_get_swipe_area (HDY_GET_HELPER (swipeable), navigation_direction, is_drag, rect); +} + +static void +hdy_deck_class_init (HdyDeckClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = (GtkWidgetClass*) klass; + GtkContainerClass *container_class = (GtkContainerClass*) klass; + + object_class->get_property = hdy_deck_get_property; + object_class->set_property = hdy_deck_set_property; + object_class->finalize = hdy_deck_finalize; + + widget_class->realize = hdy_deck_realize; + widget_class->unrealize = hdy_deck_unrealize; + widget_class->map = hdy_deck_map; + widget_class->unmap = hdy_deck_unmap; + widget_class->get_preferred_width = hdy_deck_get_preferred_width; + widget_class->get_preferred_height = hdy_deck_get_preferred_height; + widget_class->get_preferred_width_for_height = hdy_deck_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = hdy_deck_get_preferred_height_for_width; + widget_class->size_allocate = hdy_deck_size_allocate; + widget_class->draw = hdy_deck_draw; + widget_class->direction_changed = hdy_deck_direction_changed; + + container_class->add = hdy_deck_add; + container_class->remove = hdy_deck_remove; + container_class->forall = hdy_deck_forall; + container_class->set_child_property = hdy_deck_set_child_property; + container_class->get_child_property = hdy_deck_get_child_property; + gtk_container_class_handle_border_width (container_class); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + /** + * HdyDeck:hhomogeneous: + * + * Horizontally homogeneous sizing. + * + * Since: 1.0 + */ + props[PROP_HHOMOGENEOUS] = + g_param_spec_boolean ("hhomogeneous", + _("Horizontally homogeneous"), + _("Horizontally homogeneous sizing"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:vhomogeneous: + * + * Vertically homogeneous sizing. + * + * Since: 1.0 + */ + props[PROP_VHOMOGENEOUS] = + g_param_spec_boolean ("vhomogeneous", + _("Vertically homogeneous"), + _("Vertically homogeneous sizing"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:visible-child: + * + * The widget currently visible. + * + * Since: 1.0 + */ + props[PROP_VISIBLE_CHILD] = + g_param_spec_object ("visible-child", + _("Visible child"), + _("The widget currently visible"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:visible-child-name: + * + * The name of the widget currently visible. + * + * Since: 1.0 + */ + props[PROP_VISIBLE_CHILD_NAME] = + g_param_spec_string ("visible-child-name", + _("Name of visible child"), + _("The name of the widget currently visible"), + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:transition-type: + * + * The type of animation that will be used for transitions between + * children. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the child that is about + * to become current. + * + * Since: 1.0 + */ + props[PROP_TRANSITION_TYPE] = + g_param_spec_enum ("transition-type", + _("Transition type"), + _("The type of animation used to transition between children"), + HDY_TYPE_DECK_TRANSITION_TYPE, HDY_DECK_TRANSITION_TYPE_OVER, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:transition-duration: + * + * The transition animation duration, in milliseconds. + * + * Since: 1.0 + */ + props[PROP_TRANSITION_DURATION] = + g_param_spec_uint ("transition-duration", + _("Transition duration"), + _("The transition animation duration, in milliseconds"), + 0, G_MAXUINT, 200, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:transition-running: + * + * Whether or not the transition is currently running. + * + * Since: 1.0 + */ + props[PROP_TRANSITION_RUNNING] = + g_param_spec_boolean ("transition-running", + _("Transition running"), + _("Whether or not the transition is currently running"), + FALSE, + G_PARAM_READABLE); + + /** + * HdyDeck:interpolate-size: + * + * Whether or not the size should smoothly change when changing between + * differently sized children. + * + * Since: 1.0 + */ + props[PROP_INTERPOLATE_SIZE] = + g_param_spec_boolean ("interpolate-size", + _("Interpolate size"), + _("Whether or not the size should smoothly change when changing between differently sized children"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:can-swipe-back: + * + * Whether or not the deck allows switching to the previous child via a swipe + * gesture. + * + * Since: 1.0 + */ + props[PROP_CAN_SWIPE_BACK] = + g_param_spec_boolean ("can-swipe-back", + _("Can swipe back"), + _("Whether or not swipe gesture can be used to switch to the previous child"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyDeck:can-swipe-forward: + * + * Whether or not the deck allows switching to the next child via a swipe + * gesture. + * + * Since: 1.0 + */ + props[PROP_CAN_SWIPE_FORWARD] = + g_param_spec_boolean ("can-swipe-forward", + _("Can swipe forward"), + _("Whether or not swipe gesture can be used to switch to the next child"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + child_props[CHILD_PROP_NAME] = + g_param_spec_string ("name", + _("Name"), + _("The name of the child page"), + NULL, + G_PARAM_READWRITE); + + gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props); + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL); + gtk_widget_class_set_css_name (widget_class, "deck"); +} + +GtkWidget * +hdy_deck_new (void) +{ + return g_object_new (HDY_TYPE_DECK, NULL); +} + +#define NOTIFY(func, prop) \ +static void \ +func (HdyDeck *self) { \ + g_object_notify_by_pspec (G_OBJECT (self), props[prop]); \ +} + +NOTIFY (notify_hhomogeneous_folded_cb, PROP_HHOMOGENEOUS); +NOTIFY (notify_vhomogeneous_folded_cb, PROP_VHOMOGENEOUS); +NOTIFY (notify_visible_child_cb, PROP_VISIBLE_CHILD); +NOTIFY (notify_visible_child_name_cb, PROP_VISIBLE_CHILD_NAME); +NOTIFY (notify_transition_type_cb, PROP_TRANSITION_TYPE); +NOTIFY (notify_child_transition_duration_cb, PROP_TRANSITION_DURATION); +NOTIFY (notify_child_transition_running_cb, PROP_TRANSITION_RUNNING); +NOTIFY (notify_interpolate_size_cb, PROP_INTERPOLATE_SIZE); +NOTIFY (notify_can_swipe_back_cb, PROP_CAN_SWIPE_BACK); +NOTIFY (notify_can_swipe_forward_cb, PROP_CAN_SWIPE_FORWARD); + +static void +notify_orientation_cb (HdyDeck *self) +{ + g_object_notify (G_OBJECT (self), "orientation"); +} + +static void +hdy_deck_init (HdyDeck *self) +{ + HdyDeckPrivate *priv = hdy_deck_get_instance_private (self); + + priv->box = hdy_stackable_box_new (GTK_CONTAINER (self), + GTK_CONTAINER_CLASS (hdy_deck_parent_class), + FALSE); + + g_signal_connect_object (priv->box, "notify::hhomogeneous-folded", G_CALLBACK (notify_hhomogeneous_folded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::vhomogeneous-folded", G_CALLBACK (notify_vhomogeneous_folded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::visible-child", G_CALLBACK (notify_visible_child_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::visible-child-name", G_CALLBACK (notify_visible_child_name_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::transition-type", G_CALLBACK (notify_transition_type_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::child-transition-duration", G_CALLBACK (notify_child_transition_duration_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::child-transition-running", G_CALLBACK (notify_child_transition_running_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::interpolate-size", G_CALLBACK (notify_interpolate_size_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::can-swipe-back", G_CALLBACK (notify_can_swipe_back_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::can-swipe-forward", G_CALLBACK (notify_can_swipe_forward_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::orientation", G_CALLBACK (notify_orientation_cb), self, G_CONNECT_SWAPPED); +} + +static void +hdy_deck_swipeable_init (HdySwipeableInterface *iface) +{ + iface->switch_child = hdy_deck_switch_child; + iface->get_swipe_tracker = hdy_deck_get_swipe_tracker; + iface->get_distance = hdy_deck_get_distance; + iface->get_snap_points = hdy_deck_get_snap_points; + iface->get_progress = hdy_deck_get_progress; + iface->get_cancel_progress = hdy_deck_get_cancel_progress; + iface->get_swipe_area = hdy_deck_get_swipe_area; +} diff --git a/subprojects/libhandy/src/hdy-deck.h b/subprojects/libhandy/src/hdy-deck.h new file mode 100644 index 0000000..3c7da18 --- /dev/null +++ b/subprojects/libhandy/src/hdy-deck.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-navigation-direction.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_DECK (hdy_deck_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyDeck, hdy_deck, HDY, DECK, GtkContainer) + +typedef enum { + HDY_DECK_TRANSITION_TYPE_OVER, + HDY_DECK_TRANSITION_TYPE_UNDER, + HDY_DECK_TRANSITION_TYPE_SLIDE, +} HdyDeckTransitionType; + +/** + * HdyDeckClass + * @parent_class: The parent class + */ +struct _HdyDeckClass +{ + GtkContainerClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_deck_new (void); +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_deck_get_visible_child (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_visible_child (HdyDeck *self, + GtkWidget *visible_child); +HDY_AVAILABLE_IN_ALL +const gchar *hdy_deck_get_visible_child_name (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_visible_child_name (HdyDeck *self, + const gchar *name); +HDY_AVAILABLE_IN_ALL +gboolean hdy_deck_get_homogeneous (HdyDeck *self, + GtkOrientation orientation); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_homogeneous (HdyDeck *self, + GtkOrientation orientation, + gboolean homogeneous); +HDY_AVAILABLE_IN_ALL +HdyDeckTransitionType hdy_deck_get_transition_type (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_transition_type (HdyDeck *self, + HdyDeckTransitionType transition); + +HDY_AVAILABLE_IN_ALL +guint hdy_deck_get_transition_duration (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_transition_duration (HdyDeck *self, + guint duration); +HDY_AVAILABLE_IN_ALL +gboolean hdy_deck_get_transition_running (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +gboolean hdy_deck_get_interpolate_size (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_interpolate_size (HdyDeck *self, + gboolean interpolate_size); +HDY_AVAILABLE_IN_ALL +gboolean hdy_deck_get_can_swipe_back (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_can_swipe_back (HdyDeck *self, + gboolean can_swipe_back); +HDY_AVAILABLE_IN_ALL +gboolean hdy_deck_get_can_swipe_forward (HdyDeck *self); +HDY_AVAILABLE_IN_ALL +void hdy_deck_set_can_swipe_forward (HdyDeck *self, + gboolean can_swipe_forward); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_deck_get_adjacent_child (HdyDeck *self, + HdyNavigationDirection direction); +HDY_AVAILABLE_IN_ALL +gboolean hdy_deck_navigate (HdyDeck *self, + HdyNavigationDirection direction); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_deck_get_child_by_name (HdyDeck *self, + const gchar *name); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-deprecation-macros.h b/subprojects/libhandy/src/hdy-deprecation-macros.h new file mode 100644 index 0000000..e889135 --- /dev/null +++ b/subprojects/libhandy/src/hdy-deprecation-macros.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2017 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#if defined(HDY_DISABLE_DEPRECATION_WARNINGS) || defined(HANDY_COMPILATION) +# define _HDY_DEPRECATED +# define _HDY_DEPRECATED_FOR(f) +# define _HDY_DEPRECATED_MACRO +# define _HDY_DEPRECATED_MACRO_FOR(f) +# define _HDY_DEPRECATED_ENUMERATOR +# define _HDY_DEPRECATED_ENUMERATOR_FOR(f) +# define _HDY_DEPRECATED_TYPE +# define _HDY_DEPRECATED_TYPE_FOR(f) +#else +# define _HDY_DEPRECATED G_DEPRECATED +# define _HDY_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) +# define _HDY_DEPRECATED_MACRO G_DEPRECATED +# define _HDY_DEPRECATED_MACRO_FOR(f) G_DEPRECATED_FOR(f) +# define _HDY_DEPRECATED_ENUMERATOR G_DEPRECATED +# define _HDY_DEPRECATED_ENUMERATOR_FOR(f) G_DEPRECATED_FOR(f) +# define _HDY_DEPRECATED_TYPE G_DEPRECATED +# define _HDY_DEPRECATED_TYPE_FOR(f) G_DEPRECATED_FOR(f) +#endif diff --git a/subprojects/libhandy/src/hdy-enum-value-object.c b/subprojects/libhandy/src/hdy-enum-value-object.c new file mode 100644 index 0000000..58ca13a --- /dev/null +++ b/subprojects/libhandy/src/hdy-enum-value-object.c @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-enum-value-object.h" + +/** + * SECTION:hdy-enum-value-object + * @short_description: An object representing a #GEnumValue. + * @Title: HdyEnumValueObject + * + * The #HdyEnumValueObject object represents a #GEnumValue, allowing it to be + * used with #GListModel. + * + * Since: 0.0.6 + */ + +struct _HdyEnumValueObject +{ + GObject parent_instance; + + GEnumValue enum_value; +}; + +G_DEFINE_TYPE (HdyEnumValueObject, hdy_enum_value_object, G_TYPE_OBJECT) + +HdyEnumValueObject * +hdy_enum_value_object_new (GEnumValue *enum_value) +{ + HdyEnumValueObject *self = g_object_new (HDY_TYPE_ENUM_VALUE_OBJECT, NULL); + + self->enum_value = *enum_value; + + return self; +} + +static void +hdy_enum_value_object_class_init (HdyEnumValueObjectClass *klass) +{ +} + +static void +hdy_enum_value_object_init (HdyEnumValueObject *self) +{ +} + +gint +hdy_enum_value_object_get_value (HdyEnumValueObject *self) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), 0); + + return self->enum_value.value; +} + +const gchar * +hdy_enum_value_object_get_name (HdyEnumValueObject *self) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), NULL); + + return self->enum_value.value_name; +} + +const gchar * +hdy_enum_value_object_get_nick (HdyEnumValueObject *self) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), NULL); + + return self->enum_value.value_nick; +} diff --git a/subprojects/libhandy/src/hdy-enum-value-object.h b/subprojects/libhandy/src/hdy-enum-value-object.h new file mode 100644 index 0000000..b961a62 --- /dev/null +++ b/subprojects/libhandy/src/hdy-enum-value-object.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gio/gio.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_ENUM_VALUE_OBJECT (hdy_enum_value_object_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyEnumValueObject, hdy_enum_value_object, HDY, ENUM_VALUE_OBJECT, GObject) + +HDY_AVAILABLE_IN_ALL +HdyEnumValueObject *hdy_enum_value_object_new (GEnumValue *enum_value); + +HDY_AVAILABLE_IN_ALL +gint hdy_enum_value_object_get_value (HdyEnumValueObject *self); +HDY_AVAILABLE_IN_ALL +const gchar *hdy_enum_value_object_get_name (HdyEnumValueObject *self); +HDY_AVAILABLE_IN_ALL +const gchar *hdy_enum_value_object_get_nick (HdyEnumValueObject *self); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-enums-private.c.in b/subprojects/libhandy/src/hdy-enums-private.c.in new file mode 100644 index 0000000..5a11cda --- /dev/null +++ b/subprojects/libhandy/src/hdy-enums-private.c.in @@ -0,0 +1,38 @@ +/*** BEGIN file-header ***/ + +#include "config.h" +#include "hdy-enums-private.h" +#include "hdy-stackable-box-private.h" + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static GType etype = 0; + if (G_UNLIKELY(etype == 0)) { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + } + return etype; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ + +/*** END file-tail ***/ diff --git a/subprojects/libhandy/src/hdy-enums-private.h.in b/subprojects/libhandy/src/hdy-enums-private.h.in new file mode 100644 index 0000000..1955f4e --- /dev/null +++ b/subprojects/libhandy/src/hdy-enums-private.h.in @@ -0,0 +1,27 @@ +/*** BEGIN file-header ***/ +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <glib-object.h> + +#include "hdy-enums.h" + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@basename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void); +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS +/*** END file-tail ***/ diff --git a/subprojects/libhandy/src/hdy-enums.c.in b/subprojects/libhandy/src/hdy-enums.c.in new file mode 100644 index 0000000..a630555 --- /dev/null +++ b/subprojects/libhandy/src/hdy-enums.c.in @@ -0,0 +1,44 @@ +/*** BEGIN file-header ***/ + +#include "config.h" +#include "hdy-deck.h" +#include "hdy-enums.h" +#include "hdy-header-bar.h" +#include "hdy-header-group.h" +#include "hdy-leaflet.h" +#include "hdy-navigation-direction.h" +#include "hdy-squeezer.h" +#include "hdy-view-switcher.h" + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static GType etype = 0; + if (G_UNLIKELY(etype == 0)) { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + } + return etype; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ + +/*** END file-tail ***/ diff --git a/subprojects/libhandy/src/hdy-enums.h.in b/subprojects/libhandy/src/hdy-enums.h.in new file mode 100644 index 0000000..7b39850 --- /dev/null +++ b/subprojects/libhandy/src/hdy-enums.h.in @@ -0,0 +1,28 @@ +/*** BEGIN file-header ***/ +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <glib-object.h> + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@basename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ + +HDY_AVAILABLE_IN_ALL GType @enum_name@_get_type (void); +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS +/*** END file-tail ***/ diff --git a/subprojects/libhandy/src/hdy-expander-row.c b/subprojects/libhandy/src/hdy-expander-row.c new file mode 100644 index 0000000..55bf695 --- /dev/null +++ b/subprojects/libhandy/src/hdy-expander-row.c @@ -0,0 +1,762 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include "hdy-expander-row.h" + +#include <glib/gi18n-lib.h> +#include "hdy-action-row.h" + +/** + * SECTION:hdy-expander-row + * @short_description: A #GtkListBox row used to reveal widgets. + * @Title: HdyExpanderRow + * + * The #HdyExpanderRow allows the user to reveal or hide widgets below it. It + * also allows the user to enable the expansion of the row, allowing to disable + * all that the row contains. + * + * It also supports adding a child as an action widget by specifying “action” as + * the “type” attribute of a <child> element. It also supports setting a + * child as a prefix widget by specifying “prefix” as the “type” attribute of a + * <child> element. + * + * # CSS nodes + * + * #HdyExpanderRow has a main CSS node with name row, and the .expander style + * class. It has the .empty style class when it contains no children. + * + * It contains the subnodes row.header for its main embedded row, list.nested + * for the list it can expand, and image.expander-row-arrow for its arrow. + * + * When expanded, #HdyExpanderRow will add the + * .checked-expander-row-previous-sibling style class to its previous sibling, + * and remove it when retracted. + * + * Since: 0.0.6 + */ + +typedef struct +{ + GtkBox *box; + GtkBox *actions; + GtkBox *prefixes; + GtkListBox *list; + HdyActionRow *action_row; + GtkSwitch *enable_switch; + GtkImage *image; + + gboolean expanded; + gboolean enable_expansion; + gboolean show_enable_switch; +} HdyExpanderRowPrivate; + +static void hdy_expander_row_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyExpanderRow, hdy_expander_row, HDY_TYPE_PREFERENCES_ROW, + G_ADD_PRIVATE (HdyExpanderRow) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + hdy_expander_row_buildable_init)) + +static GtkBuildableIface *parent_buildable_iface; + +enum { + PROP_0, + PROP_SUBTITLE, + PROP_USE_UNDERLINE, + PROP_ICON_NAME, + PROP_EXPANDED, + PROP_ENABLE_EXPANSION, + PROP_SHOW_ENABLE_SWITCH, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +static void +update_arrow (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (self)); + GtkWidget *previous_sibling = NULL; + + if (parent) { + g_autoptr (GList) siblings = gtk_container_get_children (GTK_CONTAINER (parent)); + GList *l; + + for (l = siblings; l != NULL && l->next != NULL && l->next->data != self; l = l->next); + + if (l && l->next && l->next->data == self) + previous_sibling = l->data; + } + + if (priv->expanded) + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE); + else + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED); + + if (previous_sibling) { + GtkStyleContext *previous_sibling_context = gtk_widget_get_style_context (previous_sibling); + + if (priv->expanded) + gtk_style_context_add_class (previous_sibling_context, "checked-expander-row-previous-sibling"); + else + gtk_style_context_remove_class (previous_sibling_context, "checked-expander-row-previous-sibling"); + } +} + +static void +hdy_expander_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyExpanderRow *self = HDY_EXPANDER_ROW (object); + + switch (prop_id) { + case PROP_SUBTITLE: + g_value_set_string (value, hdy_expander_row_get_subtitle (self)); + break; + case PROP_USE_UNDERLINE: + g_value_set_boolean (value, hdy_expander_row_get_use_underline (self)); + break; + case PROP_ICON_NAME: + g_value_set_string (value, hdy_expander_row_get_icon_name (self)); + break; + case PROP_EXPANDED: + g_value_set_boolean (value, hdy_expander_row_get_expanded (self)); + break; + case PROP_ENABLE_EXPANSION: + g_value_set_boolean (value, hdy_expander_row_get_enable_expansion (self)); + break; + case PROP_SHOW_ENABLE_SWITCH: + g_value_set_boolean (value, hdy_expander_row_get_show_enable_switch (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_expander_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyExpanderRow *self = HDY_EXPANDER_ROW (object); + + switch (prop_id) { + case PROP_SUBTITLE: + hdy_expander_row_set_subtitle (self, g_value_get_string (value)); + break; + case PROP_USE_UNDERLINE: + hdy_expander_row_set_use_underline (self, g_value_get_boolean (value)); + break; + case PROP_ICON_NAME: + hdy_expander_row_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_EXPANDED: + hdy_expander_row_set_expanded (self, g_value_get_boolean (value)); + break; + case PROP_ENABLE_EXPANSION: + hdy_expander_row_set_enable_expansion (self, g_value_get_boolean (value)); + break; + case PROP_SHOW_ENABLE_SWITCH: + hdy_expander_row_set_show_enable_switch (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_expander_row_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyExpanderRow *self = HDY_EXPANDER_ROW (container); + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + + if (include_internals) + GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->forall (container, + include_internals, + callback, + callback_data); + else { + if (priv->prefixes) + gtk_container_foreach (GTK_CONTAINER (priv->prefixes), callback, callback_data); + if (priv->actions) + gtk_container_foreach (GTK_CONTAINER (priv->actions), callback, callback_data); + if (priv->list) + gtk_container_foreach (GTK_CONTAINER (priv->list), callback, callback_data); + } +} + +static void +activate_cb (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + + hdy_expander_row_set_expanded (self, !priv->expanded); +} + +static void +count_children_cb (GtkWidget *widget, + gint *count) +{ + (*count)++; +} + +static void +list_children_changed_cb (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self)); + gint count = 0; + + gtk_container_foreach (GTK_CONTAINER (priv->list), (GtkCallback) count_children_cb, &count); + + if (count == 0) + gtk_style_context_add_class (context, "empty"); + else + gtk_style_context_remove_class (context, "empty"); +} + +static void +hdy_expander_row_add (GtkContainer *container, + GtkWidget *child) +{ + HdyExpanderRow *self = HDY_EXPANDER_ROW (container); + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + + /* When constructing the widget, we want the box to be added as the child of + * the GtkListBoxRow, as an implementation detail. + */ + if (priv->box == NULL) + GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->add (container, child); + else + gtk_container_add (GTK_CONTAINER (priv->list), child); +} + +static void +hdy_expander_row_remove (GtkContainer *container, + GtkWidget *child) +{ + HdyExpanderRow *self = HDY_EXPANDER_ROW (container); + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + + if (child == GTK_WIDGET (priv->box)) + GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->remove (container, child); + else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->actions)) + gtk_container_remove (GTK_CONTAINER (priv->actions), child); + else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->prefixes)) + gtk_container_remove (GTK_CONTAINER (priv->prefixes), child); + else + gtk_container_remove (GTK_CONTAINER (priv->list), child); +} + +static void +hdy_expander_row_class_init (HdyExpanderRowClass *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 = hdy_expander_row_get_property; + object_class->set_property = hdy_expander_row_set_property; + + container_class->add = hdy_expander_row_add; + container_class->remove = hdy_expander_row_remove; + container_class->forall = hdy_expander_row_forall; + + /** + * HdyExpanderRow:subtitle: + * + * The subtitle for this row. + * + * Since: 1.0 + */ + props[PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + _("Subtitle"), + _("The subtitle for this row"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyExpanderRow:use-underline: + * + * Whether an embedded underline in the text of the title and subtitle labels + * indicates a mnemonic. + * + * Since: 1.0 + */ + props[PROP_USE_UNDERLINE] = + g_param_spec_boolean ("use-underline", + _("Use underline"), + _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"), + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyExpanderRow:icon-name: + * + * The icon name for this row. + * + * Since: 1.0 + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + _("Icon name"), + _("Icon name"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyExpanderRow:expanded: + * + * %TRUE if the row is expanded. + */ + props[PROP_EXPANDED] = + g_param_spec_boolean ("expanded", + _("Expanded"), + _("Whether the row is expanded"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyExpanderRow:enable-expansion: + * + * %TRUE if the expansion is enabled. + */ + props[PROP_ENABLE_EXPANSION] = + g_param_spec_boolean ("enable-expansion", + _("Enable expansion"), + _("Whether the expansion is enabled"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyExpanderRow:show-enable-switch: + * + * %TRUE if the switch enabling the expansion is visible. + */ + props[PROP_SHOW_ENABLE_SWITCH] = + g_param_spec_boolean ("show-enable-switch", + _("Show enable switch"), + _("Whether the switch enabling the expansion is visible"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-expander-row.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, action_row); + gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, box); + gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, actions); + gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, list); + gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, image); + gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, enable_switch); + gtk_widget_class_bind_template_callback (widget_class, activate_cb); + gtk_widget_class_bind_template_callback (widget_class, list_children_changed_cb); +} + +#define NOTIFY(func, prop) \ +static void \ +func (gpointer this) { \ + g_object_notify_by_pspec (G_OBJECT (this), props[prop]); \ +} \ + +NOTIFY (notify_subtitle_cb, PROP_SUBTITLE); +NOTIFY (notify_use_underline_cb, PROP_USE_UNDERLINE); +NOTIFY (notify_icon_name_cb, PROP_ICON_NAME); + +static void +hdy_expander_row_init (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + + priv->prefixes = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + hdy_expander_row_set_enable_expansion (self, TRUE); + hdy_expander_row_set_expanded (self, FALSE); + + g_signal_connect_object (priv->action_row, "notify::subtitle", G_CALLBACK (notify_subtitle_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->action_row, "notify::use-underline", G_CALLBACK (notify_use_underline_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->action_row, "notify::icon-name", G_CALLBACK (notify_icon_name_cb), self, G_CONNECT_SWAPPED); +} + +static void +hdy_expander_row_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + HdyExpanderRow *self = HDY_EXPANDER_ROW (buildable); + HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self); + + if (priv->box == NULL || !type) + gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child)); + else if (type && strcmp (type, "action") == 0) + hdy_expander_row_add_action (self, GTK_WIDGET (child)); + else if (type && strcmp (type, "prefix") == 0) + hdy_expander_row_add_prefix (self, GTK_WIDGET (child)); + else + GTK_BUILDER_WARN_INVALID_CHILD_TYPE (self, type); +} + +static void +hdy_expander_row_buildable_init (GtkBuildableIface *iface) +{ + parent_buildable_iface = g_type_interface_peek_parent (iface); + iface->add_child = hdy_expander_row_buildable_add_child; +} + +/** + * hdy_expander_row_new: + * + * Creates a new #HdyExpanderRow. + * + * Returns: a new #HdyExpanderRow + * + * Since: 0.0.6 + */ +GtkWidget * +hdy_expander_row_new (void) +{ + return g_object_new (HDY_TYPE_EXPANDER_ROW, NULL); +} + +/** + * hdy_expander_row_get_subtitle: + * @self: a #HdyExpanderRow + * + * Gets the subtitle for @self. + * + * Returns: (transfer none) (nullable): the subtitle for @self, or %NULL. + * + * Since: 1.0 + */ +const gchar * +hdy_expander_row_get_subtitle (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), NULL); + + priv = hdy_expander_row_get_instance_private (self); + + return hdy_action_row_get_subtitle (priv->action_row); +} + +/** + * hdy_expander_row_set_subtitle: + * @self: a #HdyExpanderRow + * @subtitle: (nullable): the subtitle + * + * Sets the subtitle for @self. + * + * Since: 1.0 + */ +void +hdy_expander_row_set_subtitle (HdyExpanderRow *self, + const gchar *subtitle) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + + priv = hdy_expander_row_get_instance_private (self); + + hdy_action_row_set_subtitle (priv->action_row, subtitle); +} + +/** + * hdy_expander_row_get_use_underline: + * @self: a #HdyExpanderRow + * + * Gets whether an embedded underline in the text of the title and subtitle + * labels indicates a mnemonic. See hdy_expander_row_set_use_underline(). + * + * Returns: %TRUE if an embedded underline in the title and subtitle labels + * indicates the mnemonic accelerator keys. + * + * Since: 1.0 + */ +gboolean +hdy_expander_row_get_use_underline (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE); + + priv = hdy_expander_row_get_instance_private (self); + + return hdy_action_row_get_use_underline (priv->action_row); +} + +/** + * hdy_expander_row_set_use_underline: + * @self: a #HdyExpanderRow + * @use_underline: %TRUE if underlines in the text indicate mnemonics + * + * If true, an underline in the text of the title and subtitle labels indicates + * the next character should be used for the mnemonic accelerator key. + * + * Since: 1.0 + */ +void +hdy_expander_row_set_use_underline (HdyExpanderRow *self, + gboolean use_underline) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + + priv = hdy_expander_row_get_instance_private (self); + + hdy_action_row_set_use_underline (priv->action_row, use_underline); +} + +/** + * hdy_expander_row_get_icon_name: + * @self: a #HdyExpanderRow + * + * Gets the icon name for @self. + * + * Returns: the icon name for @self. + * + * Since: 1.0 + */ +const gchar * +hdy_expander_row_get_icon_name (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), NULL); + + priv = hdy_expander_row_get_instance_private (self); + + return hdy_action_row_get_icon_name (priv->action_row); +} + +/** + * hdy_expander_row_set_icon_name: + * @self: a #HdyExpanderRow + * @icon_name: the icon name + * + * Sets the icon name for @self. + * + * Since: 1.0 + */ +void +hdy_expander_row_set_icon_name (HdyExpanderRow *self, + const gchar *icon_name) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + + priv = hdy_expander_row_get_instance_private (self); + + hdy_action_row_set_icon_name (priv->action_row, icon_name); +} + +gboolean +hdy_expander_row_get_expanded (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE); + + priv = hdy_expander_row_get_instance_private (self); + + return priv->expanded; +} + +void +hdy_expander_row_set_expanded (HdyExpanderRow *self, + gboolean expanded) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + + priv = hdy_expander_row_get_instance_private (self); + + expanded = !!expanded && priv->enable_expansion; + + if (priv->expanded == expanded) + return; + + priv->expanded = expanded; + + update_arrow (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPANDED]); +} + +/** + * hdy_expander_row_get_enable_expansion: + * @self: a #HdyExpanderRow + * + * Gets whether the expansion of @self is enabled. + * + * Returns: whether the expansion of @self is enabled. + * + * Since: 0.0.6 + */ +gboolean +hdy_expander_row_get_enable_expansion (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE); + + priv = hdy_expander_row_get_instance_private (self); + + return priv->enable_expansion; +} + +/** + * hdy_expander_row_set_enable_expansion: + * @self: a #HdyExpanderRow + * @enable_expansion: %TRUE to enable the expansion + * + * Sets whether the expansion of @self is enabled. + * + * Since: 0.0.6 + */ +void +hdy_expander_row_set_enable_expansion (HdyExpanderRow *self, + gboolean enable_expansion) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + + priv = hdy_expander_row_get_instance_private (self); + + enable_expansion = !!enable_expansion; + + if (priv->enable_expansion == enable_expansion) + return; + + priv->enable_expansion = enable_expansion; + + hdy_expander_row_set_expanded (self, priv->enable_expansion); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLE_EXPANSION]); +} + +/** + * hdy_expander_row_get_show_enable_switch: + * @self: a #HdyExpanderRow + * + * Gets whether the switch enabling the expansion of @self is visible. + * + * Returns: whether the switch enabling the expansion of @self is visible. + * + * Since: 0.0.6 + */ +gboolean +hdy_expander_row_get_show_enable_switch (HdyExpanderRow *self) +{ + HdyExpanderRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE); + + priv = hdy_expander_row_get_instance_private (self); + + return priv->show_enable_switch; +} + +/** + * hdy_expander_row_set_show_enable_switch: + * @self: a #HdyExpanderRow + * @show_enable_switch: %TRUE to show the switch enabling the expansion + * + * Sets whether the switch enabling the expansion of @self is visible. + * + * Since: 0.0.6 + */ +void +hdy_expander_row_set_show_enable_switch (HdyExpanderRow *self, + gboolean show_enable_switch) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + + priv = hdy_expander_row_get_instance_private (self); + + show_enable_switch = !!show_enable_switch; + + if (priv->show_enable_switch == show_enable_switch) + return; + + priv->show_enable_switch = show_enable_switch; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_ENABLE_SWITCH]); +} + +/** + * hdy_expander_row_add_action: + * @self: a #HdyExpanderRow + * @widget: the action widget + * + * Adds an action widget to @self. + * + * Since: 1.0 + */ +void +hdy_expander_row_add_action (HdyExpanderRow *self, + GtkWidget *widget) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + g_return_if_fail (GTK_IS_WIDGET (self)); + + priv = hdy_expander_row_get_instance_private (self); + + gtk_box_pack_start (priv->actions, widget, FALSE, TRUE, 0); + gtk_widget_show (GTK_WIDGET (priv->actions)); +} + +/** + * hdy_expander_row_add_prefix: + * @self: a #HdyExpanderRow + * @widget: the prefix widget + * + * Adds a prefix widget to @self. + * + * Since: 1.0 + */ +void +hdy_expander_row_add_prefix (HdyExpanderRow *self, + GtkWidget *widget) +{ + HdyExpanderRowPrivate *priv; + + g_return_if_fail (HDY_IS_EXPANDER_ROW (self)); + g_return_if_fail (GTK_IS_WIDGET (widget)); + + priv = hdy_expander_row_get_instance_private (self); + + if (priv->prefixes == NULL) { + priv->prefixes = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12)); + gtk_widget_set_no_show_all (GTK_WIDGET (priv->prefixes), TRUE); + gtk_widget_set_can_focus (GTK_WIDGET (priv->prefixes), FALSE); + hdy_action_row_add_prefix (HDY_ACTION_ROW (priv->action_row), GTK_WIDGET (priv->prefixes)); + } + gtk_box_pack_start (priv->prefixes, widget, FALSE, TRUE, 0); + gtk_widget_show (GTK_WIDGET (priv->prefixes)); +} diff --git a/subprojects/libhandy/src/hdy-expander-row.h b/subprojects/libhandy/src/hdy-expander-row.h new file mode 100644 index 0000000..295f1d0 --- /dev/null +++ b/subprojects/libhandy/src/hdy-expander-row.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-preferences-row.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_EXPANDER_ROW (hdy_expander_row_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyExpanderRow, hdy_expander_row, HDY, EXPANDER_ROW, HdyPreferencesRow) + +/** + * HdyExpanderRowClass + * @parent_class: The parent class + */ +struct _HdyExpanderRowClass +{ + HdyPreferencesRowClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_expander_row_new (void); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_expander_row_get_subtitle (HdyExpanderRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_set_subtitle (HdyExpanderRow *self, + const gchar *subtitle); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_expander_row_get_use_underline (HdyExpanderRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_set_use_underline (HdyExpanderRow *self, + gboolean use_underline); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_expander_row_get_icon_name (HdyExpanderRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_set_icon_name (HdyExpanderRow *self, + const gchar *icon_name); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_expander_row_get_expanded (HdyExpanderRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_set_expanded (HdyExpanderRow *self, + gboolean expanded); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_expander_row_get_enable_expansion (HdyExpanderRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_set_enable_expansion (HdyExpanderRow *self, + gboolean enable_expansion); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_expander_row_get_show_enable_switch (HdyExpanderRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_set_show_enable_switch (HdyExpanderRow *self, + gboolean show_enable_switch); + +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_add_action (HdyExpanderRow *self, + GtkWidget *widget); +HDY_AVAILABLE_IN_ALL +void hdy_expander_row_add_prefix (HdyExpanderRow *self, + GtkWidget *widget); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-expander-row.ui b/subprojects/libhandy/src/hdy-expander-row.ui new file mode 100644 index 0000000..54d2650 --- /dev/null +++ b/subprojects/libhandy/src/hdy-expander-row.ui @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="HdyExpanderRow" parent="HdyPreferencesRow"> + <!-- The row must not be activatable, to be sure it doesn't conflict with + clicking nested rows. --> + <property name="activatable">False</property> + <!-- The row must be focusable for keyboard navigation to work as + expected. --> + <property name="can-focus">True</property> + <!-- The row is focusable and can still be activated via keyboard, despite + being marked as inactivatable. Activating the row should toggle its + expansion. --> + <signal name="activate" handler="activate_cb" after="yes"/> + <style> + <class name="empty"/> + <class name="expander"/> + </style> + <child> + <object class="GtkBox" id="box"> + <property name="no-show-all">True</property> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <child> + <object class="GtkListBox"> + <property name="selection-mode">none</property> + <property name="visible">True</property> + <!-- The header row is focusable, activatable, and can be activated + by clicking it or via keyboard. Activating the row should + toggle its expansion. --> + <signal name="row-activated" handler="activate_cb" after="yes" swapped="yes"/> + <child> + <object class="HdyActionRow" id="action_row"> + <!-- The header row must be activatable to toggle expansion by + clicking it or via keyboard activation. --> + <property name="activatable">True</property> + <!-- The header row must be focusable for keyboard navigation to + work as expected. --> + <property name="can-focus">True</property> + <property name="title" bind-source="HdyExpanderRow" bind-property="title" bind-flags="sync-create"/> + <property name="visible">True</property> + <style> + <class name="header"/> + </style> + <child> + <object class="GtkBox" id="actions"> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="spacing">12</property> + <property name="visible">False</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="enable_switch"> + <property name="active" bind-source="HdyExpanderRow" bind-property="enable-expansion" bind-flags="bidirectional|sync-create"/> + <property name="can-focus">True</property> + <property name="valign">center</property> + <property name="visible" bind-source="HdyExpanderRow" bind-property="show-enable-switch" bind-flags="bidirectional|sync-create"/> + </object> + </child> + <child> + <object class="GtkImage" id="image"> + <property name="can-focus">False</property> + <property name="icon-name">hdy-expander-arrow-symbolic</property> + <property name="icon-size">1</property> + <property name="sensitive" bind-source="HdyExpanderRow" bind-property="enable-expansion" bind-flags="sync-create"/> + <property name="visible">True</property> + <style> + <class name="expander-row-arrow"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkRevealer"> + <property name="reveal-child" bind-source="HdyExpanderRow" bind-property="expanded" bind-flags="sync-create"/> + <property name="transition-type">slide-up</property> + <property name="visible">True</property> + <child> + <object class="GtkListBox" id="list"> + <property name="selection-mode">none</property> + <property name="visible">True</property> + <signal name="add" handler="list_children_changed_cb" swapped="yes"/> + <signal name="remove" handler="list_children_changed_cb" swapped="yes"/> + <style> + <class name="nested"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-header-bar.c b/subprojects/libhandy/src/hdy-header-bar.c new file mode 100644 index 0000000..c07a402 --- /dev/null +++ b/subprojects/libhandy/src/hdy-header-bar.c @@ -0,0 +1,2868 @@ +/* + * Copyright (c) 2013 Red Hat, Inc. + * Copyright (C) 2019 Purism SPC + * + * 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 + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-header-bar.h" + +#include "hdy-animation-private.h" +#include "hdy-cairo-private.h" +#include "hdy-css-private.h" +#include "hdy-enums.h" +#include "hdy-window-handle-controller-private.h" +#include "gtkprogresstrackerprivate.h" +#include "gtk-window-private.h" + +/** + * SECTION:hdy-header-bar + * @short_description: A box with a centered child. + * @Title: HdyHeaderBar + * @See_also: #GtkHeaderBar, #HdyApplicationWindow, #HdyTitleBar, #HdyViewSwitcher, #HdyWindow + * + * HdyHeaderBar is similar to #GtkHeaderBar but is designed to fix some of its + * shortcomings for adaptive applications. + * + * HdyHeaderBar doesn't force the custom title widget to be vertically centered, + * hence allowing it to fill up the whole height, which is e.g. needed for + * #HdyViewSwitcher. + * + * When used in a mobile dialog, HdyHeaderBar will replace its window + * decorations by a back button allowing to close it. It doesn't have to be its + * direct child and you can use any complex contraption you like as the dialog's + * titlebar. + * + * #HdyHeaderBar can be used in window's content area rather than titlebar, and + * will still be draggable and will handle right click, middle click and double + * click as expected from a titlebar. This is particularly useful with + * #HdyWindow or #HdyApplicationWindow. + * + * # CSS nodes + * + * #HdyHeaderBar has a single CSS node with name headerbar. + */ + +/** + * HdyCenteringPolicy: + * @HDY_CENTERING_POLICY_LOOSE: Keep the title centered when possible + * @HDY_CENTERING_POLICY_STRICT: Keep the title centered at all cost + */ + +#define DEFAULT_SPACING 6 +#define MIN_TITLE_CHARS 5 + +#define MOBILE_WINDOW_WIDTH 480 +#define MOBILE_WINDOW_HEIGHT 800 + +typedef struct { + gchar *title; + gchar *subtitle; + GtkWidget *title_label; + GtkWidget *subtitle_label; + GtkWidget *label_box; + GtkWidget *label_sizing_box; + GtkWidget *subtitle_sizing_label; + GtkWidget *custom_title; + gint spacing; + gboolean has_subtitle; + + GList *children; + + gboolean shows_wm_decorations; + gchar *decoration_layout; + gboolean decoration_layout_set; + + GtkWidget *titlebar_start_box; + GtkWidget *titlebar_end_box; + + GtkWidget *titlebar_start_separator; + GtkWidget *titlebar_end_separator; + + GtkWidget *titlebar_icon; + + guint tick_id; + GtkProgressTracker tracker; + gboolean first_frame_skipped; + + HdyCenteringPolicy centering_policy; + guint transition_duration; + gboolean interpolate_size; + + gboolean is_mobile_window; + + gulong window_size_allocated_id; + + HdyWindowHandleController *controller; +} HdyHeaderBarPrivate; + +typedef struct _Child Child; +struct _Child +{ + GtkWidget *widget; + GtkPackType pack_type; +}; + +enum { + PROP_0, + PROP_TITLE, + PROP_SUBTITLE, + PROP_HAS_SUBTITLE, + PROP_CUSTOM_TITLE, + PROP_SPACING, + PROP_SHOW_CLOSE_BUTTON, + PROP_DECORATION_LAYOUT, + PROP_DECORATION_LAYOUT_SET, + PROP_CENTERING_POLICY, + PROP_TRANSITION_DURATION, + PROP_TRANSITION_RUNNING, + PROP_INTERPOLATE_SIZE, + LAST_PROP +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_PACK_TYPE, + CHILD_PROP_POSITION +}; + +static GParamSpec *props[LAST_PROP] = { NULL, }; + +static void hdy_header_bar_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyHeaderBar, hdy_header_bar, GTK_TYPE_CONTAINER, + G_ADD_PRIVATE (HdyHeaderBar) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + hdy_header_bar_buildable_init)); + +static gboolean +hdy_header_bar_transition_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (widget); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + if (priv->first_frame_skipped) + gtk_progress_tracker_advance_frame (&priv->tracker, + gdk_frame_clock_get_frame_time (frame_clock)); + else + priv->first_frame_skipped = TRUE; + + /* Finish the animation early if the widget isn't mapped anymore. */ + if (!gtk_widget_get_mapped (widget)) + gtk_progress_tracker_finish (&priv->tracker); + + gtk_widget_queue_resize (widget); + + if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) { + priv->tick_id = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]); + + return FALSE; + } + + return TRUE; +} + +static void +hdy_header_bar_schedule_ticks (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + if (priv->tick_id == 0) { + priv->tick_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self), hdy_header_bar_transition_cb, self, NULL); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]); + } +} + +static void +hdy_header_bar_unschedule_ticks (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + if (priv->tick_id != 0) { + gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->tick_id); + priv->tick_id = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]); + } +} + +static void +hdy_header_bar_start_transition (HdyHeaderBar *self, + guint transition_duration) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkWidget *widget = GTK_WIDGET (self); + + if (gtk_widget_get_mapped (widget) && + priv->interpolate_size && + transition_duration != 0) { + priv->first_frame_skipped = FALSE; + hdy_header_bar_schedule_ticks (self); + gtk_progress_tracker_start (&priv->tracker, + priv->transition_duration * 1000, + 0, + 1.0); + } else { + hdy_header_bar_unschedule_ticks (self); + gtk_progress_tracker_finish (&priv->tracker); + } + + gtk_widget_queue_resize (widget); +} + +static void +init_sizing_box (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkWidget *w; + GtkStyleContext *context; + + /* We use this box to always request size for the two labels (title + * and subtitle) as if they were always visible, but then allocate + * the real label box with its actual size, to keep it center-aligned + * in case we have only the title. + */ + w = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_show (w); + priv->label_sizing_box = g_object_ref_sink (w); + + w = gtk_label_new (NULL); + gtk_widget_show (w); + context = gtk_widget_get_style_context (w); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_TITLE); + gtk_box_pack_start (GTK_BOX (priv->label_sizing_box), w, FALSE, FALSE, 0); + gtk_label_set_line_wrap (GTK_LABEL (w), FALSE); + gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_END); + gtk_label_set_width_chars (GTK_LABEL (w), MIN_TITLE_CHARS); + + w = gtk_label_new (NULL); + context = gtk_widget_get_style_context (w); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUBTITLE); + gtk_box_pack_start (GTK_BOX (priv->label_sizing_box), w, FALSE, FALSE, 0); + gtk_label_set_line_wrap (GTK_LABEL (w), FALSE); + gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_END); + gtk_widget_set_visible (w, priv->has_subtitle || (priv->subtitle && priv->subtitle[0])); + priv->subtitle_sizing_label = w; +} + +static GtkWidget * +create_title_box (const char *title, + const char *subtitle, + GtkWidget **ret_title_label, + GtkWidget **ret_subtitle_label) +{ + GtkWidget *label_box; + GtkWidget *title_label; + GtkWidget *subtitle_label; + GtkStyleContext *context; + + label_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_valign (label_box, GTK_ALIGN_CENTER); + gtk_widget_show (label_box); + + title_label = gtk_label_new (title); + context = gtk_widget_get_style_context (title_label); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_TITLE); + gtk_label_set_line_wrap (GTK_LABEL (title_label), FALSE); + gtk_label_set_single_line_mode (GTK_LABEL (title_label), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (title_label), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (label_box), title_label, FALSE, FALSE, 0); + gtk_widget_show (title_label); + gtk_label_set_width_chars (GTK_LABEL (title_label), MIN_TITLE_CHARS); + + subtitle_label = gtk_label_new (subtitle); + context = gtk_widget_get_style_context (subtitle_label); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUBTITLE); + gtk_label_set_line_wrap (GTK_LABEL (subtitle_label), FALSE); + gtk_label_set_single_line_mode (GTK_LABEL (subtitle_label), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (subtitle_label), PANGO_ELLIPSIZE_END); + gtk_box_pack_start (GTK_BOX (label_box), subtitle_label, FALSE, FALSE, 0); + gtk_widget_set_no_show_all (subtitle_label, TRUE); + gtk_widget_set_visible (subtitle_label, subtitle && subtitle[0]); + + if (ret_title_label) + *ret_title_label = title_label; + if (ret_subtitle_label) + *ret_subtitle_label = subtitle_label; + + return label_box; +} + +static gboolean +hdy_header_bar_update_window_icon (HdyHeaderBar *self, + GtkWindow *window) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GdkPixbuf *pixbuf; + gint scale; + + if (priv->titlebar_icon == NULL) + return FALSE; + + scale = gtk_widget_get_scale_factor (priv->titlebar_icon); + if (GTK_IS_BUTTON (gtk_widget_get_parent (priv->titlebar_icon))) + pixbuf = hdy_gtk_window_get_icon_for_size (window, scale * 16); + else + pixbuf = hdy_gtk_window_get_icon_for_size (window, scale * 20); + + if (pixbuf) { + g_autoptr (cairo_surface_t) surface = + gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, gtk_widget_get_window (priv->titlebar_icon)); + + gtk_image_set_from_surface (GTK_IMAGE (priv->titlebar_icon), surface); + g_object_unref (pixbuf); + gtk_widget_show (priv->titlebar_icon); + + return TRUE; + } + + return FALSE; +} + +static void +_hdy_header_bar_update_separator_visibility (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + gboolean have_visible_at_start = FALSE; + gboolean have_visible_at_end = FALSE; + GList *l; + + for (l = priv->children; l != NULL; l = l->next) { + Child *child = l->data; + + if (gtk_widget_get_visible (child->widget)) { + if (child->pack_type == GTK_PACK_START) + have_visible_at_start = TRUE; + else + have_visible_at_end = TRUE; + } + } + + if (priv->titlebar_start_separator != NULL) + gtk_widget_set_visible (priv->titlebar_start_separator, have_visible_at_start); + + if (priv->titlebar_end_separator != NULL) + gtk_widget_set_visible (priv->titlebar_end_separator, have_visible_at_end); +} + +static void +hdy_header_bar_update_window_buttons (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkWidget *widget = GTK_WIDGET (self), *toplevel; + GtkWindow *window; + GtkTextDirection direction; + gchar *layout_desc; + gchar **tokens, **t; + gint i, j; + GMenuModel *menu; + gboolean shown_by_shell; + gboolean is_sovereign_window; + gboolean is_mobile_dialog; + + toplevel = gtk_widget_get_toplevel (widget); + if (!gtk_widget_is_toplevel (toplevel)) + return; + + if (priv->titlebar_start_box) { + gtk_widget_unparent (priv->titlebar_start_box); + priv->titlebar_start_box = NULL; + priv->titlebar_start_separator = NULL; + } + if (priv->titlebar_end_box) { + gtk_widget_unparent (priv->titlebar_end_box); + priv->titlebar_end_box = NULL; + priv->titlebar_end_separator = NULL; + } + + priv->titlebar_icon = NULL; + + if (!priv->shows_wm_decorations) + return; + + direction = gtk_widget_get_direction (widget); + + g_object_get (gtk_widget_get_settings (widget), + "gtk-shell-shows-app-menu", &shown_by_shell, + "gtk-decoration-layout", &layout_desc, + NULL); + + if (priv->decoration_layout_set) { + g_free (layout_desc); + layout_desc = g_strdup (priv->decoration_layout); + } + + window = GTK_WINDOW (toplevel); + + if (!shown_by_shell && gtk_window_get_application (window)) + menu = gtk_application_get_app_menu (gtk_window_get_application (window)); + else + menu = NULL; + + is_sovereign_window = (!gtk_window_get_modal (window) && + gtk_window_get_transient_for (window) == NULL && + gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL); + + is_mobile_dialog= (priv->is_mobile_window && !is_sovereign_window); + + tokens = g_strsplit (layout_desc, ":", 2); + if (tokens) { + for (i = 0; i < 2; i++) { + GtkWidget *box; + GtkWidget *separator; + int n_children = 0; + + if (tokens[i] == NULL) + break; + + t = g_strsplit (tokens[i], ",", -1); + + separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL); + gtk_widget_set_no_show_all (separator, TRUE); + gtk_style_context_add_class (gtk_widget_get_style_context (separator), "titlebutton"); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, priv->spacing); + + for (j = 0; t[j]; j++) { + GtkWidget *button = NULL; + GtkWidget *image = NULL; + AtkObject *accessible; + + if (strcmp (t[j], "icon") == 0 && + is_sovereign_window) { + button = gtk_image_new (); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + priv->titlebar_icon = button; + gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton"); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "icon"); + gtk_widget_set_size_request (button, 20, 20); + gtk_widget_show (button); + + if (!hdy_header_bar_update_window_icon (self, window)) + { + gtk_widget_destroy (button); + priv->titlebar_icon = NULL; + button = NULL; + } + } else if (strcmp (t[j], "menu") == 0 && + menu != NULL && + is_sovereign_window) { + button = gtk_menu_button_new (); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (button), menu); + gtk_menu_button_set_use_popover (GTK_MENU_BUTTON (button), TRUE); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton"); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "appmenu"); + image = gtk_image_new (); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_widget_set_can_focus (button, FALSE); + gtk_widget_show_all (button); + + accessible = gtk_widget_get_accessible (button); + if (GTK_IS_ACCESSIBLE (accessible)) + atk_object_set_name (accessible, _("Application menu")); + + priv->titlebar_icon = image; + if (!hdy_header_bar_update_window_icon (self, window)) + gtk_image_set_from_icon_name (GTK_IMAGE (priv->titlebar_icon), + "application-x-executable-symbolic", GTK_ICON_SIZE_MENU); + } else if (strcmp (t[j], "minimize") == 0 && + is_sovereign_window) { + button = gtk_button_new (); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton"); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "minimize"); + image = gtk_image_new_from_icon_name ("window-minimize-symbolic", GTK_ICON_SIZE_MENU); + g_object_set (image, "use-fallback", TRUE, NULL); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_widget_set_can_focus (button, FALSE); + gtk_widget_show_all (button); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (gtk_window_iconify), window); + + accessible = gtk_widget_get_accessible (button); + if (GTK_IS_ACCESSIBLE (accessible)) + atk_object_set_name (accessible, _("Minimize")); + } else if (strcmp (t[j], "maximize") == 0 && + gtk_window_get_resizable (window) && + is_sovereign_window) { + const gchar *icon_name; + gboolean maximized = gtk_window_is_maximized (window); + + icon_name = maximized ? "window-restore-symbolic" : "window-maximize-symbolic"; + button = gtk_button_new (); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton"); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "maximize"); + image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU); + g_object_set (image, "use-fallback", TRUE, NULL); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_widget_set_can_focus (button, FALSE); + gtk_widget_show_all (button); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (hdy_gtk_window_toggle_maximized), window); + + accessible = gtk_widget_get_accessible (button); + if (GTK_IS_ACCESSIBLE (accessible)) + atk_object_set_name (accessible, maximized ? _("Restore") : _("Maximize")); + } else if (strcmp (t[j], "close") == 0 && + gtk_window_get_deletable (window) && + !is_mobile_dialog) { + button = gtk_button_new (); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + image = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton"); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "close"); + g_object_set (image, "use-fallback", TRUE, NULL); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_widget_set_can_focus (button, FALSE); + gtk_widget_show_all (button); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (gtk_window_close), window); + + accessible = gtk_widget_get_accessible (button); + if (GTK_IS_ACCESSIBLE (accessible)) + atk_object_set_name (accessible, _("Close")); + } else if (i == 0 && /* Only at the start. */ + gtk_window_get_deletable (window) && + is_mobile_dialog) { + button = gtk_button_new (); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + image = gtk_image_new_from_icon_name ("go-previous-symbolic", GTK_ICON_SIZE_BUTTON); + g_object_set (image, "use-fallback", TRUE, NULL); + gtk_container_add (GTK_CONTAINER (button), image); + gtk_widget_set_can_focus (button, TRUE); + gtk_widget_show_all (button); + g_signal_connect_swapped (button, "clicked", + G_CALLBACK (gtk_window_close), window); + + accessible = gtk_widget_get_accessible (button); + if (GTK_IS_ACCESSIBLE (accessible)) + atk_object_set_name (accessible, _("Back")); + } + + if (button) { + gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0); + n_children ++; + } + } + g_strfreev (t); + + if (n_children == 0) { + g_object_ref_sink (box); + g_object_unref (box); + g_object_ref_sink (separator); + g_object_unref (separator); + continue; + } + + gtk_box_pack_start (GTK_BOX (box), separator, FALSE, FALSE, 0); + if (i == 1) + gtk_box_reorder_child (GTK_BOX (box), separator, 0); + + if ((direction == GTK_TEXT_DIR_LTR && i == 0) || + (direction == GTK_TEXT_DIR_RTL && i == 1)) + gtk_style_context_add_class (gtk_widget_get_style_context (box), GTK_STYLE_CLASS_LEFT); + else + gtk_style_context_add_class (gtk_widget_get_style_context (box), GTK_STYLE_CLASS_RIGHT); + + gtk_widget_show (box); + gtk_widget_set_parent (box, GTK_WIDGET (self)); + + if (i == 0) { + priv->titlebar_start_box = box; + priv->titlebar_start_separator = separator; + } else { + priv->titlebar_end_box = box; + priv->titlebar_end_separator = separator; + } + } + g_strfreev (tokens); + } + g_free (layout_desc); + + _hdy_header_bar_update_separator_visibility (self); +} + +static gboolean +compute_is_mobile_window (GtkWindow *window) +{ + gint window_width, window_height; + + gtk_window_get_size (window, &window_width, &window_height); + + if (window_width <= MOBILE_WINDOW_WIDTH && + gtk_window_is_maximized (window)) + return TRUE; + + /* Mobile landscape mode. */ + if (window_width <= MOBILE_WINDOW_HEIGHT && + window_height <= MOBILE_WINDOW_WIDTH && + gtk_window_is_maximized (window)) + return TRUE; + + return FALSE; +} + +static void +update_is_mobile_window (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + gboolean was_mobile_window = priv->is_mobile_window; + + if (!gtk_widget_is_toplevel (toplevel)) + return; + + priv->is_mobile_window = compute_is_mobile_window (GTK_WINDOW (toplevel)); + + if (priv->is_mobile_window != was_mobile_window) + hdy_header_bar_update_window_buttons (self); +} + +static void +construct_label_box (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_assert (priv->label_box == NULL); + + priv->label_box = create_title_box (priv->title, + priv->subtitle, + &priv->title_label, + &priv->subtitle_label); + gtk_widget_set_parent (priv->label_box, GTK_WIDGET (self)); +} + +static gint +count_visible_children (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + Child *child; + gint n; + + n = 0; + for (l = priv->children; l; l = l->next) { + child = l->data; + if (gtk_widget_get_visible (child->widget)) + n++; + } + + return n; +} + +static gint +count_visible_children_for_pack_type (HdyHeaderBar *self, GtkPackType pack_type) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + Child *child; + gint n; + + n = 0; + for (l = priv->children; l; l = l->next) { + child = l->data; + if (gtk_widget_get_visible (child->widget) && child->pack_type == pack_type) + n++; + } + + return n; +} + +static gboolean +add_child_size (GtkWidget *child, + GtkOrientation orientation, + gint *minimum, + gint *natural) +{ + gint child_minimum, child_natural; + + if (!gtk_widget_get_visible (child)) + return FALSE; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width (child, &child_minimum, &child_natural); + else + gtk_widget_get_preferred_height (child, &child_minimum, &child_natural); + + if (GTK_ORIENTATION_HORIZONTAL == orientation) { + *minimum += child_minimum; + *natural += child_natural; + } else { + *minimum = MAX (*minimum, child_minimum); + *natural = MAX (*natural, child_natural); + } + + return TRUE; +} + +static void +hdy_header_bar_get_size (GtkWidget *widget, + GtkOrientation orientation, + gint *minimum, + gint *natural) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (widget); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + gint n_start_children = 0, n_end_children = 0; + gint start_min = 0, start_nat = 0; + gint end_min = 0, end_nat = 0; + gint center_min = 0, center_nat = 0; + + for (l = priv->children; l; l = l->next) { + Child *child = l->data; + + if (child->pack_type == GTK_PACK_START) { + if (add_child_size (child->widget, orientation, &start_min, &start_nat)) + n_start_children += 1; + } else { + if (add_child_size (child->widget, orientation, &end_min, &end_nat)) + n_end_children += 1; + } + } + + if (priv->label_box != NULL) { + if (orientation == GTK_ORIENTATION_HORIZONTAL) + add_child_size (priv->label_box, orientation, ¢er_min, ¢er_nat); + else + add_child_size (priv->label_sizing_box, orientation, ¢er_min, ¢er_nat); + } + + if (priv->custom_title != NULL) + add_child_size (priv->custom_title, orientation, ¢er_min, ¢er_nat); + + if (priv->titlebar_start_box != NULL) { + if (add_child_size (priv->titlebar_start_box, orientation, &start_min, &start_nat)) + n_start_children += 1; + } + + if (priv->titlebar_end_box != NULL) { + if (add_child_size (priv->titlebar_end_box, orientation, &end_min, &end_nat)) + n_end_children += 1; + } + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + gdouble strict_centering_t; + gint start_min_spaced = start_min + n_start_children * priv->spacing; + gint end_min_spaced = end_min + n_end_children * priv->spacing; + gint start_nat_spaced = start_nat + n_start_children * priv->spacing; + gint end_nat_spaced = end_nat + n_end_children * priv->spacing; + + if (gtk_progress_tracker_get_state (&priv->tracker) != GTK_PROGRESS_STATE_AFTER) { + strict_centering_t = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE); + if (priv->centering_policy != HDY_CENTERING_POLICY_STRICT) + strict_centering_t = 1.0 - strict_centering_t; + } else + strict_centering_t = priv->centering_policy == HDY_CENTERING_POLICY_STRICT ? 1.0 : 0.0; + + *minimum = center_min + n_start_children * priv->spacing + + hdy_lerp (start_min_spaced + end_min_spaced, + 2 * MAX (start_min_spaced, end_min_spaced), + strict_centering_t); + *natural = center_nat + n_start_children * priv->spacing + + hdy_lerp (start_nat_spaced + end_nat_spaced, + 2 * MAX (start_nat_spaced, end_nat_spaced), + strict_centering_t); + } else { + *minimum = MAX (MAX (start_min, end_min), center_min); + *natural = MAX (MAX (start_nat, end_nat), center_nat); + } +} + +static void +hdy_header_bar_compute_size_for_orientation (GtkWidget *widget, + gint avail_size, + gint *minimum_size, + gint *natural_size) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (widget); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *children; + gint required_size = 0; + gint required_natural = 0; + gint child_size; + gint child_natural; + gint nvis_children = 0; + + for (children = priv->children; children != NULL; children = children->next) { + Child *child = children->data; + + if (gtk_widget_get_visible (child->widget)) { + gtk_widget_get_preferred_width_for_height (child->widget, + avail_size, &child_size, &child_natural); + + required_size += child_size; + required_natural += child_natural; + + nvis_children += 1; + } + } + + if (priv->label_box != NULL) { + gtk_widget_get_preferred_width (priv->label_sizing_box, + &child_size, &child_natural); + required_size += child_size; + required_natural += child_natural; + } + + if (priv->custom_title != NULL && + gtk_widget_get_visible (priv->custom_title)) { + gtk_widget_get_preferred_width (priv->custom_title, + &child_size, &child_natural); + required_size += child_size; + required_natural += child_natural; + } + + if (priv->titlebar_start_box != NULL) { + gtk_widget_get_preferred_width (priv->titlebar_start_box, + &child_size, &child_natural); + required_size += child_size; + required_natural += child_natural; + nvis_children += 1; + } + + if (priv->titlebar_end_box != NULL) { + gtk_widget_get_preferred_width (priv->titlebar_end_box, + &child_size, &child_natural); + required_size += child_size; + required_natural += child_natural; + nvis_children += 1; + } + + required_size += nvis_children * priv->spacing; + required_natural += nvis_children * priv->spacing; + + *minimum_size = required_size; + *natural_size = required_natural; +} + +static void +hdy_header_bar_compute_size_for_opposing_orientation (GtkWidget *widget, + gint avail_size, + gint *minimum_size, + gint *natural_size) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (widget); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + Child *child; + GList *children; + gint nvis_children; + gint computed_minimum = 0; + gint computed_natural = 0; + GtkRequestedSize *sizes; + GtkPackType packing; + gint i; + gint child_size; + gint child_minimum; + gint child_natural; + gint center_min, center_nat; + + nvis_children = count_visible_children (self); + + if (nvis_children <= 0) + return; + + sizes = g_newa (GtkRequestedSize, nvis_children); + + /* Retrieve desired size for visible children */ + for (i = 0, children = priv->children; children; children = children->next) { + child = children->data; + + if (gtk_widget_get_visible (child->widget)) { + gtk_widget_get_preferred_width (child->widget, + &sizes[i].minimum_size, + &sizes[i].natural_size); + + sizes[i].data = child; + i += 1; + } + } + + /* Bring children up to size first */ + gtk_distribute_natural_allocation (MAX (0, avail_size), nvis_children, sizes); + + /* Allocate child positions. */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; ++packing) { + for (i = 0, children = priv->children; children; children = children->next) { + child = children->data; + + /* If widget is not visible, skip it. */ + if (!gtk_widget_get_visible (child->widget)) + continue; + + /* If widget is packed differently skip it, but still increment i, + * since widget is visible and will be handled in next loop + * iteration. + */ + if (child->pack_type != packing) { + i++; + continue; + } + + child_size = sizes[i].minimum_size; + + gtk_widget_get_preferred_height_for_width (child->widget, + child_size, &child_minimum, &child_natural); + + computed_minimum = MAX (computed_minimum, child_minimum); + computed_natural = MAX (computed_natural, child_natural); + } + } + + center_min = center_nat = 0; + if (priv->label_box != NULL) { + gtk_widget_get_preferred_height (priv->label_sizing_box, + ¢er_min, ¢er_nat); + } + + if (priv->custom_title != NULL && + gtk_widget_get_visible (priv->custom_title)) { + gtk_widget_get_preferred_height (priv->custom_title, + ¢er_min, ¢er_nat); + } + + if (priv->titlebar_start_box != NULL) { + gtk_widget_get_preferred_height (priv->titlebar_start_box, + &child_minimum, &child_natural); + computed_minimum = MAX (computed_minimum, child_minimum); + computed_natural = MAX (computed_natural, child_natural); + } + + if (priv->titlebar_end_box != NULL) { + gtk_widget_get_preferred_height (priv->titlebar_end_box, + &child_minimum, &child_natural); + computed_minimum = MAX (computed_minimum, child_minimum); + computed_natural = MAX (computed_natural, child_natural); + } + + *minimum_size = computed_minimum; + *natural_size = computed_natural; +} + +static void +hdy_header_bar_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + gint css_width, css_height; + + gtk_style_context_get (gtk_widget_get_style_context (widget), + gtk_widget_get_state_flags (widget), + "min-width", &css_width, + "min-height", &css_height, + NULL); + + if (for_size < 0) + hdy_header_bar_get_size (widget, orientation, minimum, natural); + else if (orientation == GTK_ORIENTATION_HORIZONTAL) + hdy_header_bar_compute_size_for_orientation (widget, MAX (for_size, css_height), minimum, natural); + else + hdy_header_bar_compute_size_for_opposing_orientation (widget, MAX (for_size, css_width), minimum, natural); + + hdy_css_measure (widget, orientation, minimum, natural); +} + +static void +hdy_header_bar_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_header_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum, natural, + NULL, NULL); +} + +static void +hdy_header_bar_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_header_bar_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum, natural, + NULL, NULL); +} + +static void +hdy_header_bar_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + hdy_header_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum, natural, + NULL, NULL); +} + +static void +hdy_header_bar_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + hdy_header_bar_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, + NULL, NULL); +} + +static GtkWidget * +get_title_size (HdyHeaderBar *self, + gint for_height, + GtkRequestedSize *size, + gint *expanded) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkWidget *title_widget; + + if (priv->custom_title != NULL && + gtk_widget_get_visible (priv->custom_title)) + title_widget = priv->custom_title; + else if (priv->label_box != NULL) + title_widget = priv->label_box; + else + return NULL; + + gtk_widget_get_preferred_width_for_height (title_widget, + for_height, + &(size->minimum_size), + &(size->natural_size)); + + *expanded = gtk_widget_compute_expand (title_widget, GTK_ORIENTATION_HORIZONTAL); + + return title_widget; +} + +static void +children_allocate (HdyHeaderBar *self, + GtkAllocation *allocation, + GtkAllocation **allocations, + GtkRequestedSize *sizes, + gint decoration_width[2], + gint uniform_expand_bonus[2], + gint leftover_expand_bonus[2]) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkPackType packing; + GtkAllocation child_allocation; + gint x; + gint i; + GList *l; + Child *child; + gint child_size; + /* GtkTextDirection direction; */ + + /* Allocate the children on both sides of the title. */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + child_allocation.y = allocation->y; + child_allocation.height = allocation->height; + if (packing == GTK_PACK_START) + x = allocation->x + decoration_width[0]; + else + x = allocation->x + allocation->width - decoration_width[1]; + + i = 0; + for (l = priv->children; l != NULL; l = l->next) { + child = l->data; + if (!gtk_widget_get_visible (child->widget)) + continue; + + if (child->pack_type != packing) + goto next; + + child_size = sizes[i].minimum_size; + + /* If this child is expanded, give it extra space from the reserves. */ + if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL)) { + gint expand_bonus; + + expand_bonus = uniform_expand_bonus[packing]; + + if (leftover_expand_bonus[packing] > 0) { + expand_bonus++; + leftover_expand_bonus[packing]--; + } + + child_size += expand_bonus; + } + + child_allocation.width = child_size; + + if (packing == GTK_PACK_START) { + child_allocation.x = x; + x += child_size; + x += priv->spacing; + } else { + x -= child_size; + child_allocation.x = x; + x -= priv->spacing; + } + + if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) + child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width; + + (*allocations)[i] = child_allocation; + + next: + i++; + } + } +} + +static void +get_loose_centering_allocations (HdyHeaderBar *self, + GtkAllocation *allocation, + GtkAllocation **allocations, + GtkAllocation *title_allocation, + gint decoration_width[2]) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkRequestedSize *sizes; + gint width; + gint nvis_children; + GtkRequestedSize title_size = { 0 }; + gboolean title_expands = FALSE; + gint side[2] = { 0 }; + gint uniform_expand_bonus[2] = { 0 }; + gint leftover_expand_bonus[2] = { 0 }; + gint side_free_space[2] = { 0 }; + gint center_free_space[2] = { 0 }; + gint nexpand_children[2] = { 0 }; + gint center_free_space_min; + GList *l; + gint i; + Child *child; + GtkPackType packing; + + nvis_children = count_visible_children (self); + sizes = g_newa (GtkRequestedSize, nvis_children); + + width = allocation->width - nvis_children * priv->spacing; + + i = 0; + for (l = priv->children; l; l = l->next) { + child = l->data; + if (!gtk_widget_get_visible (child->widget)) + continue; + + if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL)) + nexpand_children[child->pack_type]++; + + gtk_widget_get_preferred_width_for_height (child->widget, + allocation->height, + &sizes[i].minimum_size, + &sizes[i].natural_size); + width -= sizes[i].minimum_size; + i++; + } + + get_title_size (self, allocation->height, &title_size, &title_expands); + width -= title_size.minimum_size; + + /* Compute the nominal size of the children filling up each side of the title + * in titlebar. + */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + i = 0; + for (l = priv->children; l != NULL; l = l->next) { + child = l->data; + if (!gtk_widget_get_visible (child->widget)) + continue; + + if (child->pack_type == packing) + side[packing] += sizes[i].minimum_size + priv->spacing; + + i++; + } + } + + /* Distribute the available space for natural expansion of the children. */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) + width -= decoration_width[packing]; + width = gtk_distribute_natural_allocation (MAX (0, width), 1, &title_size); + width = gtk_distribute_natural_allocation (MAX (0, width), nvis_children, sizes); + + /* Compute the nominal size of the children filling up each side of the title + * in titlebar. + */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + i = 0; + side[packing] = 0; + for (l = priv->children; l != NULL; l = l->next) { + child = l->data; + if (!gtk_widget_get_visible (child->widget)) + continue; + + if (child->pack_type == packing) + side[packing] += sizes[i].minimum_size + priv->spacing; + + i++; + } + } + + /* Figure out how much space is left on each side of the title, and earkmark + * that space for the expanded children. + * + * If the title itself is expanded, then it gets half the spoils from each + * side. + */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + side_free_space[packing] = MIN (MAX (allocation->width / 2 - title_size.natural_size / 2 - decoration_width[packing] - side[packing], 0), width); + if (title_expands) + center_free_space[packing] = nexpand_children[packing] > 0 ? + side_free_space[packing] / 2 : + side_free_space[packing]; + } + center_free_space_min = MIN (center_free_space[0], center_free_space[1]); + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + center_free_space[packing] = center_free_space_min; + side_free_space[packing] -= center_free_space[packing]; + width -= side_free_space[packing]; + + if (nexpand_children[packing] == 0) + continue; + + uniform_expand_bonus[packing] = (side_free_space[packing]) / nexpand_children[packing]; + leftover_expand_bonus[packing] = (side_free_space[packing]) % nexpand_children[packing]; + } + + children_allocate (self, allocation, allocations, sizes, decoration_width, uniform_expand_bonus, leftover_expand_bonus); + + /* We don't enforce css borders on the center widget, to make title/subtitle + * combinations fit without growing the header. + */ + title_allocation->y = allocation->y; + title_allocation->height = allocation->height; + + title_allocation->width = MIN (allocation->width - decoration_width[0] - side[0] - decoration_width[1] - side[1], + title_size.natural_size); + title_allocation->x = allocation->x + (allocation->width - title_allocation->width) / 2; + + /* If the title widget is expanded, then grow it by all the available free + * space, and recenter it. + */ + if (title_expands && width > 0) { + title_allocation->width += width; + title_allocation->x -= width / 2; + } + + if (allocation->x + decoration_width[0] + side[0] > title_allocation->x) + title_allocation->x = allocation->x + decoration_width[0] + side[0]; + else if (allocation->x + allocation->width - decoration_width[1] - side[1] < title_allocation->x + title_allocation->width) + title_allocation->x = allocation->x + allocation->width - decoration_width[1] - side[1] - title_allocation->width; + + if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) + title_allocation->x = allocation->x + allocation->width - (title_allocation->x - allocation->x) - title_allocation->width; +} + +static void +get_strict_centering_allocations (HdyHeaderBar *self, + GtkAllocation *allocation, + GtkAllocation **allocations, + GtkAllocation *title_allocation, + gint decoration_width[2]) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + GtkRequestedSize *children_sizes = { 0 }; + GtkRequestedSize *children_sizes_for_side[2] = { 0 }; + GtkRequestedSize side_size[2] = { 0 }; /* The size requested by each side. */ + GtkRequestedSize title_size = { 0 }; /* The size requested by the title. */ + GtkRequestedSize side_request = { 0 }; /* The maximum size requested by each side, decoration included. */ + gint side_max; /* The maximum space allocatable to each side, decoration included. */ + gint title_leftover; /* The or 0px or 1px leftover from ensuring each side is allocated the same size. */ + /* The space available for expansion on each side, including for the title. */ + gint free_space[2] = { 0 }; + /* The space the title will take from the free space of each side for its expansion. */ + gint title_expand_bonus = 0; + gint uniform_expand_bonus[2] = { 0 }; + gint leftover_expand_bonus[2] = { 0 }; + + gint nvis_children, n_side_vis_children[2] = { 0 }; + gint nexpand_children[2] = { 0 }; + gboolean title_expands = FALSE; + GList *l; + gint i; + Child *child; + GtkPackType packing; + + get_title_size (self, allocation->height, &title_size, &title_expands); + + nvis_children = count_visible_children (self); + children_sizes = g_newa (GtkRequestedSize, nvis_children); + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + n_side_vis_children[packing] = count_visible_children_for_pack_type (self, packing); + children_sizes_for_side[packing] = packing == 0 ? children_sizes : children_sizes + n_side_vis_children[packing - 1]; + free_space[packing] = (allocation->width - title_size.minimum_size) / 2 - decoration_width[packing]; + } + + /* Compute the nominal size of the children filling up each side of the title + * in titlebar. + */ + i = 0; + for (l = priv->children; l; l = l->next) { + child = l->data; + if (!gtk_widget_get_visible (child->widget)) + continue; + + if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL)) + nexpand_children[child->pack_type]++; + + gtk_widget_get_preferred_width_for_height (child->widget, + allocation->height, + &children_sizes[i].minimum_size, + &children_sizes[i].natural_size); + side_size[child->pack_type].minimum_size += children_sizes[i].minimum_size + priv->spacing; + side_size[child->pack_type].natural_size += children_sizes[i].natural_size + priv->spacing; + free_space[child->pack_type] -= children_sizes[i].minimum_size + priv->spacing; + + i++; + } + + /* Figure out the space maximum size requests from each side to help centering + * the title. + */ + side_request.minimum_size = MAX (side_size[GTK_PACK_START].minimum_size + decoration_width[GTK_PACK_START], + side_size[GTK_PACK_END].minimum_size + decoration_width[GTK_PACK_END]); + side_request.natural_size = MAX (side_size[GTK_PACK_START].natural_size + decoration_width[GTK_PACK_START], + side_size[GTK_PACK_END].natural_size + decoration_width[GTK_PACK_END]); + title_leftover = (allocation->width - title_size.natural_size) % 2; + side_max = MAX ((allocation->width - title_size.natural_size) / 2, side_request.minimum_size); + + /* Distribute the available space for natural expansion of the children and + * figure out how much space is left on each side of the title, free to be + * used for expansion. + */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + gint leftovers = side_max - side_size[packing].minimum_size - decoration_width[packing]; + free_space[packing] = gtk_distribute_natural_allocation (leftovers, n_side_vis_children[packing], children_sizes_for_side[packing]); + } + + /* Compute how much of each side's free space should be distributed to the + * title for its expansion. + */ + title_expand_bonus = !title_expands ? 0 : + MIN (nexpand_children[GTK_PACK_START] > 0 ? free_space[GTK_PACK_START] / 2 : + free_space[GTK_PACK_START], + nexpand_children[GTK_PACK_END] > 0 ? free_space[GTK_PACK_END] / 2 : + free_space[GTK_PACK_END]); + + /* Remove the space the title takes from each side for its expansion. */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) + free_space[packing] -= title_expand_bonus; + + /* Distribute the free space for expansion of the children. */ + for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + if (nexpand_children[packing] == 0) + continue; + + uniform_expand_bonus[packing] = free_space[packing] / nexpand_children[packing]; + leftover_expand_bonus[packing] = free_space[packing] % nexpand_children[packing]; + } + + children_allocate (self, allocation, allocations, children_sizes, decoration_width, uniform_expand_bonus, leftover_expand_bonus); + + /* We don't enforce css borders on the center widget, to make title/subtitle + * combinations fit without growing the header. + */ + title_allocation->y = allocation->y; + title_allocation->height = allocation->height; + + title_allocation->width = MIN (allocation->width - 2 * side_max + title_leftover, + title_size.natural_size); + title_allocation->x = allocation->x + (allocation->width - title_allocation->width) / 2; + + /* If the title widget is expanded, then grow it by the free space available + * for it. + */ + if (title_expands) { + title_allocation->width += 2 * title_expand_bonus; + title_allocation->x -= title_expand_bonus; + } + + if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) + title_allocation->x = allocation->x + allocation->width - (title_allocation->x - allocation->x) - title_allocation->width; +} + +static void +hdy_header_bar_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (widget); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GtkAllocation *allocations; + GtkAllocation title_allocation; + GtkAllocation clip; + gint nvis_children; + GList *l; + gint i; + Child *child; + GtkAllocation child_allocation; + GtkTextDirection direction; + GtkWidget *decoration_box[2] = { priv->titlebar_start_box, priv->titlebar_end_box }; + gint decoration_width[2] = { 0 }; + + gtk_render_background_get_clip (gtk_widget_get_style_context (widget), + allocation->x, + allocation->y, + allocation->width, + allocation->height, + &clip); + + gtk_widget_set_allocation (widget, allocation); + + if (gtk_widget_get_realized (widget)) + gdk_window_move_resize (gtk_widget_get_window (widget), + allocation->x, + allocation->y, + allocation->width, + allocation->height); + + hdy_css_size_allocate (widget, allocation); + + direction = gtk_widget_get_direction (widget); + nvis_children = count_visible_children (self); + allocations = g_newa (GtkAllocation, nvis_children); + + /* Get the decoration width. */ + for (GtkPackType packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) { + gint min, nat; + + if (decoration_box[packing] == NULL) + continue; + + gtk_widget_get_preferred_width_for_height (decoration_box[packing], + allocation->height, + &min, &nat); + decoration_width[packing] = nat + priv->spacing; + } + + /* Allocate the decoration widgets. */ + child_allocation.y = allocation->y; + child_allocation.height = allocation->height; + + if (priv->titlebar_start_box) { + if (direction == GTK_TEXT_DIR_LTR) + child_allocation.x = allocation->x; + else + child_allocation.x = allocation->x + allocation->width - decoration_width[GTK_PACK_START] + priv->spacing; + child_allocation.width = decoration_width[GTK_PACK_START] - priv->spacing; + gtk_widget_size_allocate (priv->titlebar_start_box, &child_allocation); + } + + if (priv->titlebar_end_box) { + if (direction != GTK_TEXT_DIR_LTR) + child_allocation.x = allocation->x; + else + child_allocation.x = allocation->x + allocation->width - decoration_width[GTK_PACK_END] + priv->spacing; + child_allocation.width = decoration_width[GTK_PACK_END] - priv->spacing; + gtk_widget_size_allocate (priv->titlebar_end_box, &child_allocation); + } + + /* Get the allocation for widgets on both side of the title. */ + if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) { + if (priv->centering_policy == HDY_CENTERING_POLICY_STRICT) + get_strict_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width); + else + get_loose_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width); + } else { + /* For memory usage optimisation's sake, we will use the allocations + * variable to store the loose centering allocations and the + * title_allocation variable to store the loose title allocation. + */ + GtkAllocation *strict_allocations = g_newa (GtkAllocation, nvis_children); + GtkAllocation strict_title_allocation; + gdouble strict_centering_t = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE); + + if (priv->centering_policy != HDY_CENTERING_POLICY_STRICT) + strict_centering_t = 1.0 - strict_centering_t; + + get_loose_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width); + get_strict_centering_allocations (self, allocation, &strict_allocations, &strict_title_allocation, decoration_width); + + for (i = 0; i < nvis_children; i++) { + allocations[i].x = hdy_lerp (allocations[i].x, strict_allocations[i].x, strict_centering_t); + allocations[i].y = hdy_lerp (allocations[i].y, strict_allocations[i].y, strict_centering_t); + allocations[i].width = hdy_lerp (allocations[i].width, strict_allocations[i].width, strict_centering_t); + allocations[i].height = hdy_lerp (allocations[i].height, strict_allocations[i].height, strict_centering_t); + } + title_allocation.x = hdy_lerp (title_allocation.x, strict_title_allocation.x, strict_centering_t); + title_allocation.y = hdy_lerp (title_allocation.y, strict_title_allocation.y, strict_centering_t); + title_allocation.width = hdy_lerp (title_allocation.width, strict_title_allocation.width, strict_centering_t); + title_allocation.height = hdy_lerp (title_allocation.height, strict_title_allocation.height, strict_centering_t); + } + + /* Allocate the children on both sides of the title. */ + i = 0; + for (l = priv->children; l != NULL; l = l->next) { + child = l->data; + if (!gtk_widget_get_visible (child->widget)) + continue; + + gtk_widget_size_allocate (child->widget, &allocations[i]); + i++; + } + + /* Allocate the title widget. */ + if (priv->custom_title != NULL && gtk_widget_get_visible (priv->custom_title)) + gtk_widget_size_allocate (priv->custom_title, &title_allocation); + else if (priv->label_box != NULL) + gtk_widget_size_allocate (priv->label_box, &title_allocation); + + gtk_widget_set_clip (widget, &clip); +} + +static void +hdy_header_bar_destroy (GtkWidget *widget) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (widget)); + + if (priv->label_sizing_box) { + gtk_widget_destroy (priv->label_sizing_box); + g_clear_object (&priv->label_sizing_box); + } + + if (priv->custom_title) { + gtk_widget_unparent (priv->custom_title); + priv->custom_title = NULL; + } + + if (priv->label_box) { + gtk_widget_unparent (priv->label_box); + priv->label_box = NULL; + } + + if (priv->titlebar_start_box) { + gtk_widget_unparent (priv->titlebar_start_box); + priv->titlebar_start_box = NULL; + priv->titlebar_start_separator = NULL; + } + + if (priv->titlebar_end_box) { + gtk_widget_unparent (priv->titlebar_end_box); + priv->titlebar_end_box = NULL; + priv->titlebar_end_separator = NULL; + } + + GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->destroy (widget); +} + +static void +hdy_header_bar_finalize (GObject *object) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (object)); + + g_clear_pointer (&priv->title, g_free); + g_clear_pointer (&priv->subtitle, g_free); + g_clear_pointer (&priv->decoration_layout, g_free); + g_clear_object (&priv->controller); + + G_OBJECT_CLASS (hdy_header_bar_parent_class)->finalize (object); +} + +static void +hdy_header_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (object); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, priv->title); + break; + case PROP_SUBTITLE: + g_value_set_string (value, priv->subtitle); + break; + case PROP_CUSTOM_TITLE: + g_value_set_object (value, priv->custom_title); + break; + case PROP_SPACING: + g_value_set_int (value, priv->spacing); + break; + case PROP_SHOW_CLOSE_BUTTON: + g_value_set_boolean (value, hdy_header_bar_get_show_close_button (self)); + break; + case PROP_HAS_SUBTITLE: + g_value_set_boolean (value, hdy_header_bar_get_has_subtitle (self)); + break; + case PROP_DECORATION_LAYOUT: + g_value_set_string (value, hdy_header_bar_get_decoration_layout (self)); + break; + case PROP_DECORATION_LAYOUT_SET: + g_value_set_boolean (value, priv->decoration_layout_set); + break; + case PROP_CENTERING_POLICY: + g_value_set_enum (value, hdy_header_bar_get_centering_policy (self)); + break; + case PROP_TRANSITION_DURATION: + g_value_set_uint (value, hdy_header_bar_get_transition_duration (self)); + break; + case PROP_TRANSITION_RUNNING: + g_value_set_boolean (value, hdy_header_bar_get_transition_running (self)); + break; + case PROP_INTERPOLATE_SIZE: + g_value_set_boolean (value, hdy_header_bar_get_interpolate_size (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_header_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (object); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + switch (prop_id) { + case PROP_TITLE: + hdy_header_bar_set_title (self, g_value_get_string (value)); + break; + case PROP_SUBTITLE: + hdy_header_bar_set_subtitle (self, g_value_get_string (value)); + break; + case PROP_CUSTOM_TITLE: + hdy_header_bar_set_custom_title (self, g_value_get_object (value)); + break; + case PROP_SPACING: + if (priv->spacing != g_value_get_int (value)) { + priv->spacing = g_value_get_int (value); + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (object, pspec); + } + break; + case PROP_SHOW_CLOSE_BUTTON: + hdy_header_bar_set_show_close_button (self, g_value_get_boolean (value)); + break; + case PROP_HAS_SUBTITLE: + hdy_header_bar_set_has_subtitle (self, g_value_get_boolean (value)); + break; + case PROP_DECORATION_LAYOUT: + hdy_header_bar_set_decoration_layout (self, g_value_get_string (value)); + break; + case PROP_DECORATION_LAYOUT_SET: + priv->decoration_layout_set = g_value_get_boolean (value); + break; + case PROP_CENTERING_POLICY: + hdy_header_bar_set_centering_policy (self, g_value_get_enum (value)); + break; + case PROP_TRANSITION_DURATION: + hdy_header_bar_set_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_INTERPOLATE_SIZE: + hdy_header_bar_set_interpolate_size (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +notify_child_cb (GObject *child, + GParamSpec *pspec, + HdyHeaderBar *self) +{ + _hdy_header_bar_update_separator_visibility (self); +} + +static void +hdy_header_bar_pack (HdyHeaderBar *self, + GtkWidget *widget, + GtkPackType pack_type) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + Child *child; + + g_return_if_fail (gtk_widget_get_parent (widget) == NULL); + + child = g_new (Child, 1); + child->widget = widget; + child->pack_type = pack_type; + + priv->children = g_list_append (priv->children, child); + + gtk_widget_freeze_child_notify (widget); + gtk_widget_set_parent (widget, GTK_WIDGET (self)); + g_signal_connect (widget, "notify::visible", G_CALLBACK (notify_child_cb), self); + gtk_widget_child_notify (widget, "pack-type"); + gtk_widget_child_notify (widget, "position"); + gtk_widget_thaw_child_notify (widget); + + _hdy_header_bar_update_separator_visibility (self); +} + +static void +hdy_header_bar_add (GtkContainer *container, + GtkWidget *child) +{ + hdy_header_bar_pack (HDY_HEADER_BAR (container), child, GTK_PACK_START); +} + +static GList * +find_child_link (HdyHeaderBar *self, + GtkWidget *widget, + gint *position) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + Child *child; + gint i; + + for (l = priv->children, i = 0; l; l = l->next, i++) { + child = l->data; + if (child->widget == widget) { + if (position) + *position = i; + + return l; + } + } + + return NULL; +} + +static void +hdy_header_bar_remove (GtkContainer *container, + GtkWidget *widget) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (container); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + Child *child; + + l = find_child_link (self, widget, NULL); + if (l) { + child = l->data; + g_signal_handlers_disconnect_by_func (widget, notify_child_cb, self); + gtk_widget_unparent (child->widget); + priv->children = g_list_delete_link (priv->children, l); + g_free (child); + gtk_widget_queue_resize (GTK_WIDGET (container)); + _hdy_header_bar_update_separator_visibility (self); + } +} + +static void +hdy_header_bar_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (container); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + Child *child; + GList *children; + + if (include_internals && priv->titlebar_start_box != NULL) + (* callback) (priv->titlebar_start_box, callback_data); + + children = priv->children; + while (children) { + child = children->data; + children = children->next; + if (child->pack_type == GTK_PACK_START) + (* callback) (child->widget, callback_data); + } + + if (priv->custom_title != NULL) + (* callback) (priv->custom_title, callback_data); + + if (include_internals && priv->label_box != NULL) + (* callback) (priv->label_box, callback_data); + + children = priv->children; + while (children) { + child = children->data; + children = children->next; + if (child->pack_type == GTK_PACK_END) + (* callback) (child->widget, callback_data); + } + + if (include_internals && priv->titlebar_end_box != NULL) + (* callback) (priv->titlebar_end_box, callback_data); +} + +static void +hdy_header_bar_reorder_child (HdyHeaderBar *self, + GtkWidget *widget, + gint position) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + gint old_position; + Child *child; + + l = find_child_link (self, widget, &old_position); + + if (l == NULL) + return; + + if (old_position == position) + return; + + child = l->data; + priv->children = g_list_delete_link (priv->children, l); + + if (position < 0) + l = NULL; + else + l = g_list_nth (priv->children, position); + + priv->children = g_list_insert_before (priv->children, l, child); + gtk_widget_child_notify (widget, "position"); + gtk_widget_queue_resize (widget); +} + +static GType +hdy_header_bar_child_type (GtkContainer *container) +{ + return GTK_TYPE_WIDGET; +} + +static void +hdy_header_bar_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (container); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + GList *l; + Child *child; + + l = find_child_link (self, widget, NULL); + if (l == NULL) { + g_param_value_set_default (pspec, value); + + return; + } + + child = l->data; + + switch (property_id) { + case CHILD_PROP_PACK_TYPE: + g_value_set_enum (value, child->pack_type); + break; + + case CHILD_PROP_POSITION: + g_value_set_int (value, g_list_position (priv->children, l)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_header_bar_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (container); + GList *l; + Child *child; + + l = find_child_link (self, widget, NULL); + if (l == NULL) + return; + + child = l->data; + + switch (property_id) { + case CHILD_PROP_PACK_TYPE: + child->pack_type = g_value_get_enum (value); + _hdy_header_bar_update_separator_visibility (self); + gtk_widget_queue_resize (widget); + break; + + case CHILD_PROP_POSITION: + hdy_header_bar_reorder_child (self, widget, g_value_get_int (value)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static gboolean +hdy_header_bar_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkStyleContext *context; + + context = gtk_widget_get_style_context (widget); + /* GtkWidget draws nothing by default so we have to render the background + * explicitly for HdyHederBar to render the typical titlebar background. + */ + gtk_render_background (context, + cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + /* Ditto for the borders. */ + gtk_render_frame (context, + cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + + return GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->draw (widget, cr); +} + +static void +hdy_header_bar_realize (GtkWidget *widget) +{ + GtkSettings *settings; + GtkAllocation allocation; + GdkWindowAttr attributes; + gint attributes_mask; + GdkWindow *window; + + settings = gtk_widget_get_settings (widget); + g_signal_connect_swapped (settings, "notify::gtk-shell-shows-app-menu", + G_CALLBACK (hdy_header_bar_update_window_buttons), widget); + g_signal_connect_swapped (settings, "notify::gtk-decoration-layout", + G_CALLBACK (hdy_header_bar_update_window_buttons), widget); + update_is_mobile_window (HDY_HEADER_BAR (widget)); + hdy_header_bar_update_window_buttons (HDY_HEADER_BAR (widget)); + + gtk_widget_get_allocation (widget, &allocation); + gtk_widget_set_realized (widget, TRUE); + + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.event_mask = gtk_widget_get_events (widget); + attributes.visual = gtk_widget_get_visual (widget); + attributes.wclass = GDK_INPUT_OUTPUT; + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + window = gdk_window_new (gtk_widget_get_parent_window (widget), + &attributes, + attributes_mask); + gtk_widget_set_window (widget, window); + gtk_widget_register_window (widget, window); +} + +static void +hdy_header_bar_unrealize (GtkWidget *widget) +{ + GtkSettings *settings; + + settings = gtk_widget_get_settings (widget); + + g_signal_handlers_disconnect_by_func (settings, hdy_header_bar_update_window_buttons, widget); + + GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->unrealize (widget); +} + +static gboolean +window_state_changed (GtkWidget *window, + GdkEventWindowState *event, + gpointer data) +{ + HdyHeaderBar *self = HDY_HEADER_BAR (data); + + if (event->changed_mask & (GDK_WINDOW_STATE_FULLSCREEN | + GDK_WINDOW_STATE_MAXIMIZED | + GDK_WINDOW_STATE_TILED | + GDK_WINDOW_STATE_TOP_TILED | + GDK_WINDOW_STATE_RIGHT_TILED | + GDK_WINDOW_STATE_BOTTOM_TILED | + GDK_WINDOW_STATE_LEFT_TILED)) + hdy_header_bar_update_window_buttons (self); + + return FALSE; +} + +static void +hdy_header_bar_hierarchy_changed (GtkWidget *widget, + GtkWidget *previous_toplevel) +{ + GtkWidget *toplevel; + HdyHeaderBar *self = HDY_HEADER_BAR (widget); + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + toplevel = gtk_widget_get_toplevel (widget); + + if (previous_toplevel) + g_signal_handlers_disconnect_by_func (previous_toplevel, + window_state_changed, widget); + + if (toplevel) + g_signal_connect_after (toplevel, "window-state-event", + G_CALLBACK (window_state_changed), widget); + + if (priv->window_size_allocated_id > 0) { + g_signal_handler_disconnect (previous_toplevel, priv->window_size_allocated_id); + priv->window_size_allocated_id = 0; + } + + if (GTK_IS_WINDOW (toplevel)) + priv->window_size_allocated_id = + g_signal_connect_swapped (toplevel, "size-allocate", + G_CALLBACK (update_is_mobile_window), self); + + update_is_mobile_window (self); + hdy_header_bar_update_window_buttons (self); +} + +static void +hdy_header_bar_class_init (HdyHeaderBarClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class); + + object_class->finalize = hdy_header_bar_finalize; + object_class->get_property = hdy_header_bar_get_property; + object_class->set_property = hdy_header_bar_set_property; + + widget_class->destroy = hdy_header_bar_destroy; + widget_class->size_allocate = hdy_header_bar_size_allocate; + widget_class->get_preferred_width = hdy_header_bar_get_preferred_width; + widget_class->get_preferred_height = hdy_header_bar_get_preferred_height; + widget_class->get_preferred_height_for_width = hdy_header_bar_get_preferred_height_for_width; + widget_class->get_preferred_width_for_height = hdy_header_bar_get_preferred_width_for_height; + widget_class->draw = hdy_header_bar_draw; + widget_class->realize = hdy_header_bar_realize; + widget_class->unrealize = hdy_header_bar_unrealize; + widget_class->hierarchy_changed = hdy_header_bar_hierarchy_changed; + + container_class->add = hdy_header_bar_add; + container_class->remove = hdy_header_bar_remove; + container_class->forall = hdy_header_bar_forall; + container_class->child_type = hdy_header_bar_child_type; + container_class->set_child_property = hdy_header_bar_set_child_property; + container_class->get_child_property = hdy_header_bar_get_child_property; + gtk_container_class_handle_border_width (container_class); + + gtk_container_class_install_child_property (container_class, + CHILD_PROP_PACK_TYPE, + g_param_spec_enum ("pack-type", + _("Pack type"), + _("A GtkPackType indicating whether the child is packed with reference to the start or end of the parent"), + GTK_TYPE_PACK_TYPE, GTK_PACK_START, + G_PARAM_READWRITE)); + gtk_container_class_install_child_property (container_class, + CHILD_PROP_POSITION, + g_param_spec_int ("position", + _("Position"), + _("The index of the child in the parent"), + -1, G_MAXINT, 0, + G_PARAM_READWRITE)); + + props[PROP_TITLE] = + g_param_spec_string ("title", + _("Title"), + _("The title to display"), + NULL, + G_PARAM_READWRITE); + + props[PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + _("Subtitle"), + _("The subtitle to display"), + NULL, + G_PARAM_READWRITE); + + props[PROP_CUSTOM_TITLE] = + g_param_spec_object ("custom-title", + _("Custom Title"), + _("Custom title widget to display"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS); + + props[PROP_SPACING] = + g_param_spec_int ("spacing", + _("Spacing"), + _("The amount of space between children"), + 0, G_MAXINT, + DEFAULT_SPACING, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyHeaderBar:show-close-button: + * + * Whether to show window decorations. + * + * Which buttons are actually shown and where is determined + * by the #HdyHeaderBar:decoration-layout property, and by + * the state of the window (e.g. a close button will not be + * shown if the window can't be closed). + * + * Since: 0.0.10 + */ + props[PROP_SHOW_CLOSE_BUTTON] = + g_param_spec_boolean ("show-close-button", + _("Show decorations"), + _("Whether to show window decorations"), + FALSE, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyHeaderBar:decoration-layout: + * + * The decoration layout for buttons. If this property is + * not set, the #GtkSettings:gtk-decoration-layout setting + * is used. + * + * See hdy_header_bar_set_decoration_layout() for information + * about the format of this string. + * + * Since: 0.0.10 + */ + props[PROP_DECORATION_LAYOUT] = + g_param_spec_string ("decoration-layout", + _("Decoration Layout"), + _("The layout for window decorations"), + NULL, + G_PARAM_READWRITE); + + /** + * HdyHeaderBar:decoration-layout-set: + * + * Set to %TRUE if #HdyHeaderBar:decoration-layout is set. + * + * Since: 0.0.10 + */ + props[PROP_DECORATION_LAYOUT_SET] = + g_param_spec_boolean ("decoration-layout-set", + _("Decoration Layout Set"), + _("Whether the decoration-layout property has been set"), + FALSE, + G_PARAM_READWRITE); + + /** + * HdyHeaderBar:has-subtitle: + * + * If %TRUE, reserve space for a subtitle, even if none + * is currently set. + * + * Since: 0.0.10 + */ + props[PROP_HAS_SUBTITLE] = + g_param_spec_boolean ("has-subtitle", + _("Has Subtitle"), + _("Whether to reserve space for a subtitle"), + TRUE, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_CENTERING_POLICY] = + g_param_spec_enum ("centering-policy", + _("Centering policy"), + _("The policy to horizontally align the center widget"), + HDY_TYPE_CENTERING_POLICY, HDY_CENTERING_POLICY_LOOSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TRANSITION_DURATION] = + g_param_spec_uint ("transition-duration", + _("Transition duration"), + _("The animation duration, in milliseconds"), + 0, G_MAXUINT, 200, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TRANSITION_RUNNING] = + g_param_spec_boolean ("transition-running", + _("Transition running"), + _("Whether or not the transition is currently running"), + FALSE, + G_PARAM_READABLE); + + props[PROP_INTERPOLATE_SIZE] = + g_param_spec_boolean ("interpolate-size", + _("Interpolate size"), + _("Whether or not the size should smoothly change when changing between differently sized children"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL); + gtk_widget_class_set_css_name (widget_class, "headerbar"); +} + +static void +hdy_header_bar_init (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv; + GtkStyleContext *context; + + priv = hdy_header_bar_get_instance_private (self); + + priv->title = NULL; + priv->subtitle = NULL; + priv->custom_title = NULL; + priv->children = NULL; + priv->spacing = DEFAULT_SPACING; + priv->has_subtitle = TRUE; + priv->decoration_layout = NULL; + priv->decoration_layout_set = FALSE; + priv->transition_duration = 200; + + init_sizing_box (self); + construct_label_box (self); + + priv->controller = hdy_window_handle_controller_new (GTK_WIDGET (self)); + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + /* Ensure the widget has the titlebar style class. */ + gtk_style_context_add_class (context, "titlebar"); +} + +static void +hdy_header_bar_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + if (type && strcmp (type, "title") == 0) + hdy_header_bar_set_custom_title (HDY_HEADER_BAR (buildable), GTK_WIDGET (child)); + else if (!type) + gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child)); + else + GTK_BUILDER_WARN_INVALID_CHILD_TYPE (HDY_HEADER_BAR (buildable), type); +} + +static void +hdy_header_bar_buildable_init (GtkBuildableIface *iface) +{ + iface->add_child = hdy_header_bar_buildable_add_child; +} + +/** + * hdy_header_bar_new: + * + * Creates a new #HdyHeaderBar widget. + * + * Returns: a new #HdyHeaderBar + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_header_bar_new (void) +{ + return GTK_WIDGET (g_object_new (HDY_TYPE_HEADER_BAR, NULL)); +} + +/** + * hdy_header_bar_pack_start: + * @self: A #HdyHeaderBar + * @child: the #GtkWidget to be added to @self: + * + * Adds @child to @self:, packed with reference to the + * start of the @self:. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_pack_start (HdyHeaderBar *self, + GtkWidget *child) +{ + hdy_header_bar_pack (self, child, GTK_PACK_START); +} + +/** + * hdy_header_bar_pack_end: + * @self: A #HdyHeaderBar + * @child: the #GtkWidget to be added to @self: + * + * Adds @child to @self:, packed with reference to the + * end of the @self:. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_pack_end (HdyHeaderBar *self, + GtkWidget *child) +{ + hdy_header_bar_pack (self, child, GTK_PACK_END); +} + +/** + * hdy_header_bar_set_title: + * @self: a #HdyHeaderBar + * @title: (nullable): a title, or %NULL + * + * Sets the title of the #HdyHeaderBar. The title should help a user + * identify the current view. A good title should not include the + * application name. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_title (HdyHeaderBar *self, + const gchar *title) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + gchar *new_title; + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + new_title = g_strdup (title); + g_free (priv->title); + priv->title = new_title; + + if (priv->title_label != NULL) { + gtk_label_set_label (GTK_LABEL (priv->title_label), priv->title); + gtk_widget_queue_resize (GTK_WIDGET (self)); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +/** + * hdy_header_bar_get_title: + * @self: a #HdyHeaderBar + * + * Retrieves the title of the header. See hdy_header_bar_set_title(). + * + * Returns: (nullable): the title of the header, or %NULL if none has + * been set explicitly. The returned string is owned by the widget + * and must not be modified or freed. + * + * Since: 0.0.10 + */ +const gchar * +hdy_header_bar_get_title (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL); + + return priv->title; +} + +/** + * hdy_header_bar_set_subtitle: + * @self: a #HdyHeaderBar + * @subtitle: (nullable): a subtitle, or %NULL + * + * Sets the subtitle of the #HdyHeaderBar. The title should give a user + * an additional detail to help them identify the current view. + * + * Note that HdyHeaderBar by default reserves room for the subtitle, + * even if none is currently set. If this is not desired, set the + * #HdyHeaderBar:has-subtitle property to %FALSE. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_subtitle (HdyHeaderBar *self, + const gchar *subtitle) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + gchar *new_subtitle; + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + new_subtitle = g_strdup (subtitle); + g_free (priv->subtitle); + priv->subtitle = new_subtitle; + + if (priv->subtitle_label != NULL) { + gtk_label_set_label (GTK_LABEL (priv->subtitle_label), priv->subtitle); + gtk_widget_set_visible (priv->subtitle_label, priv->subtitle && priv->subtitle[0]); + gtk_widget_queue_resize (GTK_WIDGET (self)); + } + + gtk_widget_set_visible (priv->subtitle_sizing_label, priv->has_subtitle || (priv->subtitle && priv->subtitle[0])); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]); +} + +/** + * hdy_header_bar_get_subtitle: + * @self: a #HdyHeaderBar + * + * Retrieves the subtitle of the header. See hdy_header_bar_set_subtitle(). + * + * Returns: (nullable): the subtitle of the header, or %NULL if none has + * been set explicitly. The returned string is owned by the widget + * and must not be modified or freed. + * + * Since: 0.0.10 + */ +const gchar * +hdy_header_bar_get_subtitle (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL); + + return priv->subtitle; +} + +/** + * hdy_header_bar_set_custom_title: + * @self: a #HdyHeaderBar + * @title_widget: (nullable): a custom widget to use for a title + * + * Sets a custom title for the #HdyHeaderBar. + * + * The title should help a user identify the current view. This + * supersedes any title set by hdy_header_bar_set_title() or + * hdy_header_bar_set_subtitle(). To achieve the same style as + * the builtin title and subtitle, use the “title” and “subtitle” + * style classes. + * + * You should set the custom title to %NULL, for the header title + * label to be visible again. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_custom_title (HdyHeaderBar *self, + GtkWidget *title_widget) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + if (title_widget) + g_return_if_fail (GTK_IS_WIDGET (title_widget)); + + /* No need to do anything if the custom widget stays the same */ + if (priv->custom_title == title_widget) + return; + + if (priv->custom_title) { + GtkWidget *custom = priv->custom_title; + + priv->custom_title = NULL; + gtk_widget_unparent (custom); + } + + if (title_widget != NULL) { + priv->custom_title = title_widget; + + gtk_widget_set_parent (priv->custom_title, GTK_WIDGET (self)); + + if (priv->label_box != NULL) { + GtkWidget *label_box = priv->label_box; + + priv->label_box = NULL; + priv->title_label = NULL; + priv->subtitle_label = NULL; + gtk_widget_unparent (label_box); + } + } else { + if (priv->label_box == NULL) + construct_label_box (self); + } + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CUSTOM_TITLE]); +} + +/** + * hdy_header_bar_get_custom_title: + * @self: a #HdyHeaderBar + * + * Retrieves the custom title widget of the header. See + * hdy_header_bar_set_custom_title(). + * + * Returns: (nullable) (transfer none): the custom title widget + * of the header, or %NULL if none has been set explicitly. + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_header_bar_get_custom_title (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL); + + return priv->custom_title; +} + +/** + * hdy_header_bar_get_show_close_button: + * @self: a #HdyHeaderBar + * + * Returns whether this header bar shows the standard window + * decorations. + * + * Returns: %TRUE if the decorations are shown + * + * Since: 0.0.10 + */ +gboolean +hdy_header_bar_get_show_close_button (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv; + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE); + + priv = hdy_header_bar_get_instance_private (self); + + return priv->shows_wm_decorations; +} + +/** + * hdy_header_bar_set_show_close_button: + * @self: a #HdyHeaderBar + * @setting: %TRUE to show standard window decorations + * + * Sets whether this header bar shows the standard window decorations, + * including close, maximize, and minimize. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_show_close_button (HdyHeaderBar *self, + gboolean setting) +{ + HdyHeaderBarPrivate *priv; + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + priv = hdy_header_bar_get_instance_private (self); + + setting = setting != FALSE; + + if (priv->shows_wm_decorations == setting) + return; + + priv->shows_wm_decorations = setting; + hdy_header_bar_update_window_buttons (self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_CLOSE_BUTTON]); +} + +/** + * hdy_header_bar_set_has_subtitle: + * @self: a #HdyHeaderBar + * @setting: %TRUE to reserve space for a subtitle + * + * Sets whether the header bar should reserve space + * for a subtitle, even if none is currently set. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_has_subtitle (HdyHeaderBar *self, + gboolean setting) +{ + HdyHeaderBarPrivate *priv; + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + priv = hdy_header_bar_get_instance_private (self); + + setting = setting != FALSE; + + if (priv->has_subtitle == setting) + return; + + priv->has_subtitle = setting; + gtk_widget_set_visible (priv->subtitle_sizing_label, setting || (priv->subtitle && priv->subtitle[0])); + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_SUBTITLE]); +} + +/** + * hdy_header_bar_get_has_subtitle: + * @self: a #HdyHeaderBar + * + * Retrieves whether the header bar reserves space for + * a subtitle, regardless if one is currently set or not. + * + * Returns: %TRUE if the header bar reserves space + * for a subtitle + * + * Since: 0.0.10 + */ +gboolean +hdy_header_bar_get_has_subtitle (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv; + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE); + + priv = hdy_header_bar_get_instance_private (self); + + return priv->has_subtitle; +} + +/** + * hdy_header_bar_set_decoration_layout: + * @self: a #HdyHeaderBar + * @layout: (nullable): a decoration layout, or %NULL to unset the layout + * + * Sets the decoration layout for this header bar, overriding + * the #GtkSettings:gtk-decoration-layout setting. + * + * There can be valid reasons for overriding the setting, such + * as a header bar design that does not allow for buttons to take + * room on the right, or only offers room for a single close button. + * Split header bars are another example for overriding the + * setting. + * + * The format of the string is button names, separated by commas. + * A colon separates the buttons that should appear on the left + * from those on the right. Recognized button names are minimize, + * maximize, close, icon (the window icon) and menu (a menu button + * for the fallback app menu). + * + * For example, “menu:minimize,maximize,close” specifies a menu + * on the left, and minimize, maximize and close buttons on the right. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_decoration_layout (HdyHeaderBar *self, + const gchar *layout) +{ + HdyHeaderBarPrivate *priv; + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + priv = hdy_header_bar_get_instance_private (self); + + g_clear_pointer (&priv->decoration_layout, g_free); + priv->decoration_layout = g_strdup (layout); + priv->decoration_layout_set = (layout != NULL); + + hdy_header_bar_update_window_buttons (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATION_LAYOUT]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATION_LAYOUT_SET]); +} + +/** + * hdy_header_bar_get_decoration_layout: + * @self: a #HdyHeaderBar + * + * Gets the decoration layout set with + * hdy_header_bar_set_decoration_layout(). + * + * Returns: the decoration layout + * + * Since: 0.0.10 + */ +const gchar * +hdy_header_bar_get_decoration_layout (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv; + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL); + + priv = hdy_header_bar_get_instance_private (self); + + return priv->decoration_layout; +} + +/** + * hdy_header_bar_get_centering_policy: + * @self: a #HdyHeaderBar + * + * Gets the policy @self follows to horizontally align its center widget. + * + * Returns: the centering policy + * + * Since: 0.0.10 + */ +HdyCenteringPolicy +hdy_header_bar_get_centering_policy (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), HDY_CENTERING_POLICY_LOOSE); + + return priv->centering_policy; +} + +/** + * hdy_header_bar_set_centering_policy: + * @self: a #HdyHeaderBar + * @centering_policy: the centering policy + * + * Sets the policy @self must follow to horizontally align its center widget. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_centering_policy (HdyHeaderBar *self, + HdyCenteringPolicy centering_policy) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + if (priv->centering_policy == centering_policy) + return; + + priv->centering_policy = centering_policy; + if (priv->interpolate_size) + hdy_header_bar_start_transition (self, priv->transition_duration); + else + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CENTERING_POLICY]); +} + +/** + * hdy_header_bar_get_transition_duration: + * @self: a #HdyHeaderBar + * + * Returns the amount of time (in milliseconds) that + * transitions between pages in @self will take. + * + * Returns: the transition duration + * + * Since: 0.0.10 + */ +guint +hdy_header_bar_get_transition_duration (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), 0); + + return priv->transition_duration; +} + +/** + * hdy_header_bar_set_transition_duration: + * @self: a #HdyHeaderBar + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between pages in @self + * will take. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_transition_duration (HdyHeaderBar *self, + guint duration) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + if (priv->transition_duration == duration) + return; + + priv->transition_duration = duration; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]); +} + +/** + * hdy_header_bar_get_transition_running: + * @self: a #HdyHeaderBar + * + * Returns whether the @self is currently in a transition from one page to + * another. + * + * Returns: %TRUE if the transition is currently running, %FALSE otherwise. + * + * Since: 0.0.10 + */ +gboolean +hdy_header_bar_get_transition_running (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE); + + return (priv->tick_id != 0); +} + +/** + * hdy_header_bar_get_interpolate_size: + * @self: A #HdyHeaderBar + * + * Gets whether @self should interpolate its size on visible child change. + * + * See hdy_header_bar_set_interpolate_size(). + * + * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not + * + * Since: 0.0.10 + */ +gboolean +hdy_header_bar_get_interpolate_size (HdyHeaderBar *self) +{ + HdyHeaderBarPrivate *priv; + + g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE); + + priv = hdy_header_bar_get_instance_private (self); + + return priv->interpolate_size; +} + +/** + * hdy_header_bar_set_interpolate_size: + * @self: A #HdyHeaderBar + * @interpolate_size: %TRUE to interpolate the size + * + * Sets whether or not @self will interpolate the size of its opposing + * orientation when changing the visible child. If %TRUE, @self will interpolate + * its size between the one of the previous visible child and the one of the new + * visible child, according to the set transition duration and the orientation, + * e.g. if @self is horizontal, it will interpolate the its height. + * + * Since: 0.0.10 + */ +void +hdy_header_bar_set_interpolate_size (HdyHeaderBar *self, + gboolean interpolate_size) +{ + HdyHeaderBarPrivate *priv; + + g_return_if_fail (HDY_IS_HEADER_BAR (self)); + + priv = hdy_header_bar_get_instance_private (self); + + interpolate_size = !!interpolate_size; + + if (priv->interpolate_size == interpolate_size) + return; + + priv->interpolate_size = interpolate_size; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]); +} diff --git a/subprojects/libhandy/src/hdy-header-bar.h b/subprojects/libhandy/src/hdy-header-bar.h new file mode 100644 index 0000000..066d847 --- /dev/null +++ b/subprojects/libhandy/src/hdy-header-bar.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2013 Red Hat, Inc. + * Copyright (C) 2019 Purism SPC + * + * 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 + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_HEADER_BAR (hdy_header_bar_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyHeaderBar, hdy_header_bar, HDY, HEADER_BAR, GtkContainer) + +typedef enum { + HDY_CENTERING_POLICY_LOOSE, + HDY_CENTERING_POLICY_STRICT, +} HdyCenteringPolicy; + +/** + * HdyHeaderBarClass + * @parent_class: The parent class + */ +struct _HdyHeaderBarClass +{ + GtkContainerClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_header_bar_new (void); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_header_bar_get_title (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_title (HdyHeaderBar *self, + const gchar *title); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_header_bar_get_subtitle (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_subtitle (HdyHeaderBar *self, + const gchar *subtitle); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_header_bar_get_custom_title (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_custom_title (HdyHeaderBar *self, + GtkWidget *title_widget); + +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_pack_start (HdyHeaderBar *self, + GtkWidget *child); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_pack_end (HdyHeaderBar *self, + GtkWidget *child); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_header_bar_get_show_close_button (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_show_close_button (HdyHeaderBar *self, + gboolean setting); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_header_bar_get_has_subtitle (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_has_subtitle (HdyHeaderBar *self, + gboolean setting); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_header_bar_get_decoration_layout (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_decoration_layout (HdyHeaderBar *self, + const gchar *layout); + +HDY_AVAILABLE_IN_ALL +HdyCenteringPolicy hdy_header_bar_get_centering_policy (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_centering_policy (HdyHeaderBar *self, + HdyCenteringPolicy centering_policy); + +HDY_AVAILABLE_IN_ALL +guint hdy_header_bar_get_transition_duration (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_transition_duration (HdyHeaderBar *self, + guint duration); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_header_bar_get_transition_running (HdyHeaderBar *self); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_header_bar_get_interpolate_size (HdyHeaderBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_bar_set_interpolate_size (HdyHeaderBar *self, + gboolean interpolate_size); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-header-group.c b/subprojects/libhandy/src/hdy-header-group.c new file mode 100644 index 0000000..e8287fa --- /dev/null +++ b/subprojects/libhandy/src/hdy-header-group.c @@ -0,0 +1,1115 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-header-group.h" + +/** + * SECTION:hdy-header-group + * @short_description: An object handling composite title bars. + * @Title: HdyHeaderGroup + * @See_also: #GtkHeaderBar, #HdyHeaderBar, #HdyLeaflet + * + * The #HdyHeaderGroup object handles the header bars of a composite title bar. + * It splits the window decoration across the header bars, giving the left side + * of the decorations to the leftmost header bar, and the right side of the + * decorations to the rightmost header bar. + * See hdy_header_bar_set_decoration_layout(). + * + * The #HdyHeaderGroup:decorate-all property can be used in conjunction with + * #HdyLeaflet:folded when the title bar is split across the pages of a + * #HdyLeaflet to automatically display the decorations on all the pages when + * the leaflet is folded. + * + * You can nest header groups, which is convenient when you nest leaflets too: + * |[ + * <object class="HdyHeaderGroup" id="inner_header_group"> + * <property name="decorate-all" bind-source="inner_leaflet" bind-property="folded" bind-flags="sync-create"/> + * <headerbars> + * <headerbar name="inner_header_bar_1"/> + * <headerbar name="inner_header_bar_2"/> + * </headerbars> + * </object> + * <object class="HdyHeaderGroup" id="outer_header_group"> + * <property name="decorate-all" bind-source="outer_leaflet" bind-property="folded" bind-flags="sync-create"/> + * <headerbars> + * <headerbar name="inner_header_group"/> + * <headerbar name="outer_header_bar"/> + * </headerbars> + * </object> + * ]| + * + * Since: 0.0.4 + */ + +/** + * HdyHeaderGroupChildType: + * @HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR: The child is a #HdyHeaderBar + * @HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR: The child is a #GtkHeaderBar + * @HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP: The child is a #HdyHeaderGroup + * + * This enumeration value describes the child types handled by #HdyHeaderGroup. + * + * New values may be added to this enumeration over time. + * + * Since: 1.0 + */ + +struct _HdyHeaderGroupChild +{ + GObject parent_instance; + + HdyHeaderGroupChildType type; + GObject *object; +}; + +enum { + SIGNAL_UPDATE_DECORATION_LAYOUTS, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +G_DEFINE_TYPE (HdyHeaderGroupChild, hdy_header_group_child, G_TYPE_OBJECT) + +struct _HdyHeaderGroup +{ + GObject parent_instance; + + GSList *children; + gboolean decorate_all; + gchar *layout; +}; + +static void hdy_header_group_buildable_init (GtkBuildableIface *iface); +static gboolean hdy_header_group_buildable_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + GMarkupParser *parser, + gpointer *data); +static void hdy_header_group_buildable_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + gpointer user_data); + +G_DEFINE_TYPE_WITH_CODE (HdyHeaderGroup, hdy_header_group, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + hdy_header_group_buildable_init)) + +enum { + PROP_0, + PROP_DECORATE_ALL, + N_PROPS +}; + +static GParamSpec *props [N_PROPS]; + +static void update_decoration_layouts (HdyHeaderGroup *self); + +static void +object_destroyed_cb (HdyHeaderGroupChild *self, + GObject *object) +{ + g_assert (HDY_IS_HEADER_GROUP_CHILD (self)); + + self->object = NULL; + + g_object_unref (self); +} + +static void +forward_update_decoration_layouts (HdyHeaderGroupChild *self) +{ + HdyHeaderGroup *header_group; + + g_assert (HDY_IS_HEADER_GROUP_CHILD (self)); + + header_group = HDY_HEADER_GROUP (g_object_get_data (G_OBJECT (self), "header-group")); + + g_assert (HDY_IS_HEADER_GROUP (header_group)); + + g_signal_emit (header_group, signals[SIGNAL_UPDATE_DECORATION_LAYOUTS], 0); + + update_decoration_layouts (header_group); +} + +static void +hdy_header_group_child_dispose (GObject *object) +{ + HdyHeaderGroupChild *self = (HdyHeaderGroupChild *)object; + + if (self->object) { + + switch (self->type) { + case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR: + case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR: + g_signal_handlers_disconnect_by_func (self->object, G_CALLBACK (object_destroyed_cb), self); + g_signal_handlers_disconnect_by_func (self->object, G_CALLBACK (forward_update_decoration_layouts), self); + break; + case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP: + g_object_weak_unref (self->object, (GWeakNotify) object_destroyed_cb, self); + break; + default: + g_assert_not_reached (); + } + + self->object = NULL; + } + + G_OBJECT_CLASS (hdy_header_group_child_parent_class)->dispose (object); +} + +static HdyHeaderGroupChild * +hdy_header_group_child_new_for_header_bar (HdyHeaderBar *header_bar) +{ + HdyHeaderGroupChild *self; + gpointer header_group; + + g_return_val_if_fail (HDY_IS_HEADER_BAR (header_bar), NULL); + + header_group = g_object_get_data (G_OBJECT (header_bar), "header-group"); + + g_return_val_if_fail (header_group == NULL, NULL); + + self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL); + self->type = HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR; + self->object = G_OBJECT (header_bar); + + g_signal_connect_swapped (header_bar, "destroy", G_CALLBACK (object_destroyed_cb), self); + + g_signal_connect_swapped (header_bar, "map", G_CALLBACK (forward_update_decoration_layouts), self); + g_signal_connect_swapped (header_bar, "unmap", G_CALLBACK (forward_update_decoration_layouts), self); + + return self; +} + +static HdyHeaderGroupChild * +hdy_header_group_child_new_for_gtk_header_bar (GtkHeaderBar *header_bar) +{ + HdyHeaderGroupChild *self; + gpointer header_group; + + g_return_val_if_fail (GTK_IS_HEADER_BAR (header_bar), NULL); + + header_group = g_object_get_data (G_OBJECT (header_bar), "header-group"); + + g_return_val_if_fail (header_group == NULL, NULL); + + self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL); + self->type = HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR; + self->object = G_OBJECT (header_bar); + + g_signal_connect_swapped (header_bar, "destroy", G_CALLBACK (object_destroyed_cb), self); + + g_signal_connect_swapped (header_bar, "map", G_CALLBACK (forward_update_decoration_layouts), self); + g_signal_connect_swapped (header_bar, "unmap", G_CALLBACK (forward_update_decoration_layouts), self); + + return self; +} + +static HdyHeaderGroupChild * +hdy_header_group_child_new_for_header_group (HdyHeaderGroup *header_group) +{ + HdyHeaderGroupChild *self; + gpointer parent_header_group; + + g_return_val_if_fail (HDY_IS_HEADER_GROUP (header_group), NULL); + + parent_header_group = g_object_get_data (G_OBJECT (header_group), "header-group"); + + g_return_val_if_fail (parent_header_group == NULL, NULL); + + self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL); + self->type = HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP; + self->object = G_OBJECT (header_group); + + g_object_weak_unref (G_OBJECT (header_group), (GWeakNotify) object_destroyed_cb, self); + + g_signal_connect_swapped (header_group, "update-decoration-layouts", G_CALLBACK (forward_update_decoration_layouts), self); + + return self; +} + +static void +hdy_header_group_child_class_init (HdyHeaderGroupChildClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = hdy_header_group_child_dispose; +} + +static void +hdy_header_group_child_init (HdyHeaderGroupChild *self) +{ +} + +static void +hdy_header_group_child_set_decoration_layout (HdyHeaderGroupChild *self, + const gchar *layout) +{ + g_assert (HDY_IS_HEADER_GROUP_CHILD (self)); + + switch (self->type) { + case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR: + hdy_header_bar_set_decoration_layout (HDY_HEADER_BAR (self->object), layout); + break; + case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR: + gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (self->object), layout); + break; + case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP: + { + HdyHeaderGroup *group = HDY_HEADER_GROUP (self->object); + + g_free (group->layout); + group->layout = g_strdup (layout); + + update_decoration_layouts (group); + } + break; + default: + g_assert_not_reached (); + } +} + +static gboolean +hdy_header_group_child_get_mapped (HdyHeaderGroupChild *self) +{ + g_assert (HDY_IS_HEADER_GROUP_CHILD (self)); + + switch (self->type) { + case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR: + case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR: + return gtk_widget_get_mapped (GTK_WIDGET (self->object)); + case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP: + for (GSList *children = HDY_HEADER_GROUP (self->object)->children; + children != NULL; + children = children->next) + if (hdy_header_group_child_get_mapped (HDY_HEADER_GROUP_CHILD (children->data))) + return TRUE; + + return FALSE; + default: + g_assert_not_reached (); + } +} + +static HdyHeaderGroupChild * +get_child_for_object (HdyHeaderGroup *self, + gpointer object) +{ + GSList *children; + + for (children = self->children; children != NULL; children = children->next) { + HdyHeaderGroupChild *child = HDY_HEADER_GROUP_CHILD (children->data); + + g_assert (child); + + if (child->object == object) + return child; + } + + return NULL; +} + +static void +update_decoration_layouts (HdyHeaderGroup *self) +{ + GSList *children; + GtkSettings *settings; + HdyHeaderGroupChild *start_child = NULL, *end_child = NULL; + g_autofree gchar *layout = NULL; + g_autofree gchar *start_layout = NULL; + g_autofree gchar *end_layout = NULL; + g_auto(GStrv) ends = NULL; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + + children = self->children; + + if (children == NULL) + return; + + settings = gtk_settings_get_default (); + if (self->layout) + layout = g_strdup (self->layout); + else + g_object_get (G_OBJECT (settings), "gtk-decoration-layout", &layout, NULL); + if (layout == NULL) + layout = g_strdup (":"); + + if (self->decorate_all) { + for (; children != NULL; children = children->next) + hdy_header_group_child_set_decoration_layout (HDY_HEADER_GROUP_CHILD (children->data), layout); + + return; + } + + for (; children != NULL; children = children->next) { + HdyHeaderGroupChild *child = HDY_HEADER_GROUP_CHILD (children->data); + + hdy_header_group_child_set_decoration_layout (child, ":"); + + if (!hdy_header_group_child_get_mapped (child)) + continue; + + /* The headerbars are in reverse order in the list. */ + start_child = child; + if (end_child == NULL) + end_child = child; + } + + if (start_child == NULL || end_child == NULL) + return; + + if (start_child == end_child) { + hdy_header_group_child_set_decoration_layout (start_child, layout); + + return; + } + + ends = g_strsplit (layout, ":", 2); + if (g_strv_length (ends) >= 2) { + start_layout = g_strdup_printf ("%s:", ends[0]); + end_layout = g_strdup_printf (":%s", ends[1]); + } else { + start_layout = g_strdup (":"); + end_layout = g_strdup (":"); + } + hdy_header_group_child_set_decoration_layout (start_child, start_layout); + hdy_header_group_child_set_decoration_layout (end_child, end_layout); +} + +static void +child_destroyed_cb (HdyHeaderGroup *self, + HdyHeaderGroupChild *child) +{ + g_assert (HDY_IS_HEADER_GROUP (self)); + g_assert (HDY_IS_HEADER_GROUP_CHILD (child)); + g_assert (g_slist_find (self->children, child) != NULL); + + self->children = g_slist_remove (self->children, child); + + g_object_unref (self); +} + +HdyHeaderGroup * +hdy_header_group_new (void) +{ + return g_object_new (HDY_TYPE_HEADER_GROUP, NULL); +} + +static void +hdy_header_group_add_child (HdyHeaderGroup *self, + HdyHeaderGroupChild *child) +{ + g_assert (HDY_IS_HEADER_GROUP (self)); + g_assert (HDY_IS_HEADER_GROUP_CHILD (child)); + g_assert (g_slist_find (self->children, child) == NULL); + + self->children = g_slist_prepend (self->children, child); + g_object_weak_ref (G_OBJECT (child), (GWeakNotify) child_destroyed_cb, self); + g_object_ref (self); + + update_decoration_layouts (self); + + g_object_set_data (G_OBJECT (child), "header-group", self); +} + +/** + * hdy_header_group_add_header_bar: + * @self: a #HdyHeaderGroup + * @header_bar: the #HdyHeaderBar to add + * + * Adds @header_bar to @self. + * When the widget is destroyed or no longer referenced elsewhere, it will + * be removed from the header group. + * + * Since: 1.0 + */ +void +hdy_header_group_add_header_bar (HdyHeaderGroup *self, + HdyHeaderBar *header_bar) +{ + HdyHeaderGroupChild *child; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (HDY_IS_HEADER_BAR (header_bar)); + g_return_if_fail (get_child_for_object (self, header_bar) == NULL); + + child = hdy_header_group_child_new_for_header_bar (header_bar); + hdy_header_group_add_child (self, child); +} + +/** + * hdy_header_group_add_gtk_header_bar: + * @self: a #HdyHeaderGroup + * @header_bar: the #GtkHeaderBar to add + * + * Adds @header_bar to @self. + * When the widget is destroyed or no longer referenced elsewhere, it will + * be removed from the header group. + * + * Since: 1.0 + */ +void +hdy_header_group_add_gtk_header_bar (HdyHeaderGroup *self, + GtkHeaderBar *header_bar) +{ + HdyHeaderGroupChild *child; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (GTK_IS_HEADER_BAR (header_bar)); + g_return_if_fail (get_child_for_object (self, header_bar) == NULL); + + child = hdy_header_group_child_new_for_gtk_header_bar (header_bar); + hdy_header_group_add_child (self, child); +} + +/** + * hdy_header_group_add_header_group: + * @self: a #HdyHeaderGroup + * @header_group: the #HdyHeaderGroup to add + * + * Adds @header_group to @self. + * When the nested group is no longer referenced elsewhere, it will be removed + * from the header group. + * + * Since: 1.0 + */ +void +hdy_header_group_add_header_group (HdyHeaderGroup *self, + HdyHeaderGroup *header_group) +{ + HdyHeaderGroupChild *child; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (HDY_IS_HEADER_GROUP (header_group)); + g_return_if_fail (get_child_for_object (self, header_group) == NULL); + + child = hdy_header_group_child_new_for_header_group (header_group); + hdy_header_group_add_child (self, child); +} + +typedef struct { + gchar *name; + gint line; + gint col; +} ItemData; + +static void +item_data_free (gpointer data) +{ + ItemData *item_data = data; + + g_free (item_data->name); + g_free (item_data); +} + +typedef struct { + GObject *object; + GtkBuilder *builder; + GSList *items; +} GSListSubParserData; + +static void +hdy_header_group_dispose (GObject *object) +{ + HdyHeaderGroup *self = (HdyHeaderGroup *)object; + + g_slist_free_full (self->children, (GDestroyNotify) g_object_unref); + self->children = NULL; + + G_OBJECT_CLASS (hdy_header_group_parent_class)->dispose (object); +} + +static void +hdy_header_group_finalize (GObject *object) +{ + HdyHeaderGroup *self = (HdyHeaderGroup *) object; + + g_free (self->layout); + + G_OBJECT_CLASS (hdy_header_group_parent_class)->finalize (object); +} + +static void +hdy_header_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyHeaderGroup *self = HDY_HEADER_GROUP (object); + + switch (prop_id) { + case PROP_DECORATE_ALL: + g_value_set_boolean (value, hdy_header_group_get_decorate_all (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_header_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyHeaderGroup *self = HDY_HEADER_GROUP (object); + + switch (prop_id) { + case PROP_DECORATE_ALL: + hdy_header_group_set_decorate_all (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +/*< private > + * @builder: a #GtkBuilder + * @context: the #GMarkupParseContext + * @parent_name: the name of the expected parent element + * @error: return location for an error + * + * Checks that the parent element of the currently handled + * start tag is @parent_name and set @error if it isn't. + * + * This is intended to be called in start_element vfuncs to + * ensure that element nesting is as intended. + * + * Returns: %TRUE if @parent_name is the parent element + */ +/* This has been copied and modified from gtkbuilder.c. */ +static gboolean +_gtk_builder_check_parent (GtkBuilder *builder, + GMarkupParseContext *context, + const gchar *parent_name, + GError **error) +{ + const GSList *stack; + gint line, col; + const gchar *parent; + const gchar *element; + + stack = g_markup_parse_context_get_element_stack (context); + + element = (const gchar *)stack->data; + parent = stack->next ? (const gchar *)stack->next->data : ""; + + if (g_str_equal (parent_name, parent) || + (g_str_equal (parent_name, "object") && g_str_equal (parent, "template"))) + return TRUE; + + g_markup_parse_context_get_position (context, &line, &col); + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_TAG, + ".:%d:%d Can't use <%s> here", + line, col, element); + + return FALSE; +} + +/*< private > + * _gtk_builder_prefix_error: + * @builder: a #GtkBuilder + * @context: the #GMarkupParseContext + * @error: an error + * + * Calls g_prefix_error() to prepend a filename:line:column marker + * to the given error. The filename is taken from @builder, and + * the line and column are obtained by calling + * g_markup_parse_context_get_position(). + * + * This is intended to be called on errors returned by + * g_markup_collect_attributes() in a start_element vfunc. + */ +/* This has been copied and modified from gtkbuilder.c. */ +static void +_gtk_builder_prefix_error (GtkBuilder *builder, + GMarkupParseContext *context, + GError **error) +{ + gint line, col; + + g_markup_parse_context_get_position (context, &line, &col); + g_prefix_error (error, ".:%d:%d ", line, col); +} + +/*< private > + * _gtk_builder_error_unhandled_tag: + * @builder: a #GtkBuilder + * @context: the #GMarkupParseContext + * @object: name of the object that is being handled + * @element_name: name of the element whose start tag is being handled + * @error: return location for the error + * + * Sets @error to a suitable error indicating that an @element_name + * tag is not expected in the custom markup for @object. + * + * This is intended to be called in a start_element vfunc. + */ +/* This has been copied and modified from gtkbuilder.c. */ +static void +_gtk_builder_error_unhandled_tag (GtkBuilder *builder, + GMarkupParseContext *context, + const gchar *object, + const gchar *element_name, + GError **error) +{ + gint line, col; + + g_markup_parse_context_get_position (context, &line, &col); + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_UNHANDLED_TAG, + ".:%d:%d Unsupported tag for %s: <%s>", + line, col, + object, element_name); +} + +/* This has been copied and modified from gtksizegroup.c. */ +static void +header_group_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **names, + const gchar **values, + gpointer user_data, + GError **error) +{ + GSListSubParserData *data = (GSListSubParserData*)user_data; + + if (strcmp (element_name, "headerbar") == 0) + { + const gchar *name; + ItemData *item_data; + + if (!_gtk_builder_check_parent (data->builder, context, "headerbars", error)) + return; + + if (!g_markup_collect_attributes (element_name, names, values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + { + _gtk_builder_prefix_error (data->builder, context, error); + return; + } + + item_data = g_new (ItemData, 1); + item_data->name = g_strdup (name); + g_markup_parse_context_get_position (context, &item_data->line, &item_data->col); + data->items = g_slist_prepend (data->items, item_data); + } + else if (strcmp (element_name, "headerbars") == 0) + { + if (!_gtk_builder_check_parent (data->builder, context, "object", error)) + return; + + if (!g_markup_collect_attributes (element_name, names, values, error, + G_MARKUP_COLLECT_INVALID, NULL, NULL, + G_MARKUP_COLLECT_INVALID)) + _gtk_builder_prefix_error (data->builder, context, error); + } + else + { + _gtk_builder_error_unhandled_tag (data->builder, context, + "HdyHeaderGroup", element_name, + error); + } +} + + +/* This has been copied and modified from gtksizegroup.c. */ +static const GMarkupParser header_group_parser = + { + header_group_start_element + }; + +/* This has been copied and modified from gtksizegroup.c. */ +static gboolean +hdy_header_group_buildable_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + GMarkupParser *parser, + gpointer *parser_data) +{ + GSListSubParserData *data; + + if (child) + return FALSE; + + if (strcmp (tagname, "headerbars") == 0) + { + data = g_slice_new0 (GSListSubParserData); + data->items = NULL; + data->object = G_OBJECT (buildable); + data->builder = builder; + + *parser = header_group_parser; + *parser_data = data; + + return TRUE; + } + + return FALSE; +} + +/* This has been copied and modified from gtksizegroup.c. */ +static void +hdy_header_group_buildable_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + gpointer user_data) +{ + GSList *l; + GSListSubParserData *data; + + if (strcmp (tagname, "headerbars") != 0) + return; + + data = (GSListSubParserData*)user_data; + data->items = g_slist_reverse (data->items); + + for (l = data->items; l; l = l->next) { + ItemData *item_data = l->data; + GObject *object = gtk_builder_get_object (builder, item_data->name); + + if (!object) + continue; + + if (GTK_IS_HEADER_BAR (object)) + hdy_header_group_add_gtk_header_bar (HDY_HEADER_GROUP (data->object), + GTK_HEADER_BAR (object)); + else if (HDY_IS_HEADER_BAR (object)) + hdy_header_group_add_header_bar (HDY_HEADER_GROUP (data->object), + HDY_HEADER_BAR (object)); + else if (HDY_IS_HEADER_GROUP (object)) + hdy_header_group_add_header_group (HDY_HEADER_GROUP (data->object), + HDY_HEADER_GROUP (object)); + } + + g_slist_free_full (data->items, item_data_free); + g_slice_free (GSListSubParserData, data); +} + +static void +hdy_header_group_class_init (HdyHeaderGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = hdy_header_group_dispose; + object_class->finalize = hdy_header_group_finalize; + object_class->get_property = hdy_header_group_get_property; + object_class->set_property = hdy_header_group_set_property; + + /** + * HdyHeaderGroup:decorate-all: + * + * Whether the elements of the group should all receive the full decoration. + * This is useful in conjunction with #HdyLeaflet:folded when the leaflet + * contains the header bars of the group, as you want them all to display the + * complete decoration when the leaflet is folded. + * + * Since: 1.0 + */ + props[PROP_DECORATE_ALL] = + g_param_spec_boolean ("decorate-all", + _("Decorate all"), + _("Whether the elements of the group should all receive the full decoration"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, props); + + /** + * HdyHeaderGroup::update-decoration-layouts: + * @self: The #HdyHeaderGroup instance + * + * This signal is emitted before updating the decoration layouts. + * + * Since: 1.0 + */ + signals[SIGNAL_UPDATE_DECORATION_LAYOUTS] = + g_signal_new ("update-decoration-layouts", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +static void +hdy_header_group_init (HdyHeaderGroup *self) +{ + GtkSettings *settings = gtk_settings_get_default (); + + g_signal_connect_swapped (settings, "notify::gtk-decoration-layout", G_CALLBACK (update_decoration_layouts), self); +} + +static void +hdy_header_group_buildable_init (GtkBuildableIface *iface) +{ + iface->custom_tag_start = hdy_header_group_buildable_custom_tag_start; + iface->custom_finished = hdy_header_group_buildable_custom_finished; +} + +/** + * hdy_header_group_child_get_header_bar: + * @self: a #HdyHeaderGroupChild + * + * Gets the child #HdyHeaderBar. + * Use hdy_header_group_child_get_child_type() to check the child type. + * + * Returns: (transfer none): the child #HdyHeaderBar, or %NULL in case of error. + * + * Since: 1.0 + */ +HdyHeaderBar * +hdy_header_group_child_get_header_bar (HdyHeaderGroupChild *self) +{ + g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL); + g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR, NULL); + + return HDY_HEADER_BAR (self->object); +} + +/** + * hdy_header_group_child_get_gtk_header_bar: + * @self: a #HdyHeaderGroupChild + * + * Gets the child #GtkHeaderBar. + * Use hdy_header_group_child_get_child_type() to check the child type. + * + * Returns: (transfer none): the child #GtkHeaderBar, or %NULL in case of error. + * + * Since: 1.0 + */ +GtkHeaderBar * +hdy_header_group_child_get_gtk_header_bar (HdyHeaderGroupChild *self) +{ + g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL); + g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR, NULL); + + return GTK_HEADER_BAR (self->object); +} + +/** + * hdy_header_group_child_get_header_group: + * @self: a #HdyHeaderGroupChild + * + * Gets the child #HdyHeaderGroup. + * Use hdy_header_group_child_get_child_type() to check the child type. + * + * Returns: (transfer none): the child #HdyHeaderGroup, or %NULL in case of error. + * + * Since: 1.0 + */ +HdyHeaderGroup * +hdy_header_group_child_get_header_group (HdyHeaderGroupChild *self) +{ + g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL); + g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP, NULL); + + return HDY_HEADER_GROUP (self->object); +} + +/** + * hdy_header_group_child_get_child_type: + * @self: a #HdyHeaderGroupChild + * + * Gets the child type. + * + * Returns: the child type. + * + * Since: 1.0 + */ +HdyHeaderGroupChildType +hdy_header_group_child_get_child_type (HdyHeaderGroupChild *self) +{ + g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR); + + return self->type; +} + +/** + * hdy_header_group_get_children: + * @self: a #HdyHeaderGroup + * + * Returns the list of children associated with @self. + * + * Returns: (element-type HdyHeaderGroupChild) (transfer none): the #GSList of + * children. The list is owned by libhandy and should not be modified. + * + * Since: 1.0 + */ +GSList * +hdy_header_group_get_children (HdyHeaderGroup *self) +{ + g_return_val_if_fail (HDY_IS_HEADER_GROUP (self), NULL); + + return self->children; +} + +static void +remove_child (HdyHeaderGroup *self, + HdyHeaderGroupChild *child) +{ + self->children = g_slist_remove (self->children, child); + + g_object_weak_unref (G_OBJECT (child), (GWeakNotify) child_destroyed_cb, self); + + g_object_unref (self); + g_object_unref (child); +} + +/** + * hdy_header_group_remove_header_bar: + * @self: a #HdyHeaderGroup + * @header_bar: the #HdyHeaderBar to remove + * + * Removes @header_bar from @self. + * + * Since: 1.0 + */ +void +hdy_header_group_remove_header_bar (HdyHeaderGroup *self, + HdyHeaderBar *header_bar) +{ + HdyHeaderGroupChild *child; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (HDY_IS_HEADER_BAR (header_bar)); + + child = get_child_for_object (self, header_bar); + + g_return_if_fail (child != NULL); + + remove_child (self, child); +} + +/** + * hdy_header_group_remove_gtk_header_bar: + * @self: a #HdyHeaderGroup + * @header_bar: the #GtkHeaderBar to remove + * + * Removes @header_bar from @self. + * + * Since: 1.0 + */ +void +hdy_header_group_remove_gtk_header_bar (HdyHeaderGroup *self, + GtkHeaderBar *header_bar) +{ + HdyHeaderGroupChild *child; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (GTK_IS_HEADER_BAR (header_bar)); + + child = get_child_for_object (self, header_bar); + + g_return_if_fail (child != NULL); + + remove_child (self, child); +} + +/** + * hdy_header_group_remove_header_group: + * @self: a #HdyHeaderGroup + * @header_group: the #HdyHeaderGroup to remove + * + * Removes a nested #HdyHeaderGroup from a #HdyHeaderGroup + * + * Since: 1.0 + */ +void +hdy_header_group_remove_header_group (HdyHeaderGroup *self, + HdyHeaderGroup *header_group) +{ + HdyHeaderGroupChild *child; + + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (HDY_IS_HEADER_GROUP (header_group)); + + child = get_child_for_object (self, header_group); + + g_return_if_fail (child != NULL); + + remove_child (self, child); +} + +/** + * hdy_header_group_remove_child: + * @self: a #HdyHeaderGroup + * @child: the #HdyHeaderGroupChild to remove + * + * Removes @child from @self. + * + * Since: 1.0 + */ +void +hdy_header_group_remove_child (HdyHeaderGroup *self, + HdyHeaderGroupChild *child) +{ + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + g_return_if_fail (HDY_IS_HEADER_GROUP_CHILD (child)); + g_return_if_fail (g_slist_find (self->children, child) != NULL); + + remove_child (self, child); +} + +/** + * hdy_header_group_set_decorate_all: + * @self: a #HdyHeaderGroup + * @decorate_all: whether the elements of the group should all receive the full decoration + * + * Sets whether the elements of the group should all receive the full decoration. + * + * Since: 1.0 + */ +void +hdy_header_group_set_decorate_all (HdyHeaderGroup *self, + gboolean decorate_all) +{ + g_return_if_fail (HDY_IS_HEADER_GROUP (self)); + + decorate_all = !!decorate_all; + + if (self->decorate_all == decorate_all) + return; + + self->decorate_all = decorate_all; + + update_decoration_layouts (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATE_ALL]); +} + +/** + * hdy_header_group_get_decorate_all: + * @self: a #HdyHeaderGroup + * + * Gets whether the elements of the group should all receive the full decoration. + * + * Returns: %TRUE if the elements of the group should all receive the full + * decoration, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_header_group_get_decorate_all (HdyHeaderGroup *self) +{ + g_return_val_if_fail (HDY_IS_HEADER_GROUP (self), FALSE); + + return self->decorate_all; +} diff --git a/subprojects/libhandy/src/hdy-header-group.h b/subprojects/libhandy/src/hdy-header-group.h new file mode 100644 index 0000000..dc20a76 --- /dev/null +++ b/subprojects/libhandy/src/hdy-header-group.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-header-bar.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_HEADER_GROUP_CHILD (hdy_header_group_child_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyHeaderGroupChild, hdy_header_group_child, HDY, HEADER_GROUP_CHILD, GObject) + +#define HDY_TYPE_HEADER_GROUP (hdy_header_group_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyHeaderGroup, hdy_header_group, HDY, HEADER_GROUP, GObject) + +typedef enum { + HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR, + HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR, + HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP, +} HdyHeaderGroupChildType; + +HDY_AVAILABLE_IN_ALL +HdyHeaderBar *hdy_header_group_child_get_header_bar (HdyHeaderGroupChild *self); +HDY_AVAILABLE_IN_ALL +GtkHeaderBar *hdy_header_group_child_get_gtk_header_bar (HdyHeaderGroupChild *self); +HDY_AVAILABLE_IN_ALL +HdyHeaderGroup *hdy_header_group_child_get_header_group (HdyHeaderGroupChild *self); + +HDY_AVAILABLE_IN_ALL +HdyHeaderGroupChildType hdy_header_group_child_get_child_type (HdyHeaderGroupChild *self); + +HDY_AVAILABLE_IN_ALL +HdyHeaderGroup *hdy_header_group_new (void); + +HDY_AVAILABLE_IN_ALL +void hdy_header_group_add_header_bar (HdyHeaderGroup *self, + HdyHeaderBar *header_bar); +HDY_AVAILABLE_IN_ALL +void hdy_header_group_add_gtk_header_bar (HdyHeaderGroup *self, + GtkHeaderBar *header_bar); +HDY_AVAILABLE_IN_ALL +void hdy_header_group_add_header_group (HdyHeaderGroup *self, + HdyHeaderGroup *header_group); + +HDY_AVAILABLE_IN_ALL +GSList *hdy_header_group_get_children (HdyHeaderGroup *self); + +HDY_AVAILABLE_IN_ALL +void hdy_header_group_remove_header_bar (HdyHeaderGroup *self, + HdyHeaderBar *header_bar); +HDY_AVAILABLE_IN_ALL +void hdy_header_group_remove_gtk_header_bar (HdyHeaderGroup *self, + GtkHeaderBar *header_bar); +HDY_AVAILABLE_IN_ALL +void hdy_header_group_remove_header_group (HdyHeaderGroup *self, + HdyHeaderGroup *header_group); +HDY_AVAILABLE_IN_ALL +void hdy_header_group_remove_child (HdyHeaderGroup *self, + HdyHeaderGroupChild *child); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_header_group_get_decorate_all (HdyHeaderGroup *self); +HDY_AVAILABLE_IN_ALL +void hdy_header_group_set_decorate_all (HdyHeaderGroup *self, + gboolean decorate_all); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-keypad-button-private.h b/subprojects/libhandy/src/hdy-keypad-button-private.h new file mode 100644 index 0000000..723526a --- /dev/null +++ b/subprojects/libhandy/src/hdy-keypad-button-private.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_KEYPAD_BUTTON (hdy_keypad_button_get_type()) + +G_DECLARE_FINAL_TYPE (HdyKeypadButton, hdy_keypad_button, HDY, KEYPAD_BUTTON, GtkButton) + +struct _HdyKeypadButtonClass +{ + GtkButtonClass parent_class; +}; + +GtkWidget *hdy_keypad_button_new (const gchar *symbols); +gchar hdy_keypad_button_get_digit (HdyKeypadButton *self); +const gchar *hdy_keypad_button_get_symbols (HdyKeypadButton *self); +void hdy_keypad_button_show_symbols (HdyKeypadButton *self, + gboolean visible); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-keypad-button.c b/subprojects/libhandy/src/hdy-keypad-button.c new file mode 100644 index 0000000..436555d --- /dev/null +++ b/subprojects/libhandy/src/hdy-keypad-button.c @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-keypad-button-private.h" + +/** + * PRIVATE:hdy-keypad-button + * @short_description: A button on a #HdyKeypad keypad + * @Title: HdyKeypadButton + * + * The #HdyKeypadButton widget is a single button on an #HdyKeypad. It + * can represent a single symbol (typically a digit) plus an arbitrary + * number of symbols that are displayed below it. + */ + +enum { + PROP_0, + PROP_DIGIT, + PROP_SYMBOLS, + PROP_SHOW_SYMBOLS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _HdyKeypadButton +{ + GtkButton parent_instance; + + GtkLabel *label, *secondary_label; + gchar *symbols; +}; + +G_DEFINE_TYPE (HdyKeypadButton, hdy_keypad_button, GTK_TYPE_BUTTON) + +static void +format_label(HdyKeypadButton *self) +{ + g_autofree gchar *text = NULL; + gchar *secondary_text = NULL; + + if (self->symbols != NULL && *(self->symbols) != '\0') { + secondary_text = g_utf8_find_next_char (self->symbols, NULL); + text = g_strndup (self->symbols, 1); + } + + gtk_label_set_label (self->label, text); + gtk_label_set_label (self->secondary_label, secondary_text); +} + +static void +hdy_keypad_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object); + + switch (property_id) { + case PROP_SYMBOLS: + if (g_strcmp0 (self->symbols, g_value_get_string (value)) != 0) { + g_free (self->symbols); + self->symbols = g_value_dup_string (value); + format_label(self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SYMBOLS]); + } + break; + + case PROP_SHOW_SYMBOLS: + hdy_keypad_button_show_symbols (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +hdy_keypad_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object); + + switch (property_id) { + case PROP_DIGIT: + g_value_set_schar (value, hdy_keypad_button_get_digit (self)); + break; + + case PROP_SYMBOLS: + g_value_set_string (value, hdy_keypad_button_get_symbols (self)); + break; + + case PROP_SHOW_SYMBOLS: + g_value_set_boolean (value, gtk_widget_is_visible (GTK_WIDGET (self->secondary_label))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* This private method is prefixed by the call name because it will be a virtual + * method in GTK 4. + */ +static void +hdy_keypad_button_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (hdy_keypad_button_parent_class); + gint min1, min2, nat1, nat2; + + if (for_size < 0) { + widget_class->get_preferred_width (widget, &min1, &nat1); + widget_class->get_preferred_height (widget, &min2, &nat2); + } + else { + if (orientation == GTK_ORIENTATION_HORIZONTAL) + widget_class->get_preferred_width_for_height (widget, for_size, &min1, &nat1); + else + widget_class->get_preferred_height_for_width (widget, for_size, &min1, &nat1); + min2 = nat2 = for_size; + } + + if (minimum) + *minimum = MAX (min1, min2); + if (natural) + *natural = MAX (nat1, nat2); +} + +static GtkSizeRequestMode +hdy_keypad_button_get_request_mode (GtkWidget *widget) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (hdy_keypad_button_parent_class); + gint min1, min2; + widget_class->get_preferred_width (widget, &min1, NULL); + widget_class->get_preferred_height (widget, &min2, NULL); + if (min1 < min2) + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; + else + return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; +} + +static void +hdy_keypad_button_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + hdy_keypad_button_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_keypad_button_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + hdy_keypad_button_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_keypad_button_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width) +{ + *minimum_width = height; + *natural_width = height; +} + +static void +hdy_keypad_button_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum_height, + gint *natural_height) +{ + *minimum_height = width; + *natural_height = width; +} + + +static void +hdy_keypad_button_finalize (GObject *object) +{ + HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object); + + g_clear_pointer (&self->symbols, g_free); + G_OBJECT_CLASS (hdy_keypad_button_parent_class)->finalize (object); +} + + +static void +hdy_keypad_button_class_init (HdyKeypadButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = hdy_keypad_button_set_property; + object_class->get_property = hdy_keypad_button_get_property; + + object_class->finalize = hdy_keypad_button_finalize; + + widget_class->get_request_mode = hdy_keypad_button_get_request_mode; + widget_class->get_preferred_width = hdy_keypad_button_get_preferred_width; + widget_class->get_preferred_height = hdy_keypad_button_get_preferred_height; + widget_class->get_preferred_width_for_height = hdy_keypad_button_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = hdy_keypad_button_get_preferred_height_for_width; + + props[PROP_DIGIT] = + g_param_spec_int ("digit", + _("Digit"), + _("The keypad digit of the button"), + -1, INT_MAX, 0, + G_PARAM_READABLE); + + props[PROP_SYMBOLS] = + g_param_spec_string ("symbols", + _("Symbols"), + _("The keypad symbols of the button. The first symbol is used as the digit"), + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_SHOW_SYMBOLS] = + g_param_spec_boolean ("show-symbols", + _("Show symbols"), + _("Whether the second line of symbols should be shown or not"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-keypad-button.ui"); + gtk_widget_class_bind_template_child (widget_class, HdyKeypadButton, label); + gtk_widget_class_bind_template_child (widget_class, HdyKeypadButton, secondary_label); +} + +static void +hdy_keypad_button_init (HdyKeypadButton *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->symbols = NULL; +} + +/** + * hdy_keypad_button_new: + * @symbols: (nullable): the symbols displayed on the #HdyKeypadButton + * + * Create a new #HdyKeypadButton which displays @symbols, + * where the first char is used as the main and the other symbols are shown below + * + * Returns: the newly created #HdyKeypadButton widget + */ +GtkWidget * +hdy_keypad_button_new (const gchar *symbols) +{ + return g_object_new (HDY_TYPE_KEYPAD_BUTTON, "symbols", symbols, NULL); +} + +/** + * hdy_keypad_button_get_digit: + * @self: a #HdyKeypadButton + * + * Get the #HdyKeypadButton's digit. + * + * Returns: the button's digit + */ +char +hdy_keypad_button_get_digit (HdyKeypadButton *self) +{ + g_return_val_if_fail (HDY_IS_KEYPAD_BUTTON (self), '\0'); + + if (self->symbols == NULL) + return ('\0'); + + return *(self->symbols); +} + +/** + * hdy_keypad_button_get_symbols: + * @self: a #HdyKeypadButton + * + * Get the #HdyKeypadButton's symbols. + * + * Returns: the button's symbols including the digit. + */ +const char* +hdy_keypad_button_get_symbols (HdyKeypadButton *self) +{ + g_return_val_if_fail (HDY_IS_KEYPAD_BUTTON (self), NULL); + + return self->symbols; +} + +/** + * hdy_keypad_button_show_symbols: + * @self: a #HdyKeypadButton + * @visible: whether the second line should be shown or not + * + * Sets the visibility of the second line of symbols for #HdyKeypadButton + * + */ +void +hdy_keypad_button_show_symbols (HdyKeypadButton *self, gboolean visible) +{ + gboolean old_visible; + + g_return_if_fail (HDY_IS_KEYPAD_BUTTON (self)); + + old_visible = gtk_widget_get_visible (GTK_WIDGET (self->secondary_label)); + + if (old_visible != visible) { + gtk_widget_set_visible (GTK_WIDGET (self->secondary_label), visible); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_SYMBOLS]); + } +} diff --git a/subprojects/libhandy/src/hdy-keypad-button.ui b/subprojects/libhandy/src/hdy-keypad-button.ui new file mode 100644 index 0000000..27a53dd --- /dev/null +++ b/subprojects/libhandy/src/hdy-keypad-button.ui @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.20.1 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="HdyKeypadButton" parent="GtkButton"> + <property name="can_focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <property name="margin">6</property> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="label"></property> + <style> + <class name="digit"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="secondary_label"> + <property name="visible">True</property> + <property name="no_show_all">True</property> + <property name="label"></property> + <style> + <class name="dim-label"/> + <class name="letters"/> + </style> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-keypad.c b/subprojects/libhandy/src/hdy-keypad.c new file mode 100644 index 0000000..1714d9a --- /dev/null +++ b/subprojects/libhandy/src/hdy-keypad.c @@ -0,0 +1,793 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-keypad.h" +#include "hdy-keypad-button-private.h" + +/** + * SECTION:hdy-keypad + * @short_description: A keypad for dialing numbers + * @Title: HdyKeypad + * + * The #HdyKeypad widget is a keypad for entering numbers such as phone numbers + * or PIN codes. + * + * # CSS nodes + * + * #HdyKeypad has a single CSS node with name keypad. + * + * Since: 0.0.12 + */ + +typedef struct +{ + GtkEntry *entry; + GtkWidget *grid; + GtkWidget *label_asterisk; + GtkWidget *label_hash; + GtkGesture *long_press_zero_gesture; + guint16 row_spacing; + guint16 column_spacing; + gboolean symbols_visible; + gboolean letters_visible; +} HdyKeypadPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdyKeypad, hdy_keypad, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_ROW_SPACING, + PROP_COLUMN_SPACING, + PROP_LETTERS_VISIBLE, + PROP_SYMBOLS_VISIBLE, + PROP_ENTRY, + PROP_END_ACTION, + PROP_START_ACTION, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +static void +symbol_clicked (HdyKeypad *self, + gchar symbol) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self); + g_autofree gchar *string = g_strdup_printf ("%c", symbol); + + if (!priv->entry) + return; + + g_signal_emit_by_name (priv->entry, "insert-at-cursor", string, NULL); + /* Set focus to the entry only when it can get focus + * https://gitlab.gnome.org/GNOME/gtk/issues/2204 + */ + if (gtk_widget_get_can_focus (GTK_WIDGET (priv->entry))) + gtk_entry_grab_focus_without_selecting (priv->entry); +} + + +static void +button_clicked_cb (HdyKeypad *self, + HdyKeypadButton *btn) +{ + gchar digit = hdy_keypad_button_get_digit (btn); + symbol_clicked (self, digit); + g_debug ("Button with number %c was pressed", digit); +} + + +static void +asterisk_button_clicked_cb (HdyKeypad *self, + GtkWidget *btn) +{ + symbol_clicked (self, '*'); + g_debug ("Button with * was pressed"); +} + + +static void +hash_button_clicked_cb (HdyKeypad *self, + GtkWidget *btn) +{ + symbol_clicked (self, '#'); + g_debug ("Button with # was pressed"); +} + + +static void +insert_text_cb (HdyKeypad *self, + gchar *text, + gint length, + gpointer position, + GtkEditable *editable) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self); + + g_assert (length == 1); + + if (g_ascii_isdigit (*text)) + return; + + if (!priv->symbols_visible && strchr ("#*+", *text)) + return; + + g_signal_stop_emission_by_name (editable, "insert-text"); +} + + +static void +long_press_zero_cb (HdyKeypad *self, + gdouble x, + gdouble y, + GtkGesture *gesture) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self); + + if (priv->symbols_visible) + return; + + g_debug ("Long press on zero button"); + symbol_clicked (self, '+'); + gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); +} + + +static void +hdy_keypad_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyKeypad *self = HDY_KEYPAD (object); + + switch (property_id) { + case PROP_ROW_SPACING: + hdy_keypad_set_row_spacing (self, g_value_get_uint (value)); + break; + case PROP_COLUMN_SPACING: + hdy_keypad_set_column_spacing (self, g_value_get_uint (value)); + break; + case PROP_LETTERS_VISIBLE: + hdy_keypad_set_letters_visible (self, g_value_get_boolean (value)); + break; + case PROP_SYMBOLS_VISIBLE: + hdy_keypad_set_symbols_visible (self, g_value_get_boolean (value)); + break; + case PROP_ENTRY: + hdy_keypad_set_entry (self, g_value_get_object (value)); + break; + case PROP_END_ACTION: + hdy_keypad_set_end_action (self, g_value_get_object (value)); + break; + case PROP_START_ACTION: + hdy_keypad_set_start_action (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +hdy_keypad_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + HdyKeypad *self = HDY_KEYPAD (object); + + switch (property_id) { + case PROP_ROW_SPACING: + g_value_set_uint (value, hdy_keypad_get_row_spacing (self)); + break; + case PROP_COLUMN_SPACING: + g_value_set_uint (value, hdy_keypad_get_column_spacing (self)); + break; + case PROP_LETTERS_VISIBLE: + g_value_set_boolean (value, hdy_keypad_get_letters_visible (self)); + break; + case PROP_SYMBOLS_VISIBLE: + g_value_set_boolean (value, hdy_keypad_get_symbols_visible (self)); + break; + case PROP_ENTRY: + g_value_set_object (value, hdy_keypad_get_entry (self)); + break; + case PROP_START_ACTION: + g_value_set_object (value, hdy_keypad_get_start_action (self)); + break; + case PROP_END_ACTION: + g_value_set_object (value, hdy_keypad_get_end_action (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +hdy_keypad_finalize (GObject *object) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (HDY_KEYPAD (object)); + + if (priv->long_press_zero_gesture != NULL) + g_object_unref (priv->long_press_zero_gesture); + + G_OBJECT_CLASS (hdy_keypad_parent_class)->finalize (object); +} + + +static void +hdy_keypad_class_init (HdyKeypadClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = hdy_keypad_finalize; + + object_class->set_property = hdy_keypad_set_property; + object_class->get_property = hdy_keypad_get_property; + + /** + * HdyKeypad:row-spacing: + * + * The amount of space between two consecutive rows. + * + * Since: 1.0 + */ + props[PROP_ROW_SPACING] = + g_param_spec_uint ("row-spacing", + _("Row spacing"), + _("The amount of space between two consecutive rows"), + 0, G_MAXINT16, 6, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyKeypad:column-spacing: + * + * The amount of space between two consecutive columns. + * + * Since: 1.0 + */ + props[PROP_COLUMN_SPACING] = + g_param_spec_uint ("column-spacing", + _("Column spacing"), + _("The amount of space between two consecutive columns"), + 0, G_MAXINT16, 6, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyKeypad:letters-visible: + * + * Whether the keypad should display the standard letters below the digits on + * its buttons. + * + * Since: 1.0 + */ + props[PROP_LETTERS_VISIBLE] = + g_param_spec_boolean ("letters-visible", + _("Letters visible"), + _("Whether the letters below the digits should be visible"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyKeypad:symbols-visible: + * + * Whether the keypad should display the hash and asterisk buttons, and should + * display the plus symbol at the bottom of its 0 button. + * + * Since: 1.0 + */ + props[PROP_SYMBOLS_VISIBLE] = + g_param_spec_boolean ("symbols-visible", + _("Symbols visible"), + _("Whether the hash, plus, and asterisk symbols should be visible"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyKeypad:entry: + * + * The entry widget connected to the keypad. See hdy_keypad_set_entry() for + * details. + * + * Since: 1.0 + */ + props[PROP_ENTRY] = + g_param_spec_object ("entry", + _("Entry"), + _("The entry widget connected to the keypad"), + GTK_TYPE_ENTRY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyKeypad:end-action: + * + * The widget for the lower end corner of @self. + * + * Since: 1.0 + */ + props[PROP_END_ACTION] = + g_param_spec_object ("end-action", + _("End action"), + _("The end action widget"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyKeypad:start-action: + * + * The widget for the lower start corner of @self. + * + * Since: 1.0 + */ + props[PROP_START_ACTION] = + g_param_spec_object ("start-action", + _("Start action"), + _("The start action widget"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-keypad.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, grid); + gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, label_asterisk); + gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, label_hash); + gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, long_press_zero_gesture); + + gtk_widget_class_bind_template_callback (widget_class, button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, asterisk_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, hash_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, long_press_zero_cb); + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_DIAL); + gtk_widget_class_set_css_name (widget_class, "keypad"); +} + + +static void +hdy_keypad_init (HdyKeypad *self) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self); + + priv->row_spacing = 6; + priv->column_spacing = 6; + priv->letters_visible = TRUE; + priv->symbols_visible = TRUE; + + g_type_ensure (HDY_TYPE_KEYPAD_BUTTON); + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +/** + * hdy_keypad_new: + * @symbols_visible: whether the hash, plus, and asterisk symbols should be visible + * @letters_visible: whether the letters below the digits should be visible + * + * Create a new #HdyKeypad widget. + * + * Returns: the newly created #HdyKeypad widget + * + * Since: 0.0.12 + */ +GtkWidget * +hdy_keypad_new (gboolean symbols_visible, + gboolean letters_visible) +{ + return g_object_new (HDY_TYPE_KEYPAD, + "symbols-visible", symbols_visible, + "letters-visible", letters_visible, + NULL); +} + +/** + * hdy_keypad_set_row_spacing: + * @self: a #HdyKeypad + * @spacing: the amount of space to insert between rows + * + * Sets the amount of space between rows of @self. + * + * Since: 1.0 + */ +void +hdy_keypad_set_row_spacing (HdyKeypad *self, + guint spacing) +{ + HdyKeypadPrivate *priv; + + g_return_if_fail (HDY_IS_KEYPAD (self)); + g_return_if_fail (spacing <= G_MAXINT16); + + priv = hdy_keypad_get_instance_private (self); + + if (priv->row_spacing == spacing) + return; + + priv->row_spacing = spacing; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ROW_SPACING]); +} + + +/** + * hdy_keypad_get_row_spacing: + * @self: a #HdyKeypad + * + * Returns the amount of space between the rows of @self. + * + * Returns: the row spacing of @self + * + * Since: 1.0 + */ +guint +hdy_keypad_get_row_spacing (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), 0); + + priv = hdy_keypad_get_instance_private (self); + + return priv->row_spacing; +} + + +/** + * hdy_keypad_set_column_spacing: + * @self: a #HdyKeypad + * @spacing: the amount of space to insert between columns + * + * Sets the amount of space between columns of @self. + * + * Since: 1.0 + */ +void +hdy_keypad_set_column_spacing (HdyKeypad *self, + guint spacing) +{ + HdyKeypadPrivate *priv; + + g_return_if_fail (HDY_IS_KEYPAD (self)); + g_return_if_fail (spacing <= G_MAXINT16); + + priv = hdy_keypad_get_instance_private (self); + + if (priv->column_spacing == spacing) + return; + + priv->column_spacing = spacing; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COLUMN_SPACING]); +} + + +/** + * hdy_keypad_get_column_spacing: + * @self: a #HdyKeypad + * + * Returns the amount of space between the columns of @self. + * + * Returns: the column spacing of @self + * + * Since: 1.0 + */ +guint +hdy_keypad_get_column_spacing (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), 0); + + priv = hdy_keypad_get_instance_private (self); + + return priv->column_spacing; +} + + +/** + * hdy_keypad_set_letters_visible: + * @self: a #HdyKeypad + * @letters_visible: whether the letters below the digits should be visible + * + * Sets whether @self should display the standard letters below the digits on + * its buttons. + * + * Since: 1.0 + */ +void +hdy_keypad_set_letters_visible (HdyKeypad *self, + gboolean letters_visible) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self); + + g_return_if_fail (HDY_IS_KEYPAD (self)); + + letters_visible = !!letters_visible; + + if (priv->letters_visible == letters_visible) + return; + + priv->letters_visible = letters_visible; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LETTERS_VISIBLE]); +} + + +/** + * hdy_keypad_get_letters_visible: + * @self: a #HdyKeypad + * + * Returns whether @self should display the standard letters below the digits on + * its buttons. + * + * Returns: whether the letters below the digits should be visible + * + * Since: 1.0 + */ +gboolean +hdy_keypad_get_letters_visible (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), FALSE); + + priv = hdy_keypad_get_instance_private (self); + + return priv->letters_visible; +} + + +/** + * hdy_keypad_set_symbols_visible: + * @self: a #HdyKeypad + * @symbols_visible: whether the hash, plus, and asterisk symbols should be visible + * + * Sets whether @self should display the hash and asterisk buttons, and should + * display the plus symbol at the bottom of its 0 button. + * + * Since: 1.0 + */ +void +hdy_keypad_set_symbols_visible (HdyKeypad *self, + gboolean symbols_visible) +{ + HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self); + + g_return_if_fail (HDY_IS_KEYPAD (self)); + + symbols_visible = !!symbols_visible; + + if (priv->symbols_visible == symbols_visible) + return; + + priv->symbols_visible = symbols_visible; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SYMBOLS_VISIBLE]); +} + + +/** + * hdy_keypad_get_symbols_visible: + * @self: a #HdyKeypad + * + * Returns whether @self should display the standard letters below the digits on + * its buttons. + * + * Returns Whether @self should display the hash and asterisk buttons, and + * should display the plus symbol at the bottom of its 0 button. + * + * Returns: whether the hash, plus, and asterisk symbols should be visible + * + * Since: 1.0 + */ +gboolean +hdy_keypad_get_symbols_visible (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), FALSE); + + priv = hdy_keypad_get_instance_private (self); + + return priv->symbols_visible; +} + + +/** + * hdy_keypad_set_entry: + * @self: a #HdyKeypad + * @entry: (nullable): a #GtkEntry + * + * Binds @entry to @self and blocks any input which wouldn't be possible to type + * with with the keypad. + * + * Since: 0.0.12 + */ +void +hdy_keypad_set_entry (HdyKeypad *self, + GtkEntry *entry) +{ + HdyKeypadPrivate *priv; + + g_return_if_fail (HDY_IS_KEYPAD (self)); + g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry)); + + priv = hdy_keypad_get_instance_private (self); + + if (entry == priv->entry) + return; + + g_clear_object (&priv->entry); + + if (entry) { + priv->entry = g_object_ref (entry); + + gtk_widget_show (GTK_WIDGET (priv->entry)); + /* Workaround: To keep the osk closed + * https://gitlab.gnome.org/GNOME/gtk/merge_requests/978#note_546576 */ + g_object_set (priv->entry, "im-module", "gtk-im-context-none", NULL); + + g_signal_connect_swapped (G_OBJECT (priv->entry), + "insert-text", + G_CALLBACK (insert_text_cb), + self); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY]); +} + + +/** + * hdy_keypad_get_entry: + * @self: a #HdyKeypad + * + * Get the connected entry. See hdy_keypad_set_entry() for details. + * + * Returns: (transfer none): the set #GtkEntry or %NULL if no widget was set + * + * Since: 1.0 + */ +GtkEntry * +hdy_keypad_get_entry (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL); + + priv = hdy_keypad_get_instance_private (self); + + return priv->entry; +} + + +/** + * hdy_keypad_set_start_action: + * @self: a #HdyKeypad + * @start_action: (nullable): the start action widget + * + * Sets the widget for the lower left corner (or right, in RTL locales) of + * @self. + * + * Since: 1.0 + */ +void +hdy_keypad_set_start_action (HdyKeypad *self, + GtkWidget *start_action) +{ + HdyKeypadPrivate *priv; + GtkWidget *old_widget; + + g_return_if_fail (HDY_IS_KEYPAD (self)); + g_return_if_fail (start_action == NULL || GTK_IS_WIDGET (start_action)); + + priv = hdy_keypad_get_instance_private (self); + + old_widget = gtk_grid_get_child_at (GTK_GRID (priv->grid), 0, 3); + + if (old_widget == start_action) + return; + + if (old_widget != NULL) + gtk_container_remove (GTK_CONTAINER (priv->grid), old_widget); + + if (start_action != NULL) + gtk_grid_attach (GTK_GRID (priv->grid), start_action, 0, 3, 1, 1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION]); +} + + +/** + * hdy_keypad_get_start_action: + * @self: a #HdyKeypad + * + * Returns the widget for the lower left corner (or right, in RTL locales) of + * @self. + * + * Returns: (transfer none) (nullable): the start action widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_keypad_get_start_action (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL); + + priv = hdy_keypad_get_instance_private (self); + + return gtk_grid_get_child_at (GTK_GRID (priv->grid), 0, 3); +} + + +/** + * hdy_keypad_set_end_action: + * @self: a #HdyKeypad + * @end_action: (nullable): the end action widget + * + * Sets the widget for the lower right corner (or left, in RTL locales) of + * @self. + * + * Since: 1.0 + */ +void +hdy_keypad_set_end_action (HdyKeypad *self, + GtkWidget *end_action) +{ + HdyKeypadPrivate *priv; + GtkWidget *old_widget; + + g_return_if_fail (HDY_IS_KEYPAD (self)); + g_return_if_fail (end_action == NULL || GTK_IS_WIDGET (end_action)); + + priv = hdy_keypad_get_instance_private (self); + + old_widget = gtk_grid_get_child_at (GTK_GRID (priv->grid), 2, 3); + + if (old_widget == end_action) + return; + + if (old_widget != NULL) + gtk_container_remove (GTK_CONTAINER (priv->grid), old_widget); + + if (end_action != NULL) + gtk_grid_attach (GTK_GRID (priv->grid), end_action, 2, 3, 1, 1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION]); +} + + +/** + * hdy_keypad_get_end_action: + * @self: a #HdyKeypad + * + * Returns the widget for the lower right corner (or left, in RTL locales) of + * @self. + * + * Returns: (transfer none) (nullable): the end action widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_keypad_get_end_action (HdyKeypad *self) +{ + HdyKeypadPrivate *priv; + + g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL); + + priv = hdy_keypad_get_instance_private (self); + + return gtk_grid_get_child_at (GTK_GRID (priv->grid), 2, 3); +} diff --git a/subprojects/libhandy/src/hdy-keypad.h b/subprojects/libhandy/src/hdy-keypad.h new file mode 100644 index 0000000..267feea --- /dev/null +++ b/subprojects/libhandy/src/hdy-keypad.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_KEYPAD (hdy_keypad_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyKeypad, hdy_keypad, HDY, KEYPAD, GtkBin) + +/** + * HdyKeypadClass: + * @parent_class: The parent class + */ +struct _HdyKeypadClass +{ + GtkBinClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_keypad_new (gboolean symbols_visible, + gboolean letters_visible); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_row_spacing (HdyKeypad *self, + guint spacing); +HDY_AVAILABLE_IN_ALL +guint hdy_keypad_get_row_spacing (HdyKeypad *self); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_column_spacing (HdyKeypad *self, + guint spacing); +HDY_AVAILABLE_IN_ALL +guint hdy_keypad_get_column_spacing (HdyKeypad *self); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_letters_visible (HdyKeypad *self, + gboolean letters_visible); +HDY_AVAILABLE_IN_ALL +gboolean hdy_keypad_get_letters_visible (HdyKeypad *self); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_symbols_visible (HdyKeypad *self, + gboolean symbols_visible); +HDY_AVAILABLE_IN_ALL +gboolean hdy_keypad_get_symbols_visible (HdyKeypad *self); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_entry (HdyKeypad *self, + GtkEntry *entry); +HDY_AVAILABLE_IN_ALL +GtkEntry *hdy_keypad_get_entry (HdyKeypad *self); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_start_action (HdyKeypad *self, + GtkWidget *start_action); +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_keypad_get_start_action (HdyKeypad *self); +HDY_AVAILABLE_IN_ALL +void hdy_keypad_set_end_action (HdyKeypad *self, + GtkWidget *end_action); +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_keypad_get_end_action (HdyKeypad *self); + + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-keypad.ui b/subprojects/libhandy/src/hdy-keypad.ui new file mode 100644 index 0000000..3f04532 --- /dev/null +++ b/subprojects/libhandy/src/hdy-keypad.ui @@ -0,0 +1,216 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="HdyKeypad" parent="GtkBin"> + <child> + <object class="GtkGrid" id="grid"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">False</property> + <property name="vexpand">False</property> + <property name="row-spacing" bind-source="HdyKeypad" bind-property="row-spacing" bind-flags="sync-create"/> + <property name="column-spacing" bind-source="HdyKeypad" bind-property="column-spacing" bind-flags="sync-create"/> + <property name="column_homogeneous">True</property> + <property name="column_homogeneous">True</property> + <child> + <object class="HdyKeypadButton" id="btn_1"> + <property name="symbols">1</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_2"> + <property name="symbols">2ABC</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_3"> + <property name="symbols">3DEF</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_4"> + <property name="symbols">4GHI</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_5"> + <property name="symbols">5JKL</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_6"> + <property name="symbols">6MNO</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_7"> + <property name="symbols">7PQRS</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_8"> + <property name="symbols">8TUV</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_9"> + <property name="symbols">9WXYZ</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="btn_asterisk"> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="visible" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="asterisk_button_clicked_cb" swapped="true"/> + <child> + <object class="GtkLabel" id="label_asterisk"> + <property name="visible">True</property> + <property name="label">∗</property> + <style> + <class name="symbol"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="HdyKeypadButton" id="btn_0"> + <property name="symbols">0+</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="show-symbols" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="button_clicked_cb" swapped="true"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkButton" id="btn_hash"> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="focus_on_click">False</property> + <property name="visible" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/> + <signal name="clicked" handler="hash_button_clicked_cb" swapped="true"/> + <child> + <object class="GtkLabel" id="label_hash"> + <property name="visible">True</property> + <property name="label">#</property> + <style> + <class name="symbol"/> + </style> + </object> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + </child> + </template> + <object class="GtkGestureLongPress" id="long_press_zero_gesture"> + <property name="widget">btn_0</property> + <signal name="pressed" handler="long_press_zero_cb" object="HdyKeypad" swapped="true"/> + </object> +</interface> diff --git a/subprojects/libhandy/src/hdy-leaflet.c b/subprojects/libhandy/src/hdy-leaflet.c new file mode 100644 index 0000000..8c1ba2a --- /dev/null +++ b/subprojects/libhandy/src/hdy-leaflet.c @@ -0,0 +1,1209 @@ +/* + * Copyright (C) 2018 Purism SPC + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-leaflet.h" +#include "hdy-stackable-box-private.h" +#include "hdy-swipeable.h" + +/** + * SECTION:hdy-leaflet + * @short_description: An adaptive container acting like a box or a stack. + * @Title: HdyLeaflet + * + * The #HdyLeaflet widget can display its children like a #GtkBox does or + * like a #GtkStack does, adapting to size changes by switching between + * the two modes. + * + * When there is enough space the children are displayed side by side, otherwise + * only one is displayed and the leaflet is said to be “folded”. + * The threshold is dictated by the preferred minimum sizes of the children. + * When a leaflet is folded, the children can be navigated using swipe gestures. + * + * The “over” and “under” stack the children one on top of the other, while the + * “slide” transition puts the children side by side. While navigating to a + * child on the side or below can be performed by swiping the current child + * away, navigating to an upper child requires dragging it from the edge where + * it resides. This doesn't affect non-dragging swipes. + * + * The “over” and “under” transitions can draw their shadow on top of the + * window's transparent areas, like the rounded corners. This is a side-effect + * of allowing shadows to be drawn on top of OpenGL areas. It can be mitigated + * by using #HdyWindow or #HdyApplicationWindow as they will crop anything drawn + * beyond the rounded corners. + * + * # CSS nodes + * + * #HdyLeaflet has a single CSS node with name leaflet. The node will get the + * style classes .folded when it is folded, .unfolded when it's not, or none if + * it didn't compute its fold yet. + */ + +/** + * HdyLeafletTransitionType: + * @HDY_LEAFLET_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order + * @HDY_LEAFLET_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order + * @HDY_LEAFLET_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order + * + * This enumeration value describes the possible transitions between modes and + * children in a #HdyLeaflet widget. + * + * New values may be added to this enumeration over time. + * + * Since: 0.0.12 + */ + +enum { + PROP_0, + PROP_FOLDED, + PROP_HHOMOGENEOUS_FOLDED, + PROP_VHOMOGENEOUS_FOLDED, + PROP_HHOMOGENEOUS_UNFOLDED, + PROP_VHOMOGENEOUS_UNFOLDED, + PROP_VISIBLE_CHILD, + PROP_VISIBLE_CHILD_NAME, + PROP_TRANSITION_TYPE, + PROP_MODE_TRANSITION_DURATION, + PROP_CHILD_TRANSITION_DURATION, + PROP_CHILD_TRANSITION_RUNNING, + PROP_INTERPOLATE_SIZE, + PROP_CAN_SWIPE_BACK, + PROP_CAN_SWIPE_FORWARD, + + /* orientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_ORIENTATION, +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_NAME, + CHILD_PROP_NAVIGATABLE, + LAST_CHILD_PROP, +}; + +typedef struct +{ + HdyStackableBox *box; +} HdyLeafletPrivate; + +static GParamSpec *props[LAST_PROP]; +static GParamSpec *child_props[LAST_CHILD_PROP]; + +static void hdy_leaflet_swipeable_init (HdySwipeableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyLeaflet, hdy_leaflet, GTK_TYPE_CONTAINER, + G_ADD_PRIVATE (HdyLeaflet) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL) + G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_leaflet_swipeable_init)) + +#define HDY_GET_HELPER(obj) (((HdyLeafletPrivate *) hdy_leaflet_get_instance_private (HDY_LEAFLET (obj)))->box) + +/** + * hdy_leaflet_get_folded: + * @self: a #HdyLeaflet + * + * Gets whether @self is folded. + * + * Returns: whether @self is folded. + */ +gboolean +hdy_leaflet_get_folded (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_get_folded (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_homogeneous: + * @self: a #HdyLeaflet + * @folded: the fold + * @orientation: the orientation + * @homogeneous: %TRUE to make @self homogeneous + * + * Sets the #HdyLeaflet to be homogeneous or not for the given fold and orientation. + * If it is homogeneous, the #HdyLeaflet will request the same + * width or height for all its children depending on the orientation. + * If it isn't and it is folded, the leaflet may change width or height + * when a different child becomes visible. + */ +void +hdy_leaflet_set_homogeneous (HdyLeaflet *self, + gboolean folded, + GtkOrientation orientation, + gboolean homogeneous) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_homogeneous (HDY_GET_HELPER (self), folded, orientation, homogeneous); +} + +/** + * hdy_leaflet_get_homogeneous: + * @self: a #HdyLeaflet + * @folded: the fold + * @orientation: the orientation + * + * Gets whether @self is homogeneous for the given fold and orientation. + * See hdy_leaflet_set_homogeneous(). + * + * Returns: whether @self is homogeneous for the given fold and orientation. + */ +gboolean +hdy_leaflet_get_homogeneous (HdyLeaflet *self, + gboolean folded, + GtkOrientation orientation) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_get_homogeneous (HDY_GET_HELPER (self), folded, orientation); +} + +/** + * hdy_leaflet_get_transition_type: + * @self: a #HdyLeaflet + * + * Gets the type of animation that will be used + * for transitions between modes and children in @self. + * + * Returns: the current transition type of @self + * + * Since: 0.0.12 + */ +HdyLeafletTransitionType +hdy_leaflet_get_transition_type (HdyLeaflet *self) +{ + HdyStackableBoxTransitionType type; + + g_return_val_if_fail (HDY_IS_LEAFLET (self), HDY_LEAFLET_TRANSITION_TYPE_OVER); + + type = hdy_stackable_box_get_transition_type (HDY_GET_HELPER (self)); + + switch (type) { + case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: + return HDY_LEAFLET_TRANSITION_TYPE_OVER; + + case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: + return HDY_LEAFLET_TRANSITION_TYPE_UNDER; + + case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: + return HDY_LEAFLET_TRANSITION_TYPE_SLIDE; + + default: + g_assert_not_reached (); + } +} + +/** + * hdy_leaflet_set_transition_type: + * @self: a #HdyLeaflet + * @transition: the new transition type + * + * Sets the type of animation that will be used for transitions between modes + * and children in @self. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the mode or child that is about to + * become current. + * + * Since: 0.0.12 + */ +void +hdy_leaflet_set_transition_type (HdyLeaflet *self, + HdyLeafletTransitionType transition) +{ + HdyStackableBoxTransitionType type; + + g_return_if_fail (HDY_IS_LEAFLET (self)); + g_return_if_fail (transition <= HDY_LEAFLET_TRANSITION_TYPE_SLIDE); + + switch (transition) { + case HDY_LEAFLET_TRANSITION_TYPE_OVER: + type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER; + break; + + case HDY_LEAFLET_TRANSITION_TYPE_UNDER: + type = HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER; + break; + + case HDY_LEAFLET_TRANSITION_TYPE_SLIDE: + type = HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE; + break; + + default: + g_assert_not_reached (); + } + + hdy_stackable_box_set_transition_type (HDY_GET_HELPER (self), type); +} + +/** + * hdy_leaflet_get_mode_transition_duration: + * @self: a #HdyLeaflet + * + * Returns the amount of time (in milliseconds) that + * transitions between modes in @self will take. + * + * Returns: the mode transition duration + */ +guint +hdy_leaflet_get_mode_transition_duration (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), 0); + + return hdy_stackable_box_get_mode_transition_duration (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_mode_transition_duration: + * @self: a #HdyLeaflet + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between modes in @self + * will take. + */ +void +hdy_leaflet_set_mode_transition_duration (HdyLeaflet *self, + guint duration) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_mode_transition_duration (HDY_GET_HELPER (self), duration); +} + +/** + * hdy_leaflet_get_child_transition_duration: + * @self: a #HdyLeaflet + * + * Returns the amount of time (in milliseconds) that + * transitions between children in @self will take. + * + * Returns: the child transition duration + */ +guint +hdy_leaflet_get_child_transition_duration (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), 0); + + return hdy_stackable_box_get_child_transition_duration (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_child_transition_duration: + * @self: a #HdyLeaflet + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between children in @self + * will take. + */ +void +hdy_leaflet_set_child_transition_duration (HdyLeaflet *self, + guint duration) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_child_transition_duration (HDY_GET_HELPER (self), duration); +} + +/** + * hdy_leaflet_get_visible_child: + * @self: a #HdyLeaflet + * + * Gets the visible child widget. + * + * Returns: (transfer none): the visible child widget + */ +GtkWidget * +hdy_leaflet_get_visible_child (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL); + + return hdy_stackable_box_get_visible_child (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_visible_child: + * @self: a #HdyLeaflet + * @visible_child: the new child + * + * Makes @visible_child visible using a transition determined by + * HdyLeaflet:transition-type and HdyLeaflet:child-transition-duration. The + * transition can be cancelled by the user, in which case visible child will + * change back to the previously visible child. + */ +void +hdy_leaflet_set_visible_child (HdyLeaflet *self, + GtkWidget *visible_child) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_visible_child (HDY_GET_HELPER (self), visible_child); +} + +/** + * hdy_leaflet_get_visible_child_name: + * @self: a #HdyLeaflet + * + * Gets the name of the currently visible child widget. + * + * Returns: (transfer none): the name of the visible child + */ +const gchar * +hdy_leaflet_get_visible_child_name (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL); + + return hdy_stackable_box_get_visible_child_name (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_visible_child_name: + * @self: a #HdyLeaflet + * @name: the name of a child + * + * Makes the child with the name @name visible. + * + * See hdy_leaflet_set_visible_child() for more details. + */ +void +hdy_leaflet_set_visible_child_name (HdyLeaflet *self, + const gchar *name) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_visible_child_name (HDY_GET_HELPER (self), name); +} + +/** + * hdy_leaflet_get_child_transition_running: + * @self: a #HdyLeaflet + * + * Returns whether @self is currently in a transition from one page to + * another. + * + * Returns: %TRUE if the transition is currently running, %FALSE otherwise. + */ +gboolean +hdy_leaflet_get_child_transition_running (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_get_child_transition_running (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_interpolate_size: + * @self: a #HdyLeaflet + * @interpolate_size: the new value + * + * Sets whether or not @self will interpolate its size when + * changing the visible child. If the #HdyLeaflet:interpolate-size + * property is set to %TRUE, @self will interpolate its size between + * the current one and the one it'll take after changing the + * visible child, according to the set transition duration. + */ +void +hdy_leaflet_set_interpolate_size (HdyLeaflet *self, + gboolean interpolate_size) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_interpolate_size (HDY_GET_HELPER (self), interpolate_size); +} + +/** + * hdy_leaflet_get_interpolate_size: + * @self: a #HdyLeaflet + * + * Returns whether the #HdyLeaflet is set up to interpolate between + * the sizes of children on page switch. + * + * Returns: %TRUE if child sizes are interpolated + */ +gboolean +hdy_leaflet_get_interpolate_size (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_get_interpolate_size (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_can_swipe_back: + * @self: a #HdyLeaflet + * @can_swipe_back: the new value + * + * Sets whether or not @self allows switching to the previous child that has + * 'navigatable' child property set to %TRUE via a swipe gesture + * + * Since: 0.0.12 + */ +void +hdy_leaflet_set_can_swipe_back (HdyLeaflet *self, + gboolean can_swipe_back) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_can_swipe_back (HDY_GET_HELPER (self), can_swipe_back); +} + +/** + * hdy_leaflet_get_can_swipe_back + * @self: a #HdyLeaflet + * + * Returns whether the #HdyLeaflet allows swiping to the previous child. + * + * Returns: %TRUE if back swipe is enabled. + * + * Since: 0.0.12 + */ +gboolean +hdy_leaflet_get_can_swipe_back (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_get_can_swipe_back (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_set_can_swipe_forward: + * @self: a #HdyLeaflet + * @can_swipe_forward: the new value + * + * Sets whether or not @self allows switching to the next child that has + * 'navigatable' child property set to %TRUE via a swipe gesture. + * + * Since: 0.0.12 + */ +void +hdy_leaflet_set_can_swipe_forward (HdyLeaflet *self, + gboolean can_swipe_forward) +{ + g_return_if_fail (HDY_IS_LEAFLET (self)); + + hdy_stackable_box_set_can_swipe_forward (HDY_GET_HELPER (self), can_swipe_forward); +} + +/** + * hdy_leaflet_get_can_swipe_forward + * @self: a #HdyLeaflet + * + * Returns whether the #HdyLeaflet allows swiping to the next child. + * + * Returns: %TRUE if forward swipe is enabled. + * + * Since: 0.0.12 + */ +gboolean +hdy_leaflet_get_can_swipe_forward (HdyLeaflet *self) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_get_can_swipe_forward (HDY_GET_HELPER (self)); +} + +/** + * hdy_leaflet_get_adjacent_child + * @self: a #HdyLeaflet + * @direction: the direction + * + * Gets the previous or next child that doesn't have 'navigatable' child + * property set to %FALSE, or %NULL if it doesn't exist. This will be the same + * widget hdy_leaflet_navigate() will navigate to. + * + * Returns: (nullable) (transfer none): the previous or next child, or + * %NULL if it doesn't exist. + * + * Since: 1.0 + */ +GtkWidget * +hdy_leaflet_get_adjacent_child (HdyLeaflet *self, + HdyNavigationDirection direction) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL); + + return hdy_stackable_box_get_adjacent_child (HDY_GET_HELPER (self), direction); +} + +/** + * hdy_leaflet_navigate + * @self: a #HdyLeaflet + * @direction: the direction + * + * Switches to the previous or next child that doesn't have 'navigatable' child + * property set to %FALSE, similar to performing a swipe gesture to go in + * @direction. + * + * Returns: %TRUE if visible child was changed, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_leaflet_navigate (HdyLeaflet *self, + HdyNavigationDirection direction) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE); + + return hdy_stackable_box_navigate (HDY_GET_HELPER (self), direction); +} + +/** + * hdy_leaflet_get_child_by_name: + * @self: a #HdyLeaflet + * @name: the name of the child to find + * + * Finds the child of @self with the name given as the argument. Returns %NULL + * if there is no child with this name. + * + * Returns: (transfer none) (nullable): the requested child of @self + * + * Since: 1.0 + */ +GtkWidget * +hdy_leaflet_get_child_by_name (HdyLeaflet *self, + const gchar *name) +{ + g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL); + + return hdy_stackable_box_get_child_by_name (HDY_GET_HELPER (self), name); +} + +/* This private method is prefixed by the call name because it will be a virtual + * method in GTK 4. + */ +static void +hdy_leaflet_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + hdy_stackable_box_measure (HDY_GET_HELPER (widget), + orientation, for_size, + minimum, natural, + minimum_baseline, natural_baseline); +} + +static void +hdy_leaflet_get_preferred_width (GtkWidget *widget, + gint *minimum_width, + gint *natural_width) +{ + hdy_leaflet_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_leaflet_get_preferred_height (GtkWidget *widget, + gint *minimum_height, + gint *natural_height) +{ + hdy_leaflet_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_leaflet_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width) +{ + hdy_leaflet_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum_width, natural_width, NULL, NULL); +} + +static void +hdy_leaflet_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum_height, + gint *natural_height) +{ + hdy_leaflet_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum_height, natural_height, NULL, NULL); +} + +static void +hdy_leaflet_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + hdy_stackable_box_size_allocate (HDY_GET_HELPER (widget), allocation); +} + +static gboolean +hdy_leaflet_draw (GtkWidget *widget, + cairo_t *cr) +{ + return hdy_stackable_box_draw (HDY_GET_HELPER (widget), cr); +} + +static void +hdy_leaflet_direction_changed (GtkWidget *widget, + GtkTextDirection previous_direction) +{ + hdy_stackable_box_direction_changed (HDY_GET_HELPER (widget), previous_direction); +} + +static void +hdy_leaflet_add (GtkContainer *container, + GtkWidget *widget) +{ + hdy_stackable_box_add (HDY_GET_HELPER (container), widget); +} + +static void +hdy_leaflet_remove (GtkContainer *container, + GtkWidget *widget) +{ + hdy_stackable_box_remove (HDY_GET_HELPER (container), widget); +} + +static void +hdy_leaflet_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + hdy_stackable_box_forall (HDY_GET_HELPER (container), include_internals, callback, callback_data); +} + +static void +hdy_leaflet_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyLeaflet *self = HDY_LEAFLET (object); + + switch (prop_id) { + case PROP_FOLDED: + g_value_set_boolean (value, hdy_leaflet_get_folded (self)); + break; + case PROP_HHOMOGENEOUS_FOLDED: + g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL)); + break; + case PROP_VHOMOGENEOUS_FOLDED: + g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL)); + break; + case PROP_HHOMOGENEOUS_UNFOLDED: + g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL)); + break; + case PROP_VHOMOGENEOUS_UNFOLDED: + g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL)); + break; + case PROP_VISIBLE_CHILD: + g_value_set_object (value, hdy_leaflet_get_visible_child (self)); + break; + case PROP_VISIBLE_CHILD_NAME: + g_value_set_string (value, hdy_leaflet_get_visible_child_name (self)); + break; + case PROP_TRANSITION_TYPE: + g_value_set_enum (value, hdy_leaflet_get_transition_type (self)); + break; + case PROP_MODE_TRANSITION_DURATION: + g_value_set_uint (value, hdy_leaflet_get_mode_transition_duration (self)); + break; + case PROP_CHILD_TRANSITION_DURATION: + g_value_set_uint (value, hdy_leaflet_get_child_transition_duration (self)); + break; + case PROP_CHILD_TRANSITION_RUNNING: + g_value_set_boolean (value, hdy_leaflet_get_child_transition_running (self)); + break; + case PROP_INTERPOLATE_SIZE: + g_value_set_boolean (value, hdy_leaflet_get_interpolate_size (self)); + break; + case PROP_CAN_SWIPE_BACK: + g_value_set_boolean (value, hdy_leaflet_get_can_swipe_back (self)); + break; + case PROP_CAN_SWIPE_FORWARD: + g_value_set_boolean (value, hdy_leaflet_get_can_swipe_forward (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, hdy_stackable_box_get_orientation (HDY_GET_HELPER (self))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_leaflet_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyLeaflet *self = HDY_LEAFLET (object); + + switch (prop_id) { + case PROP_HHOMOGENEOUS_FOLDED: + hdy_leaflet_set_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value)); + break; + case PROP_VHOMOGENEOUS_FOLDED: + hdy_leaflet_set_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value)); + break; + case PROP_HHOMOGENEOUS_UNFOLDED: + hdy_leaflet_set_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value)); + break; + case PROP_VHOMOGENEOUS_UNFOLDED: + hdy_leaflet_set_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value)); + break; + case PROP_VISIBLE_CHILD: + hdy_leaflet_set_visible_child (self, g_value_get_object (value)); + break; + case PROP_VISIBLE_CHILD_NAME: + hdy_leaflet_set_visible_child_name (self, g_value_get_string (value)); + break; + case PROP_TRANSITION_TYPE: + hdy_leaflet_set_transition_type (self, g_value_get_enum (value)); + break; + case PROP_MODE_TRANSITION_DURATION: + hdy_leaflet_set_mode_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_CHILD_TRANSITION_DURATION: + hdy_leaflet_set_child_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_INTERPOLATE_SIZE: + hdy_leaflet_set_interpolate_size (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_BACK: + hdy_leaflet_set_can_swipe_back (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_FORWARD: + hdy_leaflet_set_can_swipe_forward (self, g_value_get_boolean (value)); + break; + case PROP_ORIENTATION: + hdy_stackable_box_set_orientation (HDY_GET_HELPER (self), g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_leaflet_finalize (GObject *object) +{ + HdyLeaflet *self = HDY_LEAFLET (object); + HdyLeafletPrivate *priv = hdy_leaflet_get_instance_private (self); + + g_clear_object (&priv->box); + + G_OBJECT_CLASS (hdy_leaflet_parent_class)->finalize (object); +} + +static void +hdy_leaflet_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case CHILD_PROP_NAME: + g_value_set_string (value, hdy_stackable_box_get_child_name (HDY_GET_HELPER (container), widget)); + break; + + case CHILD_PROP_NAVIGATABLE: + g_value_set_boolean (value, hdy_stackable_box_get_child_navigatable (HDY_GET_HELPER (container), widget)); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_leaflet_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case CHILD_PROP_NAME: + hdy_stackable_box_set_child_name (HDY_GET_HELPER (container), widget, g_value_get_string (value)); + gtk_container_child_notify_by_pspec (container, widget, pspec); + break; + + case CHILD_PROP_NAVIGATABLE: + hdy_stackable_box_set_child_navigatable (HDY_GET_HELPER (container), widget, g_value_get_boolean (value)); + gtk_container_child_notify_by_pspec (container, widget, pspec); + break; + + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_leaflet_realize (GtkWidget *widget) +{ + hdy_stackable_box_realize (HDY_GET_HELPER (widget)); +} + +static void +hdy_leaflet_unrealize (GtkWidget *widget) +{ + hdy_stackable_box_unrealize (HDY_GET_HELPER (widget)); +} + +static void +hdy_leaflet_map (GtkWidget *widget) +{ + hdy_stackable_box_map (HDY_GET_HELPER (widget)); +} + +static void +hdy_leaflet_unmap (GtkWidget *widget) +{ + hdy_stackable_box_unmap (HDY_GET_HELPER (widget)); +} + +static void +hdy_leaflet_switch_child (HdySwipeable *swipeable, + guint index, + gint64 duration) +{ + hdy_stackable_box_switch_child (HDY_GET_HELPER (swipeable), index, duration); +} + +static HdySwipeTracker * +hdy_leaflet_get_swipe_tracker (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_swipe_tracker (HDY_GET_HELPER (swipeable)); +} + +static gdouble +hdy_leaflet_get_distance (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_distance (HDY_GET_HELPER (swipeable)); +} + +static gdouble * +hdy_leaflet_get_snap_points (HdySwipeable *swipeable, + gint *n_snap_points) +{ + return hdy_stackable_box_get_snap_points (HDY_GET_HELPER (swipeable), n_snap_points); +} + +static gdouble +hdy_leaflet_get_progress (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_progress (HDY_GET_HELPER (swipeable)); +} + +static gdouble +hdy_leaflet_get_cancel_progress (HdySwipeable *swipeable) +{ + return hdy_stackable_box_get_cancel_progress (HDY_GET_HELPER (swipeable)); +} + +static void +hdy_leaflet_get_swipe_area (HdySwipeable *swipeable, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect) +{ + hdy_stackable_box_get_swipe_area (HDY_GET_HELPER (swipeable), navigation_direction, is_drag, rect); +} + +static void +hdy_leaflet_class_init (HdyLeafletClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = (GtkWidgetClass*) klass; + GtkContainerClass *container_class = (GtkContainerClass*) klass; + + object_class->get_property = hdy_leaflet_get_property; + object_class->set_property = hdy_leaflet_set_property; + object_class->finalize = hdy_leaflet_finalize; + + widget_class->realize = hdy_leaflet_realize; + widget_class->unrealize = hdy_leaflet_unrealize; + widget_class->map = hdy_leaflet_map; + widget_class->unmap = hdy_leaflet_unmap; + widget_class->get_preferred_width = hdy_leaflet_get_preferred_width; + widget_class->get_preferred_height = hdy_leaflet_get_preferred_height; + widget_class->get_preferred_width_for_height = hdy_leaflet_get_preferred_width_for_height; + widget_class->get_preferred_height_for_width = hdy_leaflet_get_preferred_height_for_width; + widget_class->size_allocate = hdy_leaflet_size_allocate; + widget_class->draw = hdy_leaflet_draw; + widget_class->direction_changed = hdy_leaflet_direction_changed; + + container_class->add = hdy_leaflet_add; + container_class->remove = hdy_leaflet_remove; + container_class->forall = hdy_leaflet_forall; + container_class->set_child_property = hdy_leaflet_set_child_property; + container_class->get_child_property = hdy_leaflet_get_child_property; + gtk_container_class_handle_border_width (container_class); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + /** + * HdyLeaflet:folded: + * + * %TRUE if the leaflet is folded. + * + * The leaflet will be folded if the size allocated to it is smaller than the + * sum of the natural size of its children, it will be unfolded otherwise. + */ + props[PROP_FOLDED] = + g_param_spec_boolean ("folded", + _("Folded"), + _("Whether the widget is folded"), + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:hhomogeneous_folded: + * + * %TRUE if the leaflet allocates the same width for all children when folded. + */ + props[PROP_HHOMOGENEOUS_FOLDED] = + g_param_spec_boolean ("hhomogeneous-folded", + _("Horizontally homogeneous folded"), + _("Horizontally homogeneous sizing when the leaflet is folded"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:vhomogeneous_folded: + * + * %TRUE if the leaflet allocates the same height for all children when folded. + */ + props[PROP_VHOMOGENEOUS_FOLDED] = + g_param_spec_boolean ("vhomogeneous-folded", + _("Vertically homogeneous folded"), + _("Vertically homogeneous sizing when the leaflet is folded"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:hhomogeneous_unfolded: + * + * %TRUE if the leaflet allocates the same width for all children when unfolded. + */ + props[PROP_HHOMOGENEOUS_UNFOLDED] = + g_param_spec_boolean ("hhomogeneous-unfolded", + _("Box horizontally homogeneous"), + _("Horizontally homogeneous sizing when the leaflet is unfolded"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:vhomogeneous_unfolded: + * + * %TRUE if the leaflet allocates the same height for all children when unfolded. + */ + props[PROP_VHOMOGENEOUS_UNFOLDED] = + g_param_spec_boolean ("vhomogeneous-unfolded", + _("Box vertically homogeneous"), + _("Vertically homogeneous sizing when the leaflet is unfolded"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_VISIBLE_CHILD] = + g_param_spec_object ("visible-child", + _("Visible child"), + _("The widget currently visible when the leaflet is folded"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_VISIBLE_CHILD_NAME] = + g_param_spec_string ("visible-child-name", + _("Name of visible child"), + _("The name of the widget currently visible when the children are stacked"), + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:transition-type: + * + * The type of animation that will be used for transitions between modes and + * children. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the mode or child that is about + * to become current. + * + * Since: 0.0.12 + */ + props[PROP_TRANSITION_TYPE] = + g_param_spec_enum ("transition-type", + _("Transition type"), + _("The type of animation used to transition between modes and children"), + HDY_TYPE_LEAFLET_TRANSITION_TYPE, HDY_LEAFLET_TRANSITION_TYPE_OVER, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_MODE_TRANSITION_DURATION] = + g_param_spec_uint ("mode-transition-duration", + _("Mode transition duration"), + _("The mode transition animation duration, in milliseconds"), + 0, G_MAXUINT, 250, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_CHILD_TRANSITION_DURATION] = + g_param_spec_uint ("child-transition-duration", + _("Child transition duration"), + _("The child transition animation duration, in milliseconds"), + 0, G_MAXUINT, 200, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_CHILD_TRANSITION_RUNNING] = + g_param_spec_boolean ("child-transition-running", + _("Child transition running"), + _("Whether or not the child transition is currently running"), + FALSE, + G_PARAM_READABLE); + + props[PROP_INTERPOLATE_SIZE] = + g_param_spec_boolean ("interpolate-size", + _("Interpolate size"), + _("Whether or not the size should smoothly change when changing between differently sized children"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:can-swipe-back: + * + * Whether or not the leaflet allows switching to the previous child that has + * 'navigatable' child property set to %TRUE via a swipe gesture. + * + * Since: 0.0.12 + */ + props[PROP_CAN_SWIPE_BACK] = + g_param_spec_boolean ("can-swipe-back", + _("Can swipe back"), + _("Whether or not swipe gesture can be used to switch to the previous child"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyLeaflet:can-swipe-forward: + * + * Whether or not the leaflet allows switching to the next child that has + * 'navigatable' child property set to %TRUE via a swipe gesture. + * + * Since: 0.0.12 + */ + props[PROP_CAN_SWIPE_FORWARD] = + g_param_spec_boolean ("can-swipe-forward", + _("Can swipe forward"), + _("Whether or not swipe gesture can be used to switch to the next child"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + child_props[CHILD_PROP_NAME] = + g_param_spec_string ("name", + _("Name"), + _("The name of the child page"), + NULL, + G_PARAM_READWRITE); + + /** + * HdyLeaflet:navigatable: + * + * Whether the child can be navigated to when folded. + * If %FALSE, the child will be ignored by hdy_leaflet_get_adjacent_child(), + * hdy_leaflet_navigate(), and swipe gestures. + * + * This can be used used to prevent switching to widgets like separators. + * + * Since: 1.0 + */ + child_props[CHILD_PROP_NAVIGATABLE] = + g_param_spec_boolean ("navigatable", + _("Navigatable"), + _("Whether the child can be navigated to"), + TRUE, + G_PARAM_READWRITE); + + gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props); + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL); + gtk_widget_class_set_css_name (widget_class, "leaflet"); +} + +GtkWidget * +hdy_leaflet_new (void) +{ + return g_object_new (HDY_TYPE_LEAFLET, NULL); +} + +#define NOTIFY(func, prop) \ +static void \ +func (HdyLeaflet *self) { \ + g_object_notify_by_pspec (G_OBJECT (self), props[prop]); \ +} + +NOTIFY (notify_folded_cb, PROP_FOLDED); +NOTIFY (notify_hhomogeneous_folded_cb, PROP_HHOMOGENEOUS_FOLDED); +NOTIFY (notify_vhomogeneous_folded_cb, PROP_VHOMOGENEOUS_FOLDED); +NOTIFY (notify_hhomogeneous_unfolded_cb, PROP_HHOMOGENEOUS_UNFOLDED); +NOTIFY (notify_vhomogeneous_unfolded_cb, PROP_VHOMOGENEOUS_UNFOLDED); +NOTIFY (notify_visible_child_cb, PROP_VISIBLE_CHILD); +NOTIFY (notify_visible_child_name_cb, PROP_VISIBLE_CHILD_NAME); +NOTIFY (notify_transition_type_cb, PROP_TRANSITION_TYPE); +NOTIFY (notify_mode_transition_duration_cb, PROP_MODE_TRANSITION_DURATION); +NOTIFY (notify_child_transition_duration_cb, PROP_CHILD_TRANSITION_DURATION); +NOTIFY (notify_child_transition_running_cb, PROP_CHILD_TRANSITION_RUNNING); +NOTIFY (notify_interpolate_size_cb, PROP_INTERPOLATE_SIZE); +NOTIFY (notify_can_swipe_back_cb, PROP_CAN_SWIPE_BACK); +NOTIFY (notify_can_swipe_forward_cb, PROP_CAN_SWIPE_FORWARD); + +static void +notify_orientation_cb (HdyLeaflet *self) +{ + g_object_notify (G_OBJECT (self), "orientation"); +} + +static void +hdy_leaflet_init (HdyLeaflet *self) +{ + HdyLeafletPrivate *priv = hdy_leaflet_get_instance_private (self); + + priv->box = hdy_stackable_box_new (GTK_CONTAINER (self), + GTK_CONTAINER_CLASS (hdy_leaflet_parent_class), + TRUE); + + g_signal_connect_object (priv->box, "notify::folded", G_CALLBACK (notify_folded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::hhomogeneous-folded", G_CALLBACK (notify_hhomogeneous_folded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::vhomogeneous-folded", G_CALLBACK (notify_vhomogeneous_folded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::hhomogeneous-unfolded", G_CALLBACK (notify_hhomogeneous_unfolded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::vhomogeneous-unfolded", G_CALLBACK (notify_vhomogeneous_unfolded_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::visible-child", G_CALLBACK (notify_visible_child_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::visible-child-name", G_CALLBACK (notify_visible_child_name_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::transition-type", G_CALLBACK (notify_transition_type_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::mode-transition-duration", G_CALLBACK (notify_mode_transition_duration_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::child-transition-duration", G_CALLBACK (notify_child_transition_duration_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::child-transition-running", G_CALLBACK (notify_child_transition_running_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::interpolate-size", G_CALLBACK (notify_interpolate_size_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::can-swipe-back", G_CALLBACK (notify_can_swipe_back_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::can-swipe-forward", G_CALLBACK (notify_can_swipe_forward_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (priv->box, "notify::orientation", G_CALLBACK (notify_orientation_cb), self, G_CONNECT_SWAPPED); +} + +static void +hdy_leaflet_swipeable_init (HdySwipeableInterface *iface) +{ + iface->switch_child = hdy_leaflet_switch_child; + iface->get_swipe_tracker = hdy_leaflet_get_swipe_tracker; + iface->get_distance = hdy_leaflet_get_distance; + iface->get_snap_points = hdy_leaflet_get_snap_points; + iface->get_progress = hdy_leaflet_get_progress; + iface->get_cancel_progress = hdy_leaflet_get_cancel_progress; + iface->get_swipe_area = hdy_leaflet_get_swipe_area; +} diff --git a/subprojects/libhandy/src/hdy-leaflet.h b/subprojects/libhandy/src/hdy-leaflet.h new file mode 100644 index 0000000..4d9324d --- /dev/null +++ b/subprojects/libhandy/src/hdy-leaflet.h @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-enums.h" +#include "hdy-navigation-direction.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_LEAFLET (hdy_leaflet_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyLeaflet, hdy_leaflet, HDY, LEAFLET, GtkContainer) + +typedef enum { + HDY_LEAFLET_TRANSITION_TYPE_OVER, + HDY_LEAFLET_TRANSITION_TYPE_UNDER, + HDY_LEAFLET_TRANSITION_TYPE_SLIDE, +} HdyLeafletTransitionType; + +/** + * HdyLeafletClass + * @parent_class: The parent class + */ +struct _HdyLeafletClass +{ + GtkContainerClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_leaflet_new (void); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_get_folded (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_leaflet_get_visible_child (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_visible_child (HdyLeaflet *self, + GtkWidget *visible_child); +HDY_AVAILABLE_IN_ALL +const gchar *hdy_leaflet_get_visible_child_name (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_visible_child_name (HdyLeaflet *self, + const gchar *name); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_get_homogeneous (HdyLeaflet *self, + gboolean folded, + GtkOrientation orientation); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_homogeneous (HdyLeaflet *self, + gboolean folded, + GtkOrientation orientation, + gboolean homogeneous); +HDY_AVAILABLE_IN_ALL +HdyLeafletTransitionType hdy_leaflet_get_transition_type (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_transition_type (HdyLeaflet *self, + HdyLeafletTransitionType transition); + +HDY_AVAILABLE_IN_ALL +guint hdy_leaflet_get_mode_transition_duration (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_mode_transition_duration (HdyLeaflet *self, + guint duration); + +HDY_AVAILABLE_IN_ALL +guint hdy_leaflet_get_child_transition_duration (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_child_transition_duration (HdyLeaflet *self, + guint duration); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_get_child_transition_running (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_get_interpolate_size (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_interpolate_size (HdyLeaflet *self, + gboolean interpolate_size); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_get_can_swipe_back (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_can_swipe_back (HdyLeaflet *self, + gboolean can_swipe_back); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_get_can_swipe_forward (HdyLeaflet *self); +HDY_AVAILABLE_IN_ALL +void hdy_leaflet_set_can_swipe_forward (HdyLeaflet *self, + gboolean can_swipe_forward); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_leaflet_get_adjacent_child (HdyLeaflet *self, + HdyNavigationDirection direction); +HDY_AVAILABLE_IN_ALL +gboolean hdy_leaflet_navigate (HdyLeaflet *self, + HdyNavigationDirection direction); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_leaflet_get_child_by_name (HdyLeaflet *self, + const gchar *name); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-main-private.h b/subprojects/libhandy/src/hdy-main-private.h new file mode 100644 index 0000000..3ad6ad1 --- /dev/null +++ b/subprojects/libhandy/src/hdy-main-private.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-main.h" + +G_BEGIN_DECLS + +/* Initializes the public GObject types, which is needed to ensure they are + * discoverable, for example so they can easily be used with GtkBuilder. + * + * The function is implemented in hdy-public-types.c which is generated at + * compile time by gen-public-types.sh + */ +void hdy_init_public_types (void); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-main.c b/subprojects/libhandy/src/hdy-main.c new file mode 100644 index 0000000..6c5df7b --- /dev/null +++ b/subprojects/libhandy/src/hdy-main.c @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2018-2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ +#include "config.h" +#include "hdy-main-private.h" +#include <gio/gio.h> +#include <glib/gi18n-lib.h> +#include <gtk/gtk.h> + +static gint hdy_initialized = FALSE; + +/** + * SECTION:hdy-main + * @short_description: Library initialization. + * @Title: hdy-main + * + * Before using the Handy library you should initialize it by calling the + * hdy_init() function. + * This makes sure translations, types, themes, and icons for the Handy library + * are set up properly. + */ + +/* The style provider priority to use for libhandy widgets custom styling. It is + * higher than themes and settings, allowing to override theme defaults, but + * lower than applications and user provided styles, so application developers + * can nonetheless apply custom styling on top of it. */ +#define HDY_STYLE_PROVIDER_PRIORITY_OVERRIDE (GTK_STYLE_PROVIDER_PRIORITY_SETTINGS + 1) + +#define HDY_THEMES_PATH "/sm/puri/handy/themes/" + +static inline gboolean +hdy_resource_exists (const gchar *resource_path) +{ + return g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL); +} + +static gchar * +hdy_themes_get_theme_name (gboolean *prefer_dark_theme) +{ + gchar *theme_name = NULL; + gchar *p; + + g_assert (prefer_dark_theme); + + theme_name = g_strdup (g_getenv ("GTK_THEME")); + + if (theme_name == NULL) { + g_object_get (gtk_settings_get_default (), + "gtk-theme-name", &theme_name, + "gtk-application-prefer-dark-theme", prefer_dark_theme, + NULL); + + return theme_name; + } + + /* Theme variants are specified with the syntax + * "<theme>:<variant>" e.g. "Adwaita:dark" */ + if (NULL != (p = strrchr (theme_name, ':'))) { + *p = '\0'; + p++; + *prefer_dark_theme = g_strcmp0 (p, "dark") == 0; + } + + return theme_name; +} + +static void +hdy_themes_update (GtkCssProvider *css_provider) +{ + g_autofree gchar *theme_name = NULL; + g_autofree gchar *resource_path = NULL; + gboolean prefer_dark_theme = FALSE; + + g_assert (GTK_IS_CSS_PROVIDER (css_provider)); + + theme_name = hdy_themes_get_theme_name (&prefer_dark_theme); + + /* First check with full path to theme+variant */ + resource_path = g_strdup_printf (HDY_THEMES_PATH"%s%s.css", + theme_name, prefer_dark_theme ? "-dark" : ""); + + if (!hdy_resource_exists (resource_path)) { + /* Now try without the theme variant */ + g_free (resource_path); + resource_path = g_strdup_printf (HDY_THEMES_PATH"%s.css", theme_name); + + if (!hdy_resource_exists (resource_path)) { + /* Now fallback to shared styling */ + g_free (resource_path); + resource_path = g_strdup (HDY_THEMES_PATH"shared.css"); + + g_assert (hdy_resource_exists (resource_path)); + } + } + + gtk_css_provider_load_from_resource (css_provider, resource_path); +} + +static void +load_fallback_style (void) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + + css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_FALLBACK); + + gtk_css_provider_load_from_resource (css_provider, HDY_THEMES_PATH"fallback.css"); +} + +/** + * hdy_style_init: + * + * Initializes the style classes. This must be called once GTK has been + * initialized. + * + * Since: 1.0 + */ +static void +hdy_style_init (void) +{ + static volatile gsize guard = 0; + g_autoptr (GtkCssProvider) css_provider = NULL; + GtkSettings *settings; + + if (!g_once_init_enter (&guard)) + return; + + css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (css_provider), + HDY_STYLE_PROVIDER_PRIORITY_OVERRIDE); + + settings = gtk_settings_get_default (); + g_signal_connect_swapped (settings, + "notify::gtk-theme-name", + G_CALLBACK (hdy_themes_update), + css_provider); + g_signal_connect_swapped (settings, + "notify::gtk-application-prefer-dark-theme", + G_CALLBACK (hdy_themes_update), + css_provider); + + hdy_themes_update (css_provider); + + load_fallback_style (); + + g_once_init_leave (&guard, 1); +} + +/** + * hdy_icons_init: + * + * Initializes the embedded icons. This must be called once GTK has been + * initialized. + * + * Since: 1.0 + */ +static void +hdy_icons_init (void) +{ + static volatile gsize guard = 0; + + if (!g_once_init_enter (&guard)) + return; + + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/sm/puri/handy/icons"); + + g_once_init_leave (&guard, 1); +} + +/** + * hdy_init: + * + * Call this function just after initializing GTK, if you are using + * #GtkApplication it means it must be called when the #GApplication::startup + * signal is emitted. If libhandy has already been initialized, the function + * will simply return. + * + * This makes sure translations, types, themes, and icons for the Handy library + * are set up properly. + */ +void +hdy_init (void) +{ + if (hdy_initialized) + return; + + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + hdy_init_public_types (); + + hdy_style_init (); + hdy_icons_init (); + + hdy_initialized = TRUE; +} diff --git a/subprojects/libhandy/src/hdy-main.h b/subprojects/libhandy/src/hdy-main.h new file mode 100644 index 0000000..f960a69 --- /dev/null +++ b/subprojects/libhandy/src/hdy-main.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <glib.h> + +G_BEGIN_DECLS + +HDY_AVAILABLE_IN_ALL +void hdy_init (void); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-navigation-direction.c b/subprojects/libhandy/src/hdy-navigation-direction.c new file mode 100644 index 0000000..b4a2d23 --- /dev/null +++ b/subprojects/libhandy/src/hdy-navigation-direction.c @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include "hdy-navigation-direction.h" + +/** + * SECTION:hdy-navigation-direction + * @short_description: Swipe navigation directions. + * @title: HdyNavigationDirection + * @See_also: #HdyDeck, #HdyLeaflet + */ + +/** + * HdyNavigationDirection: + * @HDY_NAVIGATION_DIRECTION_BACK: Corresponds to start or top, depending on orientation and text direction + * @HDY_NAVIGATION_DIRECTION_FORWARD: Corresponds to end or bottom, depending on orientation and text direction + * + * Represents direction of a swipe navigation gesture in #HdyDeck and + * #HdyLeaflet. + * + * Since: 1.0 + */ diff --git a/subprojects/libhandy/src/hdy-navigation-direction.h b/subprojects/libhandy/src/hdy-navigation-direction.h new file mode 100644 index 0000000..ea63ef5 --- /dev/null +++ b/subprojects/libhandy/src/hdy-navigation-direction.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <glib-object.h> +#include "hdy-enums.h" + +G_BEGIN_DECLS + +typedef enum { + HDY_NAVIGATION_DIRECTION_BACK, + HDY_NAVIGATION_DIRECTION_FORWARD, +} HdyNavigationDirection; + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-nothing-private.h b/subprojects/libhandy/src/hdy-nothing-private.h new file mode 100644 index 0000000..19d35c9 --- /dev/null +++ b/subprojects/libhandy/src/hdy-nothing-private.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_NOTHING (hdy_nothing_get_type()) + +G_DECLARE_FINAL_TYPE (HdyNothing, hdy_nothing, HDY, NOTHING, GtkWidget) + +GtkWidget *hdy_nothing_new (void); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-nothing.c b/subprojects/libhandy/src/hdy-nothing.c new file mode 100644 index 0000000..036537a --- /dev/null +++ b/subprojects/libhandy/src/hdy-nothing.c @@ -0,0 +1,47 @@ +#include "hdy-nothing-private.h" + +/** + * PRIVATE:hdy-nothing + * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow + * @title: HdyNothing + * @See_also: #HdyApplicationWindow, #HdyWindow, #HdyWindowMixin + * @stability: Private + * + * The HdyNothing widget does nothing. It's used as the titlebar for + * #HdyWindow and #HdyApplicationWindow. + * + * Since: 1.0 + */ + +struct _HdyNothing +{ + GtkWidget parent_instance; +}; + +G_DEFINE_TYPE (HdyNothing, hdy_nothing, GTK_TYPE_WIDGET) + +static void +hdy_nothing_class_init (HdyNothingClass *klass) +{ +} + +static void +hdy_nothing_init (HdyNothing *self) +{ +} + +/** + * hdy_nothing_new: + * + * Creates a new #HdyNothing. + * + * Returns: (transfer full): a newly created #HdyNothing + * + * Since: 1.0 + */ +GtkWidget * +hdy_nothing_new (void) +{ + return g_object_new (HDY_TYPE_NOTHING, NULL); +} + diff --git a/subprojects/libhandy/src/hdy-preferences-group-private.h b/subprojects/libhandy/src/hdy-preferences-group-private.h new file mode 100644 index 0000000..731e2fa --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-group-private.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include "hdy-preferences-group.h" + +G_BEGIN_DECLS + +void hdy_preferences_group_add_preferences_to_model (HdyPreferencesGroup *self, + GListStore *model); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-preferences-group.c b/subprojects/libhandy/src/hdy-preferences-group.c new file mode 100644 index 0000000..8000627 --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-group.c @@ -0,0 +1,449 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-preferences-group-private.h" + +#include "hdy-preferences-row.h" + +/** + * SECTION:hdy-preferences-group + * @short_description: A group gathering preferences rows. + * @Title: HdyPreferencesGroup + * + * A #HdyPreferencesGroup represents a group or tightly related preferences, + * which in turn are represented by HdyPreferencesRow. + * + * To summarize the role of the preferences it gathers, a group can have both a + * title and a description. The title will be used by #HdyPreferencesWindow to + * let the user look for a preference. + * + * # CSS nodes + * + * #HdyPreferencesGroup has a single CSS node with name preferencesgroup. + * + * Since: 0.0.10 + */ + +typedef struct +{ + GtkBox *box; + GtkLabel *description; + GtkListBox *listbox; + GtkBox *listbox_box; + GtkLabel *title; +} HdyPreferencesGroupPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesGroup, hdy_preferences_group, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_DESCRIPTION, + PROP_TITLE, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +static void +update_title_visibility (HdyPreferencesGroup *self) +{ + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + + /* Show the listbox only if it has children to avoid having showing the + * listbox as an empty frame, parasiting the look of non-GtkListBoxRow + * children. + */ + gtk_widget_set_visible (GTK_WIDGET (priv->title), + gtk_label_get_text (priv->title) != NULL && + g_strcmp0 (gtk_label_get_text (priv->title), "") != 0); +} + +static void +update_description_visibility (HdyPreferencesGroup *self) +{ + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + + gtk_widget_set_visible (GTK_WIDGET (priv->description), + gtk_label_get_text (priv->description) != NULL && + g_strcmp0 (gtk_label_get_text (priv->description), "") != 0); +} + +static void +update_listbox_visibility (HdyPreferencesGroup *self) +{ + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + g_autoptr(GList) children = NULL; + + /* We must wait until listob has been built and added. */ + if (priv->listbox == NULL) + return; + + children = gtk_container_get_children (GTK_CONTAINER (priv->listbox)); + + gtk_widget_set_visible (GTK_WIDGET (priv->listbox), children != NULL); +} + +typedef struct { + HdyPreferencesGroup *group; + GtkCallback callback; + gpointer callback_data; +} ForallData; + +static void +for_non_internal_child (GtkWidget *widget, + gpointer callback_data) +{ + ForallData *data = callback_data; + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (data->group); + + if (widget != (GtkWidget *) priv->listbox) + data->callback (widget, data->callback_data); +} + +static void +hdy_preferences_group_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container); + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + ForallData data; + + if (include_internals) { + GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data); + + return; + } + + data.group = self; + data.callback = callback; + data.callback_data = callback_data; + + if (priv->listbox) + GTK_CONTAINER_GET_CLASS (priv->listbox)->forall (GTK_CONTAINER (priv->listbox), include_internals, callback, callback_data); + if (priv->listbox_box) + GTK_CONTAINER_GET_CLASS (priv->listbox_box)->forall (GTK_CONTAINER (priv->listbox_box), include_internals, for_non_internal_child, &data); +} + +static void +hdy_preferences_group_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object); + + switch (prop_id) { + case PROP_DESCRIPTION: + g_value_set_string (value, hdy_preferences_group_get_description (self)); + break; + case PROP_TITLE: + g_value_set_string (value, hdy_preferences_group_get_title (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_group_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object); + + switch (prop_id) { + case PROP_DESCRIPTION: + hdy_preferences_group_set_description (self, g_value_get_string (value)); + break; + case PROP_TITLE: + hdy_preferences_group_set_title (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_group_dispose (GObject *object) +{ + HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object); + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + + /* + * Since we overload forall(), the inherited destroy() won't work as normal. + * Remove internal widgets ourself. + */ + g_clear_pointer ((GtkWidget **) &priv->description, gtk_widget_destroy); + g_clear_pointer ((GtkWidget **) &priv->listbox, gtk_widget_destroy); + g_clear_pointer ((GtkWidget **) &priv->listbox_box, gtk_widget_destroy); + g_clear_pointer ((GtkWidget **) &priv->title, gtk_widget_destroy); + + G_OBJECT_CLASS (hdy_preferences_group_parent_class)->dispose (object); +} + +static void +hdy_preferences_group_add (GtkContainer *container, + GtkWidget *child) +{ + HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container); + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + + if (priv->title == NULL || priv->description == NULL || priv->listbox_box == NULL) { + GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->add (container, child); + + return; + } + + if (HDY_IS_PREFERENCES_ROW (child)) + gtk_container_add (GTK_CONTAINER (priv->listbox), child); + else + gtk_container_add (GTK_CONTAINER (priv->listbox_box), child); +} + +static void +hdy_preferences_group_remove (GtkContainer *container, + GtkWidget *child) +{ + HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container); + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + + if (child == GTK_WIDGET (priv->box)) + GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->remove (container, child); + else if (HDY_IS_PREFERENCES_ROW (child)) + gtk_container_remove (GTK_CONTAINER (priv->listbox), child); + else if (child != GTK_WIDGET (priv->listbox)) + gtk_container_remove (GTK_CONTAINER (priv->listbox_box), child); +} + +static void +hdy_preferences_group_class_init (HdyPreferencesGroupClass *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 = hdy_preferences_group_get_property; + object_class->set_property = hdy_preferences_group_set_property; + object_class->dispose = hdy_preferences_group_dispose; + + container_class->add = hdy_preferences_group_add; + container_class->remove = hdy_preferences_group_remove; + container_class->forall = hdy_preferences_group_forall; + + /** + * HdyPreferencesGroup:description: + * + * The description for this group of preferences. + * + * Since: 0.0.10 + */ + props[PROP_DESCRIPTION] = + g_param_spec_string ("description", + _("Description"), + _("Description"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * HdyPreferencesGroup:title: + * + * The title for this group of preferences. + * + * Since: 0.0.10 + */ + props[PROP_TITLE] = + g_param_spec_string ("title", + _("Title"), + _("Title"), + "", + 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, "preferencesgroup"); + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-preferences-group.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, box); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, description); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, listbox); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, listbox_box); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, title); + gtk_widget_class_bind_template_callback (widget_class, update_listbox_visibility); +} + +static void +hdy_preferences_group_init (HdyPreferencesGroup *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + update_description_visibility (self); + update_title_visibility (self); + update_listbox_visibility (self); +} + +/** + * hdy_preferences_group_new: + * + * Creates a new #HdyPreferencesGroup. + * + * Returns: a new #HdyPreferencesGroup + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_preferences_group_new (void) +{ + return g_object_new (HDY_TYPE_PREFERENCES_GROUP, NULL); +} + +/** + * hdy_preferences_group_get_title: + * @self: a #HdyPreferencesGroup + * + * Gets the title of @self. + * + * Returns: the title of @self. + * + * Since: 0.0.10 + */ +const gchar * +hdy_preferences_group_get_title (HdyPreferencesGroup *self) +{ + HdyPreferencesGroupPrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_GROUP (self), NULL); + + priv = hdy_preferences_group_get_instance_private (self); + + return gtk_label_get_text (priv->title); +} + +/** + * hdy_preferences_group_set_title: + * @self: a #HdyPreferencesGroup + * @title: the title + * + * Sets the title for @self. + * + * Since: 0.0.10 + */ +void +hdy_preferences_group_set_title (HdyPreferencesGroup *self, + const gchar *title) +{ + HdyPreferencesGroupPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self)); + + priv = hdy_preferences_group_get_instance_private (self); + + if (g_strcmp0 (gtk_label_get_label (priv->title), title) == 0) + return; + + gtk_label_set_label (priv->title, title); + update_title_visibility (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +/** + * hdy_preferences_group_get_description: + * @self: a #HdyPreferencesGroup + * + * + * Returns: the description of @self. + * + * Since: 0.0.10 + */ +const gchar * +hdy_preferences_group_get_description (HdyPreferencesGroup *self) +{ + HdyPreferencesGroupPrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_GROUP (self), NULL); + + priv = hdy_preferences_group_get_instance_private (self); + + return gtk_label_get_text (priv->description); +} + +/** + * hdy_preferences_group_set_description: + * @self: a #HdyPreferencesGroup + * @description: the description + * + * Sets the description for @self. + * + * Since: 0.0.10 + */ +void +hdy_preferences_group_set_description (HdyPreferencesGroup *self, + const gchar *description) +{ + HdyPreferencesGroupPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self)); + + priv = hdy_preferences_group_get_instance_private (self); + + if (g_strcmp0 (gtk_label_get_label (priv->description), description) == 0) + return; + + gtk_label_set_label (priv->description, description); + update_description_visibility (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESCRIPTION]); +} + +static void +add_preferences_to_model (HdyPreferencesRow *row, + GListStore *model) +{ + const gchar *title; + + g_assert (HDY_IS_PREFERENCES_ROW (row)); + g_assert (G_IS_LIST_STORE (model)); + + if (!gtk_widget_get_visible (GTK_WIDGET (row))) + return; + + title = hdy_preferences_row_get_title (row); + + if (!title || !*title) + return; + + g_list_store_append (model, row); +} + +/** + * hdy_preferences_group_add_preferences_to_model: (skip) + * @self: a #HdyPreferencesGroup + * @model: the model + * + * Add preferences from @self to the model. + * + * Since: 0.0.10 + */ +void +hdy_preferences_group_add_preferences_to_model (HdyPreferencesGroup *self, + GListStore *model) +{ + HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self); + + g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self)); + g_return_if_fail (G_IS_LIST_STORE (model)); + + if (!gtk_widget_get_visible (GTK_WIDGET (self))) + return; + + gtk_container_foreach (GTK_CONTAINER (priv->listbox), (GtkCallback) add_preferences_to_model, model); +} diff --git a/subprojects/libhandy/src/hdy-preferences-group.h b/subprojects/libhandy/src/hdy-preferences-group.h new file mode 100644 index 0000000..2a7952c --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-group.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_PREFERENCES_GROUP (hdy_preferences_group_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyPreferencesGroup, hdy_preferences_group, HDY, PREFERENCES_GROUP, GtkBin) + +/** + * HdyPreferencesGroupClass + * @parent_class: The parent class + */ +struct _HdyPreferencesGroupClass +{ + GtkBinClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_preferences_group_new (void); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_preferences_group_get_title (HdyPreferencesGroup *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_group_set_title (HdyPreferencesGroup *self, + const gchar *title); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_preferences_group_get_description (HdyPreferencesGroup *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_group_set_description (HdyPreferencesGroup *self, + const gchar *description); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-preferences-group.ui b/subprojects/libhandy/src/hdy-preferences-group.ui new file mode 100644 index 0000000..60a5ae1 --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-group.ui @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyPreferencesGroup" parent="GtkBin"> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="title"> + <property name="can_focus">False</property> + <property name="ellipsize">end</property> + <property name="halign">start</property> + <property name="xalign">0</property> + <style> + <!-- Requires Adwaita from GTK 3.24.14. --> + <class name="heading"/> + <!-- Matching elementary class. --> + <class name="h4"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="description"> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="wrap">True</property> + <property name="xalign">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox" id="listbox_box"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <child> + <object class="GtkListBox" id="listbox"> + <property name="selection_mode">none</property> + <property name="visible">True</property> + <signal name="add" handler="update_listbox_visibility" after="yes" swapped="yes"/> + <signal name="remove" handler="update_listbox_visibility" after="yes" swapped="yes"/> + <style> + <class name="content"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-preferences-page-private.h b/subprojects/libhandy/src/hdy-preferences-page-private.h new file mode 100644 index 0000000..a93ccfd --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-page-private.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include "hdy-preferences-page.h" + +G_BEGIN_DECLS + +GtkAdjustment *hdy_preferences_page_get_vadjustment (HdyPreferencesPage *self); + +void hdy_preferences_page_add_preferences_to_model (HdyPreferencesPage *self, + GListStore *model); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-preferences-page.c b/subprojects/libhandy/src/hdy-preferences-page.c new file mode 100644 index 0000000..51fdc0d --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-page.c @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-preferences-page-private.h" + +#include "hdy-preferences-group-private.h" + +/** + * SECTION:hdy-preferences-page + * @short_description: A page from the preferences window. + * @Title: HdyPreferencesPage + * + * The #HdyPreferencesPage widget gathers preferences groups into a single page + * of a preferences window. + * + * # CSS nodes + * + * #HdyPreferencesPage has a single CSS node with name preferencespage. + * + * Since: 0.0.10 + */ + +typedef struct +{ + GtkBox *box; + GtkScrolledWindow *scrolled_window; + + gchar *icon_name; + gchar *title; +} HdyPreferencesPagePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesPage, hdy_preferences_page, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_TITLE, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +typedef struct { + HdyPreferencesPage *preferences_page; + GtkCallback callback; + gpointer data; +} CallbackData; + +static void +hdy_preferences_page_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object); + + switch (prop_id) { + case PROP_ICON_NAME: + g_value_set_string (value, hdy_preferences_page_get_icon_name (self)); + break; + case PROP_TITLE: + g_value_set_string (value, hdy_preferences_page_get_title (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_page_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object); + + switch (prop_id) { + case PROP_ICON_NAME: + hdy_preferences_page_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_TITLE: + hdy_preferences_page_set_title (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_page_finalize (GObject *object) +{ + HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object); + HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self); + + g_clear_pointer (&priv->icon_name, g_free); + g_clear_pointer (&priv->title, g_free); + + G_OBJECT_CLASS (hdy_preferences_page_parent_class)->finalize (object); +} + +static void +hdy_preferences_page_add (GtkContainer *container, + GtkWidget *child) +{ + HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container); + HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self); + + if (priv->scrolled_window == NULL) + GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->add (container, child); + else if (HDY_IS_PREFERENCES_GROUP (child)) + gtk_container_add (GTK_CONTAINER (priv->box), child); + else + g_warning ("Can't add children of type %s to %s", + G_OBJECT_TYPE_NAME (child), + G_OBJECT_TYPE_NAME (container)); +} + +static void +hdy_preferences_page_remove (GtkContainer *container, + GtkWidget *child) +{ + HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container); + HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self); + + if (child == GTK_WIDGET (priv->scrolled_window)) + GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->remove (container, child); + else + gtk_container_remove (GTK_CONTAINER (priv->box), child); +} + +static void +hdy_preferences_page_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container); + HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self); + + if (include_internals) + GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->forall (container, + include_internals, + callback, + callback_data); + else if (priv->box) + gtk_container_foreach (GTK_CONTAINER (priv->box), callback, callback_data); +} + +static void +hdy_preferences_page_class_init (HdyPreferencesPageClass *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 = hdy_preferences_page_get_property; + object_class->set_property = hdy_preferences_page_set_property; + object_class->finalize = hdy_preferences_page_finalize; + + container_class->add = hdy_preferences_page_add; + container_class->remove = hdy_preferences_page_remove; + container_class->forall = hdy_preferences_page_forall; + + /** + * HdyPreferencesPage:icon-name: + * + * The icon name for this page of preferences. + * + * Since: 0.0.10 + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + _("Icon name"), + _("Icon name"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyPreferencesPage:title: + * + * The title for this page of preferences. + * + * Since: 0.0.10 + */ + props[PROP_TITLE] = + g_param_spec_string ("title", + _("Title"), + _("Title"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-preferences-page.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesPage, box); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesPage, scrolled_window); + + gtk_widget_class_set_css_name (widget_class, "preferencespage"); +} + +static void +hdy_preferences_page_init (HdyPreferencesPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +/** + * hdy_preferences_page_new: + * + * Creates a new #HdyPreferencesPage. + * + * Returns: a new #HdyPreferencesPage + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_preferences_page_new (void) +{ + return g_object_new (HDY_TYPE_PREFERENCES_PAGE, NULL); +} + +/** + * hdy_preferences_page_get_icon_name: + * @self: a #HdyPreferencesPage + * + * Gets the icon name for @self, or %NULL. + * + * Returns: (transfer none) (nullable): the icon name for @self, or %NULL. + * + * Since: 0.0.10 + */ +const gchar * +hdy_preferences_page_get_icon_name (HdyPreferencesPage *self) +{ + HdyPreferencesPagePrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL); + + priv = hdy_preferences_page_get_instance_private (self); + + return priv->icon_name; +} + +/** + * hdy_preferences_page_set_icon_name: + * @self: a #HdyPreferencesPage + * @icon_name: (nullable): the icon name, or %NULL + * + * Sets the icon name for @self. + * + * Since: 0.0.10 + */ +void +hdy_preferences_page_set_icon_name (HdyPreferencesPage *self, + const gchar *icon_name) +{ + HdyPreferencesPagePrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self)); + + priv = hdy_preferences_page_get_instance_private (self); + + if (g_strcmp0 (priv->icon_name, icon_name) == 0) + return; + + g_clear_pointer (&priv->icon_name, g_free); + priv->icon_name = g_strdup (icon_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + +/** + * hdy_preferences_page_get_title: + * @self: a #HdyPreferencesPage + * + * Gets the title of @self, or %NULL. + * + * Returns: (transfer none) (nullable): the title of the @self, or %NULL. + * + * Since: 0.0.10 + */ +const gchar * +hdy_preferences_page_get_title (HdyPreferencesPage *self) +{ + HdyPreferencesPagePrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL); + + priv = hdy_preferences_page_get_instance_private (self); + + return priv->title; +} + +/** + * hdy_preferences_page_set_title: + * @self: a #HdyPreferencesPage + * @title: (nullable): the title of the page, or %NULL + * + * Sets the title of @self. + * + * Since: 0.0.10 + */ +void +hdy_preferences_page_set_title (HdyPreferencesPage *self, + const gchar *title) +{ + HdyPreferencesPagePrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self)); + + priv = hdy_preferences_page_get_instance_private (self); + + if (g_strcmp0 (priv->title, title) == 0) + return; + + g_clear_pointer (&priv->title, g_free); + priv->title = g_strdup (title); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +GtkAdjustment * +hdy_preferences_page_get_vadjustment (HdyPreferencesPage *self) +{ + HdyPreferencesPagePrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL); + + priv = hdy_preferences_page_get_instance_private (self); + + return gtk_scrolled_window_get_vadjustment (priv->scrolled_window); +} + +/** + * hdy_preferences_page_add_preferences_to_model: (skip) + * @self: a #HdyPreferencesPage + * @model: the model + * + * Add preferences from @self to the model. + * + * Since: 0.0.10 + */ +void +hdy_preferences_page_add_preferences_to_model (HdyPreferencesPage *self, + GListStore *model) +{ + HdyPreferencesPagePrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self)); + g_return_if_fail (G_IS_LIST_STORE (model)); + + if (!gtk_widget_get_visible (GTK_WIDGET (self))) + return; + + priv = hdy_preferences_page_get_instance_private (self); + + gtk_container_foreach (GTK_CONTAINER (priv->box), (GtkCallback) hdy_preferences_group_add_preferences_to_model, model); +} diff --git a/subprojects/libhandy/src/hdy-preferences-page.h b/subprojects/libhandy/src/hdy-preferences-page.h new file mode 100644 index 0000000..158c18c --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-page.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_PREFERENCES_PAGE (hdy_preferences_page_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyPreferencesPage, hdy_preferences_page, HDY, PREFERENCES_PAGE, GtkBin) + +/** + * HdyPreferencesPageClass + * @parent_class: The parent class + */ +struct _HdyPreferencesPageClass +{ + GtkBinClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_preferences_page_new (void); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_preferences_page_get_icon_name (HdyPreferencesPage *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_page_set_icon_name (HdyPreferencesPage *self, + const gchar *icon_name); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_preferences_page_get_title (HdyPreferencesPage *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_page_set_title (HdyPreferencesPage *self, + const gchar *title); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-preferences-page.ui b/subprojects/libhandy/src/hdy-preferences-page.ui new file mode 100644 index 0000000..809dee7 --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-page.ui @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyPreferencesPage" parent="GtkBin"> + <child> + <object class="GtkScrolledWindow" id="scrolled_window"> + <property name="visible">True</property> + <property name="hscrollbar-policy">never</property> + <child> + <object class="GtkViewport"> + <property name="shadow-type">none</property> + <property name="visible">True</property> + <child> + <object class="HdyClamp"> + <property name="margin-bottom">18</property> + <property name="margin-end">12</property> + <property name="margin-start">12</property> + <property name="margin-top">18</property> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box"> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-preferences-row.c b/subprojects/libhandy/src/hdy-preferences-row.c new file mode 100644 index 0000000..9327509 --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-row.c @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-preferences-row.h" + +/** + * SECTION:hdy-preferences-row + * @short_description: A #GtkListBox row used to present preferences. + * @Title: HdyPreferencesRow + * + * The #HdyPreferencesRow widget has a title that #HdyPreferencesWindow will use + * to let the user look for a preference. It doesn't present the title in any + * way and it lets you present the preference as you please. + * + * #HdyActionRow and its derivatives are convenient to use as preference rows as + * they take care of presenting the preference's title while letting you compose + * the inputs of the preference around it. + * + * Since: 0.0.10 + */ + +typedef struct +{ + gchar *title; + + gboolean use_underline; +} HdyPreferencesRowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesRow, hdy_preferences_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + PROP_0, + PROP_TITLE, + PROP_USE_UNDERLINE, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +static void +hdy_preferences_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, hdy_preferences_row_get_title (self)); + break; + case PROP_USE_UNDERLINE: + g_value_set_boolean (value, hdy_preferences_row_get_use_underline (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object); + + switch (prop_id) { + case PROP_TITLE: + hdy_preferences_row_set_title (self, g_value_get_string (value)); + break; + case PROP_USE_UNDERLINE: + hdy_preferences_row_set_use_underline (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_row_finalize (GObject *object) +{ + HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object); + HdyPreferencesRowPrivate *priv = hdy_preferences_row_get_instance_private (self); + + g_free (priv->title); + + G_OBJECT_CLASS (hdy_preferences_row_parent_class)->finalize (object); +} + +static void +hdy_preferences_row_class_init (HdyPreferencesRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = hdy_preferences_row_get_property; + object_class->set_property = hdy_preferences_row_set_property; + object_class->finalize = hdy_preferences_row_finalize; + + /** + * HdyPreferencesRow:title: + * + * The title of the preference represented by this row. + * + * Since: 0.0.10 + */ + props[PROP_TITLE] = + g_param_spec_string ("title", + _("Title"), + _("The title of the preference"), + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyPreferencesRow:use-underline: + * + * Whether an embedded underline in the text of the title indicates a + * mnemonic. + * + * Since: 0.0.10 + */ + props[PROP_USE_UNDERLINE] = + g_param_spec_boolean ("use-underline", + _("Use underline"), + _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +static void +hdy_preferences_row_init (HdyPreferencesRow *self) +{ +} + +/** + * hdy_preferences_row_new: + * + * Creates a new #HdyPreferencesRow. + * + * Returns: a new #HdyPreferencesRow + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_preferences_row_new (void) +{ + return g_object_new (HDY_TYPE_PREFERENCES_ROW, NULL); +} + +/** + * hdy_preferences_row_get_title: + * @self: a #HdyPreferencesRow + * + * Gets the title of the preference represented by @self. + * + * Returns: (transfer none) (nullable): the title of the preference represented + * by @self, or %NULL. + * + * Since: 0.0.10 + */ +const gchar * +hdy_preferences_row_get_title (HdyPreferencesRow *self) +{ + HdyPreferencesRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_ROW (self), NULL); + + priv = hdy_preferences_row_get_instance_private (self); + + return priv->title; +} + +/** + * hdy_preferences_row_set_title: + * @self: a #HdyPreferencesRow + * @title: (nullable): the title, or %NULL. + * + * Sets the title of the preference represented by @self. + * + * Since: 0.0.10 + */ +void +hdy_preferences_row_set_title (HdyPreferencesRow *self, + const gchar *title) +{ + HdyPreferencesRowPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_ROW (self)); + + priv = hdy_preferences_row_get_instance_private (self); + + if (g_strcmp0 (priv->title, title) == 0) + return; + + g_free (priv->title); + priv->title = g_strdup (title); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +/** + * hdy_preferences_row_get_use_underline: + * @self: a #HdyPreferencesRow + * + * Gets whether an embedded underline in the text of the title indicates a + * mnemonic. See hdy_preferences_row_set_use_underline(). + * + * Returns: %TRUE if an embedded underline in the title indicates the mnemonic + * accelerator keys. + * + * Since: 0.0.10 + */ +gboolean +hdy_preferences_row_get_use_underline (HdyPreferencesRow *self) +{ + HdyPreferencesRowPrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_ROW (self), FALSE); + + priv = hdy_preferences_row_get_instance_private (self); + + return priv->use_underline; +} + +/** + * hdy_preferences_row_set_use_underline: + * @self: a #HdyPreferencesRow + * @use_underline: %TRUE if underlines in the text indicate mnemonics + * + * If true, an underline in the text of the title indicates the next character + * should be used for the mnemonic accelerator key. + * + * Since: 0.0.10 + */ +void +hdy_preferences_row_set_use_underline (HdyPreferencesRow *self, + gboolean use_underline) +{ + HdyPreferencesRowPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_ROW (self)); + + priv = hdy_preferences_row_get_instance_private (self); + + if (priv->use_underline == !!use_underline) + return; + + priv->use_underline = !!use_underline; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_UNDERLINE]); +} diff --git a/subprojects/libhandy/src/hdy-preferences-row.h b/subprojects/libhandy/src/hdy-preferences-row.h new file mode 100644 index 0000000..f5e926b --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-row.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_PREFERENCES_ROW (hdy_preferences_row_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyPreferencesRow, hdy_preferences_row, HDY, PREFERENCES_ROW, GtkListBoxRow) + +/** + * HdyPreferencesRowClass + * @parent_class: The parent class + */ +struct _HdyPreferencesRowClass +{ + GtkListBoxRowClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_preferences_row_new (void); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_preferences_row_get_title (HdyPreferencesRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_row_set_title (HdyPreferencesRow *self, + const gchar *title); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_preferences_row_get_use_underline (HdyPreferencesRow *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_row_set_use_underline (HdyPreferencesRow *self, + gboolean use_underline); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-preferences-window.c b/subprojects/libhandy/src/hdy-preferences-window.c new file mode 100644 index 0000000..3618d58 --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-window.c @@ -0,0 +1,721 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-preferences-window.h" + +#include "hdy-animation.h" +#include "hdy-action-row.h" +#include "hdy-deck.h" +#include "hdy-preferences-group-private.h" +#include "hdy-preferences-page-private.h" +#include "hdy-view-switcher.h" +#include "hdy-view-switcher-bar.h" +#include "hdy-view-switcher-title.h" + +/** + * SECTION:hdy-preferences-window + * @short_description: A window to present an application's preferences. + * @Title: HdyPreferencesWindow + * + * The #HdyPreferencesWindow widget presents an application's preferences + * gathered into pages and groups. The preferences are searchable by the user. + * + * Since: 0.0.10 + */ + +typedef struct +{ + HdyDeck *subpages_deck; + GtkWidget *preferences; + GtkStack *content_stack; + GtkStack *pages_stack; + GtkToggleButton *search_button; + GtkSearchEntry *search_entry; + GtkListBox *search_results; + GtkStack *search_stack; + GtkStack *title_stack; + HdyViewSwitcherBar *view_switcher_bar; + HdyViewSwitcherTitle *view_switcher_title; + + gboolean search_enabled; + gboolean can_swipe_back; + gint n_last_search_results; + GtkWidget *subpage; +} HdyPreferencesWindowPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesWindow, hdy_preferences_window, HDY_TYPE_WINDOW) + +enum { + PROP_0, + PROP_SEARCH_ENABLED, + PROP_CAN_SWIPE_BACK, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + +static gboolean +filter_search_results (HdyActionRow *row, + HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + g_autofree gchar *text = g_utf8_casefold (gtk_entry_get_text (GTK_ENTRY (priv->search_entry)), -1); + g_autofree gchar *title = g_utf8_casefold (hdy_preferences_row_get_title (HDY_PREFERENCES_ROW (row)), -1); + g_autofree gchar *subtitle = NULL; + + /* The CSS engine works in such a way that invisible children are treated as + * visible widgets, which breaks the expectations of the .preferences style + * class when filtering a row, leading to straight corners when the first row + * or last row are filtered out. + * + * This works around it by explicitly toggling the row's visibility, while + * keeping GtkListBox's filtering logic. + * + * See https://gitlab.gnome.org/GNOME/libhandy/-/merge_requests/424 + */ + + if (strstr (title, text)) { + priv->n_last_search_results++; + gtk_widget_show (GTK_WIDGET (row)); + + return TRUE; + } + + subtitle = g_utf8_casefold (hdy_action_row_get_subtitle (row), -1); + + if (!!strstr (subtitle, text)) { + priv->n_last_search_results++; + gtk_widget_show (GTK_WIDGET (row)); + + return TRUE; + } + + gtk_widget_hide (GTK_WIDGET (row)); + + return FALSE; +} + +static GtkWidget * +new_search_row_for_preference (HdyPreferencesRow *row, + HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + HdyActionRow *widget; + HdyPreferencesGroup *group; + HdyPreferencesPage *page; + const gchar *group_title, *page_title; + GtkWidget *parent; + + g_assert (HDY_IS_PREFERENCES_ROW (row)); + + widget = HDY_ACTION_ROW (hdy_action_row_new ()); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (widget), TRUE); + g_object_bind_property (row, "title", widget, "title", G_BINDING_SYNC_CREATE); + g_object_bind_property (row, "use-underline", widget, "use-underline", G_BINDING_SYNC_CREATE); + + for (parent = gtk_widget_get_parent (GTK_WIDGET (row)); + parent != NULL && !HDY_IS_PREFERENCES_GROUP (parent); + parent = gtk_widget_get_parent (parent)); + group = parent != NULL ? HDY_PREFERENCES_GROUP (parent) : NULL; + group_title = group != NULL ? hdy_preferences_group_get_title (group) : NULL; + if (g_strcmp0 (group_title, "") == 0) + group_title = NULL; + + for (parent = gtk_widget_get_parent (GTK_WIDGET (group)); + parent != NULL && !HDY_IS_PREFERENCES_PAGE (parent); + parent = gtk_widget_get_parent (parent)); + page = parent != NULL ? HDY_PREFERENCES_PAGE (parent) : NULL; + page_title = page != NULL ? hdy_preferences_page_get_title (page) : NULL; + if (g_strcmp0 (page_title, "") == 0) + page_title = NULL; + + if (group_title && !hdy_view_switcher_title_get_title_visible (priv->view_switcher_title)) + hdy_action_row_set_subtitle (widget, group_title); + if (group_title) { + g_autofree gchar *subtitle = g_strdup_printf ("%s → %s", page_title != NULL ? page_title : _("Untitled page"), group_title); + hdy_action_row_set_subtitle (widget, subtitle); + } else if (page_title) + hdy_action_row_set_subtitle (widget, page_title); + + gtk_widget_show (GTK_WIDGET (widget)); + + g_object_set_data (G_OBJECT (widget), "page", page); + g_object_set_data (G_OBJECT (widget), "row", row); + + return GTK_WIDGET (widget); +} + +static void +update_search_results (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + g_autoptr (GListStore) model; + + model = g_list_store_new (HDY_TYPE_PREFERENCES_ROW); + gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), (GtkCallback) hdy_preferences_page_add_preferences_to_model, model); + gtk_container_foreach (GTK_CONTAINER (priv->search_results), (GtkCallback) gtk_widget_destroy, NULL); + for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++) + gtk_container_add (GTK_CONTAINER (priv->search_results), + new_search_row_for_preference ((HdyPreferencesRow *) g_list_model_get_item (G_LIST_MODEL (model), i), self)); +} + +static void +search_result_activated_cb (HdyPreferencesWindow *self, + HdyActionRow *widget) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + HdyPreferencesPage *page; + HdyPreferencesRow *row; + GtkAdjustment *adjustment; + GtkAllocation allocation; + gint y = 0; + + gtk_toggle_button_set_active (priv->search_button, FALSE); + page = HDY_PREFERENCES_PAGE (g_object_get_data (G_OBJECT (widget), "page")); + row = HDY_PREFERENCES_ROW (g_object_get_data (G_OBJECT (widget), "row")); + + g_assert (page != NULL); + g_assert (row != NULL); + + adjustment = hdy_preferences_page_get_vadjustment (page); + + g_assert (adjustment != NULL); + + gtk_stack_set_visible_child (priv->pages_stack, GTK_WIDGET (page)); + gtk_widget_set_can_focus (GTK_WIDGET (row), TRUE); + gtk_widget_grab_focus (GTK_WIDGET (row)); + + if (!gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (page), 0, 0, NULL, &y)) + return; + + gtk_container_set_focus_child (GTK_CONTAINER (page), GTK_WIDGET (row)); + y += gtk_adjustment_get_value (adjustment); + gtk_widget_get_allocation (GTK_WIDGET (row), &allocation); + gtk_adjustment_clamp_page (adjustment, y, y + allocation.height); +} + +static gboolean +key_press_event_cb (GtkWidget *sender, + GdkEvent *event, + HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + GdkModifierType default_modifiers = gtk_accelerator_get_default_mod_mask (); + guint keyval; + GdkModifierType state; + + if (priv->subpage) + return GDK_EVENT_PROPAGATE; + + gdk_event_get_keyval (event, &keyval); + gdk_event_get_state (event, &state); + + if (priv->search_enabled && + (keyval == GDK_KEY_f || keyval == GDK_KEY_F) && + (state & default_modifiers) == GDK_CONTROL_MASK) { + gtk_toggle_button_set_active (priv->search_button, TRUE); + + return GDK_EVENT_STOP; + } + + if (priv->search_enabled && + gtk_search_entry_handle_event (priv->search_entry, event)) { + gtk_toggle_button_set_active (priv->search_button, TRUE); + + return GDK_EVENT_STOP; + } + + return GDK_EVENT_PROPAGATE; +} + +static void +try_remove_subpages (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (hdy_deck_get_transition_running (priv->subpages_deck)) + return; + + if (hdy_deck_get_visible_child (priv->subpages_deck) == priv->preferences) + priv->subpage = NULL; + + for (GList *child = gtk_container_get_children (GTK_CONTAINER (priv->subpages_deck)); + child; + child = child->next) + if (child->data != priv->preferences && child->data != priv->subpage) + gtk_container_remove (GTK_CONTAINER (priv->subpages_deck), child->data); +} + +static void +subpages_deck_transition_running_cb (HdyPreferencesWindow *self) +{ + try_remove_subpages (self); +} + +static void +subpages_deck_visible_child_cb (HdyPreferencesWindow *self) +{ + try_remove_subpages (self); +} + +static void +header_bar_size_allocate_cb (HdyPreferencesWindow *self, + GdkRectangle *allocation) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + hdy_view_switcher_title_set_view_switcher_enabled (priv->view_switcher_title, allocation->width > 360); +} + +static void +title_stack_notify_transition_running_cb (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (gtk_stack_get_transition_running (priv->title_stack) || + gtk_stack_get_visible_child (priv->title_stack) != GTK_WIDGET (priv->view_switcher_title)) + return; + + gtk_entry_set_text (GTK_ENTRY (priv->search_entry), ""); +} + +static void +title_stack_notify_visible_child_cb (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (hdy_get_enable_animations (GTK_WIDGET (priv->title_stack)) || + gtk_stack_get_visible_child (priv->title_stack) != GTK_WIDGET (priv->view_switcher_title)) + return; + + gtk_entry_set_text (GTK_ENTRY (priv->search_entry), ""); +} + + +static void +search_button_notify_active_cb (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (gtk_toggle_button_get_active (priv->search_button)) { + update_search_results (self); + gtk_stack_set_visible_child_name (priv->title_stack, "search"); + gtk_stack_set_visible_child_name (priv->content_stack, "search"); + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->search_entry)); + /* Grabbing without selecting puts the cursor at the start of the buffer, so + * for "type to search" to work we must move the cursor at the end. We can't + * use GTK_MOVEMENT_BUFFER_ENDS because it causes a sound to be played. + */ + g_signal_emit_by_name (priv->search_entry, "move-cursor", + GTK_MOVEMENT_LOGICAL_POSITIONS, G_MAXINT, FALSE, NULL); + } else { + gtk_stack_set_visible_child_name (priv->title_stack, "pages"); + gtk_stack_set_visible_child_name (priv->content_stack, "pages"); + } +} + +static void +search_changed_cb (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + priv->n_last_search_results = 0; + gtk_list_box_invalidate_filter (priv->search_results); + gtk_stack_set_visible_child_name (priv->search_stack, + priv->n_last_search_results > 0 ? "results" : "no-results"); +} + +static void +on_page_icon_name_changed (HdyPreferencesPage *page, + GParamSpec *pspec, + HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page), + "icon-name", hdy_preferences_page_get_icon_name (page), + NULL); +} + +static void +stop_search_cb (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + gtk_toggle_button_set_active (priv->search_button, FALSE); +} + +static void +on_page_title_changed (HdyPreferencesPage *page, + GParamSpec *pspec, + HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page), + "title", hdy_preferences_page_get_title (page), + NULL); +} + +static void +hdy_preferences_window_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (object); + + switch (prop_id) { + case PROP_SEARCH_ENABLED: + g_value_set_boolean (value, hdy_preferences_window_get_search_enabled (self)); + break; + case PROP_CAN_SWIPE_BACK: + g_value_set_boolean (value, hdy_preferences_window_get_can_swipe_back (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_window_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (object); + + switch (prop_id) { + case PROP_SEARCH_ENABLED: + hdy_preferences_window_set_search_enabled (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_BACK: + hdy_preferences_window_set_can_swipe_back (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_preferences_window_add (GtkContainer *container, + GtkWidget *child) +{ + HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container); + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (priv->content_stack == NULL) + GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->add (container, child); + else if (HDY_IS_PREFERENCES_PAGE (child)) { + gtk_container_add (GTK_CONTAINER (priv->pages_stack), child); + on_page_icon_name_changed (HDY_PREFERENCES_PAGE (child), NULL, self); + on_page_title_changed (HDY_PREFERENCES_PAGE (child), NULL, self); + g_signal_connect (child, "notify::icon-name", + G_CALLBACK (on_page_icon_name_changed), self); + g_signal_connect (child, "notify::title", + G_CALLBACK (on_page_title_changed), self); + } else + g_warning ("Can't add children of type %s to %s", + G_OBJECT_TYPE_NAME (child), + G_OBJECT_TYPE_NAME (container)); +} + +static void +hdy_preferences_window_remove (GtkContainer *container, + GtkWidget *child) +{ + HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container); + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (child == GTK_WIDGET (priv->content_stack)) + GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->remove (container, child); + else + gtk_container_remove (GTK_CONTAINER (priv->pages_stack), child); +} + +static void +hdy_preferences_window_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container); + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + if (include_internals) + GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->forall (container, + include_internals, + callback, + callback_data); + else if (priv->pages_stack) + gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), callback, callback_data); +} + +static void +hdy_preferences_window_class_init (HdyPreferencesWindowClass *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 = hdy_preferences_window_get_property; + object_class->set_property = hdy_preferences_window_set_property; + + container_class->add = hdy_preferences_window_add; + container_class->remove = hdy_preferences_window_remove; + container_class->forall = hdy_preferences_window_forall; + + /** + * HdyPreferencesWindow:search-enabled: + * + * Whether search is enabled. + * + * Since: 1.0 + */ + props[PROP_SEARCH_ENABLED] = + g_param_spec_boolean ("search-enabled", + _("Search enabled"), + _("Whether search is enabled"), + TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyPreferencesWindow:can-swipe-back: + * + * Whether or not the window allows closing the subpage via a swipe gesture. + * + * Since: 1.0 + */ + props[PROP_CAN_SWIPE_BACK] = + g_param_spec_boolean ("can-swipe-back", + _("Can swipe back"), + _("Whether or not swipe gesture can be used to switch from a subpage to the preferences"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-preferences-window.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, subpages_deck); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, preferences); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, content_stack); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, pages_stack); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_button); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_entry); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_results); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_stack); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, title_stack); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_bar); + gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_title); + gtk_widget_class_bind_template_callback (widget_class, subpages_deck_transition_running_cb); + gtk_widget_class_bind_template_callback (widget_class, subpages_deck_visible_child_cb); + gtk_widget_class_bind_template_callback (widget_class, header_bar_size_allocate_cb); + gtk_widget_class_bind_template_callback (widget_class, title_stack_notify_transition_running_cb); + gtk_widget_class_bind_template_callback (widget_class, title_stack_notify_visible_child_cb); + gtk_widget_class_bind_template_callback (widget_class, key_press_event_cb); + gtk_widget_class_bind_template_callback (widget_class, search_button_notify_active_cb); + gtk_widget_class_bind_template_callback (widget_class, search_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, search_result_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, stop_search_cb); +} + +static void +hdy_preferences_window_init (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self); + + priv->search_enabled = TRUE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_filter_func (priv->search_results, (GtkListBoxFilterFunc) filter_search_results, self, NULL); +} + +/** + * hdy_preferences_window_new: + * + * Creates a new #HdyPreferencesWindow. + * + * Returns: a new #HdyPreferencesWindow + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_preferences_window_new (void) +{ + return g_object_new (HDY_TYPE_PREFERENCES_WINDOW, NULL); +} + +/** + * hdy_preferences_window_get_search_enabled: + * @self: a #HdyPreferencesWindow + * + * Gets whether search is enabled for @self. + * + * Returns: whether search is enabled for @self. + * + * Since: 1.0 + */ +gboolean +hdy_preferences_window_get_search_enabled (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_WINDOW (self), FALSE); + + priv = hdy_preferences_window_get_instance_private (self); + + return priv->search_enabled; +} + +/** + * hdy_preferences_window_set_search_enabled: + * @self: a #HdyPreferencesWindow + * @search_enabled: %TRUE to enable search, %FALSE to disable it + * + * Sets whether search is enabled for @self. + * + * Since: 1.0 + */ +void +hdy_preferences_window_set_search_enabled (HdyPreferencesWindow *self, + gboolean search_enabled) +{ + HdyPreferencesWindowPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self)); + + priv = hdy_preferences_window_get_instance_private (self); + + search_enabled = !!search_enabled; + + if (priv->search_enabled == search_enabled) + return; + + priv->search_enabled = search_enabled; + gtk_widget_set_visible (GTK_WIDGET (priv->search_button), search_enabled); + if (!search_enabled) + gtk_toggle_button_set_active (priv->search_button, FALSE); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_ENABLED]); +} + +/** + * hdy_preferences_window_set_can_swipe_back: + * @self: a #HdyPreferencesWindow + * @can_swipe_back: the new value + * + * Sets whether or not @self allows switching from a subpage to the preferences + * via a swipe gesture. + * + * Since: 1.0 + */ +void +hdy_preferences_window_set_can_swipe_back (HdyPreferencesWindow *self, + gboolean can_swipe_back) +{ + HdyPreferencesWindowPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self)); + + priv = hdy_preferences_window_get_instance_private (self); + + can_swipe_back = !!can_swipe_back; + + if (priv->can_swipe_back == can_swipe_back) + return; + + priv->can_swipe_back = can_swipe_back; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_BACK]); +} + +/** + * hdy_preferences_window_get_can_swipe_back + * @self: a #HdyPreferencesWindow + * + * Returns whether or not @self allows switching from a subpage to the + * preferences via a swipe gesture. + * + * Returns: %TRUE if back swipe is enabled. + * + * Since: 1.0 + */ +gboolean +hdy_preferences_window_get_can_swipe_back (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv; + + g_return_val_if_fail (HDY_IS_PREFERENCES_WINDOW (self), FALSE); + + priv = hdy_preferences_window_get_instance_private (self); + + return priv->can_swipe_back; +} + +/** + * hdy_preferences_window_present_subpage: + * @self: a #HdyPreferencesWindow + * @subpage: the subpage + * + * Sets @subpage as the window's subpage and present it. + * The transition can be cancelled by the user, in which case visible child will + * change back to the previously visible child. + * + * Since: 1.0 + */ +void +hdy_preferences_window_present_subpage (HdyPreferencesWindow *self, + GtkWidget *subpage) +{ + HdyPreferencesWindowPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self)); + g_return_if_fail (GTK_IS_WIDGET (subpage)); + + priv = hdy_preferences_window_get_instance_private (self); + + if (priv->subpage == subpage) + return; + + priv->subpage = subpage; + + /* The check below avoids a warning when re-entering a subpage during the + * transition between the that subpage to the preferences. + */ + if (gtk_widget_get_parent (subpage) != GTK_WIDGET (priv->subpages_deck)) + gtk_container_add (GTK_CONTAINER (priv->subpages_deck), subpage); + + hdy_deck_set_visible_child (priv->subpages_deck, subpage); +} + +/** + * hdy_preferences_window_close_subpage: + * @self: a #HdyPreferencesWindow + * + * Closes the current subpage to return back to the preferences, if there is no + * presented subpage, this does nothing. + * + * Since: 1.0 + */ +void +hdy_preferences_window_close_subpage (HdyPreferencesWindow *self) +{ + HdyPreferencesWindowPrivate *priv; + + g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self)); + + priv = hdy_preferences_window_get_instance_private (self); + + if (priv->subpage == NULL) + return; + + hdy_deck_set_visible_child (priv->subpages_deck, priv->preferences); +} diff --git a/subprojects/libhandy/src/hdy-preferences-window.h b/subprojects/libhandy/src/hdy-preferences-window.h new file mode 100644 index 0000000..427a94a --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-window.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-window.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_PREFERENCES_WINDOW (hdy_preferences_window_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyPreferencesWindow, hdy_preferences_window, HDY, PREFERENCES_WINDOW, HdyWindow) + +/** + * HdyPreferencesWindowClass + * @parent_class: The parent class + */ +struct _HdyPreferencesWindowClass +{ + HdyWindowClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_preferences_window_new (void); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_preferences_window_get_search_enabled (HdyPreferencesWindow *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_window_set_search_enabled (HdyPreferencesWindow *self, + gboolean search_enabled); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_preferences_window_get_can_swipe_back (HdyPreferencesWindow *self); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_window_set_can_swipe_back (HdyPreferencesWindow *self, + gboolean can_swipe_back); + +HDY_AVAILABLE_IN_ALL +void hdy_preferences_window_present_subpage (HdyPreferencesWindow *self, + GtkWidget *subpage); +HDY_AVAILABLE_IN_ALL +void hdy_preferences_window_close_subpage (HdyPreferencesWindow *self); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-preferences-window.ui b/subprojects/libhandy/src/hdy-preferences-window.ui new file mode 100644 index 0000000..5f764fc --- /dev/null +++ b/subprojects/libhandy/src/hdy-preferences-window.ui @@ -0,0 +1,248 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyPreferencesWindow" parent="HdyWindow"> + <property name="modal">True</property> + <property name="window_position">center</property> + <property name="destroy_with_parent">True</property> + <property name="icon_name">gtk-preferences</property> + <property name="title" translatable="yes">Preferences</property> + <property name="type_hint">dialog</property> + <property name="default-width">640</property> + <property name="default-height">576</property> + <signal name="key-press-event" handler="key_press_event_cb" after="yes" swapped="no"/> + <child> + <object class="HdyDeck" id="subpages_deck"> + <property name="can-swipe-back" bind-source="HdyPreferencesWindow" bind-property="can-swipe-back" bind-flags="sync-create"/> + <property name="visible">True</property> + <property name="width-request">360</property> + <signal name="notify::transition-running" handler="subpages_deck_transition_running_cb" swapped="yes"/> + <signal name="notify::visible-child" handler="subpages_deck_visible_child_cb" swapped="yes"/> + <child> + <object class="GtkBox" id="preferences"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="HdyHeaderBar"> + <property name="centering_policy">strict</property> + <property name="show_close_button">True</property> + <property name="visible">True</property> + <signal name="size-allocate" handler="header_bar_size_allocate_cb" swapped="yes"/> + <child type="title"> + <object class="GtkStack" id="title_stack"> + <property name="transition-type">crossfade</property> + <property name="visible">True</property> + <signal name="notify::visible-child" handler="title_stack_notify_visible_child_cb" swapped="true"/> + <signal name="notify::transition-running" handler="title_stack_notify_transition_running_cb" swapped="true"/> + <child> + <object class="HdyViewSwitcherTitle" id="view_switcher_title"> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="stack">pages_stack</property> + <property name="title" bind-source="HdyPreferencesWindow" bind-property="title" bind-flags="sync-create"/> + <property name="visible">True</property> + </object> + <packing> + <property name="name">pages</property> + </packing> + </child> + <child> + <object class="HdyClamp"> + <property name="tightening-threshold">300</property> + <property name="maximum-size">400</property> + <property name="visible">True</property> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="hexpand">True</property> + <property name="visible">True</property> + <signal name="search-changed" handler="search_changed_cb" swapped="yes"/> + <signal name="stop-search" handler="stop_search_cb" swapped="yes"/> + </object> + </child> + </object> + <packing> + <property name="name">search</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkToggleButton" id="search_button"> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="visible">True</property> + <signal name="notify::active" handler="search_button_notify_active_cb" swapped="yes"/> + <style> + <class name="image-button"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-search"> + <property name="accessible-name" translatable="yes">Search</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="can_focus">False</property> + <property name="icon_name">edit-find-symbolic</property> + <property name="icon_size">1</property> + <property name="visible">True</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="content_stack"> + <property name="transition-type">crossfade</property> + <property name="vhomogeneous">False</property> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <child> + <object class="GtkStack" id="pages_stack"> + <property name="transition-type">crossfade</property> + <property name="vexpand">True</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyViewSwitcherBar" id="view_switcher_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stack">pages_stack</property> + <property name="reveal" bind-source="view_switcher_title" bind-property="title-visible" bind-flags="sync-create"/> + </object> + </child> + </object> + <packing> + <property name="name">pages</property> + </packing> + </child> + <child> + <object class="GtkStack" id="search_stack"> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="visible">True</property> + <child> + <object class="GtkScrolledWindow" id="scrolled_window"> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="visible">True</property> + <child> + <object class="HdyClamp"> + <property name="margin_bottom">18</property> + <property name="margin_end">12</property> + <property name="margin_start">12</property> + <property name="margin_top">18</property> + <property name="visible">True</property> + <child> + <object class="GtkListBox" id="search_results"> + <property name="selection-mode">none</property> + <property name="valign">start</property> + <property name="visible">True</property> + <signal name="row-activated" handler="search_result_activated_cb" swapped="yes"/> + <style> + <class name="content"/> + </style> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">results</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="visible">True</property> + <child> + <object class="GtkImage"> + <property name="can_focus">False</property> + <property name="icon_name">edit-find-symbolic</property> + <property name="icon_size">0</property> + <property name="margin_bottom">18</property> + <property name="pixel_size">128</property> + <property name="valign">center</property> + <property name="visible">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="margin_end">12</property> + <property name="margin_start">12</property> + <property name="orientation">vertical</property> + <property name="visible">True</property> + <child> + <object class="GtkLabel"> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="justify">center</property> + <property name="label" translatable="yes">No Results Found</property> + <property name="margin_bottom">12</property> + <property name="opacity">0.5</property> + <property name="visible">True</property> + <property name="wrap">True</property> + <attributes> + <attribute name="scale" value="2"/> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="can_focus">False</property> + <property name="justify">center</property> + <property name="label" translatable="yes">Try a different search</property> + <property name="margin_bottom">6</property> + <property name="opacity">0.5</property> + <property name="use_markup">True</property> + <property name="visible">True</property> + <property name="wrap">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">no-results</property> + </packing> + </child> + </object> + <packing> + <property name="name">search</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-search-bar.c b/subprojects/libhandy/src/hdy-search-bar.c new file mode 100644 index 0000000..decbda4 --- /dev/null +++ b/subprojects/libhandy/src/hdy-search-bar.c @@ -0,0 +1,659 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 2013 Red Hat, Inc. + * Copyright (C) 2018 Purism SPC + * + * Authors: + * - Bastien Nocera <bnocera@redhat.com> + * - Adrien Plazas <adrien.plazas@puri.sm> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/* + * Modified by the GTK+ Team and others 2013. See the AUTHORS + * file for a list of people on the GTK Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK at ftp://ftp.gtk.org/pub/gtk/. + */ + +/* + * Forked from the GTK+ 3.94.0 GtkSearchBar widget and modified for libhandy by + * Adrien Plazas on behalf of Purism SPC 2018. + * + * The AUTHORS file referenced above is part of GTK and not present in + * libhandy. At the time of the fork it was available here: + * https://gitlab.gnome.org/GNOME/gtk/blob/faba0f0145b1281facba20fb90699e3db594fbb0/AUTHORS + * + * The ChangeLog file referenced above was not present in GTK+ at the time of + * the fork. + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-search-bar.h" + +/** + * SECTION:hdy-search-bar + * @short_description: A toolbar to integrate a search entry with. + * @Title: HdySearchBar + * + * #HdySearchBar is a container made to have a search entry (possibly + * with additional connex widgets, such as drop-down menus, or buttons) + * built-in. The search bar would appear when a search is started through + * typing on the keyboard, or the application’s search mode is toggled on. + * + * For keyboard presses to start a search, events will need to be + * forwarded from the top-level window that contains the search bar. + * See hdy_search_bar_handle_event() for example code. Common shortcuts + * such as Ctrl+F should be handled as an application action, or through + * the menu items. + * + * You will also need to tell the search bar about which entry you + * are using as your search entry using hdy_search_bar_connect_entry(). + * The following example shows you how to create a more complex search + * entry. + * + * HdySearchBar is very similar to #GtkSearchBar, the main difference being that + * it allows the search entry to fill all the available space. This allows you + * to control your search entry's width with a #HdyClamp. + * + * # CSS nodes + * + * #HdySearchBar has a single CSS node with name searchbar. + * + * Since: 0.0.6 + */ + +typedef struct { + /* Template widgets */ + GtkWidget *revealer; + GtkWidget *tool_box; + GtkWidget *start; + GtkWidget *end; + GtkWidget *close_button; + + GtkWidget *entry; + gboolean reveal_child; + gboolean show_close_button; +} HdySearchBarPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (HdySearchBar, hdy_search_bar, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_SEARCH_MODE_ENABLED, + PROP_SHOW_CLOSE_BUTTON, + LAST_PROPERTY +}; + +static GParamSpec *props[LAST_PROPERTY] = { NULL, }; + +/* This comes from gtksearchentry.c in GTK. */ +static gboolean +gtk_search_entry_is_keynav_event (GdkEvent *event) +{ + GdkModifierType state = 0; + guint keyval; + + if (!gdk_event_get_keyval (event, &keyval)) + return FALSE; + + gdk_event_get_state (event, &state); + + if (keyval == GDK_KEY_Tab || keyval == GDK_KEY_KP_Tab || + keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up || + keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down || + keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left || + keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right || + keyval == GDK_KEY_Home || keyval == GDK_KEY_KP_Home || + keyval == GDK_KEY_End || keyval == GDK_KEY_KP_End || + keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_KP_Page_Up || + keyval == GDK_KEY_Page_Down || keyval == GDK_KEY_KP_Page_Down || + ((state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) != 0)) + return TRUE; + + /* Other navigation events should get automatically + * ignored as they will not change the content of the entry + */ + return FALSE; +} + +static void +stop_search_cb (GtkWidget *entry, + HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), FALSE); +} + +static gboolean +entry_key_pressed_event_cb (GtkWidget *widget, + GdkEvent *event, + HdySearchBar *self) +{ + if (event->key.keyval == GDK_KEY_Escape) { + stop_search_cb (widget, self); + + return GDK_EVENT_STOP; + } else { + return GDK_EVENT_PROPAGATE; + } +} + +static void +preedit_changed_cb (GtkEntry *entry, + GtkWidget *popup, + gboolean *preedit_changed) +{ + *preedit_changed = TRUE; +} + +static gboolean +hdy_search_bar_handle_event_for_entry (HdySearchBar *self, + GdkEvent *event) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + gboolean handled; + gboolean preedit_changed; + guint preedit_change_id; + gboolean res; + char *old_text, *new_text; + + if (gtk_search_entry_is_keynav_event (event) || + event->key.keyval == GDK_KEY_space || + event->key.keyval == GDK_KEY_Menu) + return GDK_EVENT_PROPAGATE; + + if (!gtk_widget_get_realized (priv->entry)) + gtk_widget_realize (priv->entry); + + handled = GDK_EVENT_PROPAGATE; + preedit_changed = FALSE; + preedit_change_id = g_signal_connect (priv->entry, "preedit-changed", + G_CALLBACK (preedit_changed_cb), &preedit_changed); + + old_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->entry))); + res = gtk_widget_event (priv->entry, event); + new_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->entry))); + + g_signal_handler_disconnect (priv->entry, preedit_change_id); + + if ((res && g_strcmp0 (new_text, old_text) != 0) || preedit_changed) + handled = GDK_EVENT_STOP; + + g_free (old_text); + g_free (new_text); + + return handled; +} + +/** + * hdy_search_bar_handle_event: + * @self: a #HdySearchBar + * @event: a #GdkEvent containing key press events + * + * This function should be called when the top-level + * window which contains the search bar received a key event. + * + * If the key event is handled by the search bar, the bar will + * be shown, the entry populated with the entered text and %GDK_EVENT_STOP + * will be returned. The caller should ensure that events are + * not propagated further. + * + * If no entry has been connected to the search bar, using + * hdy_search_bar_connect_entry(), this function will return + * immediately with a warning. + * + * ## Showing the search bar on key presses + * + * |[<!-- language="C" --> + * static gboolean + * on_key_press_event (GtkWidget *widget, + * GdkEvent *event, + * gpointer user_data) + * { + * HdySearchBar *bar = HDY_SEARCH_BAR (user_data); + * return hdy_search_bar_handle_event (self, event); + * } + * + * static void + * create_toplevel (void) + * { + * GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + * GtkWindow *search_bar = hdy_search_bar_new (); + * + * // Add more widgets to the window... + * + * g_signal_connect (window, + * "key-press-event", + * G_CALLBACK (on_key_press_event), + * search_bar); + * } + * ]| + * + * Returns: %GDK_EVENT_STOP if the key press event resulted + * in text being entered in the search entry (and revealing + * the search bar if necessary), %GDK_EVENT_PROPAGATE otherwise. + * + * Since: 0.0.6 + */ +gboolean +hdy_search_bar_handle_event (HdySearchBar *self, + GdkEvent *event) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + gboolean handled; + + if (priv->reveal_child) + return GDK_EVENT_PROPAGATE; + + if (priv->entry == NULL) { + g_warning ("The search bar does not have an entry connected to it. Call hdy_search_bar_connect_entry() to connect one."); + + return GDK_EVENT_PROPAGATE; + } + + if (GTK_IS_SEARCH_ENTRY (priv->entry)) + handled = gtk_search_entry_handle_event (GTK_SEARCH_ENTRY (priv->entry), event); + else + handled = hdy_search_bar_handle_event_for_entry (self, event); + + if (handled == GDK_EVENT_STOP) + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), TRUE); + + return handled; +} + +static void +reveal_child_changed_cb (GObject *object, + GParamSpec *pspec, + HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + gboolean reveal_child; + + g_object_get (object, "reveal-child", &reveal_child, NULL); + if (reveal_child) + gtk_widget_set_child_visible (priv->revealer, TRUE); + + if (reveal_child == priv->reveal_child) + return; + + priv->reveal_child = reveal_child; + + if (priv->entry) { + if (reveal_child) + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->entry)); + else + gtk_entry_set_text (GTK_ENTRY (priv->entry), ""); + } + + g_object_notify (G_OBJECT (self), "search-mode-enabled"); +} + +static void +child_revealed_changed_cb (GObject *object, + GParamSpec *pspec, + HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + gboolean val; + + g_object_get (object, "child-revealed", &val, NULL); + if (!val) + gtk_widget_set_child_visible (priv->revealer, FALSE); +} + +static void +close_button_clicked_cb (GtkWidget *button, + HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), FALSE); +} + +static void +hdy_search_bar_add (GtkContainer *container, + GtkWidget *child) +{ + HdySearchBar *self = HDY_SEARCH_BAR (container); + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + if (priv->revealer == NULL) { + GTK_CONTAINER_CLASS (hdy_search_bar_parent_class)->add (container, child); + } else { + gtk_box_set_center_widget (GTK_BOX (priv->tool_box), child); + gtk_container_child_set (GTK_CONTAINER (priv->tool_box), child, + "expand", TRUE, + NULL); + /* If an entry is the only child, save the developer a couple of + * lines of code + */ + if (GTK_IS_ENTRY (child)) + hdy_search_bar_connect_entry (self, GTK_ENTRY (child)); + } +} + +static void +hdy_search_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdySearchBar *self = HDY_SEARCH_BAR (object); + + switch (prop_id) { + case PROP_SEARCH_MODE_ENABLED: + hdy_search_bar_set_search_mode (self, g_value_get_boolean (value)); + break; + case PROP_SHOW_CLOSE_BUTTON: + hdy_search_bar_set_show_close_button (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_search_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdySearchBar *self = HDY_SEARCH_BAR (object); + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + switch (prop_id) { + case PROP_SEARCH_MODE_ENABLED: + g_value_set_boolean (value, priv->reveal_child); + break; + case PROP_SHOW_CLOSE_BUTTON: + g_value_set_boolean (value, hdy_search_bar_get_show_close_button (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void hdy_search_bar_set_entry (HdySearchBar *self, + GtkEntry *entry); + +static void +hdy_search_bar_dispose (GObject *object) +{ + HdySearchBar *self = HDY_SEARCH_BAR (object); + + hdy_search_bar_set_entry (self, NULL); + + G_OBJECT_CLASS (hdy_search_bar_parent_class)->dispose (object); +} + +static gboolean +hdy_search_bar_draw (GtkWidget *widget, + cairo_t *cr) +{ + gint width, height; + GtkStyleContext *context; + + width = gtk_widget_get_allocated_width (widget); + height = gtk_widget_get_allocated_height (widget); + context = gtk_widget_get_style_context (widget); + + gtk_render_background (context, cr, 0, 0, width, height); + gtk_render_frame (context, cr, 0, 0, width, height); + + GTK_WIDGET_CLASS (hdy_search_bar_parent_class)->draw (widget, cr); + + return FALSE; +} + +static void +hdy_search_bar_class_init (HdySearchBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->dispose = hdy_search_bar_dispose; + object_class->set_property = hdy_search_bar_set_property; + object_class->get_property = hdy_search_bar_get_property; + widget_class->draw = hdy_search_bar_draw; + + container_class->add = hdy_search_bar_add; + + /** + * HdySearchBar:search-mode-enabled: + * + * Whether the search mode is on and the search bar shown. + * + * See hdy_search_bar_set_search_mode() for details. + */ + props[PROP_SEARCH_MODE_ENABLED] = + g_param_spec_boolean ("search-mode-enabled", + _("Search Mode Enabled"), + _("Whether the search mode is on and the search bar shown"), + FALSE, + G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdySearchBar:show-close-button: + * + * Whether to show the close button in the toolbar. + */ + props[PROP_SHOW_CLOSE_BUTTON] = + g_param_spec_boolean ("show-close-button", + _("Show Close Button"), + _("Whether to show the close button in the toolbar"), + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROPERTY, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-search-bar.ui"); + gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, tool_box); + gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, revealer); + gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, start); + gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, end); + gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, close_button); + + gtk_widget_class_set_css_name (widget_class, "searchbar"); +} + +static void +hdy_search_bar_init (HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + /* We use child-visible to avoid the unexpanded revealer + * peaking out by 1 pixel + */ + gtk_widget_set_child_visible (priv->revealer, FALSE); + + g_signal_connect (priv->revealer, "notify::reveal-child", + G_CALLBACK (reveal_child_changed_cb), self); + g_signal_connect (priv->revealer, "notify::child-revealed", + G_CALLBACK (child_revealed_changed_cb), self); + + gtk_widget_set_no_show_all (priv->start, TRUE); + gtk_widget_set_no_show_all (priv->end, TRUE); + g_signal_connect (priv->close_button, "clicked", + G_CALLBACK (close_button_clicked_cb), self); +}; + +/** + * hdy_search_bar_new: + * + * Creates a #HdySearchBar. You will need to tell it about + * which widget is going to be your text entry using + * hdy_search_bar_connect_entry(). + * + * Returns: a new #HdySearchBar + * + * Since: 0.0.6 + */ +GtkWidget * +hdy_search_bar_new (void) +{ + return g_object_new (HDY_TYPE_SEARCH_BAR, NULL); +} + +static void +hdy_search_bar_set_entry (HdySearchBar *self, + GtkEntry *entry) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + if (priv->entry != NULL) { + if (GTK_IS_SEARCH_ENTRY (priv->entry)) + g_signal_handlers_disconnect_by_func (priv->entry, stop_search_cb, self); + else + g_signal_handlers_disconnect_by_func (priv->entry, entry_key_pressed_event_cb, self); + g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *) &priv->entry); + } + + priv->entry = GTK_WIDGET (entry); + + if (priv->entry != NULL) { + g_object_add_weak_pointer (G_OBJECT (priv->entry), (gpointer *) &priv->entry); + if (GTK_IS_SEARCH_ENTRY (priv->entry)) + g_signal_connect (priv->entry, "stop-search", + G_CALLBACK (stop_search_cb), self); + else + g_signal_connect (priv->entry, "key-press-event", + G_CALLBACK (entry_key_pressed_event_cb), self); + } +} + +/** + * hdy_search_bar_connect_entry: + * @self: a #HdySearchBar + * @entry: a #GtkEntry + * + * Connects the #GtkEntry widget passed as the one to be used in + * this search bar. The entry should be a descendant of the search bar. + * This is only required if the entry isn’t the direct child of the + * search bar (as in our main example). + * + * Since: 0.0.6 + */ +void +hdy_search_bar_connect_entry (HdySearchBar *self, + GtkEntry *entry) +{ + g_return_if_fail (HDY_IS_SEARCH_BAR (self)); + g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry)); + + hdy_search_bar_set_entry (self, entry); +} + +/** + * hdy_search_bar_get_search_mode: + * @self: a #HdySearchBar + * + * Returns whether the search mode is on or off. + * + * Returns: whether search mode is toggled on + * + * Since: 0.0.6 + */ +gboolean +hdy_search_bar_get_search_mode (HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_SEARCH_BAR (self), FALSE); + + return priv->reveal_child; +} + +/** + * hdy_search_bar_set_search_mode: + * @self: a #HdySearchBar + * @search_mode: the new state of the search mode + * + * Switches the search mode on or off. + * + * Since: 0.0.6 + */ +void +hdy_search_bar_set_search_mode (HdySearchBar *self, + gboolean search_mode) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + g_return_if_fail (HDY_IS_SEARCH_BAR (self)); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), search_mode); +} + +/** + * hdy_search_bar_get_show_close_button: + * @self: a #HdySearchBar + * + * Returns whether the close button is shown. + * + * Returns: whether the close button is shown + * + * Since: 0.0.6 + */ +gboolean +hdy_search_bar_get_show_close_button (HdySearchBar *self) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + g_return_val_if_fail (HDY_IS_SEARCH_BAR (self), FALSE); + + return priv->show_close_button; +} + +/** + * hdy_search_bar_set_show_close_button: + * @self: a #HdySearchBar + * @visible: whether the close button will be shown or not + * + * Shows or hides the close button. Applications that + * already have a “search” toggle button should not show a close + * button in their search bar, as it duplicates the role of the + * toggle button. + * + * Since: 0.0.6 + */ +void +hdy_search_bar_set_show_close_button (HdySearchBar *self, + gboolean visible) +{ + HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self); + + g_return_if_fail (HDY_IS_SEARCH_BAR (self)); + + visible = visible != FALSE; + + if (priv->show_close_button == visible) + return; + + priv->show_close_button = visible; + gtk_widget_set_visible (priv->start, visible); + gtk_widget_set_visible (priv->end, visible); + g_object_notify (G_OBJECT (self), "show-close-button"); +} diff --git a/subprojects/libhandy/src/hdy-search-bar.h b/subprojects/libhandy/src/hdy-search-bar.h new file mode 100644 index 0000000..fc6aa72 --- /dev/null +++ b/subprojects/libhandy/src/hdy-search-bar.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_SEARCH_BAR (hdy_search_bar_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdySearchBar, hdy_search_bar, HDY, SEARCH_BAR, GtkBin) + +struct _HdySearchBarClass +{ + GtkBinClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_search_bar_new (void); +HDY_AVAILABLE_IN_ALL +void hdy_search_bar_connect_entry (HdySearchBar *self, + GtkEntry *entry); +HDY_AVAILABLE_IN_ALL +gboolean hdy_search_bar_get_search_mode (HdySearchBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_search_bar_set_search_mode (HdySearchBar *self, + gboolean search_mode); +HDY_AVAILABLE_IN_ALL +gboolean hdy_search_bar_get_show_close_button (HdySearchBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_search_bar_set_show_close_button (HdySearchBar *self, + gboolean visible); +HDY_AVAILABLE_IN_ALL +gboolean hdy_search_bar_handle_event (HdySearchBar *self, + GdkEvent *event); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-search-bar.ui b/subprojects/libhandy/src/hdy-search-bar.ui new file mode 100644 index 0000000..5e79042 --- /dev/null +++ b/subprojects/libhandy/src/hdy-search-bar.ui @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="HdySearchBar" parent="GtkBin"> + <child> + <object class="GtkRevealer" id="revealer"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <child> + <object class="GtkBox" id="tool_box"> + <property name="visible">True</property> + <property name="border-width">6</property> + <property name="spacing">6</property> + <child> + <object class="GtkBox" id="start"> + <property name="visible">False</property> + <property name="halign">start</property> + <property name="orientation">vertical</property> + </object> + </child> + <child> + <object class="GtkBox" id="end"> + <property name="visible">False</property> + <property name="halign">end</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkButton" id="close_button"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">1</property> + <property name="relief">none</property> + <style> + <class name="close"/> + </style> + <child> + <object class="GtkImage" id="close_image"> + <property name="visible">True</property> + <property name="icon-size">1</property> + <property name="icon-name">window-close-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + </packing> + </child> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup"> + <property name="mode">horizontal</property> + <widgets> + <widget name="start"/> + <widget name="end"/> + </widgets> + </object> +</interface> diff --git a/subprojects/libhandy/src/hdy-shadow-helper-private.h b/subprojects/libhandy/src/hdy-shadow-helper-private.h new file mode 100644 index 0000000..4d96e11 --- /dev/null +++ b/subprojects/libhandy/src/hdy-shadow-helper-private.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_SHADOW_HELPER (hdy_shadow_helper_get_type()) + +G_DECLARE_FINAL_TYPE (HdyShadowHelper, hdy_shadow_helper, HDY, SHADOW_HELPER, GObject) + +HdyShadowHelper *hdy_shadow_helper_new (GtkWidget *widget); + +void hdy_shadow_helper_clear_cache (HdyShadowHelper *self); + +void hdy_shadow_helper_draw_shadow (HdyShadowHelper *self, + cairo_t *cr, + gint width, + gint height, + gdouble progress, + GtkPanDirection direction); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-shadow-helper.c b/subprojects/libhandy/src/hdy-shadow-helper.c new file mode 100644 index 0000000..929f04a --- /dev/null +++ b/subprojects/libhandy/src/hdy-shadow-helper.c @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-cairo-private.h" +#include "hdy-shadow-helper-private.h" + +#include <math.h> + +/** + * PRIVATE:hdy-shadow-helper + * @short_description: Shadow helper used in #HdyLeaflet + * @title: HdyShadowHelper + * @See_also: #HdyLeaflet + * @stability: Private + * + * A helper class for drawing #HdyLeaflet transition shadow. + * + * Since: 0.0.12 + */ + +struct _HdyShadowHelper +{ + GObject parent_instance; + + GtkWidget *widget; + + gboolean is_cache_valid; + + cairo_pattern_t *dimming_pattern; + cairo_pattern_t *shadow_pattern; + cairo_pattern_t *border_pattern; + cairo_pattern_t *outline_pattern; + gint shadow_size; + gint border_size; + gint outline_size; + + GtkPanDirection last_direction; + gint last_width; + gint last_height; + gint last_scale; +}; + +G_DEFINE_TYPE (HdyShadowHelper, hdy_shadow_helper, G_TYPE_OBJECT); + +enum { + PROP_0, + PROP_WIDGET, + LAST_PROP, +}; + +static GParamSpec *props[LAST_PROP]; + + +static GtkStyleContext * +create_context (HdyShadowHelper *self, + const gchar *name, + GtkPanDirection direction) +{ + g_autoptr(GtkWidgetPath) path = NULL; + GtkStyleContext *context; + gint pos; + const gchar *direction_name; + GEnumClass *enum_class; + + enum_class = g_type_class_ref (GTK_TYPE_PAN_DIRECTION); + direction_name = g_enum_get_value (enum_class, direction)->value_nick; + + path = gtk_widget_path_copy (gtk_widget_get_path (self->widget)); + + pos = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET); + gtk_widget_path_iter_set_object_name (path, pos, name); + + gtk_widget_path_iter_add_class (path, pos, direction_name); + + context = gtk_style_context_new (); + gtk_style_context_set_path (context, path); + + g_type_class_unref (enum_class); + + return context; +} + +static gint +get_element_size (GtkStyleContext *context, + GtkPanDirection direction) +{ + gint width, height; + + gtk_style_context_get (context, + gtk_style_context_get_state (context), + "min-width", &width, + "min-height", &height, + NULL); + + switch (direction) { + case GTK_PAN_DIRECTION_LEFT: + case GTK_PAN_DIRECTION_RIGHT: + return width; + case GTK_PAN_DIRECTION_UP: + case GTK_PAN_DIRECTION_DOWN: + return height; + default: + g_assert_not_reached (); + } + + return 0; +} + +static cairo_pattern_t * +create_element_pattern (GtkStyleContext *context, + gint width, + gint height) +{ + g_autoptr (cairo_surface_t) surface = + cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + g_autoptr (cairo_t) cr = cairo_create (surface); + cairo_pattern_t *pattern; + + gtk_render_background (context, cr, 0, 0, width, height); + gtk_render_frame (context, cr, 0, 0, width, height); + + pattern = cairo_pattern_create_for_surface (surface); + + return pattern; +} + +static void +cache_shadow (HdyShadowHelper *self, + gint width, + gint height, + GtkPanDirection direction) +{ + g_autoptr(GtkStyleContext) dim_context = NULL; + g_autoptr(GtkStyleContext) shadow_context = NULL; + g_autoptr(GtkStyleContext) border_context = NULL; + g_autoptr(GtkStyleContext) outline_context = NULL; + gint shadow_size, border_size, outline_size, scale; + + scale = gtk_widget_get_scale_factor (self->widget); + + if (self->last_direction == direction && + self->last_width == width && + self->last_height == height && + self->last_scale == scale && + self->is_cache_valid) + return; + + hdy_shadow_helper_clear_cache (self); + + dim_context = create_context (self, "dimming", direction); + shadow_context = create_context (self, "shadow", direction); + border_context = create_context (self, "border", direction); + outline_context = create_context (self, "outline", direction); + + shadow_size = get_element_size (shadow_context, direction); + border_size = get_element_size (border_context, direction); + outline_size = get_element_size (outline_context, direction); + + self->dimming_pattern = create_element_pattern (dim_context, width, height); + if (direction == GTK_PAN_DIRECTION_LEFT || direction == GTK_PAN_DIRECTION_RIGHT) { + self->shadow_pattern = create_element_pattern (shadow_context, shadow_size, height); + self->border_pattern = create_element_pattern (border_context, border_size, height); + self->outline_pattern = create_element_pattern (outline_context, outline_size, height); + } else { + self->shadow_pattern = create_element_pattern (shadow_context, width, shadow_size); + self->border_pattern = create_element_pattern (border_context, width, border_size); + self->outline_pattern = create_element_pattern (outline_context, width, outline_size); + } + + self->border_size = border_size; + self->shadow_size = shadow_size; + self->outline_size = outline_size; + + self->is_cache_valid = TRUE; + self->last_direction = direction; + self->last_width = width; + self->last_height = height; + self->last_scale = scale; +} + +static void +hdy_shadow_helper_dispose (GObject *object) +{ + HdyShadowHelper *self = HDY_SHADOW_HELPER (object); + + hdy_shadow_helper_clear_cache (self); + + if (self->widget) + g_clear_object (&self->widget); + + G_OBJECT_CLASS (hdy_shadow_helper_parent_class)->dispose (object); +} + +static void +hdy_shadow_helper_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyShadowHelper *self = HDY_SHADOW_HELPER (object); + + switch (prop_id) { + case PROP_WIDGET: + g_value_set_object (value, self->widget); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_shadow_helper_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyShadowHelper *self = HDY_SHADOW_HELPER (object); + + switch (prop_id) { + case PROP_WIDGET: + self->widget = GTK_WIDGET (g_object_ref (g_value_get_object (value))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_shadow_helper_class_init (HdyShadowHelperClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = hdy_shadow_helper_dispose; + object_class->get_property = hdy_shadow_helper_get_property; + object_class->set_property = hdy_shadow_helper_set_property; + + /** + * HdyShadowHelper:widget: + * + * The widget the shadow will be drawn for. Must not be %NULL + * + * Since: 0.0.11 + */ + props[PROP_WIDGET] = + g_param_spec_object ("widget", + _("Widget"), + _("The widget the shadow will be drawn for"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +static void +hdy_shadow_helper_init (HdyShadowHelper *self) +{ +} + +/** + * hdy_shadow_helper_new: + * + * Creates a new #HdyShadowHelper object. + * + * Returns: The newly created #HdyShadowHelper object + * + * Since: 0.0.12 + */ +HdyShadowHelper * +hdy_shadow_helper_new (GtkWidget *widget) +{ + return g_object_new (HDY_TYPE_SHADOW_HELPER, + "widget", widget, + NULL); +} + +/** + * hdy_shadow_helper_clear_cache: + * @self: a #HdyShadowHelper + * + * Clears shadow cache. This should be used after a transition is done. + * + * Since: 0.0.12 + */ +void +hdy_shadow_helper_clear_cache (HdyShadowHelper *self) +{ + if (!self->is_cache_valid) + return; + + cairo_pattern_destroy (self->dimming_pattern); + cairo_pattern_destroy (self->shadow_pattern); + cairo_pattern_destroy (self->border_pattern); + cairo_pattern_destroy (self->outline_pattern); + self->border_size = 0; + self->shadow_size = 0; + self->outline_size = 0; + + self->last_direction = 0; + self->last_width = 0; + self->last_height = 0; + self->last_scale = 0; + + self->is_cache_valid = FALSE; +} + +/** + * hdy_shadow_helper_draw_shadow: + * @self: a #HdyShadowHelper + * @cr: a Cairo context to draw to + * @width: the width of the shadow rectangle + * @height: the height of the shadow rectangle + * @progress: transition progress, changes from 0 to 1 + * @direction: shadow direction + * + * Draws a transition shadow. For caching to work, @width, @height and + * @direction shouldn't change between calls. + * + * Since: 0.0.12 + */ +void +hdy_shadow_helper_draw_shadow (HdyShadowHelper *self, + cairo_t *cr, + gint width, + gint height, + gdouble progress, + GtkPanDirection direction) +{ + gdouble remaining_distance, shadow_opacity; + gint shadow_size, border_size, outline_size, distance; + + if (progress <= 0 || progress >= 1) + return; + + cache_shadow (self, width, height, direction); + + shadow_size = self->shadow_size; + border_size = self->border_size; + outline_size = self->outline_size; + + switch (direction) { + case GTK_PAN_DIRECTION_LEFT: + case GTK_PAN_DIRECTION_RIGHT: + distance = width; + break; + case GTK_PAN_DIRECTION_UP: + case GTK_PAN_DIRECTION_DOWN: + distance = height; + break; + default: + g_assert_not_reached (); + } + + remaining_distance = (1 - progress) * (gdouble) distance; + shadow_opacity = 1; + if (remaining_distance < shadow_size) + shadow_opacity = (remaining_distance / shadow_size); + + cairo_save (cr); + + switch (direction) { + case GTK_PAN_DIRECTION_LEFT: + cairo_rectangle (cr, -outline_size, 0, width + outline_size, height); + break; + case GTK_PAN_DIRECTION_RIGHT: + cairo_rectangle (cr, 0, 0, width + outline_size, height); + break; + case GTK_PAN_DIRECTION_UP: + cairo_rectangle (cr, 0, -outline_size, width, height + outline_size); + break; + case GTK_PAN_DIRECTION_DOWN: + cairo_rectangle (cr, 0, 0, width, height + outline_size); + break; + default: + g_assert_not_reached (); + } + cairo_clip (cr); + gdk_window_mark_paint_from_clip (gtk_widget_get_window (self->widget), cr); + + cairo_set_source (cr, self->dimming_pattern); + cairo_paint_with_alpha (cr, 1 - progress); + + switch (direction) { + case GTK_PAN_DIRECTION_RIGHT: + cairo_translate (cr, width - shadow_size, 0); + break; + case GTK_PAN_DIRECTION_DOWN: + cairo_translate (cr, 0, height - shadow_size); + break; + case GTK_PAN_DIRECTION_LEFT: + case GTK_PAN_DIRECTION_UP: + break; + default: + g_assert_not_reached (); + } + + cairo_set_source (cr, self->shadow_pattern); + cairo_paint_with_alpha (cr, shadow_opacity); + + switch (direction) { + case GTK_PAN_DIRECTION_RIGHT: + cairo_translate (cr, shadow_size - border_size, 0); + break; + case GTK_PAN_DIRECTION_DOWN: + cairo_translate (cr, 0, shadow_size - border_size); + break; + case GTK_PAN_DIRECTION_LEFT: + case GTK_PAN_DIRECTION_UP: + break; + default: + g_assert_not_reached (); + } + + cairo_set_source (cr, self->border_pattern); + cairo_paint (cr); + + switch (direction) { + case GTK_PAN_DIRECTION_RIGHT: + cairo_translate (cr, border_size, 0); + break; + case GTK_PAN_DIRECTION_DOWN: + cairo_translate (cr, 0, border_size); + break; + case GTK_PAN_DIRECTION_LEFT: + cairo_translate (cr, -outline_size, 0); + break; + case GTK_PAN_DIRECTION_UP: + cairo_translate (cr, 0, -outline_size); + break; + default: + g_assert_not_reached (); + } + + cairo_set_source (cr, self->outline_pattern); + cairo_paint (cr); + + cairo_restore (cr); +} diff --git a/subprojects/libhandy/src/hdy-squeezer.c b/subprojects/libhandy/src/hdy-squeezer.c new file mode 100644 index 0000000..1995661 --- /dev/null +++ b/subprojects/libhandy/src/hdy-squeezer.c @@ -0,0 +1,1576 @@ +/* + * Copyright (C) 2013 Red Hat, Inc. + * Copyright (C) 2019 Purism SPC + * + * Author: Alexander Larsson <alexl@redhat.com> + * Author: Adrien Plazas <adrien.plazas@puri.sm> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/* + * Forked from the GTK+ 3.24.2 GtkStack widget initially written by Alexander + * Larsson, and heavily modified for libhandy by Adrien Plazas on behalf of + * Purism SPC 2019. + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-squeezer.h" + +#include "gtkprogresstrackerprivate.h" +#include "hdy-animation-private.h" +#include "hdy-cairo-private.h" +#include "hdy-css-private.h" + +/** + * SECTION:hdy-squeezer + * @short_description: A best fit container. + * @Title: HdySqueezer + * + * The HdySqueezer widget is a container which only shows the first of its + * children that fits in the available size. It is convenient to offer different + * widgets to represent the same data with different levels of detail, making + * the widget seem to squeeze itself to fit in the available space. + * + * Transitions between children can be animated as fades. This can be controlled + * with hdy_squeezer_set_transition_type(). + * + * # CSS nodes + * + * #HdySqueezer has a single CSS node with name squeezer. + */ + +/** + * HdySqueezerTransitionType: + * @HDY_SQUEEZER_TRANSITION_TYPE_NONE: No transition + * @HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE: A cross-fade + * + * These enumeration values describe the possible transitions between children + * in a #HdySqueezer widget. + */ + +enum { + PROP_0, + PROP_HOMOGENEOUS, + PROP_VISIBLE_CHILD, + PROP_TRANSITION_DURATION, + PROP_TRANSITION_TYPE, + PROP_TRANSITION_RUNNING, + PROP_INTERPOLATE_SIZE, + PROP_XALIGN, + PROP_YALIGN, + + /* Overridden properties */ + PROP_ORIENTATION, + + LAST_PROP = PROP_YALIGN + 1, +}; + +enum { + CHILD_PROP_0, + CHILD_PROP_ENABLED, + + LAST_CHILD_PROP, +}; + +typedef struct { + GtkWidget *widget; + gboolean enabled; + GtkWidget *last_focus; +} HdySqueezerChildInfo; + +struct _HdySqueezer +{ + GtkContainer parent_instance; + + GList *children; + + GdkWindow* bin_window; + GdkWindow* view_window; + + HdySqueezerChildInfo *visible_child; + + gboolean homogeneous; + + HdySqueezerTransitionType transition_type; + guint transition_duration; + + HdySqueezerChildInfo *last_visible_child; + cairo_surface_t *last_visible_surface; + GtkAllocation last_visible_surface_allocation; + guint tick_id; + GtkProgressTracker tracker; + gboolean first_frame_skipped; + + gint last_visible_widget_width; + gint last_visible_widget_height; + + HdySqueezerTransitionType active_transition_type; + + gboolean interpolate_size; + + gfloat xalign; + gfloat yalign; + + GtkOrientation orientation; +}; + +static GParamSpec *props[LAST_PROP]; +static GParamSpec *child_props[LAST_CHILD_PROP]; + +G_DEFINE_TYPE_WITH_CODE (HdySqueezer, hdy_squeezer, GTK_TYPE_CONTAINER, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +static GtkOrientation +get_orientation (HdySqueezer *self) +{ + return self->orientation; +} + +static void +set_orientation (HdySqueezer *self, + GtkOrientation orientation) +{ + if (self->orientation == orientation) + return; + + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); +} + +static HdySqueezerChildInfo * +find_child_info_for_widget (HdySqueezer *self, + GtkWidget *child) +{ + HdySqueezerChildInfo *info; + GList *l; + + for (l = self->children; l != NULL; l = l->next) { + info = l->data; + if (info->widget == child) + return info; + } + + return NULL; +} + +static void +hdy_squeezer_progress_updated (HdySqueezer *self) +{ + gtk_widget_queue_draw (GTK_WIDGET (self)); + + if (!self->homogeneous) + gtk_widget_queue_resize (GTK_WIDGET (self)); + + if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) { + if (self->last_visible_surface != NULL) { + cairo_surface_destroy (self->last_visible_surface); + self->last_visible_surface = NULL; + } + + if (self->last_visible_child != NULL) { + gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE); + self->last_visible_child = NULL; + } + } +} + +static gboolean +hdy_squeezer_transition_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + + if (self->first_frame_skipped) { + gtk_progress_tracker_advance_frame (&self->tracker, + gdk_frame_clock_get_frame_time (frame_clock)); + } else { + self->first_frame_skipped = TRUE; + } + + /* Finish the animation early if the widget isn't mapped anymore. */ + if (!gtk_widget_get_mapped (widget)) + gtk_progress_tracker_finish (&self->tracker); + + hdy_squeezer_progress_updated (HDY_SQUEEZER (widget)); + + if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) { + self->tick_id = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]); + + return FALSE; + } + + return TRUE; +} + +static void +hdy_squeezer_schedule_ticks (HdySqueezer *self) +{ + if (self->tick_id == 0) { + self->tick_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self), hdy_squeezer_transition_cb, self, NULL); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]); + } +} + +static void +hdy_squeezer_unschedule_ticks (HdySqueezer *self) +{ + if (self->tick_id != 0) { + gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_id); + self->tick_id = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]); + } +} + +static void +hdy_squeezer_start_transition (HdySqueezer *self, + HdySqueezerTransitionType transition_type, + guint transition_duration) +{ + GtkWidget *widget = GTK_WIDGET (self); + + if (gtk_widget_get_mapped (widget) && + hdy_get_enable_animations (widget) && + transition_type != HDY_SQUEEZER_TRANSITION_TYPE_NONE && + transition_duration != 0 && + self->last_visible_child != NULL) { + self->active_transition_type = transition_type; + self->first_frame_skipped = FALSE; + hdy_squeezer_schedule_ticks (self); + gtk_progress_tracker_start (&self->tracker, + self->transition_duration * 1000, + 0, + 1.0); + } else { + hdy_squeezer_unschedule_ticks (self); + self->active_transition_type = HDY_SQUEEZER_TRANSITION_TYPE_NONE; + gtk_progress_tracker_finish (&self->tracker); + } + + hdy_squeezer_progress_updated (HDY_SQUEEZER (widget)); +} + +static void +set_visible_child (HdySqueezer *self, + HdySqueezerChildInfo *child_info, + HdySqueezerTransitionType transition_type, + guint transition_duration) +{ + HdySqueezerChildInfo *info; + GtkWidget *widget = GTK_WIDGET (self); + GList *l; + GtkWidget *toplevel; + GtkWidget *focus; + gboolean contains_focus = FALSE; + + /* If we are being destroyed, do not bother with transitions and + * notifications. + */ + if (gtk_widget_in_destruction (widget)) + return; + + /* If none, pick the first visible. */ + if (child_info == NULL) { + for (l = self->children; l != NULL; l = l->next) { + info = l->data; + if (gtk_widget_get_visible (info->widget)) { + child_info = info; + break; + } + } + } + + if (child_info == self->visible_child) + return; + + toplevel = gtk_widget_get_toplevel (widget); + if (GTK_IS_WINDOW (toplevel)) { + focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); + if (focus && + self->visible_child && + self->visible_child->widget && + gtk_widget_is_ancestor (focus, self->visible_child->widget)) { + contains_focus = TRUE; + + if (self->visible_child->last_focus) + g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus), + (gpointer *)&self->visible_child->last_focus); + self->visible_child->last_focus = focus; + g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus), + (gpointer *)&self->visible_child->last_focus); + } + } + + if (self->last_visible_child != NULL) + gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE); + self->last_visible_child = NULL; + + if (self->last_visible_surface != NULL) + cairo_surface_destroy (self->last_visible_surface); + self->last_visible_surface = NULL; + + if (self->visible_child && self->visible_child->widget) { + if (gtk_widget_is_visible (widget)) { + GtkAllocation allocation; + + self->last_visible_child = self->visible_child; + gtk_widget_get_allocated_size (self->last_visible_child->widget, &allocation, NULL); + self->last_visible_widget_width = allocation.width; + self->last_visible_widget_height = allocation.height; + } else { + gtk_widget_set_child_visible (self->visible_child->widget, FALSE); + } + } + + self->visible_child = child_info; + + if (child_info) { + gtk_widget_set_child_visible (child_info->widget, TRUE); + + if (contains_focus) { + if (child_info->last_focus) + gtk_widget_grab_focus (child_info->last_focus); + else + gtk_widget_child_focus (child_info->widget, GTK_DIR_TAB_FORWARD); + } + } + + if (self->homogeneous) + gtk_widget_queue_allocate (widget); + else + gtk_widget_queue_resize (widget); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]); + + hdy_squeezer_start_transition (self, transition_type, transition_duration); +} + +static void +stack_child_visibility_notify_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + HdySqueezer *self = HDY_SQUEEZER (user_data); + GtkWidget *child = GTK_WIDGET (obj); + HdySqueezerChildInfo *child_info; + + child_info = find_child_info_for_widget (self, child); + + if (self->visible_child == NULL && + gtk_widget_get_visible (child)) + set_visible_child (self, child_info, self->transition_type, self->transition_duration); + else if (self->visible_child == child_info && + !gtk_widget_get_visible (child)) + set_visible_child (self, NULL, self->transition_type, self->transition_duration); + + if (child_info == self->last_visible_child) { + gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE); + self->last_visible_child = NULL; + } +} + +static void +hdy_squeezer_add (GtkContainer *container, + GtkWidget *child) +{ + HdySqueezer *self = HDY_SQUEEZER (container); + HdySqueezerChildInfo *child_info; + + g_return_if_fail (child != NULL); + + child_info = g_slice_new (HdySqueezerChildInfo); + child_info->widget = child; + child_info->enabled = TRUE; + child_info->last_focus = NULL; + + self->children = g_list_append (self->children, child_info); + + gtk_widget_set_child_visible (child, FALSE); + gtk_widget_set_parent_window (child, self->bin_window); + gtk_widget_set_parent (child, GTK_WIDGET (self)); + + if (self->bin_window != NULL) { + gdk_window_set_events (self->bin_window, + gdk_window_get_events (self->bin_window) | + gtk_widget_get_events (child)); + } + + g_signal_connect (child, "notify::visible", + G_CALLBACK (stack_child_visibility_notify_cb), self); + + if (self->visible_child == NULL && + gtk_widget_get_visible (child)) + set_visible_child (self, child_info, self->transition_type, self->transition_duration); + + if (self->visible_child == child_info) + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +hdy_squeezer_remove (GtkContainer *container, + GtkWidget *child) +{ + HdySqueezer *self = HDY_SQUEEZER (container); + HdySqueezerChildInfo *child_info; + gboolean was_visible; + + child_info = find_child_info_for_widget (self, child); + if (child_info == NULL) + return; + + self->children = g_list_remove (self->children, child_info); + + g_signal_handlers_disconnect_by_func (child, + stack_child_visibility_notify_cb, + self); + + was_visible = gtk_widget_get_visible (child); + + child_info->widget = NULL; + + if (self->visible_child == child_info) + set_visible_child (self, NULL, self->transition_type, self->transition_duration); + + if (self->last_visible_child == child_info) + self->last_visible_child = NULL; + + gtk_widget_unparent (child); + + if (child_info->last_focus) + g_object_remove_weak_pointer (G_OBJECT (child_info->last_focus), + (gpointer *)&child_info->last_focus); + + g_slice_free (HdySqueezerChildInfo, child_info); + + if (self->homogeneous && was_visible) + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +hdy_squeezer_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + HdySqueezer *self = HDY_SQUEEZER (object); + + switch (property_id) { + case PROP_HOMOGENEOUS: + g_value_set_boolean (value, hdy_squeezer_get_homogeneous (self)); + break; + case PROP_VISIBLE_CHILD: + g_value_set_object (value, hdy_squeezer_get_visible_child (self)); + break; + case PROP_TRANSITION_DURATION: + g_value_set_uint (value, hdy_squeezer_get_transition_duration (self)); + break; + case PROP_TRANSITION_TYPE: + g_value_set_enum (value, hdy_squeezer_get_transition_type (self)); + break; + case PROP_TRANSITION_RUNNING: + g_value_set_boolean (value, hdy_squeezer_get_transition_running (self)); + break; + case PROP_INTERPOLATE_SIZE: + g_value_set_boolean (value, hdy_squeezer_get_interpolate_size (self)); + break; + case PROP_XALIGN: + g_value_set_float (value, hdy_squeezer_get_xalign (self)); + break; + case PROP_YALIGN: + g_value_set_float (value, hdy_squeezer_get_yalign (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, get_orientation (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +hdy_squeezer_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + HdySqueezer *self = HDY_SQUEEZER (object); + + switch (property_id) { + case PROP_HOMOGENEOUS: + hdy_squeezer_set_homogeneous (self, g_value_get_boolean (value)); + break; + case PROP_TRANSITION_DURATION: + hdy_squeezer_set_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_TRANSITION_TYPE: + hdy_squeezer_set_transition_type (self, g_value_get_enum (value)); + break; + case PROP_INTERPOLATE_SIZE: + hdy_squeezer_set_interpolate_size (self, g_value_get_boolean (value)); + break; + case PROP_XALIGN: + hdy_squeezer_set_xalign (self, g_value_get_float (value)); + break; + case PROP_YALIGN: + hdy_squeezer_set_yalign (self, g_value_get_float (value)); + break; + case PROP_ORIENTATION: + set_orientation (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +hdy_squeezer_realize (GtkWidget *widget) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + GtkAllocation allocation; + GdkWindowAttr attributes = { 0 }; + GdkWindowAttributesType attributes_mask; + HdySqueezerChildInfo *info; + GList *l; + + gtk_widget_set_realized (widget, TRUE); + gtk_widget_set_window (widget, g_object_ref (gtk_widget_get_parent_window (widget))); + + gtk_widget_get_allocation (widget, &allocation); + + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (widget); + attributes.event_mask = + gtk_widget_get_events (widget); + attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL; + + self->view_window = + gdk_window_new (gtk_widget_get_window (GTK_WIDGET (self)), + &attributes, attributes_mask); + gtk_widget_register_window (widget, self->view_window); + + attributes.x = 0; + attributes.y = 0; + attributes.width = allocation.width; + attributes.height = allocation.height; + + for (l = self->children; l != NULL; l = l->next) { + info = l->data; + attributes.event_mask |= gtk_widget_get_events (info->widget); + } + + self->bin_window = + gdk_window_new (self->view_window, &attributes, attributes_mask); + gtk_widget_register_window (widget, self->bin_window); + + for (l = self->children; l != NULL; l = l->next) { + info = l->data; + + gtk_widget_set_parent_window (info->widget, self->bin_window); + } + + gdk_window_show (self->bin_window); +} + +static void +hdy_squeezer_unrealize (GtkWidget *widget) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + + gtk_widget_unregister_window (widget, self->bin_window); + gdk_window_destroy (self->bin_window); + self->bin_window = NULL; + gtk_widget_unregister_window (widget, self->view_window); + gdk_window_destroy (self->view_window); + self->view_window = NULL; + + GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->unrealize (widget); +} + +static void +hdy_squeezer_map (GtkWidget *widget) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + + GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->map (widget); + + gdk_window_show (self->view_window); +} + +static void +hdy_squeezer_unmap (GtkWidget *widget) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + + gdk_window_hide (self->view_window); + + GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->unmap (widget); +} + +static void +hdy_squeezer_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + HdySqueezer *self = HDY_SQUEEZER (container); + HdySqueezerChildInfo *child_info; + GList *l; + + l = self->children; + while (l) { + child_info = l->data; + l = l->next; + + (* callback) (child_info->widget, callback_data); + } +} + +static void +hdy_squeezer_compute_expand (GtkWidget *widget, + gboolean *hexpand_p, + gboolean *vexpand_p) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + gboolean hexpand, vexpand; + HdySqueezerChildInfo *child_info; + GtkWidget *child; + GList *l; + + hexpand = FALSE; + vexpand = FALSE; + for (l = self->children; l != NULL; l = l->next) { + child_info = l->data; + child = child_info->widget; + + if (!hexpand && + gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL)) + hexpand = TRUE; + + if (!vexpand && + gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL)) + vexpand = TRUE; + + if (hexpand && vexpand) + break; + } + + *hexpand_p = hexpand; + *vexpand_p = vexpand; +} + +static void +hdy_squeezer_draw_crossfade (GtkWidget *widget, + cairo_t *cr) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + gdouble progress = gtk_progress_tracker_get_progress (&self->tracker, FALSE); + + cairo_push_group (cr); + gtk_container_propagate_draw (GTK_CONTAINER (self), + self->visible_child->widget, + cr); + cairo_save (cr); + + /* Multiply alpha by progress. */ + cairo_set_source_rgba (cr, 1, 1, 1, progress); + cairo_set_operator (cr, CAIRO_OPERATOR_DEST_IN); + cairo_paint (cr); + + if (self->last_visible_surface != NULL) { + gint width_diff = gtk_widget_get_allocated_width (widget) - self->last_visible_surface_allocation.width; + gint height_diff = gtk_widget_get_allocated_height (widget) - self->last_visible_surface_allocation.height; + + cairo_set_source_surface (cr, self->last_visible_surface, + width_diff * self->xalign, + height_diff * self->yalign); + cairo_set_operator (cr, CAIRO_OPERATOR_ADD); + cairo_paint_with_alpha (cr, MAX (1.0 - progress, 0)); + } + + cairo_restore (cr); + + cairo_pop_group_to_source (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + cairo_paint (cr); +} + +static gboolean +hdy_squeezer_draw (GtkWidget *widget, + cairo_t *cr) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + + if (gtk_cairo_should_draw_window (cr, self->view_window)) { + GtkStyleContext *context; + + context = gtk_widget_get_style_context (widget); + gtk_render_background (context, + cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + } + + if (self->visible_child) { + if (gtk_progress_tracker_get_state (&self->tracker) != GTK_PROGRESS_STATE_AFTER) { + if (self->last_visible_surface == NULL && + self->last_visible_child != NULL) { + g_autoptr (cairo_t) pattern_cr = NULL; + + gtk_widget_get_allocation (self->last_visible_child->widget, + &self->last_visible_surface_allocation); + self->last_visible_surface = + gdk_window_create_similar_surface (gtk_widget_get_window (widget), + CAIRO_CONTENT_COLOR_ALPHA, + self->last_visible_surface_allocation.width, + self->last_visible_surface_allocation.height); + pattern_cr = cairo_create (self->last_visible_surface); + /* We don't use propagate_draw here, because we don't want to apply the + * bin_window offset. + */ + gtk_widget_draw (self->last_visible_child->widget, pattern_cr); + } + + cairo_rectangle (cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + cairo_clip (cr); + + switch (self->active_transition_type) { + case HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE: + if (gtk_cairo_should_draw_window (cr, self->bin_window)) + hdy_squeezer_draw_crossfade (widget, cr); + break; + case HDY_SQUEEZER_TRANSITION_TYPE_NONE: + default: + g_assert_not_reached (); + } + + } else if (gtk_cairo_should_draw_window (cr, self->bin_window)) + gtk_container_propagate_draw (GTK_CONTAINER (self), + self->visible_child->widget, + cr); + } + + return FALSE; +} + +static void +hdy_squeezer_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + HdySqueezerChildInfo *child_info = NULL; + GtkWidget *child = NULL; + gint child_min; + GList *l; + GtkAllocation child_allocation; + + hdy_css_size_allocate (widget, allocation); + + gtk_widget_set_allocation (widget, allocation); + + for (l = self->children; l != NULL; l = l->next) { + child_info = l->data; + child = child_info->widget; + + if (!gtk_widget_get_visible (child)) + continue; + + if (!child_info->enabled) + continue; + + if (self->orientation == GTK_ORIENTATION_VERTICAL) { + if (gtk_widget_get_request_mode (child) != GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH) + gtk_widget_get_preferred_height (child, &child_min, NULL); + else + gtk_widget_get_preferred_height_for_width (child, allocation->width, &child_min, NULL); + + if (child_min <= allocation->height) + break; + } else { + if (gtk_widget_get_request_mode (child) != GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT) + gtk_widget_get_preferred_width (child, &child_min, NULL); + else + gtk_widget_get_preferred_width_for_height (child, allocation->height, &child_min, NULL); + + if (child_min <= allocation->width) + break; + } + } + + set_visible_child (self, child_info, + self->transition_type, + self->transition_duration); + + child_allocation.x = 0; + child_allocation.y = 0; + + if (gtk_widget_get_realized (widget)) { + gdk_window_move_resize (self->view_window, + allocation->x, allocation->y, + allocation->width, allocation->height); + gdk_window_move_resize (self->bin_window, + 0, 0, + allocation->width, allocation->height); + } + + if (self->last_visible_child != NULL) { + int min, nat; + gtk_widget_get_preferred_width (self->last_visible_child->widget, &min, &nat); + child_allocation.width = MAX (min, allocation->width); + gtk_widget_get_preferred_height_for_width (self->last_visible_child->widget, + child_allocation.width, + &min, &nat); + child_allocation.height = MAX (min, allocation->height); + + gtk_widget_size_allocate (self->last_visible_child->widget, &child_allocation); + } + + child_allocation.width = allocation->width; + child_allocation.height = allocation->height; + + if (self->visible_child) { + int min, nat; + GtkAlign valign; + + gtk_widget_get_preferred_height_for_width (self->visible_child->widget, + allocation->width, + &min, &nat); + if (self->interpolate_size) { + valign = gtk_widget_get_valign (self->visible_child->widget); + child_allocation.height = MAX (nat, allocation->height); + if (valign == GTK_ALIGN_END && + child_allocation.height > allocation->height) + child_allocation.y -= nat - allocation->height; + else if (valign == GTK_ALIGN_CENTER && + child_allocation.height > allocation->height) + child_allocation.y -= (nat - allocation->height) / 2; + } + + gtk_widget_size_allocate (self->visible_child->widget, &child_allocation); + } +} + +/* This private method is prefixed by the class name because it will be a + * virtual method in GTK 4. + */ +static void +hdy_squeezer_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + HdySqueezer *self = HDY_SQUEEZER (widget); + HdySqueezerChildInfo *child_info; + GtkWidget *child; + gint child_min, child_nat; + GList *l; + + *minimum = 0; + *natural = 0; + + for (l = self->children; l != NULL; l = l->next) { + child_info = l->data; + child = child_info->widget; + + if (self->orientation != orientation && !self->homogeneous && + self->visible_child != child_info) + continue; + + if (!gtk_widget_get_visible (child)) + continue; + + /* Disabled children are taken into account when measuring the widget, to + * keep its size request and allocation consistent. This avoids the + * appearant size and position of a child to changes suddenly when a larger + * child gets enabled/disabled. + */ + + if (orientation == GTK_ORIENTATION_VERTICAL) { + if (for_size < 0) + gtk_widget_get_preferred_height (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat); + } else { + if (for_size < 0) + gtk_widget_get_preferred_width (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat); + } + + if (self->orientation == orientation) + *minimum = *minimum == 0 ? child_min : MIN (*minimum, child_min); + else + *minimum = MAX (*minimum, child_min); + *natural = MAX (*natural, child_nat); + } + + if (self->orientation != orientation && !self->homogeneous && + self->interpolate_size && + self->last_visible_child != NULL) { + gdouble t = gtk_progress_tracker_get_ease_out_cubic (&self->tracker, FALSE); + if (orientation == GTK_ORIENTATION_VERTICAL) { + *minimum = hdy_lerp (self->last_visible_widget_height, *minimum, t); + *natural = hdy_lerp (self->last_visible_widget_height, *natural, t); + } else { + *minimum = hdy_lerp (self->last_visible_widget_width, *minimum, t); + *natural = hdy_lerp (self->last_visible_widget_width, *natural, t); + } + } + + hdy_css_measure (widget, orientation, minimum, natural); +} + +static void +hdy_squeezer_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_squeezer_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_squeezer_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + hdy_squeezer_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum, natural, NULL, NULL); +} + +static void +hdy_squeezer_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_squeezer_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_squeezer_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + hdy_squeezer_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, NULL, NULL); +} + +static void +hdy_squeezer_get_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + HdySqueezer *self = HDY_SQUEEZER (container); + HdySqueezerChildInfo *child_info; + + child_info = find_child_info_for_widget (self, widget); + if (child_info == NULL) { + g_param_value_set_default (pspec, value); + + return; + } + + switch (property_id) { + case CHILD_PROP_ENABLED: + g_value_set_boolean (value, hdy_squeezer_get_child_enabled (self, widget)); + break; + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_squeezer_set_child_property (GtkContainer *container, + GtkWidget *widget, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + HdySqueezer *self = HDY_SQUEEZER (container); + HdySqueezerChildInfo *child_info; + + child_info = find_child_info_for_widget (self, widget); + if (child_info == NULL) + return; + + switch (property_id) { + case CHILD_PROP_ENABLED: + hdy_squeezer_set_child_enabled (self, widget, g_value_get_boolean (value)); + break; + default: + GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec); + break; + } +} + +static void +hdy_squeezer_dispose (GObject *object) +{ + HdySqueezer *self = HDY_SQUEEZER (object); + + self->visible_child = NULL; + + G_OBJECT_CLASS (hdy_squeezer_parent_class)->dispose (object); +} + +static void +hdy_squeezer_finalize (GObject *object) +{ + HdySqueezer *self = HDY_SQUEEZER (object); + + hdy_squeezer_unschedule_ticks (self); + + if (self->last_visible_surface != NULL) + cairo_surface_destroy (self->last_visible_surface); + + G_OBJECT_CLASS (hdy_squeezer_parent_class)->finalize (object); +} + +static void +hdy_squeezer_class_init (HdySqueezerClass *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 = hdy_squeezer_get_property; + object_class->set_property = hdy_squeezer_set_property; + object_class->dispose = hdy_squeezer_dispose; + object_class->finalize = hdy_squeezer_finalize; + + widget_class->size_allocate = hdy_squeezer_size_allocate; + widget_class->draw = hdy_squeezer_draw; + widget_class->realize = hdy_squeezer_realize; + widget_class->unrealize = hdy_squeezer_unrealize; + widget_class->map = hdy_squeezer_map; + widget_class->unmap = hdy_squeezer_unmap; + widget_class->get_preferred_height = hdy_squeezer_get_preferred_height; + widget_class->get_preferred_height_for_width = hdy_squeezer_get_preferred_height_for_width; + widget_class->get_preferred_width = hdy_squeezer_get_preferred_width; + widget_class->get_preferred_width_for_height = hdy_squeezer_get_preferred_width_for_height; + widget_class->compute_expand = hdy_squeezer_compute_expand; + + container_class->add = hdy_squeezer_add; + container_class->remove = hdy_squeezer_remove; + container_class->forall = hdy_squeezer_forall; + container_class->set_child_property = hdy_squeezer_set_child_property; + container_class->get_child_property = hdy_squeezer_get_child_property; + gtk_container_class_handle_border_width (container_class); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + props[PROP_HOMOGENEOUS] = + g_param_spec_boolean ("homogeneous", + _("Homogeneous"), + _("Homogeneous sizing"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_VISIBLE_CHILD] = + g_param_spec_object ("visible-child", + _("Visible child"), + _("The widget currently visible in the squeezer"), + GTK_TYPE_WIDGET, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TRANSITION_DURATION] = + g_param_spec_uint ("transition-duration", + _("Transition duration"), + _("The animation duration, in milliseconds"), + 0, G_MAXUINT, 200, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TRANSITION_TYPE] = + g_param_spec_enum ("transition-type", + _("Transition type"), + _("The type of animation used to transition"), + HDY_TYPE_SQUEEZER_TRANSITION_TYPE, + HDY_SQUEEZER_TRANSITION_TYPE_NONE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TRANSITION_RUNNING] = + g_param_spec_boolean ("transition-running", + _("Transition running"), + _("Whether or not the transition is currently running"), + FALSE, + G_PARAM_READABLE); + + props[PROP_INTERPOLATE_SIZE] = + g_param_spec_boolean ("interpolate-size", + _("Interpolate size"), + _("Whether or not the size should smoothly change when changing between differently sized children"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdySqueezer:xalign: + * + * The xalign property determines the horizontal aligment of the children + * inside the squeezer's size allocation. + * Compare this to #GtkWidget:halign, which determines how the squeezer's size + * allocation is positioned in the space available for the squeezer. + * The range goes from 0 (start) to 1 (end). + * + * This will affect the position of children too wide to fit in the squeezer + * as they are fading out. + * + * Since: 1.0 + */ + props[PROP_XALIGN] = + g_param_spec_float ("xalign", + _("X align"), + _("The horizontal alignment, from 0 (start) to 1 (end)"), + 0.0, 1.0, + 0.5, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdySqueezer:yalign: + * + * The yalign property determines the vertical aligment of the children inside + * the squeezer's size allocation. + * Compare this to #GtkWidget:valign, which determines how the squeezer's size + * allocation is positioned in the space available for the squeezer. + * The range goes from 0 (top) to 1 (bottom). + * + * This will affect the position of children too tall to fit in the squeezer + * as they are fading out. + * + * Since: 1.0 + */ + props[PROP_YALIGN] = + g_param_spec_float ("yalign", + _("Y align"), + _("The vertical alignment, from 0 (top) to 1 (bottom)"), + 0.0, 1.0, + 0.5, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + child_props[CHILD_PROP_ENABLED] = + g_param_spec_boolean ("enabled", + _("Enabled"), + _("Whether the child can be picked or should be ignored when looking for the child fitting the available size best"), + TRUE, + G_PARAM_READWRITE); + + gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props); + + gtk_widget_class_set_css_name (widget_class, "squeezer"); +} + +static void +hdy_squeezer_init (HdySqueezer *self) +{ + + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + + self->homogeneous = TRUE; + self->transition_duration = 200; + self->transition_type = HDY_SQUEEZER_TRANSITION_TYPE_NONE; + self->xalign = 0.5; + self->yalign = 0.5; +} + +/** + * hdy_squeezer_new: + * + * Creates a new #HdySqueezer container. + * + * Returns: a new #HdySqueezer + */ +GtkWidget * +hdy_squeezer_new (void) +{ + return g_object_new (HDY_TYPE_SQUEEZER, NULL); +} + +/** + * hdy_squeezer_get_homogeneous: + * @self: a #HdySqueezer + * + * Gets whether @self is homogeneous. + * + * See hdy_squeezer_set_homogeneous(). + * + * Returns: %TRUE if @self is homogeneous, %FALSE is not + * + * Since: 0.0.10 + */ +gboolean +hdy_squeezer_get_homogeneous (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE); + + return self->homogeneous; +} + +/** + * hdy_squeezer_set_homogeneous: + * @self: a #HdySqueezer + * @homogeneous: %TRUE to make @self homogeneous + * + * Sets @self to be homogeneous or not. If it is homogeneous, @self will request + * the same size for all its children for its opposite orientation, e.g. if + * @self is oriented horizontally and is homogeneous, it will request the same + * height for all its children. If it isn't, @self may change size when a + * different child becomes visible. + * + * Since: 0.0.10 + */ +void +hdy_squeezer_set_homogeneous (HdySqueezer *self, + gboolean homogeneous) +{ + g_return_if_fail (HDY_IS_SQUEEZER (self)); + + homogeneous = !!homogeneous; + + if (self->homogeneous == homogeneous) + return; + + self->homogeneous = homogeneous; + + if (gtk_widget_get_visible (GTK_WIDGET(self))) + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOMOGENEOUS]); +} + +/** + * hdy_squeezer_get_transition_duration: + * @self: a #HdySqueezer + * + * Gets the amount of time (in milliseconds) that transitions between children + * in @self will take. + * + * Returns: the transition duration + */ +guint +hdy_squeezer_get_transition_duration (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0); + + return self->transition_duration; +} + +/** + * hdy_squeezer_set_transition_duration: + * @self: a #HdySqueezer + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between children in @self will take. + */ +void +hdy_squeezer_set_transition_duration (HdySqueezer *self, + guint duration) +{ + g_return_if_fail (HDY_IS_SQUEEZER (self)); + + if (self->transition_duration == duration) + return; + + self->transition_duration = duration; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]); +} + +/** + * hdy_squeezer_get_transition_type: + * @self: a #HdySqueezer + * + * Gets the type of animation that will be used for transitions between children + * in @self. + * + * Returns: the current transition type of @self + */ +HdySqueezerTransitionType +hdy_squeezer_get_transition_type (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), HDY_SQUEEZER_TRANSITION_TYPE_NONE); + + return self->transition_type; +} + +/** + * hdy_squeezer_set_transition_type: + * @self: a #HdySqueezer + * @transition: the new transition type + * + * Sets the type of animation that will be used for transitions between children + * in @self. Available types include various kinds of fades and slides. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the child that is about to become + * current. + */ +void +hdy_squeezer_set_transition_type (HdySqueezer *self, + HdySqueezerTransitionType transition) +{ + g_return_if_fail (HDY_IS_SQUEEZER (self)); + + if (self->transition_type == transition) + return; + + self->transition_type = transition; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_TYPE]); +} + +/** + * hdy_squeezer_get_transition_running: + * @self: a #HdySqueezer + * + * Gets whether @self is currently in a transition from one child to another. + * + * Returns: %TRUE if the transition is currently running, %FALSE otherwise. + */ +gboolean +hdy_squeezer_get_transition_running (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE); + + return (self->tick_id != 0); +} + +/** + * hdy_squeezer_get_interpolate_size: + * @self: A #HdySqueezer + * + * Gets whether @self should interpolate its size on visible child change. + * + * See hdy_squeezer_set_interpolate_size(). + * + * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not + * + * Since: 0.0.10 + */ +gboolean +hdy_squeezer_get_interpolate_size (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE); + + return self->interpolate_size; +} + +/** + * hdy_squeezer_set_interpolate_size: + * @self: A #HdySqueezer + * @interpolate_size: %TRUE to interpolate the size + * + * Sets whether or not @self will interpolate the size of its opposing + * orientation when changing the visible child. If %TRUE, @self will interpolate + * its size between the one of the previous visible child and the one of the new + * visible child, according to the set transition duration and the orientation, + * e.g. if @self is horizontal, it will interpolate the its height. + * + * Since: 0.0.10 + */ +void +hdy_squeezer_set_interpolate_size (HdySqueezer *self, + gboolean interpolate_size) +{ + g_return_if_fail (HDY_IS_SQUEEZER (self)); + + interpolate_size = !!interpolate_size; + + if (self->interpolate_size == interpolate_size) + return; + + self->interpolate_size = interpolate_size; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]); +} + +/** + * hdy_squeezer_get_visible_child: + * @self: a #HdySqueezer + * + * Gets the currently visible child of @self, or %NULL if there are no visible + * children. + * + * Returns: (transfer none) (nullable): the visible child of the #HdySqueezer + */ +GtkWidget * +hdy_squeezer_get_visible_child (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), NULL); + + return self->visible_child ? self->visible_child->widget : NULL; +} + +/** + * hdy_squeezer_get_child_enabled: + * @self: a #HdySqueezer + * @child: a child of @self + * + * Gets whether @child is enabled. + * + * See hdy_squeezer_set_child_enabled(). + * + * Returns: %TRUE if @child is enabled, %FALSE otherwise. + */ +gboolean +hdy_squeezer_get_child_enabled (HdySqueezer *self, + GtkWidget *child) +{ + HdySqueezerChildInfo *child_info; + + g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE); + g_return_val_if_fail (GTK_IS_WIDGET (child), FALSE); + + child_info = find_child_info_for_widget (self, child); + + g_return_val_if_fail (child_info != NULL, FALSE); + + return child_info->enabled; +} + +/** + * hdy_squeezer_set_child_enabled: + * @self: a #HdySqueezer + * @child: a child of @self + * @enabled: %TRUE to enable the child, %FALSE to disable it + * + * Make @self enable or disable @child. If a child is disabled, it will be + * ignored when looking for the child fitting the available size best. This + * allows to programmatically and prematurely hide a child of @self even if it + * fits in the available space. + * + * This can be used e.g. to ensure a certain child is hidden below a certain + * window width, or any other constraint you find suitable. + */ +void +hdy_squeezer_set_child_enabled (HdySqueezer *self, + GtkWidget *child, + gboolean enabled) +{ + HdySqueezerChildInfo *child_info; + + g_return_if_fail (HDY_IS_SQUEEZER (self)); + g_return_if_fail (GTK_IS_WIDGET (child)); + + child_info = find_child_info_for_widget (self, child); + + g_return_if_fail (child_info != NULL); + + enabled = !!enabled; + + if (child_info->enabled == enabled) + return; + + child_info->enabled = enabled; + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +/** + * hdy_squeezer_get_xalign: + * @self: a #HdySqueezer + * + * Gets the #HdySqueezer:xalign property for @self. + * + * Returns: the xalign property + * + * Since: 1.0 + */ +gfloat +hdy_squeezer_get_xalign (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0.5); + + return self->xalign; +} + +/** + * hdy_squeezer_set_xalign: + * @self: a #HdySqueezer + * @xalign: the new xalign value, between 0 and 1 + * + * Sets the #HdySqueezer:xalign property for @self. + * + * Since: 1.0 + */ +void +hdy_squeezer_set_xalign (HdySqueezer *self, + gfloat xalign) +{ + g_return_if_fail (HDY_IS_SQUEEZER (self)); + + xalign = CLAMP (xalign, 0.0, 1.0); + + if (self->xalign == xalign) + return; + + self->xalign = xalign; + gtk_widget_queue_draw (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_XALIGN]); +} + +/** + * hdy_squeezer_get_yalign: + * @self: a #HdySqueezer + * + * Gets the #HdySqueezer:yalign property for @self. + * + * Returns: the yalign property + * + * Since: 1.0 + */ +gfloat +hdy_squeezer_get_yalign (HdySqueezer *self) +{ + g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0.5); + + return self->yalign; +} + +/** + * hdy_squeezer_set_yalign: + * @self: a #HdySqueezer + * @yalign: the new yalign value, between 0 and 1 + * + * Sets the #HdySqueezer:yalign property for @self. + * + * Since: 1.0 + */ +void +hdy_squeezer_set_yalign (HdySqueezer *self, + gfloat yalign) +{ + g_return_if_fail (HDY_IS_SQUEEZER (self)); + + yalign = CLAMP (yalign, 0.0, 1.0); + + if (self->yalign == yalign) + return; + + self->yalign = yalign; + gtk_widget_queue_draw (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_YALIGN]); +} diff --git a/subprojects/libhandy/src/hdy-squeezer.h b/subprojects/libhandy/src/hdy-squeezer.h new file mode 100644 index 0000000..9b98116 --- /dev/null +++ b/subprojects/libhandy/src/hdy-squeezer.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-enums.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_SQUEEZER (hdy_squeezer_get_type ()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdySqueezer, hdy_squeezer, HDY, SQUEEZER, GtkContainer) + +typedef enum { + HDY_SQUEEZER_TRANSITION_TYPE_NONE, + HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE, +} HdySqueezerTransitionType; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_squeezer_new (void); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_squeezer_get_homogeneous (HdySqueezer *self); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_homogeneous (HdySqueezer *self, + gboolean homogeneous); + +HDY_AVAILABLE_IN_ALL +guint hdy_squeezer_get_transition_duration (HdySqueezer *self); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_transition_duration (HdySqueezer *self, + guint duration); + +HDY_AVAILABLE_IN_ALL +HdySqueezerTransitionType hdy_squeezer_get_transition_type (HdySqueezer *self); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_transition_type (HdySqueezer *self, + HdySqueezerTransitionType transition); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_squeezer_get_transition_running (HdySqueezer *self); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_squeezer_get_interpolate_size (HdySqueezer *self); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_interpolate_size (HdySqueezer *self, + gboolean interpolate_size); + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_squeezer_get_visible_child (HdySqueezer *self); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_squeezer_get_child_enabled (HdySqueezer *self, + GtkWidget *child); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_child_enabled (HdySqueezer *self, + GtkWidget *child, + gboolean enabled); + +HDY_AVAILABLE_IN_ALL +gfloat hdy_squeezer_get_xalign (HdySqueezer *self); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_xalign (HdySqueezer *self, + gfloat xalign); + +HDY_AVAILABLE_IN_ALL +gfloat hdy_squeezer_get_yalign (HdySqueezer *self); +HDY_AVAILABLE_IN_ALL +void hdy_squeezer_set_yalign (HdySqueezer *self, + gfloat yalign); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-stackable-box-private.h b/subprojects/libhandy/src/hdy-stackable-box-private.h new file mode 100644 index 0000000..d72c75a --- /dev/null +++ b/subprojects/libhandy/src/hdy-stackable-box-private.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> +#include "hdy-navigation-direction.h" +#include "hdy-swipe-tracker.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_STACKABLE_BOX (hdy_stackable_box_get_type()) + +G_DECLARE_FINAL_TYPE (HdyStackableBox, hdy_stackable_box, HDY, STACKABLE_BOX, GObject) + +typedef enum { + HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER, + HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER, + HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE, +} HdyStackableBoxTransitionType; + +HdyStackableBox *hdy_stackable_box_new (GtkContainer *container, + GtkContainerClass *klass, + gboolean can_unfold); +gboolean hdy_stackable_box_get_folded (HdyStackableBox *self); +GtkWidget *hdy_stackable_box_get_visible_child (HdyStackableBox *self); +void hdy_stackable_box_set_visible_child (HdyStackableBox *self, + GtkWidget *visible_child); +const gchar *hdy_stackable_box_get_visible_child_name (HdyStackableBox *self); +void hdy_stackable_box_set_visible_child_name (HdyStackableBox *self, + const gchar *name); +gboolean hdy_stackable_box_get_homogeneous (HdyStackableBox *self, + gboolean folded, + GtkOrientation orientation); +void hdy_stackable_box_set_homogeneous (HdyStackableBox *self, + gboolean folded, + GtkOrientation orientation, + gboolean homogeneous); +HdyStackableBoxTransitionType hdy_stackable_box_get_transition_type (HdyStackableBox *self); +void hdy_stackable_box_set_transition_type (HdyStackableBox *self, + HdyStackableBoxTransitionType transition); + +guint hdy_stackable_box_get_mode_transition_duration (HdyStackableBox *self); +void hdy_stackable_box_set_mode_transition_duration (HdyStackableBox *self, + guint duration); + +guint hdy_stackable_box_get_child_transition_duration (HdyStackableBox *self); +void hdy_stackable_box_set_child_transition_duration (HdyStackableBox *self, + guint duration); +gboolean hdy_stackable_box_get_child_transition_running (HdyStackableBox *self); +gboolean hdy_stackable_box_get_interpolate_size (HdyStackableBox *self); +void hdy_stackable_box_set_interpolate_size (HdyStackableBox *self, + gboolean interpolate_size); +gboolean hdy_stackable_box_get_can_swipe_back (HdyStackableBox *self); +void hdy_stackable_box_set_can_swipe_back (HdyStackableBox *self, + gboolean can_swipe_back); +gboolean hdy_stackable_box_get_can_swipe_forward (HdyStackableBox *self); +void hdy_stackable_box_set_can_swipe_forward (HdyStackableBox *self, + gboolean can_swipe_forward); + +GtkWidget *hdy_stackable_box_get_adjacent_child (HdyStackableBox *self, + HdyNavigationDirection direction); +gboolean hdy_stackable_box_navigate (HdyStackableBox *self, + HdyNavigationDirection direction); + +GtkWidget *hdy_stackable_box_get_child_by_name (HdyStackableBox *self, + const gchar *name); + +GtkOrientation hdy_stackable_box_get_orientation (HdyStackableBox *self); +void hdy_stackable_box_set_orientation (HdyStackableBox *self, + GtkOrientation orientation); + +const gchar *hdy_stackable_box_get_child_name (HdyStackableBox *self, + GtkWidget *widget); +void hdy_stackable_box_set_child_name (HdyStackableBox *self, + GtkWidget *widget, + const gchar *name); +gboolean hdy_stackable_box_get_child_navigatable (HdyStackableBox *self, + GtkWidget *widget); +void hdy_stackable_box_set_child_navigatable (HdyStackableBox *self, + GtkWidget *widget, + gboolean navigatable); + +void hdy_stackable_box_switch_child (HdyStackableBox *self, + guint index, + gint64 duration); + +HdySwipeTracker *hdy_stackable_box_get_swipe_tracker (HdyStackableBox *self); +gdouble hdy_stackable_box_get_distance (HdyStackableBox *self); +gdouble *hdy_stackable_box_get_snap_points (HdyStackableBox *self, + gint *n_snap_points); +gdouble hdy_stackable_box_get_progress (HdyStackableBox *self); +gdouble hdy_stackable_box_get_cancel_progress (HdyStackableBox *self); +void hdy_stackable_box_get_swipe_area (HdyStackableBox *self, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect); + +void hdy_stackable_box_add (HdyStackableBox *self, + GtkWidget *widget); +void hdy_stackable_box_remove (HdyStackableBox *self, + GtkWidget *widget); +void hdy_stackable_box_forall (HdyStackableBox *self, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data); + +void hdy_stackable_box_measure (HdyStackableBox *self, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline); +void hdy_stackable_box_size_allocate (HdyStackableBox *self, + GtkAllocation *allocation); +gboolean hdy_stackable_box_draw (HdyStackableBox *self, + cairo_t *cr); +void hdy_stackable_box_realize (HdyStackableBox *self); +void hdy_stackable_box_unrealize (HdyStackableBox *self); +void hdy_stackable_box_map (HdyStackableBox *self); +void hdy_stackable_box_unmap (HdyStackableBox *self); +void hdy_stackable_box_direction_changed (HdyStackableBox *self, + GtkTextDirection previous_direction); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-stackable-box.c b/subprojects/libhandy/src/hdy-stackable-box.c new file mode 100644 index 0000000..4eb8fa3 --- /dev/null +++ b/subprojects/libhandy/src/hdy-stackable-box.c @@ -0,0 +1,3151 @@ +/* + * Copyright (C) 2018 Purism SPC + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "gtkprogresstrackerprivate.h" +#include "hdy-animation-private.h" +#include "hdy-enums-private.h" +#include "hdy-stackable-box-private.h" +#include "hdy-shadow-helper-private.h" +#include "hdy-swipeable.h" + +/** + * PRIVATE:hdy-stackable-box + * @short_description: An adaptive container acting like a box or a stack. + * @Title: HdyStackableBox + * @stability: Private + * @See_also: #HdyDeck, #HdyLeaflet + * + * The #HdyStackableBox object can arrange the widgets it manages like #GtkBox + * does or like a #GtkStack does, adapting to size changes by switching between + * the two modes. These modes are named respectively “unfoled” and “folded”. + * + * When there is enough space the children are displayed side by side, otherwise + * only one is displayed. The threshold is dictated by the preferred minimum + * sizes of the children. + * + * #HdyStackableBox is used as an internal implementation of #HdyDeck and + * #HdyLeaflet. + * + * Since: 1.0 + */ + +/** + * HdyStackableBoxTransitionType: + * @HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order + * @HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order + * @HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order + * + * This enumeration value describes the possible transitions between modes and + * children in a #HdyStackableBox widget. + * + * New values may be added to this enumeration over time. + * + * Since: 1.0 + */ + +enum { + PROP_0, + PROP_FOLDED, + PROP_HHOMOGENEOUS_FOLDED, + PROP_VHOMOGENEOUS_FOLDED, + PROP_HHOMOGENEOUS_UNFOLDED, + PROP_VHOMOGENEOUS_UNFOLDED, + PROP_VISIBLE_CHILD, + PROP_VISIBLE_CHILD_NAME, + PROP_TRANSITION_TYPE, + PROP_MODE_TRANSITION_DURATION, + PROP_CHILD_TRANSITION_DURATION, + PROP_CHILD_TRANSITION_RUNNING, + PROP_INTERPOLATE_SIZE, + PROP_CAN_SWIPE_BACK, + PROP_CAN_SWIPE_FORWARD, + PROP_ORIENTATION, + LAST_PROP, +}; + +#define HDY_FOLD_UNFOLDED FALSE +#define HDY_FOLD_FOLDED TRUE +#define HDY_FOLD_MAX 2 +#define GTK_ORIENTATION_MAX 2 +#define HDY_SWIPE_BORDER 16 + +typedef struct _HdyStackableBoxChildInfo HdyStackableBoxChildInfo; + +struct _HdyStackableBoxChildInfo +{ + GtkWidget *widget; + GdkWindow *window; + gchar *name; + gboolean navigatable; + + /* Convenience storage for per-child temporary frequently computed values. */ + GtkAllocation alloc; + GtkRequisition min; + GtkRequisition nat; + gboolean visible; +}; + +struct _HdyStackableBox +{ + GObject parent; + + GtkContainer *container; + GtkContainerClass *klass; + gboolean can_unfold; + + GList *children; + /* It is probably cheaper to store and maintain a reversed copy of the + * children list that to reverse the list every time we need to allocate or + * draw children for RTL languages on a horizontal widget. + */ + GList *children_reversed; + HdyStackableBoxChildInfo *visible_child; + HdyStackableBoxChildInfo *last_visible_child; + + GdkWindow* view_window; + + gboolean folded; + + gboolean homogeneous[HDY_FOLD_MAX][GTK_ORIENTATION_MAX]; + + GtkOrientation orientation; + + HdyStackableBoxTransitionType transition_type; + + HdySwipeTracker *tracker; + + struct { + guint duration; + + gdouble current_pos; + gdouble source_pos; + gdouble target_pos; + + gdouble start_progress; + gdouble end_progress; + guint tick_id; + GtkProgressTracker tracker; + } mode_transition; + + /* Child transition variables. */ + struct { + guint duration; + + gdouble progress; + gdouble start_progress; + gdouble end_progress; + + gboolean is_gesture_active; + gboolean is_cancelled; + + guint tick_id; + GtkProgressTracker tracker; + gboolean first_frame_skipped; + + gboolean interpolate_size; + gboolean can_swipe_back; + gboolean can_swipe_forward; + + GtkPanDirection active_direction; + gboolean is_direct_swipe; + gint swipe_direction; + } child_transition; + + HdyShadowHelper *shadow_helper; +}; + +static GParamSpec *props[LAST_PROP]; + +static gint HOMOGENEOUS_PROP[HDY_FOLD_MAX][GTK_ORIENTATION_MAX] = { + { PROP_HHOMOGENEOUS_UNFOLDED, PROP_VHOMOGENEOUS_UNFOLDED}, + { PROP_HHOMOGENEOUS_FOLDED, PROP_VHOMOGENEOUS_FOLDED}, +}; + +G_DEFINE_TYPE (HdyStackableBox, hdy_stackable_box, G_TYPE_OBJECT); + +static void +free_child_info (HdyStackableBoxChildInfo *child_info) +{ + g_free (child_info->name); + g_free (child_info); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (HdyStackableBoxChildInfo, free_child_info) + +static HdyStackableBoxChildInfo * +find_child_info_for_widget (HdyStackableBox *self, + GtkWidget *widget) +{ + GList *children; + HdyStackableBoxChildInfo *child_info; + + for (children = self->children; children; children = children->next) { + child_info = children->data; + + if (child_info->widget == widget) + return child_info; + } + + return NULL; +} + +static HdyStackableBoxChildInfo * +find_child_info_for_name (HdyStackableBox *self, + const gchar *name) +{ + GList *children; + HdyStackableBoxChildInfo *child_info; + + for (children = self->children; children; children = children->next) { + child_info = children->data; + + if (g_strcmp0 (child_info->name, name) == 0) + return child_info; + } + + return NULL; +} + +static GList * +get_directed_children (HdyStackableBox *self) +{ + return self->orientation == GTK_ORIENTATION_HORIZONTAL && + gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL ? + self->children_reversed : self->children; +} + +static GtkPanDirection +get_pan_direction (HdyStackableBox *self, + gboolean new_child_first) +{ + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + if (gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL) + return new_child_first ? GTK_PAN_DIRECTION_LEFT : GTK_PAN_DIRECTION_RIGHT; + else + return new_child_first ? GTK_PAN_DIRECTION_RIGHT : GTK_PAN_DIRECTION_LEFT; + } + else + return new_child_first ? GTK_PAN_DIRECTION_DOWN : GTK_PAN_DIRECTION_UP; +} + +static gint +get_child_window_x (HdyStackableBox *self, + HdyStackableBoxChildInfo *child_info, + gint width) +{ + gboolean is_rtl; + gint rtl_multiplier; + + if (!self->child_transition.is_gesture_active && + gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) + return 0; + + if (self->child_transition.active_direction != GTK_PAN_DIRECTION_LEFT && + self->child_transition.active_direction != GTK_PAN_DIRECTION_RIGHT) + return 0; + + is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL; + rtl_multiplier = is_rtl ? -1 : 1; + + if ((self->child_transition.active_direction == GTK_PAN_DIRECTION_RIGHT) == is_rtl) { + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->visible_child) + return width * (1 - self->child_transition.progress) * rtl_multiplier; + + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->last_visible_child) + return -width * self->child_transition.progress * rtl_multiplier; + } else { + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->visible_child) + return -width * (1 - self->child_transition.progress) * rtl_multiplier; + + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->last_visible_child) + return width * self->child_transition.progress * rtl_multiplier; + } + + return 0; +} + +static gint +get_child_window_y (HdyStackableBox *self, + HdyStackableBoxChildInfo *child_info, + gint height) +{ + if (!self->child_transition.is_gesture_active && + gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) + return 0; + + if (self->child_transition.active_direction != GTK_PAN_DIRECTION_UP && + self->child_transition.active_direction != GTK_PAN_DIRECTION_DOWN) + return 0; + + if (self->child_transition.active_direction == GTK_PAN_DIRECTION_UP) { + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->visible_child) + return height * (1 - self->child_transition.progress); + + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->last_visible_child) + return -height * self->child_transition.progress; + } else { + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->visible_child) + return -height * (1 - self->child_transition.progress); + + if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) && + child_info == self->last_visible_child) + return height * self->child_transition.progress; + } + + return 0; +} + +static void +hdy_stackable_box_child_progress_updated (HdyStackableBox *self) +{ + gtk_widget_queue_draw (GTK_WIDGET (self->container)); + + if (!self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] || + !self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL]) + gtk_widget_queue_resize (GTK_WIDGET (self->container)); + else + gtk_widget_queue_allocate (GTK_WIDGET (self->container)); + + if (!self->child_transition.is_gesture_active && + gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) { + if (self->child_transition.is_cancelled) { + if (self->last_visible_child != NULL) { + if (self->folded) { + gtk_widget_set_child_visible (self->last_visible_child->widget, TRUE); + gtk_widget_set_child_visible (self->visible_child->widget, FALSE); + } + self->visible_child = self->last_visible_child; + self->last_visible_child = NULL; + } + + self->child_transition.is_cancelled = FALSE; + + g_object_freeze_notify (G_OBJECT (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD_NAME]); + g_object_thaw_notify (G_OBJECT (self)); + } else { + if (self->last_visible_child != NULL) { + if (self->folded) + gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE); + self->last_visible_child = NULL; + } + } + + gtk_widget_queue_allocate (GTK_WIDGET (self->container)); + self->child_transition.swipe_direction = 0; + hdy_shadow_helper_clear_cache (self->shadow_helper); + } +} + +static gboolean +hdy_stackable_box_child_transition_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyStackableBox *self = HDY_STACKABLE_BOX (user_data); + gdouble progress; + + if (self->child_transition.first_frame_skipped) { + gtk_progress_tracker_advance_frame (&self->child_transition.tracker, + gdk_frame_clock_get_frame_time (frame_clock)); + progress = gtk_progress_tracker_get_ease_out_cubic (&self->child_transition.tracker, FALSE); + self->child_transition.progress = + hdy_lerp (self->child_transition.start_progress, + self->child_transition.end_progress, progress); + } else + self->child_transition.first_frame_skipped = TRUE; + + /* Finish animation early if not mapped anymore */ + if (!gtk_widget_get_mapped (widget)) + gtk_progress_tracker_finish (&self->child_transition.tracker); + + hdy_stackable_box_child_progress_updated (self); + + if (gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) { + self->child_transition.tick_id = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]); + + return FALSE; + } + + return TRUE; +} + +static void +hdy_stackable_box_schedule_child_ticks (HdyStackableBox *self) +{ + if (self->child_transition.tick_id == 0) { + self->child_transition.tick_id = + gtk_widget_add_tick_callback (GTK_WIDGET (self->container), + hdy_stackable_box_child_transition_cb, + self, NULL); + if (!self->child_transition.is_gesture_active) + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]); + } +} + +static void +hdy_stackable_box_unschedule_child_ticks (HdyStackableBox *self) +{ + if (self->child_transition.tick_id != 0) { + gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), self->child_transition.tick_id); + self->child_transition.tick_id = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]); + } +} + +static void +hdy_stackable_box_stop_child_transition (HdyStackableBox *self) +{ + hdy_stackable_box_unschedule_child_ticks (self); + gtk_progress_tracker_finish (&self->child_transition.tracker); + if (self->last_visible_child != NULL) { + gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE); + self->last_visible_child = NULL; + } + + self->child_transition.swipe_direction = 0; + hdy_shadow_helper_clear_cache (self->shadow_helper); +} + +static void +hdy_stackable_box_start_child_transition (HdyStackableBox *self, + guint transition_duration, + GtkPanDirection transition_direction) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + + if (gtk_widget_get_mapped (widget) && + ((hdy_get_enable_animations (widget) && + transition_duration != 0) || + self->child_transition.is_gesture_active) && + self->last_visible_child != NULL && + /* Don't animate child transition when a mode transition is ongoing. */ + self->mode_transition.tick_id == 0) { + self->child_transition.active_direction = transition_direction; + self->child_transition.first_frame_skipped = FALSE; + self->child_transition.start_progress = 0; + self->child_transition.end_progress = 1; + self->child_transition.progress = 0; + self->child_transition.is_cancelled = FALSE; + + if (!self->child_transition.is_gesture_active) { + hdy_stackable_box_schedule_child_ticks (self); + gtk_progress_tracker_start (&self->child_transition.tracker, + transition_duration * 1000, + 0, + 1.0); + } + } + else { + hdy_stackable_box_unschedule_child_ticks (self); + gtk_progress_tracker_finish (&self->child_transition.tracker); + } + + hdy_stackable_box_child_progress_updated (self); +} + +static void +set_visible_child_info (HdyStackableBox *self, + HdyStackableBoxChildInfo *new_visible_child, + HdyStackableBoxTransitionType transition_type, + guint transition_duration, + gboolean emit_child_switched) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GList *children; + HdyStackableBoxChildInfo *child_info; + GtkPanDirection transition_direction = GTK_PAN_DIRECTION_LEFT; + + /* If we are being destroyed, do not bother with transitions and + * notifications. + */ + if (gtk_widget_in_destruction (widget)) + return; + + /* If none, pick first visible. */ + if (new_visible_child == NULL) { + for (children = self->children; children; children = children->next) { + child_info = children->data; + + if (gtk_widget_get_visible (child_info->widget)) { + new_visible_child = child_info; + + break; + } + } + } + + if (new_visible_child == self->visible_child) + return; + + /* FIXME Probably copied from Gtk Stack, should check whether it's needed. */ + /* toplevel = gtk_widget_get_toplevel (widget); */ + /* if (GTK_IS_WINDOW (toplevel)) { */ + /* focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); */ + /* if (focus && */ + /* self->visible_child && */ + /* self->visible_child->widget && */ + /* gtk_widget_is_ancestor (focus, self->visible_child->widget)) { */ + /* contains_focus = TRUE; */ + + /* if (self->visible_child->last_focus) */ + /* g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus), */ + /* (gpointer *)&self->visible_child->last_focus); */ + /* self->visible_child->last_focus = focus; */ + /* g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus), */ + /* (gpointer *)&self->visible_child->last_focus); */ + /* } */ + /* } */ + + if (self->last_visible_child) + gtk_widget_set_child_visible (self->last_visible_child->widget, !self->folded); + self->last_visible_child = NULL; + + hdy_shadow_helper_clear_cache (self->shadow_helper); + + if (self->visible_child && self->visible_child->widget) { + if (gtk_widget_is_visible (widget)) + self->last_visible_child = self->visible_child; + else + gtk_widget_set_child_visible (self->visible_child->widget, !self->folded); + } + + /* FIXME This comes from GtkStack and should be adapted. */ + /* hdy_stackable_box_accessible_update_visible_child (stack, */ + /* self->visible_child ? self->visible_child->widget : NULL, */ + /* new_visible_child ? new_visible_child->widget : NULL); */ + + self->visible_child = new_visible_child; + + if (new_visible_child) { + gtk_widget_set_child_visible (new_visible_child->widget, TRUE); + + /* FIXME This comes from GtkStack and should be adapted. */ + /* if (contains_focus) { */ + /* if (new_visible_child->last_focus) */ + /* gtk_widget_grab_focus (new_visible_child->last_focus); */ + /* else */ + /* gtk_widget_child_focus (new_visible_child->widget, GTK_DIR_TAB_FORWARD); */ + /* } */ + } + + if (new_visible_child == NULL || self->last_visible_child == NULL) + transition_duration = 0; + else { + gboolean new_first = FALSE; + for (children = self->children; children; children = children->next) { + if (new_visible_child == children->data) { + new_first = TRUE; + + break; + } + if (self->last_visible_child == children->data) + break; + } + + transition_direction = get_pan_direction (self, new_first); + } + + if (self->folded) { + if (self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] && + self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL]) + gtk_widget_queue_allocate (widget); + else + gtk_widget_queue_resize (widget); + + hdy_stackable_box_start_child_transition (self, transition_duration, transition_direction); + } + + if (emit_child_switched) { + gint index = 0; + + for (children = self->children; children; children = children->next) { + child_info = children->data; + + if (!child_info->navigatable) + continue; + + if (child_info == new_visible_child) + break; + + index++; + } + + hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self->container), index, + transition_duration); + } + + g_object_freeze_notify (G_OBJECT (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD_NAME]); + g_object_thaw_notify (G_OBJECT (self)); +} + +static void +hdy_stackable_box_set_position (HdyStackableBox *self, + gdouble pos) +{ + self->mode_transition.current_pos = pos; + + gtk_widget_queue_allocate (GTK_WIDGET (self->container)); +} + +static void +hdy_stackable_box_mode_progress_updated (HdyStackableBox *self) +{ + if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) == GTK_PROGRESS_STATE_AFTER) + hdy_shadow_helper_clear_cache (self->shadow_helper); +} + +static gboolean +hdy_stackable_box_mode_transition_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + HdyStackableBox *self = HDY_STACKABLE_BOX (user_data); + gdouble ease; + + gtk_progress_tracker_advance_frame (&self->mode_transition.tracker, + gdk_frame_clock_get_frame_time (frame_clock)); + ease = gtk_progress_tracker_get_ease_out_cubic (&self->mode_transition.tracker, FALSE); + hdy_stackable_box_set_position (self, + self->mode_transition.source_pos + (ease * (self->mode_transition.target_pos - self->mode_transition.source_pos))); + + hdy_stackable_box_mode_progress_updated (self); + + if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) == GTK_PROGRESS_STATE_AFTER) { + self->mode_transition.tick_id = 0; + return FALSE; + } + + return TRUE; +} + +static void +hdy_stackable_box_start_mode_transition (HdyStackableBox *self, + gdouble target) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + + if (self->mode_transition.target_pos == target) + return; + + self->mode_transition.target_pos = target; + /* FIXME PROP_REVEAL_CHILD needs to be implemented. */ + /* g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_REVEAL_CHILD]); */ + + hdy_stackable_box_stop_child_transition (self); + + if (gtk_widget_get_mapped (widget) && + self->mode_transition.duration != 0 && + hdy_get_enable_animations (widget) && + self->can_unfold) { + self->mode_transition.source_pos = self->mode_transition.current_pos; + if (self->mode_transition.tick_id == 0) + self->mode_transition.tick_id = gtk_widget_add_tick_callback (widget, hdy_stackable_box_mode_transition_cb, self, NULL); + gtk_progress_tracker_start (&self->mode_transition.tracker, + self->mode_transition.duration * 1000, + 0, + 1.0); + } + else + hdy_stackable_box_set_position (self, target); +} + +/* FIXME Use this to stop the mode transition animation when it makes sense (see * + * GtkRevealer for exmples). + */ +/* static void */ +/* hdy_stackable_box_stop_mode_animation (HdyStackableBox *self) */ +/* { */ +/* if (self->mode_transition.current_pos != self->mode_transition.target_pos) { */ +/* self->mode_transition.current_pos = self->mode_transition.target_pos; */ + /* g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_REVEALED]); */ +/* } */ +/* if (self->mode_transition.tick_id != 0) { */ +/* gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), self->mode_transition.tick_id); */ +/* self->mode_transition.tick_id = 0; */ +/* } */ +/* } */ + +/** + * hdy_stackable_box_get_folded: + * @self: a #HdyStackableBox + * + * Gets whether @self is folded. + * + * Returns: whether @self is folded. + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_get_folded (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + return self->folded; +} + +static void +hdy_stackable_box_set_folded (HdyStackableBox *self, + gboolean folded) +{ + GtkStyleContext *context; + + if (self->folded == folded) + return; + + self->folded = folded; + + hdy_stackable_box_start_mode_transition (self, folded ? 0.0 : 1.0); + + if (self->can_unfold) { + context = gtk_widget_get_style_context (GTK_WIDGET (self->container)); + if (folded) { + gtk_style_context_add_class (context, "folded"); + gtk_style_context_remove_class (context, "unfolded"); + } else { + gtk_style_context_remove_class (context, "folded"); + gtk_style_context_add_class (context, "unfolded"); + } + } + + g_object_notify_by_pspec (G_OBJECT (self), + props[PROP_FOLDED]); +} + +/** + * hdy_stackable_box_set_homogeneous: + * @self: a #HdyStackableBox + * @folded: the fold + * @orientation: the orientation + * @homogeneous: %TRUE to make @self homogeneous + * + * Sets the #HdyStackableBox to be homogeneous or not for the given fold and orientation. + * If it is homogeneous, the #HdyStackableBox will request the same + * width or height for all its children depending on the orientation. + * If it isn't and it is folded, the widget may change width or height + * when a different child becomes visible. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_homogeneous (HdyStackableBox *self, + gboolean folded, + GtkOrientation orientation, + gboolean homogeneous) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + folded = !!folded; + homogeneous = !!homogeneous; + + if (self->homogeneous[folded][orientation] == homogeneous) + return; + + self->homogeneous[folded][orientation] = homogeneous; + + if (gtk_widget_get_visible (GTK_WIDGET (self->container))) + gtk_widget_queue_resize (GTK_WIDGET (self->container)); + + g_object_notify_by_pspec (G_OBJECT (self), props[HOMOGENEOUS_PROP[folded][orientation]]); +} + +/** + * hdy_stackable_box_get_homogeneous: + * @self: a #HdyStackableBox + * @folded: the fold + * @orientation: the orientation + * + * Gets whether @self is homogeneous for the given fold and orientation. + * See hdy_stackable_box_set_homogeneous(). + * + * Returns: whether @self is homogeneous for the given fold and orientation. + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_get_homogeneous (HdyStackableBox *self, + gboolean folded, + GtkOrientation orientation) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + folded = !!folded; + + return self->homogeneous[folded][orientation]; +} + +/** + * hdy_stackable_box_get_transition_type: + * @self: a #HdyStackableBox + * + * Gets the type of animation that will be used + * for transitions between modes and children in @self. + * + * Returns: the current transition type of @self + * + * Since: 1.0 + */ +HdyStackableBoxTransitionType +hdy_stackable_box_get_transition_type (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER); + + return self->transition_type; +} + +/** + * hdy_stackable_box_set_transition_type: + * @self: a #HdyStackableBox + * @transition: the new transition type + * + * Sets the type of animation that will be used for transitions between modes + * and children in @self. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the mode or child that is about to + * become current. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_transition_type (HdyStackableBox *self, + HdyStackableBoxTransitionType transition) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + if (self->transition_type == transition) + return; + + self->transition_type = transition; + g_object_notify_by_pspec (G_OBJECT (self), + props[PROP_TRANSITION_TYPE]); +} + +/** + * hdy_stackable_box_get_mode_transition_duration: + * @self: a #HdyStackableBox + * + * Returns the amount of time (in milliseconds) that + * transitions between modes in @self will take. + * + * Returns: the mode transition duration + * + * Since: 1.0 + */ +guint +hdy_stackable_box_get_mode_transition_duration (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), 0); + + return self->mode_transition.duration; +} + +/** + * hdy_stackable_box_set_mode_transition_duration: + * @self: a #HdyStackableBox + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between modes in @self + * will take. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_mode_transition_duration (HdyStackableBox *self, + guint duration) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + if (self->mode_transition.duration == duration) + return; + + self->mode_transition.duration = duration; + g_object_notify_by_pspec (G_OBJECT (self), + props[PROP_MODE_TRANSITION_DURATION]); +} + +/** + * hdy_stackable_box_get_child_transition_duration: + * @self: a #HdyStackableBox + * + * Returns the amount of time (in milliseconds) that + * transitions between children in @self will take. + * + * Returns: the child transition duration + * + * Since: 1.0 + */ +guint +hdy_stackable_box_get_child_transition_duration (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), 0); + + return self->child_transition.duration; +} + +/** + * hdy_stackable_box_set_child_transition_duration: + * @self: a #HdyStackableBox + * @duration: the new duration, in milliseconds + * + * Sets the duration that transitions between children in @self + * will take. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_child_transition_duration (HdyStackableBox *self, + guint duration) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + if (self->child_transition.duration == duration) + return; + + self->child_transition.duration = duration; + g_object_notify_by_pspec (G_OBJECT (self), + props[PROP_CHILD_TRANSITION_DURATION]); +} + +/** + * hdy_stackable_box_get_visible_child: + * @self: a #HdyStackableBox + * + * Gets the visible child widget. + * + * Returns: (transfer none): the visible child widget + * + * Since: 1.0 + */ +GtkWidget * +hdy_stackable_box_get_visible_child (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL); + + if (self->visible_child == NULL) + return NULL; + + return self->visible_child->widget; +} + +/** + * hdy_stackable_box_set_visible_child: + * @self: a #HdyStackableBox + * @visible_child: the new child + * + * Makes @visible_child visible using a transition determined by + * HdyStackableBox:transition-type and HdyStackableBox:child-transition-duration. + * The transition can be cancelled by the user, in which case visible child will + * change back to the previously visible child. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_visible_child (HdyStackableBox *self, + GtkWidget *visible_child) +{ + HdyStackableBoxChildInfo *child_info; + gboolean contains_child; + + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + g_return_if_fail (GTK_IS_WIDGET (visible_child)); + + child_info = find_child_info_for_widget (self, visible_child); + contains_child = child_info != NULL; + + g_return_if_fail (contains_child); + + set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE); +} + +/** + * hdy_stackable_box_get_visible_child_name: + * @self: a #HdyStackableBox + * + * Gets the name of the currently visible child widget. + * + * Returns: (transfer none): the name of the visible child + * + * Since: 1.0 + */ +const gchar * +hdy_stackable_box_get_visible_child_name (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL); + + if (self->visible_child == NULL) + return NULL; + + return self->visible_child->name; +} + +/** + * hdy_stackable_box_set_visible_child_name: + * @self: a #HdyStackableBox + * @name: the name of a child + * + * Makes the child with the name @name visible. + * + * See hdy_stackable_box_set_visible_child() for more details. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_visible_child_name (HdyStackableBox *self, + const gchar *name) +{ + HdyStackableBoxChildInfo *child_info; + gboolean contains_child; + + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + g_return_if_fail (name != NULL); + + child_info = find_child_info_for_name (self, name); + contains_child = child_info != NULL; + + g_return_if_fail (contains_child); + + set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE); +} + +/** + * hdy_stackable_box_get_child_transition_running: + * @self: a #HdyStackableBox + * + * Returns whether @self is currently in a transition from one page to + * another. + * + * Returns: %TRUE if the transition is currently running, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_get_child_transition_running (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + return (self->child_transition.tick_id != 0 || + self->child_transition.is_gesture_active); +} + +/** + * hdy_stackable_box_set_interpolate_size: + * @self: a #HdyStackableBox + * @interpolate_size: the new value + * + * Sets whether or not @self will interpolate its size when + * changing the visible child. If the #HdyStackableBox:interpolate-size + * property is set to %TRUE, @self will interpolate its size between + * the current one and the one it'll take after changing the + * visible child, according to the set transition duration. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_interpolate_size (HdyStackableBox *self, + gboolean interpolate_size) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + interpolate_size = !!interpolate_size; + + if (self->child_transition.interpolate_size == interpolate_size) + return; + + self->child_transition.interpolate_size = interpolate_size; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]); +} + +/** + * hdy_stackable_box_get_interpolate_size: + * @self: a #HdyStackableBox + * + * Returns whether the #HdyStackableBox is set up to interpolate between + * the sizes of children on page switch. + * + * Returns: %TRUE if child sizes are interpolated + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_get_interpolate_size (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + return self->child_transition.interpolate_size; +} + +/** + * hdy_stackable_box_set_can_swipe_back: + * @self: a #HdyStackableBox + * @can_swipe_back: the new value + * + * Sets whether or not @self allows switching to the previous child that has + * 'navigatable' child property set to %TRUE via a swipe gesture + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_can_swipe_back (HdyStackableBox *self, + gboolean can_swipe_back) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + can_swipe_back = !!can_swipe_back; + + if (self->child_transition.can_swipe_back == can_swipe_back) + return; + + self->child_transition.can_swipe_back = can_swipe_back; + hdy_swipe_tracker_set_enabled (self->tracker, can_swipe_back || self->child_transition.can_swipe_forward); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_BACK]); +} + +/** + * hdy_stackable_box_get_can_swipe_back + * @self: a #HdyStackableBox + * + * Returns whether the #HdyStackableBox allows swiping to the previous child. + * + * Returns: %TRUE if back swipe is enabled. + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_get_can_swipe_back (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + return self->child_transition.can_swipe_back; +} + +/** + * hdy_stackable_box_set_can_swipe_forward: + * @self: a #HdyStackableBox + * @can_swipe_forward: the new value + * + * Sets whether or not @self allows switching to the next child that has + * 'navigatable' child property set to %TRUE via a swipe gesture. + * + * Since: 1.0 + */ +void +hdy_stackable_box_set_can_swipe_forward (HdyStackableBox *self, + gboolean can_swipe_forward) +{ + g_return_if_fail (HDY_IS_STACKABLE_BOX (self)); + + can_swipe_forward = !!can_swipe_forward; + + if (self->child_transition.can_swipe_forward == can_swipe_forward) + return; + + self->child_transition.can_swipe_forward = can_swipe_forward; + hdy_swipe_tracker_set_enabled (self->tracker, self->child_transition.can_swipe_back || can_swipe_forward); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_FORWARD]); +} + +/** + * hdy_stackable_box_get_can_swipe_forward + * @self: a #HdyStackableBox + * + * Returns whether the #HdyStackableBox allows swiping to the next child. + * + * Returns: %TRUE if forward swipe is enabled. + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_get_can_swipe_forward (HdyStackableBox *self) +{ + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + return self->child_transition.can_swipe_forward; +} + +static HdyStackableBoxChildInfo * +find_swipeable_child (HdyStackableBox *self, + HdyNavigationDirection direction) +{ + GList *children; + HdyStackableBoxChildInfo *child = NULL; + + children = g_list_find (self->children, self->visible_child); + do { + children = (direction == HDY_NAVIGATION_DIRECTION_BACK) ? children->prev : children->next; + + if (children == NULL) + break; + + child = children->data; + } while (child && !child->navigatable); + + return child; +} + +/** + * hdy_stackable_box_get_adjacent_child + * @self: a #HdyStackableBox + * @direction: the direction + * + * Gets the previous or next child that doesn't have 'navigatable' child + * property set to %FALSE, or %NULL if it doesn't exist. This will be the same + * widget hdy_stackable_box_navigate() will navigate to. + * + * Returns: (nullable) (transfer none): the previous or next child, or + * %NULL if it doesn't exist. + * + * Since: 1.0 + */ +GtkWidget * +hdy_stackable_box_get_adjacent_child (HdyStackableBox *self, + HdyNavigationDirection direction) +{ + HdyStackableBoxChildInfo *child; + + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL); + + child = find_swipeable_child (self, direction); + + if (!child) + return NULL; + + return child->widget; +} + +/** + * hdy_stackable_box_navigate + * @self: a #HdyStackableBox + * @direction: the direction + * + * Switches to the previous or next child that doesn't have 'navigatable' + * child property set to %FALSE, similar to performing a swipe gesture to go + * in @direction. + * + * Returns: %TRUE if visible child was changed, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_stackable_box_navigate (HdyStackableBox *self, + HdyNavigationDirection direction) +{ + HdyStackableBoxChildInfo *child; + + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE); + + child = find_swipeable_child (self, direction); + + if (!child) + return FALSE; + + set_visible_child_info (self, child, self->transition_type, self->child_transition.duration, TRUE); + + return TRUE; +} + +/** + * hdy_stackable_box_get_child_by_name: + * @self: a #HdyStackableBox + * @name: the name of the child to find + * + * Finds the child of @self with the name given as the argument. Returns %NULL + * if there is no child with this name. + * + * Returns: (transfer none) (nullable): the requested child of @self + * + * Since: 1.0 + */ +GtkWidget * +hdy_stackable_box_get_child_by_name (HdyStackableBox *self, + const gchar *name) +{ + HdyStackableBoxChildInfo *child_info; + + g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + child_info = find_child_info_for_name (self, name); + + return child_info ? child_info->widget : NULL; +} + +static void +get_preferred_size (gint *min, + gint *nat, + gboolean same_orientation, + gboolean homogeneous_folded, + gboolean homogeneous_unfolded, + gint visible_children, + gdouble visible_child_progress, + gint sum_nat, + gint max_min, + gint max_nat, + gint visible_min, + gint last_visible_min) +{ + if (same_orientation) { + *min = homogeneous_folded ? + max_min : + hdy_lerp (last_visible_min, visible_min, visible_child_progress); + *nat = homogeneous_unfolded ? + max_nat * visible_children : + sum_nat; + } + else { + *min = homogeneous_folded ? + max_min : + hdy_lerp (last_visible_min, visible_min, visible_child_progress); + *nat = max_nat; + } +} + +void +hdy_stackable_box_measure (HdyStackableBox *self, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GList *children; + HdyStackableBoxChildInfo *child_info; + gint visible_children; + gdouble visible_child_progress; + gint child_min, max_min, visible_min, last_visible_min; + gint child_nat, max_nat, sum_nat; + void (*get_preferred_size_static) (GtkWidget *widget, + gint *minimum_width, + gint *natural_width); + void (*get_preferred_size_for_size) (GtkWidget *widget, + gint height, + gint *minimum_width, + gint *natural_width); + + get_preferred_size_static = orientation == GTK_ORIENTATION_HORIZONTAL ? + gtk_widget_get_preferred_width : + gtk_widget_get_preferred_height; + get_preferred_size_for_size = orientation == GTK_ORIENTATION_HORIZONTAL ? + gtk_widget_get_preferred_width_for_height : + gtk_widget_get_preferred_height_for_width; + + visible_children = 0; + child_min = max_min = visible_min = last_visible_min = 0; + child_nat = max_nat = sum_nat = 0; + for (children = self->children; children; children = children->next) { + child_info = children->data; + + if (child_info->widget == NULL || !gtk_widget_get_visible (child_info->widget)) + continue; + + visible_children++; + if (for_size < 0) + get_preferred_size_static (child_info->widget, + &child_min, &child_nat); + else + get_preferred_size_for_size (child_info->widget, for_size, + &child_min, &child_nat); + + max_min = MAX (max_min, child_min); + max_nat = MAX (max_nat, child_nat); + sum_nat += child_nat; + } + + if (self->visible_child != NULL) { + if (for_size < 0) + get_preferred_size_static (self->visible_child->widget, + &visible_min, NULL); + else + get_preferred_size_for_size (self->visible_child->widget, for_size, + &visible_min, NULL); + } + + if (self->last_visible_child != NULL) { + if (for_size < 0) + get_preferred_size_static (self->last_visible_child->widget, + &last_visible_min, NULL); + else + get_preferred_size_for_size (self->last_visible_child->widget, for_size, + &last_visible_min, NULL); + } + + visible_child_progress = self->child_transition.interpolate_size ? self->child_transition.progress : 1.0; + + get_preferred_size (minimum, natural, + gtk_orientable_get_orientation (GTK_ORIENTABLE (self->container)) == orientation, + self->homogeneous[HDY_FOLD_FOLDED][orientation], + self->homogeneous[HDY_FOLD_UNFOLDED][orientation], + visible_children, visible_child_progress, + sum_nat, max_min, max_nat, visible_min, last_visible_min); +} + +static void +hdy_stackable_box_size_allocate_folded (HdyStackableBox *self, + GtkAllocation *allocation) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget)); + GList *directed_children, *children; + HdyStackableBoxChildInfo *child_info, *visible_child; + gint start_size, end_size, visible_size; + gint remaining_start_size, remaining_end_size, remaining_size; + gint current_pad; + gint max_child_size = 0; + gint start_position, end_position; + gboolean box_homogeneous; + HdyStackableBoxTransitionType mode_transition_type; + GtkTextDirection direction; + gboolean under; + + directed_children = get_directed_children (self); + visible_child = self->visible_child; + + if (!visible_child) + return; + + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + if (!child_info->widget) + continue; + + if (child_info->widget == visible_child->widget) + continue; + + if (self->last_visible_child && + child_info->widget == self->last_visible_child->widget) + continue; + + child_info->visible = FALSE; + } + + if (visible_child->widget == NULL) + return; + + /* FIXME is this needed? */ + if (!gtk_widget_get_visible (visible_child->widget)) { + visible_child->visible = FALSE; + + return; + } + + visible_child->visible = TRUE; + + mode_transition_type = self->transition_type; + + /* Avoid useless computations and allow visible child transitions. */ + if (self->mode_transition.current_pos <= 0.0) { + /* Child transitions should be applied only when folded and when no mode + * transition is ongoing. + */ + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + if (child_info != visible_child && + child_info != self->last_visible_child) { + child_info->visible = FALSE; + + continue; + } + + child_info->alloc.x = get_child_window_x (self, child_info, allocation->width); + child_info->alloc.y = get_child_window_y (self, child_info, allocation->height); + child_info->alloc.width = allocation->width; + child_info->alloc.height = allocation->height; + child_info->visible = TRUE; + } + + return; + } + + /* Compute visible child size. */ + visible_size = orientation == GTK_ORIENTATION_HORIZONTAL ? + MIN (allocation->width, MAX (visible_child->nat.width, (gint) (allocation->width * (1.0 - self->mode_transition.current_pos)))) : + MIN (allocation->height, MAX (visible_child->nat.height, (gint) (allocation->height * (1.0 - self->mode_transition.current_pos)))); + + /* Compute homogeneous box child size. */ + box_homogeneous = (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] && orientation == GTK_ORIENTATION_HORIZONTAL) || + (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] && orientation == GTK_ORIENTATION_VERTICAL); + if (box_homogeneous) { + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + max_child_size = orientation == GTK_ORIENTATION_HORIZONTAL ? + MAX (max_child_size, child_info->nat.width) : + MAX (max_child_size, child_info->nat.height); + } + } + + /* Compute the start size. */ + start_size = 0; + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + if (child_info == visible_child) + break; + + start_size += orientation == GTK_ORIENTATION_HORIZONTAL ? + (box_homogeneous ? max_child_size : child_info->nat.width) : + (box_homogeneous ? max_child_size : child_info->nat.height); + } + + /* Compute the end size. */ + end_size = 0; + for (children = g_list_last (directed_children); children; children = children->prev) { + child_info = children->data; + + if (child_info == visible_child) + break; + + end_size += orientation == GTK_ORIENTATION_HORIZONTAL ? + (box_homogeneous ? max_child_size : child_info->nat.width) : + (box_homogeneous ? max_child_size : child_info->nat.height); + } + + /* Compute pads. */ + remaining_size = orientation == GTK_ORIENTATION_HORIZONTAL ? + allocation->width - visible_size : + allocation->height - visible_size; + remaining_start_size = (gint) (remaining_size * ((gdouble) start_size / (gdouble) (start_size + end_size))); + remaining_end_size = remaining_size - remaining_start_size; + + /* Store start and end allocations. */ + switch (orientation) { + case GTK_ORIENTATION_HORIZONTAL: + direction = gtk_widget_get_direction (GTK_WIDGET (self->container)); + under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_LTR) || + (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_RTL); + start_position = under ? 0 : remaining_start_size - start_size; + self->mode_transition.start_progress = under ? (gdouble) remaining_size / start_size : 1; + under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_LTR) || + (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_RTL); + end_position = under ? allocation->width - end_size : remaining_start_size + visible_size; + self->mode_transition.end_progress = under ? (gdouble) remaining_end_size / end_size : 1; + break; + case GTK_ORIENTATION_VERTICAL: + under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER; + start_position = under ? 0 : remaining_start_size - start_size; + self->mode_transition.start_progress = under ? (gdouble) remaining_size / start_size : 1; + under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER; + end_position = remaining_start_size + visible_size; + self->mode_transition.end_progress = under ? (gdouble) remaining_end_size / end_size : 1; + break; + default: + g_assert_not_reached (); + } + + /* Allocate visible child. */ + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + visible_child->alloc.width = visible_size; + visible_child->alloc.height = allocation->height; + visible_child->alloc.x = remaining_start_size; + visible_child->alloc.y = 0; + visible_child->visible = TRUE; + } + else { + visible_child->alloc.width = allocation->width; + visible_child->alloc.height = visible_size; + visible_child->alloc.x = 0; + visible_child->alloc.y = remaining_start_size; + visible_child->visible = TRUE; + } + + /* Allocate starting children. */ + current_pad = start_position; + + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + if (child_info == visible_child) + break; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + child_info->alloc.width = box_homogeneous ? + max_child_size : + child_info->nat.width; + child_info->alloc.height = allocation->height; + child_info->alloc.x = current_pad; + child_info->alloc.y = 0; + child_info->visible = child_info->alloc.x + child_info->alloc.width > 0; + + current_pad += child_info->alloc.width; + } + else { + child_info->alloc.width = allocation->width; + child_info->alloc.height = box_homogeneous ? + max_child_size : + child_info->nat.height; + child_info->alloc.x = 0; + child_info->alloc.y = current_pad; + child_info->visible = child_info->alloc.y + child_info->alloc.height > 0; + + current_pad += child_info->alloc.height; + } + } + + /* Allocate ending children. */ + current_pad = end_position; + + if (!children || !children->next) + return; + + for (children = children->next; children; children = children->next) { + child_info = children->data; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + child_info->alloc.width = box_homogeneous ? + max_child_size : + child_info->nat.width; + child_info->alloc.height = allocation->height; + child_info->alloc.x = current_pad; + child_info->alloc.y = 0; + child_info->visible = child_info->alloc.x < allocation->width; + + current_pad += child_info->alloc.width; + } + else { + child_info->alloc.width = allocation->width; + child_info->alloc.height = box_homogeneous ? + max_child_size : + child_info->nat.height; + child_info->alloc.x = 0; + child_info->alloc.y = current_pad; + child_info->visible = child_info->alloc.y < allocation->height; + + current_pad += child_info->alloc.height; + } + } +} + +static void +hdy_stackable_box_size_allocate_unfolded (HdyStackableBox *self, + GtkAllocation *allocation) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget)); + GtkAllocation remaining_alloc; + GList *directed_children, *children; + HdyStackableBoxChildInfo *child_info, *visible_child; + gint homogeneous_size = 0, min_size, extra_size; + gint per_child_extra, n_extra_widgets; + gint n_visible_children, n_expand_children; + gint start_pad = 0, end_pad = 0; + gboolean box_homogeneous; + HdyStackableBoxTransitionType mode_transition_type; + GtkTextDirection direction; + gboolean under; + + directed_children = get_directed_children (self); + visible_child = self->visible_child; + + box_homogeneous = (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] && orientation == GTK_ORIENTATION_HORIZONTAL) || + (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] && orientation == GTK_ORIENTATION_VERTICAL); + + n_visible_children = n_expand_children = 0; + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + child_info->visible = child_info->widget != NULL && gtk_widget_get_visible (child_info->widget); + + if (child_info->visible) { + n_visible_children++; + if (gtk_widget_compute_expand (child_info->widget, orientation)) + n_expand_children++; + } + else { + child_info->min.width = child_info->min.height = 0; + child_info->nat.width = child_info->nat.height = 0; + } + } + + /* Compute repartition of extra space. */ + + if (box_homogeneous) { + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + homogeneous_size = n_visible_children > 0 ? allocation->width / n_visible_children : 0; + n_expand_children = n_visible_children > 0 ? allocation->width % n_visible_children : 0; + min_size = allocation->width - n_expand_children; + } + else { + homogeneous_size = n_visible_children > 0 ? allocation->height / n_visible_children : 0; + n_expand_children = n_visible_children > 0 ? allocation->height % n_visible_children : 0; + min_size = allocation->height - n_expand_children; + } + } + else { + min_size = 0; + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + min_size += child_info->nat.width; + } + } + else { + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + min_size += child_info->nat.height; + } + } + } + + remaining_alloc.x = 0; + remaining_alloc.y = 0; + remaining_alloc.width = allocation->width; + remaining_alloc.height = allocation->height; + + extra_size = orientation == GTK_ORIENTATION_HORIZONTAL ? + remaining_alloc.width - min_size : + remaining_alloc.height - min_size; + + per_child_extra = 0, n_extra_widgets = 0; + if (n_expand_children > 0) { + per_child_extra = extra_size / n_expand_children; + n_extra_widgets = extra_size % n_expand_children; + } + + /* Compute children allocation */ + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + if (!child_info->visible) + continue; + + child_info->alloc.x = remaining_alloc.x; + child_info->alloc.y = remaining_alloc.y; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + if (box_homogeneous) { + child_info->alloc.width = homogeneous_size; + if (n_extra_widgets > 0) { + child_info->alloc.width++; + n_extra_widgets--; + } + } + else { + child_info->alloc.width = child_info->nat.width; + if (gtk_widget_compute_expand (child_info->widget, orientation)) { + child_info->alloc.width += per_child_extra; + if (n_extra_widgets > 0) { + child_info->alloc.width++; + n_extra_widgets--; + } + } + } + child_info->alloc.height = remaining_alloc.height; + + remaining_alloc.x += child_info->alloc.width; + remaining_alloc.width -= child_info->alloc.width; + } + else { + if (box_homogeneous) { + child_info->alloc.height = homogeneous_size; + if (n_extra_widgets > 0) { + child_info->alloc.height++; + n_extra_widgets--; + } + } + else { + child_info->alloc.height = child_info->nat.height; + if (gtk_widget_compute_expand (child_info->widget, orientation)) { + child_info->alloc.height += per_child_extra; + if (n_extra_widgets > 0) { + child_info->alloc.height++; + n_extra_widgets--; + } + } + } + child_info->alloc.width = remaining_alloc.width; + + remaining_alloc.y += child_info->alloc.height; + remaining_alloc.height -= child_info->alloc.height; + } + } + + /* Apply animations. */ + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + start_pad = (gint) ((visible_child->alloc.x) * (1.0 - self->mode_transition.current_pos)); + end_pad = (gint) ((allocation->width - (visible_child->alloc.x + visible_child->alloc.width)) * (1.0 - self->mode_transition.current_pos)); + } + else { + start_pad = (gint) ((visible_child->alloc.y) * (1.0 - self->mode_transition.current_pos)); + end_pad = (gint) ((allocation->height - (visible_child->alloc.y + visible_child->alloc.height)) * (1.0 - self->mode_transition.current_pos)); + } + + mode_transition_type = self->transition_type; + direction = gtk_widget_get_direction (GTK_WIDGET (self->container)); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_LTR) || + (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_RTL); + else + under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER; + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + if (child_info == visible_child) + break; + + if (!child_info->visible) + continue; + + if (under) + continue; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + child_info->alloc.x -= start_pad; + else + child_info->alloc.y -= start_pad; + } + + self->mode_transition.start_progress = under ? self->mode_transition.current_pos : 1; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_LTR) || + (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_RTL); + else + under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER; + for (children = g_list_last (directed_children); children; children = children->prev) { + child_info = children->data; + + if (child_info == visible_child) + break; + + if (!child_info->visible) + continue; + + if (under) + continue; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + child_info->alloc.x += end_pad; + else + child_info->alloc.y += end_pad; + } + + self->mode_transition.end_progress = under ? self->mode_transition.current_pos : 1; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + visible_child->alloc.x -= start_pad; + visible_child->alloc.width += start_pad + end_pad; + } + else { + visible_child->alloc.y -= start_pad; + visible_child->alloc.height += start_pad + end_pad; + } +} + +static HdyStackableBoxChildInfo * +get_top_overlap_child (HdyStackableBox *self) +{ + gboolean is_rtl, start; + + if (!self->last_visible_child) + return self->visible_child; + + is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL; + + start = (self->child_transition.active_direction == GTK_PAN_DIRECTION_LEFT && !is_rtl) || + (self->child_transition.active_direction == GTK_PAN_DIRECTION_RIGHT && is_rtl) || + self->child_transition.active_direction == GTK_PAN_DIRECTION_UP; + + switch (self->transition_type) { + case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: + // Nothing overlaps in this case + return NULL; + case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: + return start ? self->visible_child : self->last_visible_child; + case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: + return start ? self->last_visible_child : self->visible_child; + default: + g_assert_not_reached (); + } +} + +static void +restack_windows (HdyStackableBox *self) +{ + HdyStackableBoxChildInfo *child_info, *overlap_child; + GList *l; + + overlap_child = get_top_overlap_child (self); + + switch (self->transition_type) { + case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: + // Nothing overlaps in this case + return; + case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: + for (l = g_list_last (self->children); l; l = l->prev) { + child_info = l->data; + + if (child_info->window) + gdk_window_raise (child_info->window); + + if (child_info == overlap_child) + break; + } + + break; + case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: + for (l = self->children; l; l = l->next) { + child_info = l->data; + + if (child_info->window) + gdk_window_raise (child_info->window); + + if (child_info == overlap_child) + break; + } + + break; + default: + g_assert_not_reached (); + } +} + +void +hdy_stackable_box_size_allocate (HdyStackableBox *self, + GtkAllocation *allocation) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget)); + GList *directed_children, *children; + HdyStackableBoxChildInfo *child_info; + gboolean folded; + + directed_children = get_directed_children (self); + + gtk_widget_set_allocation (widget, allocation); + + if (gtk_widget_get_realized (widget)) { + gdk_window_move_resize (self->view_window, + allocation->x, allocation->y, + allocation->width, allocation->height); + } + + /* Prepare children information. */ + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + gtk_widget_get_preferred_size (child_info->widget, &child_info->min, &child_info->nat); + child_info->alloc.x = child_info->alloc.y = child_info->alloc.width = child_info->alloc.height = 0; + child_info->visible = FALSE; + } + + /* Check whether the children should be stacked or not. */ + if (self->can_unfold) { + gint nat_box_size = 0, nat_max_size = 0, visible_children = 0; + + if (orientation == GTK_ORIENTATION_HORIZONTAL) { + + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + /* FIXME Check the child is visible. */ + if (!child_info->widget) + continue; + + if (child_info->nat.width <= 0) + continue; + + nat_box_size += child_info->nat.width; + nat_max_size = MAX (nat_max_size, child_info->nat.width); + visible_children++; + } + if (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL]) + nat_box_size = nat_max_size * visible_children; + folded = visible_children > 1 && allocation->width < nat_box_size; + } + else { + for (children = directed_children; children; children = children->next) { + child_info = children->data; + + /* FIXME Check the child is visible. */ + if (!child_info->widget) + continue; + + if (child_info->nat.height <= 0) + continue; + + nat_box_size += child_info->nat.height; + nat_max_size = MAX (nat_max_size, child_info->nat.height); + visible_children++; + } + if (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL]) + nat_box_size = nat_max_size * visible_children; + folded = visible_children > 1 && allocation->height < nat_box_size; + } + } else { + folded = TRUE; + } + + hdy_stackable_box_set_folded (self, folded); + + /* Allocate size to the children. */ + if (folded) + hdy_stackable_box_size_allocate_folded (self, allocation); + else + hdy_stackable_box_size_allocate_unfolded (self, allocation); + + /* Apply visibility and allocation. */ + for (children = directed_children; children; children = children->next) { + GtkAllocation alloc; + + child_info = children->data; + + gtk_widget_set_child_visible (child_info->widget, child_info->visible); + + if (child_info->window && + child_info->visible != gdk_window_is_visible (child_info->window)) { + if (child_info->visible) + gdk_window_show (child_info->window); + else + gdk_window_hide (child_info->window); + } + + if (!child_info->visible) + continue; + + if (child_info->window) + gdk_window_move_resize (child_info->window, + child_info->alloc.x, + child_info->alloc.y, + child_info->alloc.width, + child_info->alloc.height); + + alloc.x = 0; + alloc.y = 0; + alloc.width = child_info->alloc.width; + alloc.height = child_info->alloc.height; + gtk_widget_size_allocate (child_info->widget, &alloc); + + if (gtk_widget_get_realized (widget)) + gtk_widget_show (child_info->widget); + } + + restack_windows (self); +} + +gboolean +hdy_stackable_box_draw (HdyStackableBox *self, + cairo_t *cr) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GList *stacked_children, *l; + HdyStackableBoxChildInfo *child_info, *overlap_child; + gboolean is_transition; + gboolean is_vertical; + gboolean is_rtl; + gboolean is_over; + GtkAllocation shadow_rect; + gdouble shadow_progress, mode_progress; + GtkPanDirection shadow_direction; + + overlap_child = get_top_overlap_child (self); + + is_transition = self->child_transition.is_gesture_active || + gtk_progress_tracker_get_state (&self->child_transition.tracker) != GTK_PROGRESS_STATE_AFTER || + gtk_progress_tracker_get_state (&self->mode_transition.tracker) != GTK_PROGRESS_STATE_AFTER; + + if (!is_transition || + self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE || + !overlap_child) { + for (l = self->children; l; l = l->next) { + child_info = l->data; + + if (!gtk_cairo_should_draw_window (cr, child_info->window)) + continue; + + gtk_container_propagate_draw (self->container, + child_info->widget, + cr); + } + + return GDK_EVENT_PROPAGATE; + } + + stacked_children = self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ? + self->children_reversed : self->children; + + is_vertical = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget)) == GTK_ORIENTATION_VERTICAL; + is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL; + is_over = self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER; + + cairo_save (cr); + + shadow_rect.x = 0; + shadow_rect.y = 0; + shadow_rect.width = gtk_widget_get_allocated_width (widget); + shadow_rect.height = gtk_widget_get_allocated_height (widget); + + if (is_vertical) { + if (!is_over) { + shadow_rect.y = overlap_child->alloc.y + overlap_child->alloc.height; + shadow_rect.height -= shadow_rect.y; + shadow_direction = GTK_PAN_DIRECTION_UP; + mode_progress = self->mode_transition.end_progress; + } else { + shadow_rect.height = overlap_child->alloc.y; + shadow_direction = GTK_PAN_DIRECTION_DOWN; + mode_progress = self->mode_transition.start_progress; + } + } else { + if (is_over == is_rtl) { + shadow_rect.x = overlap_child->alloc.x + overlap_child->alloc.width; + shadow_rect.width -= shadow_rect.x; + shadow_direction = GTK_PAN_DIRECTION_LEFT; + mode_progress = self->mode_transition.end_progress; + } else { + shadow_rect.width = overlap_child->alloc.x; + shadow_direction = GTK_PAN_DIRECTION_RIGHT; + mode_progress = self->mode_transition.start_progress; + } + } + + if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) != GTK_PROGRESS_STATE_AFTER) { + shadow_progress = mode_progress; + } else { + GtkPanDirection direction = self->child_transition.active_direction; + GtkPanDirection left_or_right = is_rtl ? GTK_PAN_DIRECTION_RIGHT : GTK_PAN_DIRECTION_LEFT; + gint width = gtk_widget_get_allocated_width (widget); + gint height = gtk_widget_get_allocated_height (widget); + + if (direction == GTK_PAN_DIRECTION_UP || direction == left_or_right) + shadow_progress = self->child_transition.progress; + else + shadow_progress = 1 - self->child_transition.progress; + + if (is_over) + shadow_progress = 1 - shadow_progress; + + /* Normalize the shadow rect size so that we can cache the shadow */ + if (shadow_direction == GTK_PAN_DIRECTION_RIGHT) + shadow_rect.x -= (width - shadow_rect.width); + else if (shadow_direction == GTK_PAN_DIRECTION_DOWN) + shadow_rect.y -= (height - shadow_rect.height); + + shadow_rect.width = width; + shadow_rect.height = height; + } + + cairo_rectangle (cr, shadow_rect.x, shadow_rect.y, shadow_rect.width, shadow_rect.height); + cairo_clip (cr); + + for (l = stacked_children; l; l = l->next) { + child_info = l->data; + + if (!gtk_cairo_should_draw_window (cr, child_info->window)) + continue; + + if (child_info == overlap_child) + cairo_restore (cr); + + gtk_container_propagate_draw (self->container, + child_info->widget, + cr); + } + + cairo_save (cr); + cairo_translate (cr, shadow_rect.x, shadow_rect.y); + hdy_shadow_helper_draw_shadow (self->shadow_helper, cr, + shadow_rect.width, shadow_rect.height, + shadow_progress, shadow_direction); + cairo_restore (cr); + + return GDK_EVENT_PROPAGATE; +} + +static void +update_tracker_orientation (HdyStackableBox *self) +{ + gboolean reverse; + + reverse = (self->orientation == GTK_ORIENTATION_HORIZONTAL && + gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL); + + g_object_set (self->tracker, + "orientation", self->orientation, + "reversed", reverse, + NULL); +} + +void +hdy_stackable_box_direction_changed (HdyStackableBox *self, + GtkTextDirection previous_direction) +{ + update_tracker_orientation (self); +} + +static void +hdy_stackable_box_child_visibility_notify_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + HdyStackableBox *self = HDY_STACKABLE_BOX (user_data); + GtkWidget *widget = GTK_WIDGET (obj); + HdyStackableBoxChildInfo *child_info; + + child_info = find_child_info_for_widget (self, widget); + + if (self->visible_child == NULL && gtk_widget_get_visible (widget)) + set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE); + else if (self->visible_child == child_info && !gtk_widget_get_visible (widget)) + set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE); + + if (child_info == self->last_visible_child) { + gtk_widget_set_child_visible (self->last_visible_child->widget, !self->folded); + self->last_visible_child = NULL; + } +} + +static void +register_window (HdyStackableBox *self, + HdyStackableBoxChildInfo *child) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GdkWindowAttr attributes = { 0 }; + GdkWindowAttributesType attributes_mask; + + attributes.x = child->alloc.x; + attributes.y = child->alloc.y; + attributes.width = child->alloc.width; + attributes.height = child->alloc.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (widget); + attributes.event_mask = gtk_widget_get_events (widget); + attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL; + + attributes.event_mask = gtk_widget_get_events (widget) | + gtk_widget_get_events (child->widget); + + child->window = gdk_window_new (self->view_window, &attributes, attributes_mask); + gtk_widget_register_window (widget, child->window); + + gtk_widget_set_parent_window (child->widget, child->window); + + gdk_window_show (child->window); +} + +static void +unregister_window (HdyStackableBox *self, + HdyStackableBoxChildInfo *child) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + + if (!child->window) + return; + + gtk_widget_unregister_window (widget, child->window); + gdk_window_destroy (child->window); + child->window = NULL; +} + +void +hdy_stackable_box_add (HdyStackableBox *self, + GtkWidget *widget) +{ + HdyStackableBoxChildInfo *child_info; + + g_return_if_fail (gtk_widget_get_parent (widget) == NULL); + + child_info = g_new0 (HdyStackableBoxChildInfo, 1); + child_info->widget = widget; + child_info->navigatable = TRUE; + + self->children = g_list_append (self->children, child_info); + self->children_reversed = g_list_prepend (self->children_reversed, child_info); + + if (gtk_widget_get_realized (GTK_WIDGET (self->container))) + register_window (self, child_info); + + gtk_widget_set_child_visible (widget, FALSE); + gtk_widget_set_parent (widget, GTK_WIDGET (self->container)); + + g_signal_connect (widget, "notify::visible", + G_CALLBACK (hdy_stackable_box_child_visibility_notify_cb), self); + + if (hdy_stackable_box_get_visible_child (self) == NULL && + gtk_widget_get_visible (widget)) { + set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, FALSE); + } + + if (!self->folded || + (self->folded && (self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] || + self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] || + self->visible_child == child_info))) + gtk_widget_queue_resize (GTK_WIDGET (self->container)); +} + +void +hdy_stackable_box_remove (HdyStackableBox *self, + GtkWidget *widget) +{ + g_autoptr (HdyStackableBoxChildInfo) child_info = find_child_info_for_widget (self, widget); + gboolean contains_child = child_info != NULL; + + g_return_if_fail (contains_child); + + self->children = g_list_remove (self->children, child_info); + self->children_reversed = g_list_remove (self->children_reversed, child_info); + + g_signal_handlers_disconnect_by_func (widget, + hdy_stackable_box_child_visibility_notify_cb, + self); + + if (hdy_stackable_box_get_visible_child (self) == widget) + set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE); + + if (child_info == self->last_visible_child) + self->last_visible_child = NULL; + + if (gtk_widget_get_visible (widget)) + gtk_widget_queue_resize (GTK_WIDGET (self->container)); + + unregister_window (self, child_info); + + gtk_widget_unparent (widget); +} + +void +hdy_stackable_box_forall (HdyStackableBox *self, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + /* This shallow copy is needed when the callback changes the list while we are + * looping through it, for example by calling hdy_stackable_box_remove() on all + * children when destroying the HdyStackableBox_private_offset. + */ + g_autoptr (GList) children_copy = g_list_copy (self->children); + GList *children; + HdyStackableBoxChildInfo *child_info; + + for (children = children_copy; children; children = children->next) { + child_info = children->data; + + (* callback) (child_info->widget, callback_data); + } + + g_list_free (self->children_reversed); + self->children_reversed = g_list_copy (self->children); + self->children_reversed = g_list_reverse (self->children_reversed); +} + +static void +hdy_stackable_box_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyStackableBox *self = HDY_STACKABLE_BOX (object); + + switch (prop_id) { + case PROP_FOLDED: + g_value_set_boolean (value, hdy_stackable_box_get_folded (self)); + break; + case PROP_HHOMOGENEOUS_FOLDED: + g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL)); + break; + case PROP_VHOMOGENEOUS_FOLDED: + g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL)); + break; + case PROP_HHOMOGENEOUS_UNFOLDED: + g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL)); + break; + case PROP_VHOMOGENEOUS_UNFOLDED: + g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL)); + break; + case PROP_VISIBLE_CHILD: + g_value_set_object (value, hdy_stackable_box_get_visible_child (self)); + break; + case PROP_VISIBLE_CHILD_NAME: + g_value_set_string (value, hdy_stackable_box_get_visible_child_name (self)); + break; + case PROP_TRANSITION_TYPE: + g_value_set_enum (value, hdy_stackable_box_get_transition_type (self)); + break; + case PROP_MODE_TRANSITION_DURATION: + g_value_set_uint (value, hdy_stackable_box_get_mode_transition_duration (self)); + break; + case PROP_CHILD_TRANSITION_DURATION: + g_value_set_uint (value, hdy_stackable_box_get_child_transition_duration (self)); + break; + case PROP_CHILD_TRANSITION_RUNNING: + g_value_set_boolean (value, hdy_stackable_box_get_child_transition_running (self)); + break; + case PROP_INTERPOLATE_SIZE: + g_value_set_boolean (value, hdy_stackable_box_get_interpolate_size (self)); + break; + case PROP_CAN_SWIPE_BACK: + g_value_set_boolean (value, hdy_stackable_box_get_can_swipe_back (self)); + break; + case PROP_CAN_SWIPE_FORWARD: + g_value_set_boolean (value, hdy_stackable_box_get_can_swipe_forward (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, hdy_stackable_box_get_orientation (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_stackable_box_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyStackableBox *self = HDY_STACKABLE_BOX (object); + + switch (prop_id) { + case PROP_HHOMOGENEOUS_FOLDED: + hdy_stackable_box_set_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value)); + break; + case PROP_VHOMOGENEOUS_FOLDED: + hdy_stackable_box_set_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value)); + break; + case PROP_HHOMOGENEOUS_UNFOLDED: + hdy_stackable_box_set_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value)); + break; + case PROP_VHOMOGENEOUS_UNFOLDED: + hdy_stackable_box_set_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value)); + break; + case PROP_VISIBLE_CHILD: + hdy_stackable_box_set_visible_child (self, g_value_get_object (value)); + break; + case PROP_VISIBLE_CHILD_NAME: + hdy_stackable_box_set_visible_child_name (self, g_value_get_string (value)); + break; + case PROP_TRANSITION_TYPE: + hdy_stackable_box_set_transition_type (self, g_value_get_enum (value)); + break; + case PROP_MODE_TRANSITION_DURATION: + hdy_stackable_box_set_mode_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_CHILD_TRANSITION_DURATION: + hdy_stackable_box_set_child_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_INTERPOLATE_SIZE: + hdy_stackable_box_set_interpolate_size (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_BACK: + hdy_stackable_box_set_can_swipe_back (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SWIPE_FORWARD: + hdy_stackable_box_set_can_swipe_forward (self, g_value_get_boolean (value)); + break; + case PROP_ORIENTATION: + hdy_stackable_box_set_orientation (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_stackable_box_finalize (GObject *object) +{ + HdyStackableBox *self = HDY_STACKABLE_BOX (object); + + self->visible_child = NULL; + + if (self->shadow_helper) + g_clear_object (&self->shadow_helper); + + hdy_stackable_box_unschedule_child_ticks (self); + + G_OBJECT_CLASS (hdy_stackable_box_parent_class)->finalize (object); +} + +void +hdy_stackable_box_realize (HdyStackableBox *self) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GtkAllocation allocation; + GdkWindowAttr attributes = { 0 }; + GdkWindowAttributesType attributes_mask; + GList *children; + + gtk_widget_set_realized (widget, TRUE); + gtk_widget_set_window (widget, g_object_ref (gtk_widget_get_parent_window (widget))); + + gtk_widget_get_allocation (widget, &allocation); + + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gtk_widget_get_visual (widget); + attributes.event_mask = gtk_widget_get_events (widget); + attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL; + + self->view_window = gdk_window_new (gtk_widget_get_window (widget), + &attributes, attributes_mask); + gtk_widget_register_window (widget, self->view_window); + + for (children = self->children; children != NULL; children = children->next) + register_window (self, children->data); +} + +void +hdy_stackable_box_unrealize (HdyStackableBox *self) +{ + GtkWidget *widget = GTK_WIDGET (self->container); + GList *children; + + for (children = self->children; children != NULL; children = children->next) + unregister_window (self, children->data); + + gtk_widget_unregister_window (widget, self->view_window); + gdk_window_destroy (self->view_window); + self->view_window = NULL; + + GTK_WIDGET_CLASS (self->klass)->unrealize (widget); +} + +void +hdy_stackable_box_map (HdyStackableBox *self) +{ + GTK_WIDGET_CLASS (self->klass)->map (GTK_WIDGET (self->container)); + + gdk_window_show (self->view_window); +} + +void +hdy_stackable_box_unmap (HdyStackableBox *self) +{ + gdk_window_hide (self->view_window); + + GTK_WIDGET_CLASS (self->klass)->unmap (GTK_WIDGET (self->container)); +} + +HdySwipeTracker * +hdy_stackable_box_get_swipe_tracker (HdyStackableBox *self) +{ + return self->tracker; +} + +gdouble +hdy_stackable_box_get_distance (HdyStackableBox *self) +{ + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) + return gtk_widget_get_allocated_width (GTK_WIDGET (self->container)); + else + return gtk_widget_get_allocated_height (GTK_WIDGET (self->container)); +} + +static gboolean +can_swipe_in_direction (HdyStackableBox *self, + HdyNavigationDirection direction) +{ + switch (direction) { + case HDY_NAVIGATION_DIRECTION_BACK: + return self->child_transition.can_swipe_back; + case HDY_NAVIGATION_DIRECTION_FORWARD: + return self->child_transition.can_swipe_forward; + default: + g_assert_not_reached (); + } +} + +gdouble * +hdy_stackable_box_get_snap_points (HdyStackableBox *self, + gint *n_snap_points) +{ + gint n; + gdouble *points, lower, upper; + + if (self->child_transition.tick_id > 0 || + self->child_transition.is_gesture_active) { + gint current_direction; + gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL; + + switch (self->child_transition.active_direction) { + case GTK_PAN_DIRECTION_UP: + current_direction = 1; + break; + case GTK_PAN_DIRECTION_DOWN: + current_direction = -1; + break; + case GTK_PAN_DIRECTION_LEFT: + current_direction = is_rtl ? -1 : 1; + break; + case GTK_PAN_DIRECTION_RIGHT: + current_direction = is_rtl ? 1 : -1; + break; + default: + g_assert_not_reached (); + } + + lower = MIN (0, current_direction); + upper = MAX (0, current_direction); + } else { + HdyStackableBoxChildInfo *child = NULL; + + if ((can_swipe_in_direction (self, self->child_transition.swipe_direction) || + !self->child_transition.is_direct_swipe) && self->folded) + child = find_swipeable_child (self, self->child_transition.swipe_direction); + + lower = MIN (0, child ? self->child_transition.swipe_direction : 0); + upper = MAX (0, child ? self->child_transition.swipe_direction : 0); + } + + n = (lower != upper) ? 2 : 1; + + points = g_new0 (gdouble, n); + points[0] = lower; + points[n - 1] = upper; + + if (n_snap_points) + *n_snap_points = n; + + return points; +} + +gdouble +hdy_stackable_box_get_progress (HdyStackableBox *self) +{ + gboolean new_first = FALSE; + GList *children; + + if (!self->child_transition.is_gesture_active && + gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) + return 0; + + for (children = self->children; children; children = children->next) { + if (self->last_visible_child == children->data) { + new_first = TRUE; + + break; + } + if (self->visible_child == children->data) + break; + } + + return self->child_transition.progress * (new_first ? 1 : -1); +} + +gdouble +hdy_stackable_box_get_cancel_progress (HdyStackableBox *self) +{ + return 0; +} + +void +hdy_stackable_box_get_swipe_area (HdyStackableBox *self, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect) +{ + gint width = gtk_widget_get_allocated_width (GTK_WIDGET (self->container)); + gint height = gtk_widget_get_allocated_height (GTK_WIDGET (self->container)); + gdouble progress = 0; + + rect->x = 0; + rect->y = 0; + rect->width = width; + rect->height = height; + + if (!is_drag) + return; + + if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) + return; + + if (self->child_transition.is_gesture_active || + gtk_progress_tracker_get_state (&self->child_transition.tracker) != GTK_PROGRESS_STATE_AFTER) + progress = self->child_transition.progress; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL; + + if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && + navigation_direction == HDY_NAVIGATION_DIRECTION_FORWARD) { + rect->width = MAX (progress * width, HDY_SWIPE_BORDER); + rect->x = is_rtl ? 0 : width - rect->width; + } else if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && + navigation_direction == HDY_NAVIGATION_DIRECTION_BACK) { + rect->width = MAX (progress * width, HDY_SWIPE_BORDER); + rect->x = is_rtl ? width - rect->width : 0; + } + } else { + if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && + navigation_direction == HDY_NAVIGATION_DIRECTION_FORWARD) { + rect->height = MAX (progress * height, HDY_SWIPE_BORDER); + rect->y = height - rect->height; + } else if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && + navigation_direction == HDY_NAVIGATION_DIRECTION_BACK) { + rect->height = MAX (progress * height, HDY_SWIPE_BORDER); + rect->y = 0; + } + } +} + +void +hdy_stackable_box_switch_child (HdyStackableBox *self, + guint index, + gint64 duration) +{ + HdyStackableBoxChildInfo *child_info = NULL; + GList *children; + guint i = 0; + + for (children = self->children; children; children = children->next) { + child_info = children->data; + + if (!child_info->navigatable) + continue; + + if (i == index) + break; + + i++; + } + + if (child_info == NULL) { + g_critical ("Couldn't find eligible child with index %u", index); + return; + } + + set_visible_child_info (self, child_info, self->transition_type, + duration, FALSE); +} + +static void +begin_swipe_cb (HdySwipeTracker *tracker, + HdyNavigationDirection direction, + gboolean direct, + HdyStackableBox *self) +{ + self->child_transition.is_direct_swipe = direct; + self->child_transition.swipe_direction = direction; + + if (self->child_transition.tick_id > 0) { + gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), + self->child_transition.tick_id); + self->child_transition.tick_id = 0; + self->child_transition.is_gesture_active = TRUE; + self->child_transition.is_cancelled = FALSE; + } else { + HdyStackableBoxChildInfo *child; + + if ((can_swipe_in_direction (self, direction) || !direct) && self->folded) + child = find_swipeable_child (self, direction); + else + child = NULL; + + if (child) { + self->child_transition.is_gesture_active = TRUE; + set_visible_child_info (self, child, self->transition_type, + self->child_transition.duration, FALSE); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]); + } + } +} + +static void +update_swipe_cb (HdySwipeTracker *tracker, + gdouble progress, + HdyStackableBox *self) +{ + self->child_transition.progress = ABS (progress); + hdy_stackable_box_child_progress_updated (self); +} + +static void +end_swipe_cb (HdySwipeTracker *tracker, + gint64 duration, + gdouble to, + HdyStackableBox *self) +{ + if (!self->child_transition.is_gesture_active) + return; + + self->child_transition.start_progress = self->child_transition.progress; + self->child_transition.end_progress = ABS (to); + self->child_transition.is_cancelled = (to == 0); + self->child_transition.first_frame_skipped = TRUE; + + hdy_stackable_box_schedule_child_ticks (self); + if (hdy_get_enable_animations (GTK_WIDGET (self->container)) && duration != 0) { + gtk_progress_tracker_start (&self->child_transition.tracker, + duration * 1000, + 0, + 1.0); + } else { + self->child_transition.progress = self->child_transition.end_progress; + gtk_progress_tracker_finish (&self->child_transition.tracker); + } + + self->child_transition.is_gesture_active = FALSE; + hdy_stackable_box_child_progress_updated (self); + + gtk_widget_queue_draw (GTK_WIDGET (self->container)); +} + +GtkOrientation +hdy_stackable_box_get_orientation (HdyStackableBox *self) +{ + return self->orientation; +} + +void +hdy_stackable_box_set_orientation (HdyStackableBox *self, + GtkOrientation orientation) +{ + if (self->orientation == orientation) + return; + + self->orientation = orientation; + update_tracker_orientation (self); + gtk_widget_queue_resize (GTK_WIDGET (self->container)); + g_object_notify (G_OBJECT (self), "orientation"); +} + +const gchar * +hdy_stackable_box_get_child_name (HdyStackableBox *self, + GtkWidget *widget) +{ + HdyStackableBoxChildInfo *child_info; + + child_info = find_child_info_for_widget (self, widget); + + g_return_val_if_fail (child_info != NULL, NULL); + + return child_info->name; +} + +void +hdy_stackable_box_set_child_name (HdyStackableBox *self, + GtkWidget *widget, + const gchar *name) +{ + HdyStackableBoxChildInfo *child_info; + HdyStackableBoxChildInfo *child_info2; + GList *children; + + child_info = find_child_info_for_widget (self, widget); + + g_return_if_fail (child_info != NULL); + + for (children = self->children; children; children = children->next) { + child_info2 = children->data; + + if (child_info == child_info2) + continue; + if (g_strcmp0 (child_info2->name, name) == 0) { + g_warning ("Duplicate child name in HdyStackableBox: %s", name); + + break; + } + } + + g_free (child_info->name); + child_info->name = g_strdup (name); + + if (self->visible_child == child_info) + g_object_notify_by_pspec (G_OBJECT (self), + props[PROP_VISIBLE_CHILD_NAME]); +} + +gboolean +hdy_stackable_box_get_child_navigatable (HdyStackableBox *self, + GtkWidget *widget) +{ + HdyStackableBoxChildInfo *child_info; + + child_info = find_child_info_for_widget (self, widget); + + g_return_val_if_fail (child_info != NULL, FALSE); + + return child_info->navigatable; +} + +void +hdy_stackable_box_set_child_navigatable (HdyStackableBox *self, + GtkWidget *widget, + gboolean navigatable) +{ + HdyStackableBoxChildInfo *child_info; + + child_info = find_child_info_for_widget (self, widget); + + g_return_if_fail (child_info != NULL); + + child_info->navigatable = navigatable; + + if (!child_info->navigatable && + hdy_stackable_box_get_visible_child (self) == widget) + set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE); +} + +static void +hdy_stackable_box_class_init (HdyStackableBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = hdy_stackable_box_get_property; + object_class->set_property = hdy_stackable_box_set_property; + object_class->finalize = hdy_stackable_box_finalize; + + /** + * HdyStackableBox:folded: + * + * %TRUE if the widget is folded. + * + * The #HdyStackableBox will be folded if the size allocated to it is smaller + * than the sum of the natural size of its children, it will be unfolded + * otherwise. + */ + props[PROP_FOLDED] = + g_param_spec_boolean ("folded", + _("Folded"), + _("Whether the widget is folded"), + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:hhomogeneous_folded: + * + * %TRUE if the widget allocates the same width for all children when folded. + */ + props[PROP_HHOMOGENEOUS_FOLDED] = + g_param_spec_boolean ("hhomogeneous-folded", + _("Horizontally homogeneous folded"), + _("Horizontally homogeneous sizing when the widget is folded"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:vhomogeneous_folded: + * + * %TRUE if the widget allocates the same height for all children when folded. + */ + props[PROP_VHOMOGENEOUS_FOLDED] = + g_param_spec_boolean ("vhomogeneous-folded", + _("Vertically homogeneous folded"), + _("Vertically homogeneous sizing when the widget is folded"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:hhomogeneous_unfolded: + * + * %TRUE if the widget allocates the same width for all children when unfolded. + */ + props[PROP_HHOMOGENEOUS_UNFOLDED] = + g_param_spec_boolean ("hhomogeneous-unfolded", + _("Box horizontally homogeneous"), + _("Horizontally homogeneous sizing when the widget is unfolded"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:vhomogeneous_unfolded: + * + * %TRUE if the widget allocates the same height for all children when unfolded. + */ + props[PROP_VHOMOGENEOUS_UNFOLDED] = + g_param_spec_boolean ("vhomogeneous-unfolded", + _("Box vertically homogeneous"), + _("Vertically homogeneous sizing when the widget is unfolded"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_VISIBLE_CHILD] = + g_param_spec_object ("visible-child", + _("Visible child"), + _("The widget currently visible when the widget is folded"), + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_VISIBLE_CHILD_NAME] = + g_param_spec_string ("visible-child-name", + _("Name of visible child"), + _("The name of the widget currently visible when the children are stacked"), + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:transition-type: + * + * The type of animation that will be used for transitions between modes and + * children. + * + * The transition type can be changed without problems at runtime, so it is + * possible to change the animation based on the mode or child that is about + * to become current. + * + * Since: 1.0 + */ + props[PROP_TRANSITION_TYPE] = + g_param_spec_enum ("transition-type", + _("Transition type"), + _("The type of animation used to transition between modes and children"), + HDY_TYPE_STACKABLE_BOX_TRANSITION_TYPE, HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_MODE_TRANSITION_DURATION] = + g_param_spec_uint ("mode-transition-duration", + _("Mode transition duration"), + _("The mode transition animation duration, in milliseconds"), + 0, G_MAXUINT, 250, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_CHILD_TRANSITION_DURATION] = + g_param_spec_uint ("child-transition-duration", + _("Child transition duration"), + _("The child transition animation duration, in milliseconds"), + 0, G_MAXUINT, 200, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_CHILD_TRANSITION_RUNNING] = + g_param_spec_boolean ("child-transition-running", + _("Child transition running"), + _("Whether or not the child transition is currently running"), + FALSE, + G_PARAM_READABLE); + + props[PROP_INTERPOLATE_SIZE] = + g_param_spec_boolean ("interpolate-size", + _("Interpolate size"), + _("Whether or not the size should smoothly change when changing between differently sized children"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:can-swipe-back: + * + * Whether or not the widget allows switching to the previous child that has + * 'navigatable' child property set to %TRUE via a swipe gesture. + * + * Since: 1.0 + */ + props[PROP_CAN_SWIPE_BACK] = + g_param_spec_boolean ("can-swipe-back", + _("Can swipe back"), + _("Whether or not swipe gesture can be used to switch to the previous child"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdyStackableBox:can-swipe-forward: + * + * Whether or not the widget allows switching to the next child that has + * 'navigatable' child property set to %TRUE via a swipe gesture. + * + * Since: 1.0 + */ + props[PROP_CAN_SWIPE_FORWARD] = + g_param_spec_boolean ("can-swipe-forward", + _("Can swipe forward"), + _("Whether or not swipe gesture can be used to switch to the next child"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_ORIENTATION] = + g_param_spec_enum ("orientation", + _("Orientation"), + _("Orientation"), + GTK_TYPE_ORIENTATION, + GTK_ORIENTATION_HORIZONTAL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +HdyStackableBox * +hdy_stackable_box_new (GtkContainer *container, + GtkContainerClass *klass, + gboolean can_unfold) +{ + GtkWidget *widget; + HdyStackableBox *self; + + g_return_val_if_fail (GTK_IS_CONTAINER (container), NULL); + g_return_val_if_fail (GTK_IS_ORIENTABLE (container), NULL); + g_return_val_if_fail (GTK_IS_CONTAINER_CLASS (klass), NULL); + + widget = GTK_WIDGET (container); + self = g_object_new (HDY_TYPE_STACKABLE_BOX, NULL); + + self->container = container; + self->klass = klass; + self->can_unfold = can_unfold; + + self->children = NULL; + self->children_reversed = NULL; + self->visible_child = NULL; + self->folded = FALSE; + self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] = FALSE; + self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] = FALSE; + self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] = TRUE; + self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] = TRUE; + self->transition_type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER; + self->mode_transition.duration = 250; + self->child_transition.duration = 200; + self->mode_transition.current_pos = 1.0; + self->mode_transition.target_pos = 1.0; + + self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self->container)); + + g_object_set (self->tracker, "orientation", self->orientation, "enabled", FALSE, NULL); + + g_signal_connect_object (self->tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self, 0); + g_signal_connect_object (self->tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self, 0); + g_signal_connect_object (self->tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self, 0); + + self->shadow_helper = hdy_shadow_helper_new (widget); + + gtk_widget_set_has_window (widget, FALSE); + gtk_widget_set_can_focus (widget, FALSE); + gtk_widget_set_redraw_on_allocate (widget, FALSE); + + if (can_unfold) { + GtkStyleContext *context = gtk_widget_get_style_context (widget); + gtk_style_context_add_class (context, "unfolded"); + } + + return self; +} + +static void +hdy_stackable_box_init (HdyStackableBox *self) +{ +} diff --git a/subprojects/libhandy/src/hdy-swipe-group.c b/subprojects/libhandy/src/hdy-swipe-group.c new file mode 100644 index 0000000..2779a31 --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipe-group.c @@ -0,0 +1,568 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-swipe-group.h" +#include <gtk/gtk.h> +#include "hdy-navigation-direction.h" +#include "hdy-swipe-tracker-private.h" + +#define BUILDABLE_TAG_OBJECT "object" +#define BUILDABLE_TAG_SWIPEABLE "swipeable" +#define BUILDABLE_TAG_SWIPEABLES "swipeables" +#define BUILDABLE_TAG_TEMPLATE "template" + +/** + * SECTION:hdy-swipe-group + * @short_description: An object for syncing swipeable widgets. + * @title: HdySwipeGroup + * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable + * + * The #HdySwipeGroup object can be used to sync multiple swipeable widgets + * that implement the #HdySwipeable interface, such as #HdyCarousel, so that + * animating one of them also animates all the other widgets in the group. + * + * This can be useful for syncing widgets between a window's titlebar and + * content area. + * + * # #HdySwipeGroup as #GtkBuildable + * + * #HdySwipeGroup can be created in an UI definition. The list of swipeable + * widgets is specified with a <swipeables> element containing multiple + * <swipeable> elements with their ”name” attribute specifying the id of + * the widgets. + * + * |[ + * <object class="HdySwipeGroup"> + * <swipeables> + * <swipeable name="carousel1"/> + * <swipeable name="carousel2"/> + * </swipeables> + * </object> + * ]| + * + * Since: 0.0.12 + */ + +struct _HdySwipeGroup +{ + GObject parent_instance; + + GSList *swipeables; + HdySwipeable *current; + gboolean block; +}; + +static void hdy_swipe_group_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdySwipeGroup, hdy_swipe_group, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + hdy_swipe_group_buildable_init)) + +static gboolean +contains (HdySwipeGroup *self, + HdySwipeable *swipeable) +{ + GSList *swipeables; + + for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next) + if (swipeables->data == swipeable) + return TRUE; + + return FALSE; +} + +static void +swipeable_destroyed (HdySwipeGroup *self, + HdySwipeable *swipeable) +{ + g_return_if_fail (HDY_IS_SWIPE_GROUP (self)); + + self->swipeables = g_slist_remove (self->swipeables, swipeable); + + g_object_unref (self); +} + +/** + * hdy_swipe_group_new: + * + * Create a new #HdySwipeGroup object. + * + * Returns: The newly created #HdySwipeGroup object + * + * Since: 0.0.12 + */ +HdySwipeGroup * +hdy_swipe_group_new (void) +{ + return g_object_new (HDY_TYPE_SWIPE_GROUP, NULL); +} + +static void +child_switched_cb (HdySwipeGroup *self, + guint index, + gint64 duration, + HdySwipeable *swipeable) +{ + GSList *swipeables; + + if (self->block) + return; + + if (self->current != NULL && self->current != swipeable) + return; + + self->block = TRUE; + + for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next) + if (swipeables->data != swipeable) + hdy_swipeable_switch_child (swipeables->data, index, duration); + + self->block = FALSE; +} + +static void +begin_swipe_cb (HdySwipeGroup *self, + HdyNavigationDirection direction, + gboolean direct, + HdySwipeTracker *tracker) +{ + HdySwipeable *swipeable; + GSList *swipeables; + + if (self->block) + return; + + swipeable = hdy_swipe_tracker_get_swipeable (tracker); + + if (self->current != NULL && self->current != swipeable) + return; + + self->current = swipeable; + + self->block = TRUE; + + for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next) + if (swipeables->data != swipeable) + hdy_swipe_tracker_emit_begin_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data), + direction, FALSE); + + self->block = FALSE; +} + +static void +update_swipe_cb (HdySwipeGroup *self, + gdouble progress, + HdySwipeTracker *tracker) +{ + HdySwipeable *swipeable; + GSList *swipeables; + + if (self->block) + return; + + swipeable = hdy_swipe_tracker_get_swipeable (tracker); + + if (swipeable != self->current) + return; + + self->block = TRUE; + + for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next) + if (swipeables->data != swipeable) + hdy_swipe_tracker_emit_update_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data), + progress); + + self->block = FALSE; +} + +static void +end_swipe_cb (HdySwipeGroup *self, + gint64 duration, + gdouble to, + HdySwipeTracker *tracker) +{ + HdySwipeable *swipeable; + GSList *swipeables; + + if (self->block) + return; + + swipeable = hdy_swipe_tracker_get_swipeable (tracker); + + if (swipeable != self->current) + return; + + self->block = TRUE; + + for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next) + if (swipeables->data != swipeable) + hdy_swipe_tracker_emit_end_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data), + duration, to); + + self->current = NULL; + + self->block = FALSE; +} + +/** + * hdy_swipe_group_add_swipeable: + * @self: a #HdySwipeGroup + * @swipeable: the #HdySwipeable to add + * + * When the widget is destroyed or no longer referenced elsewhere, it will + * be removed from the swipe group. + * + * Since: 0.0.12 + */ +void +hdy_swipe_group_add_swipeable (HdySwipeGroup *self, + HdySwipeable *swipeable) +{ + HdySwipeTracker *tracker; + + g_return_if_fail (HDY_IS_SWIPE_GROUP (self)); + g_return_if_fail (HDY_IS_SWIPEABLE (swipeable)); + + tracker = hdy_swipeable_get_swipe_tracker (swipeable); + + g_return_if_fail (HDY_IS_SWIPE_TRACKER (tracker)); + + g_signal_connect_swapped (swipeable, "child-switched", G_CALLBACK (child_switched_cb), self); + g_signal_connect_swapped (tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self); + g_signal_connect_swapped (tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self); + g_signal_connect_swapped (tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self); + + self->swipeables = g_slist_prepend (self->swipeables, swipeable); + + g_object_ref (self); + + g_signal_connect_swapped (swipeable, "destroy", G_CALLBACK (swipeable_destroyed), self); +} + + +/** + * hdy_swipe_group_remove_swipeable: + * @self: a #HdySwipeGroup + * @swipeable: the #HdySwipeable to remove + * + * Removes a widget from a #HdySwipeGroup. + * + * Since: 0.0.12 + **/ +void +hdy_swipe_group_remove_swipeable (HdySwipeGroup *self, + HdySwipeable *swipeable) +{ + HdySwipeTracker *tracker; + + g_return_if_fail (HDY_IS_SWIPE_GROUP (self)); + g_return_if_fail (HDY_IS_SWIPEABLE (swipeable)); + g_return_if_fail (contains (self, swipeable)); + + tracker = hdy_swipeable_get_swipe_tracker (swipeable); + + self->swipeables = g_slist_remove (self->swipeables, swipeable); + + g_signal_handlers_disconnect_by_data (swipeable, self); + g_signal_handlers_disconnect_by_data (tracker, self); + + g_object_unref (self); +} + + +/** + * hdy_swipe_group_get_swipeables: + * @self: a #HdySwipeGroup + * + * Returns the list of swipeables associated with @self. + * + * Returns: (element-type HdySwipeable) (transfer none): a #GSList of + * swipeables. The list is owned by libhandy and should not be modified. + * + * Since: 0.0.12 + **/ +GSList * +hdy_swipe_group_get_swipeables (HdySwipeGroup *self) +{ + g_return_val_if_fail (HDY_IS_SWIPE_GROUP (self), NULL); + + return self->swipeables; +} + +typedef struct { + gchar *name; + gint line; + gint col; +} ItemData; + +static void +item_data_free (gpointer data) +{ + ItemData *item_data = data; + + g_free (item_data->name); + g_free (item_data); +} + +typedef struct { + GObject *object; + GtkBuilder *builder; + GSList *items; +} GSListSubParserData; + +static void +hdy_swipe_group_dispose (GObject *object) +{ + HdySwipeGroup *self = (HdySwipeGroup *)object; + + g_slist_free_full (self->swipeables, (GDestroyNotify) g_object_unref); + self->swipeables = NULL; + + G_OBJECT_CLASS (hdy_swipe_group_parent_class)->dispose (object); +} + +/*< private > + * @builder: a #GtkBuilder + * @context: the #GMarkupParseContext + * @parent_name: the name of the expected parent element + * @error: return location for an error + * + * Checks that the parent element of the currently handled + * start tag is @parent_name and set @error if it isn't. + * + * This is intended to be called in start_element vfuncs to + * ensure that element nesting is as intended. + * + * Returns: %TRUE if @parent_name is the parent element + */ +/* This has been copied and modified from gtkbuilder.c. */ +static gboolean +_gtk_builder_check_parent (GtkBuilder *builder, + GMarkupParseContext *context, + const gchar *parent_name, + GError **error) +{ + const GSList *stack; + gint line, col; + const gchar *parent; + const gchar *element; + + stack = g_markup_parse_context_get_element_stack (context); + + element = (const gchar *)stack->data; + parent = stack->next ? (const gchar *)stack->next->data : ""; + + if (g_str_equal (parent_name, parent) || + (g_str_equal (parent_name, BUILDABLE_TAG_OBJECT) && + g_str_equal (parent, BUILDABLE_TAG_TEMPLATE))) + return TRUE; + + g_markup_parse_context_get_position (context, &line, &col); + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_INVALID_TAG, + ".:%d:%d Can't use <%s> here", + line, col, element); + + return FALSE; +} + +/*< private > + * _gtk_builder_prefix_error: + * @builder: a #GtkBuilder + * @context: the #GMarkupParseContext + * @error: an error + * + * Calls g_prefix_error() to prepend a filename:line:column marker + * to the given error. The filename is taken from @builder, and + * the line and column are obtained by calling + * g_markup_parse_context_get_position(). + * + * This is intended to be called on errors returned by + * g_markup_collect_attributes() in a start_element vfunc. + */ +/* This has been copied and modified from gtkbuilder.c. */ +static void +_gtk_builder_prefix_error (GtkBuilder *builder, + GMarkupParseContext *context, + GError **error) +{ + gint line, col; + + g_markup_parse_context_get_position (context, &line, &col); + g_prefix_error (error, ".:%d:%d ", line, col); +} + +/*< private > + * _gtk_builder_error_unhandled_tag: + * @builder: a #GtkBuilder + * @context: the #GMarkupParseContext + * @object: name of the object that is being handled + * @element_name: name of the element whose start tag is being handled + * @error: return location for the error + * + * Sets @error to a suitable error indicating that an @element_name + * tag is not expected in the custom markup for @object. + * + * This is intended to be called in a start_element vfunc. + */ +/* This has been copied and modified from gtkbuilder.c. */ +static void +_gtk_builder_error_unhandled_tag (GtkBuilder *builder, + GMarkupParseContext *context, + const gchar *object, + const gchar *element_name, + GError **error) +{ + gint line, col; + + g_markup_parse_context_get_position (context, &line, &col); + g_set_error (error, + GTK_BUILDER_ERROR, + GTK_BUILDER_ERROR_UNHANDLED_TAG, + ".:%d:%d Unsupported tag for %s: <%s>", + line, col, + object, element_name); +} + +/* This has been copied and modified from gtksizegroup.c. */ +static void +swipe_group_start_element (GMarkupParseContext *context, + const gchar *element_name, + const gchar **names, + const gchar **values, + gpointer user_data, + GError **error) +{ + GSListSubParserData *data = (GSListSubParserData*)user_data; + + if (strcmp (element_name, BUILDABLE_TAG_SWIPEABLE) == 0) + { + const gchar *name; + ItemData *item_data; + + if (!_gtk_builder_check_parent (data->builder, context, BUILDABLE_TAG_SWIPEABLES, error)) + return; + + if (!g_markup_collect_attributes (element_name, names, values, error, + G_MARKUP_COLLECT_STRING, "name", &name, + G_MARKUP_COLLECT_INVALID)) + { + _gtk_builder_prefix_error (data->builder, context, error); + return; + } + + item_data = g_new (ItemData, 1); + item_data->name = g_strdup (name); + g_markup_parse_context_get_position (context, &item_data->line, &item_data->col); + data->items = g_slist_prepend (data->items, item_data); + } + else if (strcmp (element_name, BUILDABLE_TAG_SWIPEABLES) == 0) + { + if (!_gtk_builder_check_parent (data->builder, context, BUILDABLE_TAG_OBJECT, error)) + return; + + if (!g_markup_collect_attributes (element_name, names, values, error, + G_MARKUP_COLLECT_INVALID, NULL, NULL, + G_MARKUP_COLLECT_INVALID)) + _gtk_builder_prefix_error (data->builder, context, error); + } + else + { + _gtk_builder_error_unhandled_tag (data->builder, context, + "HdySwipeGroup", element_name, + error); + } +} + + +/* This has been copied and modified from gtksizegroup.c. */ +static const GMarkupParser swipe_group_parser = + { + swipe_group_start_element + }; + +/* This has been copied and modified from gtksizegroup.c. */ +static gboolean +hdy_swipe_group_buildable_custom_tag_start (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + GMarkupParser *parser, + gpointer *parser_data) +{ + GSListSubParserData *data; + + if (child) + return FALSE; + + if (strcmp (tagname, BUILDABLE_TAG_SWIPEABLES) == 0) + { + data = g_slice_new0 (GSListSubParserData); + data->items = NULL; + data->object = G_OBJECT (buildable); + data->builder = builder; + + *parser = swipe_group_parser; + *parser_data = data; + + return TRUE; + } + + return FALSE; +} + +/* This has been copied and modified from gtksizegroup.c. */ +static void +hdy_swipe_group_buildable_custom_finished (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *tagname, + gpointer user_data) +{ + GSList *l; + GSListSubParserData *data; + GObject *object; + + if (strcmp (tagname, BUILDABLE_TAG_SWIPEABLES) != 0) + return; + + data = (GSListSubParserData*)user_data; + data->items = g_slist_reverse (data->items); + + for (l = data->items; l; l = l->next) + { + ItemData *item_data = l->data; + object = gtk_builder_get_object (builder, item_data->name); + if (!object) + continue; + hdy_swipe_group_add_swipeable (HDY_SWIPE_GROUP (data->object), HDY_SWIPEABLE (object)); + } + g_slist_free_full (data->items, item_data_free); + g_slice_free (GSListSubParserData, data); +} + +static void +hdy_swipe_group_class_init (HdySwipeGroupClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = hdy_swipe_group_dispose; +} + +static void +hdy_swipe_group_init (HdySwipeGroup *self) +{ +} + +static void +hdy_swipe_group_buildable_init (GtkBuildableIface *iface) +{ + iface->custom_tag_start = hdy_swipe_group_buildable_custom_tag_start; + iface->custom_finished = hdy_swipe_group_buildable_custom_finished; +} diff --git a/subprojects/libhandy/src/hdy-swipe-group.h b/subprojects/libhandy/src/hdy-swipe-group.h new file mode 100644 index 0000000..791962e --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipe-group.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <glib-object.h> +#include "hdy-swipeable.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_SWIPE_GROUP (hdy_swipe_group_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdySwipeGroup, hdy_swipe_group, HDY, SWIPE_GROUP, GObject) + +HDY_AVAILABLE_IN_ALL +HdySwipeGroup *hdy_swipe_group_new (void); + +HDY_AVAILABLE_IN_ALL +void hdy_swipe_group_add_swipeable (HdySwipeGroup *self, + HdySwipeable *swipeable); +HDY_AVAILABLE_IN_ALL +GSList * hdy_swipe_group_get_swipeables (HdySwipeGroup *self); +HDY_AVAILABLE_IN_ALL +void hdy_swipe_group_remove_swipeable (HdySwipeGroup *self, + HdySwipeable *swipeable); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-swipe-tracker-private.h b/subprojects/libhandy/src/hdy-swipe-tracker-private.h new file mode 100644 index 0000000..d4b5541 --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipe-tracker-private.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-swipe-tracker.h" + +G_BEGIN_DECLS + +void hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self, + HdyNavigationDirection direction, + gboolean direct); +void hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self, + gdouble progress); +void hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self, + gint64 duration, + gdouble to); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.c b/subprojects/libhandy/src/hdy-swipe-tracker.c new file mode 100644 index 0000000..0cbf4a4 --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipe-tracker.c @@ -0,0 +1,1113 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-swipe-tracker-private.h" +#include "hdy-navigation-direction.h" + +#include <math.h> + +#define TOUCHPAD_BASE_DISTANCE_H 400 +#define TOUCHPAD_BASE_DISTANCE_V 300 +#define SCROLL_MULTIPLIER 10 +#define MIN_ANIMATION_DURATION 100 +#define MAX_ANIMATION_DURATION 400 +#define VELOCITY_THRESHOLD 0.4 +#define DURATION_MULTIPLIER 3 +#define ANIMATION_BASE_VELOCITY 0.002 +#define DRAG_THRESHOLD_DISTANCE 5 + +/** + * SECTION:hdy-swipe-tracker + * @short_description: Swipe tracker used in #HdyCarousel and #HdyLeaflet + * @title: HdySwipeTracker + * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable + * + * The HdySwipeTracker object can be used for implementing widgets with swipe + * gestures. It supports touch-based swipes, pointer dragging, and touchpad + * scrolling. + * + * The widgets will probably want to expose #HdySwipeTracker:enabled property. + * If they expect to use horizontal orientation, #HdySwipeTracker:reversed + * property can be used for supporting RTL text direction. + * + * Since: 1.0 + */ + +typedef enum { + HDY_SWIPE_TRACKER_STATE_NONE, + HDY_SWIPE_TRACKER_STATE_PENDING, + HDY_SWIPE_TRACKER_STATE_SCROLLING, + HDY_SWIPE_TRACKER_STATE_FINISHING, + HDY_SWIPE_TRACKER_STATE_REJECTED, +} HdySwipeTrackerState; + +struct _HdySwipeTracker +{ + GObject parent_instance; + + HdySwipeable *swipeable; + gboolean enabled; + gboolean reversed; + gboolean allow_mouse_drag; + GtkOrientation orientation; + + gint start_x; + gint start_y; + + guint32 prev_time; + gdouble velocity; + + gdouble initial_progress; + gdouble progress; + gboolean cancelled; + + gdouble prev_offset; + + gboolean is_scrolling; + + HdySwipeTrackerState state; + GtkGesture *touch_gesture; +}; + +G_DEFINE_TYPE_WITH_CODE (HdySwipeTracker, hdy_swipe_tracker, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)); + +enum { + PROP_0, + PROP_SWIPEABLE, + PROP_ENABLED, + PROP_REVERSED, + PROP_ALLOW_MOUSE_DRAG, + + /* GtkOrientable */ + PROP_ORIENTATION, + LAST_PROP = PROP_ALLOW_MOUSE_DRAG + 1, +}; + +static GParamSpec *props[LAST_PROP]; + +enum { + SIGNAL_BEGIN_SWIPE, + SIGNAL_UPDATE_SWIPE, + SIGNAL_END_SWIPE, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +static void +reset (HdySwipeTracker *self) +{ + self->state = HDY_SWIPE_TRACKER_STATE_NONE; + + self->prev_offset = 0; + + self->initial_progress = 0; + self->progress = 0; + + self->start_x = 0; + self->start_y = 0; + + self->prev_time = 0; + self->velocity = 0; + + self->cancelled = FALSE; + + if (self->swipeable) + gtk_grab_remove (GTK_WIDGET (self->swipeable)); +} + +static void +get_range (HdySwipeTracker *self, + gdouble *first, + gdouble *last) +{ + g_autofree gdouble *points = NULL; + gint n; + + points = hdy_swipeable_get_snap_points (self->swipeable, &n); + + *first = points[0]; + *last = points[n - 1]; +} + +static void +gesture_prepare (HdySwipeTracker *self, + HdyNavigationDirection direction, + gboolean is_drag) +{ + GdkRectangle rect; + + if (self->state != HDY_SWIPE_TRACKER_STATE_NONE) + return; + + hdy_swipeable_get_swipe_area (self->swipeable, direction, is_drag, &rect); + + if (self->start_x < rect.x || + self->start_x >= rect.x + rect.width || + self->start_y < rect.y || + self->start_y >= rect.y + rect.height) { + self->state = HDY_SWIPE_TRACKER_STATE_REJECTED; + + return; + } + + hdy_swipe_tracker_emit_begin_swipe (self, direction, TRUE); + + self->initial_progress = hdy_swipeable_get_progress (self->swipeable); + self->progress = self->initial_progress; + self->velocity = 0; + self->state = HDY_SWIPE_TRACKER_STATE_PENDING; +} + +static void +gesture_begin (HdySwipeTracker *self) +{ + GdkEvent *event; + + if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING) + return; + + event = gtk_get_current_event (); + self->prev_time = gdk_event_get_time (event); + self->state = HDY_SWIPE_TRACKER_STATE_SCROLLING; + + gtk_grab_add (GTK_WIDGET (self->swipeable)); +} + +static void +gesture_update (HdySwipeTracker *self, + gdouble delta) +{ + GdkEvent *event; + guint32 time; + gdouble progress; + gdouble first_point, last_point; + + if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) + return; + + event = gtk_get_current_event (); + time = gdk_event_get_time (event); + if (time != self->prev_time) + self->velocity = delta / (time - self->prev_time); + + get_range (self, &first_point, &last_point); + + progress = self->progress + delta; + progress = CLAMP (progress, first_point, last_point); + + /* FIXME: this is a hack to prevent swiping more than 1 page at once */ + progress = CLAMP (progress, self->initial_progress - 1, self->initial_progress + 1); + + self->progress = progress; + + hdy_swipe_tracker_emit_update_swipe (self, progress); + + self->prev_time = time; +} + +static void +get_closest_snap_points (HdySwipeTracker *self, + gdouble *upper, + gdouble *lower) +{ + gint i, n; + gdouble *points; + + *upper = 0; + *lower = 0; + + points = hdy_swipeable_get_snap_points (self->swipeable, &n); + + for (i = 0; i < n; i++) { + if (points[i] >= self->progress) { + *upper = points[i]; + break; + } + } + + for (i = n - 1; i >= 0; i--) { + if (points[i] <= self->progress) { + *lower = points[i]; + break; + } + } + + g_free (points); +} + +static gdouble +get_end_progress (HdySwipeTracker *self, + gdouble distance) +{ + gdouble upper, lower, middle; + + if (self->cancelled) + return hdy_swipeable_get_cancel_progress (self->swipeable); + + get_closest_snap_points (self, &upper, &lower); + middle = (upper + lower) / 2; + + if (self->progress > middle) + return (self->velocity * distance > -VELOCITY_THRESHOLD || + self->initial_progress > upper) ? upper : lower; + + return (self->velocity * distance < VELOCITY_THRESHOLD || + self->initial_progress < lower) ? lower : upper; +} + +static void +gesture_end (HdySwipeTracker *self, + gdouble distance) +{ + gdouble end_progress, velocity; + gint64 duration; + + if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) + return; + + end_progress = get_end_progress (self, distance); + + velocity = ANIMATION_BASE_VELOCITY; + if ((end_progress - self->progress) * self->velocity > 0) + velocity = self->velocity; + + duration = ABS ((self->progress - end_progress) / velocity * DURATION_MULTIPLIER); + if (self->progress != end_progress) + duration = CLAMP (duration, MIN_ANIMATION_DURATION, MAX_ANIMATION_DURATION); + + hdy_swipe_tracker_emit_end_swipe (self, duration, end_progress); + + if (self->cancelled) + reset (self); + else + self->state = HDY_SWIPE_TRACKER_STATE_FINISHING; +} + +static void +gesture_cancel (HdySwipeTracker *self, + gdouble distance) +{ + if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING && + self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) + return; + + self->cancelled = TRUE; + gesture_end (self, distance); +} + +static void +drag_begin_cb (HdySwipeTracker *self, + gdouble start_x, + gdouble start_y, + GtkGestureDrag *gesture) +{ + if (self->state != HDY_SWIPE_TRACKER_STATE_NONE) + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED); + + self->start_x = start_x; + self->start_y = start_y; +} + +static void +drag_update_cb (HdySwipeTracker *self, + gdouble offset_x, + gdouble offset_y, + GtkGestureDrag *gesture) +{ + gdouble offset, distance; + gboolean is_vertical, is_offset_vertical; + + distance = hdy_swipeable_get_distance (self->swipeable); + + is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL); + if (is_vertical) + offset = -offset_y / distance; + else + offset = -offset_x / distance; + + if (self->reversed) + offset = -offset; + + is_offset_vertical = (ABS (offset_y) > ABS (offset_x)); + + if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) { + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED); + return; + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) { + if (is_vertical == is_offset_vertical) + gesture_prepare (self, offset > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, TRUE); + else + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED); + return; + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_PENDING) { + gdouble drag_distance; + gdouble first_point, last_point; + gboolean is_overshooting; + + get_range (self, &first_point, &last_point); + + drag_distance = sqrt (offset_x * offset_x + offset_y * offset_y); + is_overshooting = (offset < 0 && self->progress <= first_point) || + (offset > 0 && self->progress >= last_point); + + if (drag_distance >= DRAG_THRESHOLD_DISTANCE) { + if ((is_vertical == is_offset_vertical) && !is_overshooting) { + gesture_begin (self); + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_CLAIMED); + } else { + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED); + } + } + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) { + gesture_update (self, offset - self->prev_offset); + self->prev_offset = offset; + } +} + +static void +drag_end_cb (HdySwipeTracker *self, + gdouble offset_x, + gdouble offset_y, + GtkGestureDrag *gesture) +{ + gdouble distance; + + distance = hdy_swipeable_get_distance (self->swipeable); + + if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) { + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED); + + reset (self); + return; + } + + if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) { + gesture_cancel (self, distance); + gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED); + return; + } + + gesture_end (self, distance); +} + +static void +drag_cancel_cb (HdySwipeTracker *self, + GdkEventSequence *sequence, + GtkGesture *gesture) +{ + gdouble distance; + + distance = hdy_swipeable_get_distance (self->swipeable); + + gesture_cancel (self, distance); + gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); +} + +static gboolean +handle_scroll_event (HdySwipeTracker *self, + GdkEvent *event, + gboolean capture) +{ + GdkDevice *source_device; + GdkInputSource input_source; + gdouble dx, dy, delta, distance; + gboolean is_vertical; + gboolean is_delta_vertical; + + is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL); + distance = is_vertical ? TOUCHPAD_BASE_DISTANCE_V : TOUCHPAD_BASE_DISTANCE_H; + + if (gdk_event_get_scroll_direction (event, NULL)) + return GDK_EVENT_PROPAGATE; + + source_device = gdk_event_get_source_device (event); + input_source = gdk_device_get_source (source_device); + if (input_source != GDK_SOURCE_TOUCHPAD) + return GDK_EVENT_PROPAGATE; + + gdk_event_get_scroll_deltas (event, &dx, &dy); + delta = is_vertical ? dy : dx; + if (self->reversed) + delta = -delta; + + is_delta_vertical = (ABS (dy) > ABS (dx)); + + if (self->is_scrolling) { + gesture_cancel (self, distance); + + if (gdk_event_is_scroll_stop_event (event)) + self->is_scrolling = FALSE; + + return GDK_EVENT_PROPAGATE; + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) { + if (gdk_event_is_scroll_stop_event (event)) + reset (self); + + return GDK_EVENT_PROPAGATE; + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) { + if (gdk_event_is_scroll_stop_event (event)) + return GDK_EVENT_PROPAGATE; + + if (is_vertical == is_delta_vertical) { + if (!capture) { + GtkWidget *widget = gtk_get_event_widget (event); + gdouble event_x, event_y; + + gdk_event_get_coords (event, &event_x, &event_y); + gtk_widget_translate_coordinates (widget, GTK_WIDGET (self->swipeable), + event_x, event_y, + &self->start_x, &self->start_y); + + gesture_prepare (self, delta > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, FALSE); + } + } else { + self->is_scrolling = TRUE; + return GDK_EVENT_PROPAGATE; + } + } + + if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_PENDING) { + gboolean is_overshooting; + gdouble first_point, last_point; + + get_range (self, &first_point, &last_point); + + is_overshooting = (delta < 0 && self->progress <= first_point) || + (delta > 0 && self->progress >= last_point); + + if ((is_vertical == is_delta_vertical) && !is_overshooting) + gesture_begin (self); + else + gesture_cancel (self, distance); + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) { + if (gdk_event_is_scroll_stop_event (event)) { + gesture_end (self, distance); + } else { + gesture_update (self, delta / distance * SCROLL_MULTIPLIER); + return GDK_EVENT_STOP; + } + } + + if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_FINISHING) + reset (self); + + return GDK_EVENT_PROPAGATE; +} + +static gboolean +is_window_handle (GtkWidget *widget) +{ + gboolean window_dragging; + GtkWidget *parent, *window, *titlebar; + + gtk_widget_style_get (widget, "window-dragging", &window_dragging, NULL); + + if (window_dragging) + return TRUE; + + /* Window titlebar area is always draggable, so check if we're inside. */ + window = gtk_widget_get_toplevel (widget); + if (!GTK_IS_WINDOW (window)) + return FALSE; + + titlebar = gtk_window_get_titlebar (GTK_WINDOW (window)); + if (!titlebar) + return FALSE; + + parent = widget; + while (parent && parent != titlebar) + parent = gtk_widget_get_parent (parent); + + return parent == titlebar; +} + +static gboolean +handle_event_cb (HdySwipeTracker *self, + GdkEvent *event) +{ + GdkEventSequence *sequence; + gboolean retval; + GtkEventSequenceState state; + GtkWidget *widget; + + if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) + return GDK_EVENT_PROPAGATE; + + if (event->type == GDK_SCROLL) + return handle_scroll_event (self, event, FALSE); + + if (event->type != GDK_BUTTON_PRESS && + event->type != GDK_BUTTON_RELEASE && + event->type != GDK_MOTION_NOTIFY && + event->type != GDK_TOUCH_BEGIN && + event->type != GDK_TOUCH_END && + event->type != GDK_TOUCH_UPDATE && + event->type != GDK_TOUCH_CANCEL) + return GDK_EVENT_PROPAGATE; + + widget = gtk_get_event_widget (event); + if (is_window_handle (widget)) + return GDK_EVENT_PROPAGATE; + + sequence = gdk_event_get_event_sequence (event); + retval = gtk_event_controller_handle_event (GTK_EVENT_CONTROLLER (self->touch_gesture), event); + state = gtk_gesture_get_sequence_state (self->touch_gesture, sequence); + + if (state == GTK_EVENT_SEQUENCE_DENIED) { + gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->touch_gesture)); + return GDK_EVENT_PROPAGATE; + } + + if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) { + return GDK_EVENT_STOP; + } else if (self->state == HDY_SWIPE_TRACKER_STATE_FINISHING) { + reset (self); + return GDK_EVENT_STOP; + } + return retval; +} + +static gboolean +captured_event_cb (HdySwipeable *swipeable, + GdkEvent *event) +{ + HdySwipeTracker *self = hdy_swipeable_get_swipe_tracker (swipeable); + + g_assert (HDY_IS_SWIPE_TRACKER (self)); + + if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) + return GDK_EVENT_PROPAGATE; + + if (event->type != GDK_SCROLL) + return GDK_EVENT_PROPAGATE; + + return handle_scroll_event (self, event, TRUE); +} + +static void +hdy_swipe_tracker_constructed (GObject *object) +{ + HdySwipeTracker *self = HDY_SWIPE_TRACKER (object); + + g_assert (self->swipeable); + + gtk_widget_add_events (GTK_WIDGET (self->swipeable), + GDK_SMOOTH_SCROLL_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON_MOTION_MASK | + GDK_TOUCH_MASK); + + self->touch_gesture = g_object_new (GTK_TYPE_GESTURE_DRAG, + "widget", self->swipeable, + "propagation-phase", GTK_PHASE_NONE, + "touch-only", !self->allow_mouse_drag, + NULL); + + g_signal_connect_swapped (self->touch_gesture, "drag-begin", G_CALLBACK (drag_begin_cb), self); + g_signal_connect_swapped (self->touch_gesture, "drag-update", G_CALLBACK (drag_update_cb), self); + g_signal_connect_swapped (self->touch_gesture, "drag-end", G_CALLBACK (drag_end_cb), self); + g_signal_connect_swapped (self->touch_gesture, "cancel", G_CALLBACK (drag_cancel_cb), self); + + g_signal_connect_object (self->swipeable, "event", G_CALLBACK (handle_event_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->swipeable, "unrealize", G_CALLBACK (reset), self, G_CONNECT_SWAPPED); + + /* + * HACK: GTK3 has no other way to get events on capture phase. + * This is a reimplementation of _gtk_widget_set_captured_event_handler(), + * which is private. In GTK4 it can be replaced with GtkEventControllerLegacy + * with capture propagation phase + */ + g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", captured_event_cb); + + G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->constructed (object); +} + +static void +hdy_swipe_tracker_dispose (GObject *object) +{ + HdySwipeTracker *self = HDY_SWIPE_TRACKER (object); + + if (self->swipeable) + gtk_grab_remove (GTK_WIDGET (self->swipeable)); + + if (self->touch_gesture) + g_signal_handlers_disconnect_by_data (self->touch_gesture, self); + + g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", NULL); + + g_clear_object (&self->touch_gesture); + g_clear_object (&self->swipeable); + + G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->dispose (object); +} + +static void +hdy_swipe_tracker_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdySwipeTracker *self = HDY_SWIPE_TRACKER (object); + + switch (prop_id) { + case PROP_SWIPEABLE: + g_value_set_object (value, hdy_swipe_tracker_get_swipeable (self)); + break; + + case PROP_ENABLED: + g_value_set_boolean (value, hdy_swipe_tracker_get_enabled (self)); + break; + + case PROP_REVERSED: + g_value_set_boolean (value, hdy_swipe_tracker_get_reversed (self)); + break; + + case PROP_ALLOW_MOUSE_DRAG: + g_value_set_boolean (value, hdy_swipe_tracker_get_allow_mouse_drag (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_swipe_tracker_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdySwipeTracker *self = HDY_SWIPE_TRACKER (object); + + switch (prop_id) { + case PROP_SWIPEABLE: + self->swipeable = HDY_SWIPEABLE (g_object_ref (g_value_get_object (value))); + break; + + case PROP_ENABLED: + hdy_swipe_tracker_set_enabled (self, g_value_get_boolean (value)); + break; + + case PROP_REVERSED: + hdy_swipe_tracker_set_reversed (self, g_value_get_boolean (value)); + break; + + case PROP_ALLOW_MOUSE_DRAG: + hdy_swipe_tracker_set_allow_mouse_drag (self, g_value_get_boolean (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = g_value_get_enum (value); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_swipe_tracker_class_init (HdySwipeTrackerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = hdy_swipe_tracker_constructed; + object_class->dispose = hdy_swipe_tracker_dispose; + object_class->get_property = hdy_swipe_tracker_get_property; + object_class->set_property = hdy_swipe_tracker_set_property; + + /** + * HdySwipeTracker:swipeable: + * + * The widget the swipe tracker is attached to. Must not be %NULL. + * + * Since: 1.0 + */ + props[PROP_SWIPEABLE] = + g_param_spec_object ("swipeable", + _("Swipeable"), + _("The swipeable the swipe tracker is attached to"), + HDY_TYPE_SWIPEABLE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + /** + * HdySwipeTracker:enabled: + * + * Whether the swipe tracker is enabled. When it's not enabled, no events + * will be processed. Usually widgets will want to expose this via a property. + * + * Since: 1.0 + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + _("Enabled"), + _("Whether the swipe tracker processes events"), + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdySwipeTracker:reversed: + * + * Whether to reverse the swipe direction. If the swipe tracker is horizontal, + * it can be used for supporting RTL text direction. + * + * Since: 1.0 + */ + props[PROP_REVERSED] = + g_param_spec_boolean ("reversed", + _("Reversed"), + _("Whether swipe direction is reversed"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * HdySwipeTracker:allow-mouse-drag: + * + * Whether to allow dragging with mouse pointer. This should usually be + * %FALSE. + * + * Since: 1.0 + */ + props[PROP_ALLOW_MOUSE_DRAG] = + g_param_spec_boolean ("allow-mouse-drag", + _("Allow mouse drag"), + _("Whether to allow dragging with mouse pointer"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * HdySwipeTracker::begin-swipe: + * @self: The #HdySwipeTracker instance + * @direction: The direction of the swipe + * @direct: %TRUE if the swipe is directly triggered by a gesture, + * %FALSE if it's triggered via a #HdySwipeGroup + * + * This signal is emitted when a possible swipe is detected. + * + * The @direction value can be used to restrict the swipe to a certain + * direction. + * + * Since: 1.0 + */ + signals[SIGNAL_BEGIN_SWIPE] = + g_signal_new ("begin-swipe", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + HDY_TYPE_NAVIGATION_DIRECTION, G_TYPE_BOOLEAN); + + /** + * HdySwipeTracker::update-swipe: + * @self: The #HdySwipeTracker instance + * @progress: The current animation progress value + * + * This signal is emitted every time the progress value changes. + * + * Since: 1.0 + */ + signals[SIGNAL_UPDATE_SWIPE] = + g_signal_new ("update-swipe", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_DOUBLE); + + /** + * HdySwipeTracker::end-swipe: + * @self: The #HdySwipeTracker instance + * @duration: Snap-back animation duration in milliseconds + * @to: The progress value to animate to + * + * This signal is emitted as soon as the gesture has stopped. + * + * Since: 1.0 + */ + signals[SIGNAL_END_SWIPE] = + g_signal_new ("end-swipe", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_INT64, G_TYPE_DOUBLE); +} + +static void +hdy_swipe_tracker_init (HdySwipeTracker *self) +{ + reset (self); + self->orientation = GTK_ORIENTATION_HORIZONTAL; + self->enabled = TRUE; +} + +/** + * hdy_swipe_tracker_new: + * @swipeable: a #GtkWidget to add the tracker on + * + * Create a new #HdySwipeTracker object on @widget. + * + * Returns: the newly created #HdySwipeTracker object + * + * Since: 1.0 + */ +HdySwipeTracker * +hdy_swipe_tracker_new (HdySwipeable *swipeable) +{ + g_return_val_if_fail (HDY_IS_SWIPEABLE (swipeable), NULL); + + return g_object_new (HDY_TYPE_SWIPE_TRACKER, + "swipeable", swipeable, + NULL); +} + +/** + * hdy_swipe_tracker_get_swipeable: + * @self: a #HdySwipeTracker + * + * Get @self's swipeable widget. + * + * Returns: (transfer none): the swipeable widget + * + * Since: 1.0 + */ +HdySwipeable * +hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self) +{ + g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), NULL); + + return self->swipeable; +} + +/** + * hdy_swipe_tracker_get_enabled: + * @self: a #HdySwipeTracker + * + * Get whether @self is enabled. When it's not enabled, no events will be + * processed. Generally widgets will want to expose this via a property. + * + * Returns: %TRUE if @self is enabled + * + * Since: 1.0 + */ +gboolean +hdy_swipe_tracker_get_enabled (HdySwipeTracker *self) +{ + g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE); + + return self->enabled; +} + +/** + * hdy_swipe_tracker_set_enabled: + * @self: a #HdySwipeTracker + * @enabled: whether to enable to swipe tracker + * + * Set whether @self is enabled. When it's not enabled, no events will be + * processed. Usually widgets will want to expose this via a property. + * + * Since: 1.0 + */ +void +hdy_swipe_tracker_set_enabled (HdySwipeTracker *self, + gboolean enabled) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + enabled = !!enabled; + + if (self->enabled == enabled) + return; + + self->enabled = enabled; + + if (!enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) + reset (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + +/** + * hdy_swipe_tracker_get_reversed: + * @self: a #HdySwipeTracker + * + * Get whether @self is reversing the swipe direction. + * + * Returns: %TRUE is the direction is reversed + * + * Since: 1.0 + */ +gboolean +hdy_swipe_tracker_get_reversed (HdySwipeTracker *self) +{ + g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE); + + return self->reversed; +} + +/** + * hdy_swipe_tracker_set_reversed: + * @self: a #HdySwipeTracker + * @reversed: whether to reverse the swipe direction + * + * Set whether to reverse the swipe direction. If @self is horizontal, + * can be used for supporting RTL text direction. + * + * Since: 1.0 + */ +void +hdy_swipe_tracker_set_reversed (HdySwipeTracker *self, + gboolean reversed) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + reversed = !!reversed; + + if (self->reversed == reversed) + return; + + self->reversed = reversed; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVERSED]); +} + +/** + * hdy_swipe_tracker_get_allow_mouse_drag: + * @self: a #HdySwipeTracker + * + * Get whether @self can be dragged with mouse pointer. + * + * Returns: %TRUE is mouse dragging is allowed + * + * Since: 1.0 + */ +gboolean +hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self) +{ + g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE); + + return self->allow_mouse_drag; +} + +/** + * hdy_swipe_tracker_set_allow_mouse_drag: + * @self: a #HdySwipeTracker + * @allow_mouse_drag: whether to allow mouse dragging + * + * Set whether @self can be dragged with mouse pointer. This should usually be + * %FALSE. + * + * Since: 1.0 + */ +void +hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self, + gboolean allow_mouse_drag) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + allow_mouse_drag = !!allow_mouse_drag; + + if (self->allow_mouse_drag == allow_mouse_drag) + return; + + self->allow_mouse_drag = allow_mouse_drag; + + if (self->touch_gesture) + g_object_set (self->touch_gesture, "touch-only", !allow_mouse_drag, NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]); +} + +/** + * hdy_swipe_tracker_shift_position: + * @self: a #HdySwipeTracker + * @delta: the position delta + * + * Move the current progress value by @delta. This can be used to adjust the + * current position if snap points move during the gesture. + * + * Since: 1.0 + */ +void +hdy_swipe_tracker_shift_position (HdySwipeTracker *self, + gdouble delta) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING && + self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) + return; + + self->progress += delta; + self->initial_progress += delta; +} + +void +hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self, + HdyNavigationDirection direction, + gboolean direct) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + g_signal_emit (self, signals[SIGNAL_BEGIN_SWIPE], 0, direction, direct); +} + +void +hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self, + gdouble progress) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + g_signal_emit (self, signals[SIGNAL_UPDATE_SWIPE], 0, progress); +} + +void +hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self, + gint64 duration, + gdouble to) +{ + g_return_if_fail (HDY_IS_SWIPE_TRACKER (self)); + + g_signal_emit (self, signals[SIGNAL_END_SWIPE], 0, duration, to); +} diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.h b/subprojects/libhandy/src/hdy-swipe-tracker.h new file mode 100644 index 0000000..20fe751 --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipe-tracker.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-swipeable.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_SWIPE_TRACKER (hdy_swipe_tracker_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdySwipeTracker, hdy_swipe_tracker, HDY, SWIPE_TRACKER, GObject) + +HDY_AVAILABLE_IN_ALL +HdySwipeTracker *hdy_swipe_tracker_new (HdySwipeable *swipeable); + +HDY_AVAILABLE_IN_ALL +HdySwipeable *hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_swipe_tracker_get_enabled (HdySwipeTracker *self); +HDY_AVAILABLE_IN_ALL +void hdy_swipe_tracker_set_enabled (HdySwipeTracker *self, + gboolean enabled); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_swipe_tracker_get_reversed (HdySwipeTracker *self); +HDY_AVAILABLE_IN_ALL +void hdy_swipe_tracker_set_reversed (HdySwipeTracker *self, + gboolean reversed); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self); +HDY_AVAILABLE_IN_ALL +void hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self, + gboolean allow_mouse_drag); + +HDY_AVAILABLE_IN_ALL +void hdy_swipe_tracker_shift_position (HdySwipeTracker *self, + gdouble delta); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-swipeable.c b/subprojects/libhandy/src/hdy-swipeable.c new file mode 100644 index 0000000..6be1713 --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipeable.c @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-swipeable.h" + +/** + * SECTION:hdy-swipeable + * @short_description: An interface for swipeable widgets. + * @title: HdySwipeable + * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeGroup + * + * The #HdySwipeable interface is implemented by all swipeable widgets. They + * can be synced using #HdySwipeGroup. + * + * See #HdySwipeTracker for details about implementing it. + * + * Since: 0.0.12 + */ + +G_DEFINE_INTERFACE (HdySwipeable, hdy_swipeable, GTK_TYPE_WIDGET) + +enum { + SIGNAL_CHILD_SWITCHED, + SIGNAL_LAST_SIGNAL, +}; + +static guint signals[SIGNAL_LAST_SIGNAL]; + +static void +hdy_swipeable_default_init (HdySwipeableInterface *iface) +{ + /** + * HdySwipeable::child-switched: + * @self: The #HdySwipeable instance + * @index: the index of the child to switch to + * @duration: Animation duration in milliseconds + * + * This signal should be emitted when the widget's visible child is changed. + * + * @duration can be 0 if the child is switched without animation. + * + * This is used by #HdySwipeGroup, applications should not connect to it. + * + * Since: 1.0 + */ + signals[SIGNAL_CHILD_SWITCHED] = + g_signal_new ("child-switched", + G_TYPE_FROM_INTERFACE (iface), + G_SIGNAL_RUN_FIRST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_UINT, G_TYPE_INT64); +} + +/** + * hdy_swipeable_switch_child: + * @self: a #HdySwipeable + * @index: the index of the child to switch to + * @duration: Animation duration in milliseconds + * + * See HdySwipeable::child-switched. + * + * Since: 1.0 + */ +void +hdy_swipeable_switch_child (HdySwipeable *self, + guint index, + gint64 duration) +{ + HdySwipeableInterface *iface; + + g_return_if_fail (HDY_IS_SWIPEABLE (self)); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + g_return_if_fail (iface->switch_child != NULL); + + iface->switch_child (self, index, duration); +} + +/** + * hdy_swipeable_emit_child_switched: + * @self: a #HdySwipeable + * @index: the index of the child to switch to + * @duration: Animation duration in milliseconds + * + * Emits HdySwipeable::child-switched signal. This should be called when the + * widget switches visible child widget. + * + * @duration can be 0 if the child is switched without animation. + * + * Since: 1.0 + */ +void +hdy_swipeable_emit_child_switched (HdySwipeable *self, + guint index, + gint64 duration) +{ + g_return_if_fail (HDY_IS_SWIPEABLE (self)); + + g_signal_emit (self, signals[SIGNAL_CHILD_SWITCHED], 0, index, duration); +} + +/** + * hdy_swipeable_get_swipe_tracker: + * @self: a #HdySwipeable + * + * Gets the #HdySwipeTracker used by this swipeable widget. + * + * Returns: (transfer none): the swipe tracker + * + * Since: 1.0 + */ +HdySwipeTracker * +hdy_swipeable_get_swipe_tracker (HdySwipeable *self) +{ + HdySwipeableInterface *iface; + + g_return_val_if_fail (HDY_IS_SWIPEABLE (self), NULL); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + g_return_val_if_fail (iface->get_swipe_tracker != NULL, NULL); + + return iface->get_swipe_tracker (self); +} + +/** + * hdy_swipeable_get_distance: + * @self: a #HdySwipeable + * + * Gets the swipe distance of @self. This corresponds to how many pixels + * 1 unit represents. + * + * Returns: the swipe distance in pixels + * + * Since: 1.0 + */ +gdouble +hdy_swipeable_get_distance (HdySwipeable *self) +{ + HdySwipeableInterface *iface; + + g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + g_return_val_if_fail (iface->get_distance != NULL, 0); + + return iface->get_distance (self); +} + +/** + * hdy_swipeable_get_snap_points: (virtual get_snap_points) + * @self: a #HdySwipeable + * @n_snap_points: (out): location to return the number of the snap points + * + * Gets the snap points of @self. Each snap point represents a progress value + * that is considered acceptable to end the swipe on. + * + * Returns: (array length=n_snap_points) (transfer full): the snap points of + * @self. The array must be freed with g_free(). + * + * Since: 1.0 + */ +gdouble * +hdy_swipeable_get_snap_points (HdySwipeable *self, + gint *n_snap_points) +{ + HdySwipeableInterface *iface; + + g_return_val_if_fail (HDY_IS_SWIPEABLE (self), NULL); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + g_return_val_if_fail (iface->get_snap_points != NULL, NULL); + + return iface->get_snap_points (self, n_snap_points); +} + +/** + * hdy_swipeable_get_progress: + * @self: a #HdySwipeable + * + * Gets the current progress of @self + * + * Returns: the current progress, unitless + * + * Since: 1.0 + */ +gdouble +hdy_swipeable_get_progress (HdySwipeable *self) +{ + HdySwipeableInterface *iface; + + g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + g_return_val_if_fail (iface->get_progress != NULL, 0); + + return iface->get_progress (self); +} + +/** + * hdy_swipeable_get_cancel_progress: + * @self: a #HdySwipeable + * + * Gets the progress @self will snap back to after the gesture is canceled. + * + * Returns: the cancel progress, unitless + * + * Since: 1.0 + */ +gdouble +hdy_swipeable_get_cancel_progress (HdySwipeable *self) +{ + HdySwipeableInterface *iface; + + g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + g_return_val_if_fail (iface->get_cancel_progress != NULL, 0); + + return iface->get_cancel_progress (self); +} + +/** + * hdy_swipeable_get_swipe_area: + * @self: a #HdySwipeable + * @navigation_direction: the direction of the swipe + * @is_drag: whether the swipe is caused by a dragging gesture + * @rect: (out): a pointer to a #GdkRectangle to store the swipe area + * + * Gets the area @self can start a swipe from for the given direction and + * gesture type. + * This can be used to restrict swipes to only be possible from a certain area, + * for example, to only allow edge swipes, or to have a draggable element and + * ignore swipes elsewhere. + * + * Swipe area is only considered for direct swipes (as in, not initiated by + * #HdySwipeGroup). + * + * If not implemented, the default implementation returns the allocation of + * @self, allowing swipes from anywhere. + * + * Since: 1.0 + */ +void +hdy_swipeable_get_swipe_area (HdySwipeable *self, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect) +{ + HdySwipeableInterface *iface; + + g_return_if_fail (HDY_IS_SWIPEABLE (self)); + g_return_if_fail (rect != NULL); + + iface = HDY_SWIPEABLE_GET_IFACE (self); + + if (iface->get_swipe_area) { + iface->get_swipe_area (self, navigation_direction, is_drag, rect); + return; + } + + rect->x = 0; + rect->y = 0; + rect->width = gtk_widget_get_allocated_width (GTK_WIDGET (self)); + rect->height = gtk_widget_get_allocated_height (GTK_WIDGET (self)); +} diff --git a/subprojects/libhandy/src/hdy-swipeable.h b/subprojects/libhandy/src/hdy-swipeable.h new file mode 100644 index 0000000..9cb6cde --- /dev/null +++ b/subprojects/libhandy/src/hdy-swipeable.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> +#include "hdy-navigation-direction.h" +#include "hdy-types.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_SWIPEABLE (hdy_swipeable_get_type ()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_INTERFACE (HdySwipeable, hdy_swipeable, HDY, SWIPEABLE, GtkWidget) + +/** + * HdySwipeableInterface: + * @parent: The parent interface. + * @switch_child: Switches visible child. + * @get_swipe_tracker: Gets the swipe tracker. + * @get_distance: Gets the swipe distance. + * @get_snap_points: Gets the snap points + * @get_progress: Gets the current progress. + * @get_cancel_progress: Gets the cancel progress. + * @get_swipe_area: Gets the swipeable rectangle. + * + * An interface for swipeable widgets. + * + * Since: 1.0 + **/ +struct _HdySwipeableInterface +{ + GTypeInterface parent; + + void (*switch_child) (HdySwipeable *self, + guint index, + gint64 duration); + + HdySwipeTracker * (*get_swipe_tracker) (HdySwipeable *self); + gdouble (*get_distance) (HdySwipeable *self); + gdouble * (*get_snap_points) (HdySwipeable *self, + gint *n_snap_points); + gdouble (*get_progress) (HdySwipeable *self); + gdouble (*get_cancel_progress) (HdySwipeable *self); + void (*get_swipe_area) (HdySwipeable *self, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect); + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +void hdy_swipeable_switch_child (HdySwipeable *self, + guint index, + gint64 duration); + +HDY_AVAILABLE_IN_ALL +void hdy_swipeable_emit_child_switched (HdySwipeable *self, + guint index, + gint64 duration); + +HDY_AVAILABLE_IN_ALL +HdySwipeTracker *hdy_swipeable_get_swipe_tracker (HdySwipeable *self); +HDY_AVAILABLE_IN_ALL +gdouble hdy_swipeable_get_distance (HdySwipeable *self); +HDY_AVAILABLE_IN_ALL +gdouble *hdy_swipeable_get_snap_points (HdySwipeable *self, + gint *n_snap_points); +HDY_AVAILABLE_IN_ALL +gdouble hdy_swipeable_get_progress (HdySwipeable *self); +HDY_AVAILABLE_IN_ALL +gdouble hdy_swipeable_get_cancel_progress (HdySwipeable *self); +HDY_AVAILABLE_IN_ALL +void hdy_swipeable_get_swipe_area (HdySwipeable *self, + HdyNavigationDirection navigation_direction, + gboolean is_drag, + GdkRectangle *rect); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-title-bar.c b/subprojects/libhandy/src/hdy-title-bar.c new file mode 100644 index 0000000..fd5371a --- /dev/null +++ b/subprojects/libhandy/src/hdy-title-bar.c @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include "hdy-title-bar.h" + +#include <glib/gi18n-lib.h> + +/** + * SECTION:hdy-title-bar + * @short_description: A simple title bar container. + * @Title: HdyTitleBar + * + * HdyTitleBar is meant to be used as the top-level widget of your window's + * title bar. It will be drawn with the same style as a GtkHeaderBar but it + * won't force a widget layout on you: you can put whatever widget you want in + * it, including a GtkHeaderBar. + * + * HdyTitleBar becomes really useful when you want to animate header bars, like + * an adaptive application using #HdyLeaflet would do. + * + * # CSS nodes + * + * #HdyTitleBar has a single CSS node with name headerbar. + */ + +enum { + PROP_0, + PROP_SELECTION_MODE, + LAST_PROP, +}; + +struct _HdyTitleBar +{ + GtkBin parent_instance; + + gboolean selection_mode; +}; + +G_DEFINE_TYPE (HdyTitleBar, hdy_title_bar, GTK_TYPE_BIN) + +static GParamSpec *props[LAST_PROP]; + +/** + * hdy_title_bar_set_selection_mode: + * @self: a #HdyTitleBar + * @selection_mode: %TRUE to enable the selection mode + * + * Sets whether @self is in selection mode. + */ +void +hdy_title_bar_set_selection_mode (HdyTitleBar *self, + gboolean selection_mode) +{ + GtkStyleContext *context; + + g_return_if_fail (HDY_IS_TITLE_BAR (self)); + + selection_mode = !!selection_mode; + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + if (self->selection_mode == selection_mode) + return; + + self->selection_mode = selection_mode; + + if (selection_mode) + gtk_style_context_add_class (context, "selection-mode"); + else + gtk_style_context_remove_class (context, "selection-mode"); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTION_MODE]); +} + +/** + * hdy_title_bar_get_selection_mode: + * @self: a #HdyTitleBar + * + * Returns whether whether @self is in selection mode. + * + * Returns: %TRUE if the title bar is in selection mode + */ +gboolean +hdy_title_bar_get_selection_mode (HdyTitleBar *self) +{ + g_return_val_if_fail (HDY_IS_TITLE_BAR (self), FALSE); + + return self->selection_mode; +} + +static void +style_updated_cb (HdyTitleBar *self) +{ + GtkStyleContext *context; + gboolean selection_mode; + + g_assert (HDY_IS_TITLE_BAR (self)); + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + selection_mode = gtk_style_context_has_class (context, "selection-mode"); + + if (self->selection_mode == selection_mode) + return; + + self->selection_mode = selection_mode; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTION_MODE]); +} + +static void +hdy_title_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyTitleBar *self = HDY_TITLE_BAR (object); + + switch (prop_id) { + case PROP_SELECTION_MODE: + g_value_set_boolean (value, hdy_title_bar_get_selection_mode (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_title_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyTitleBar *self = HDY_TITLE_BAR (object); + + switch (prop_id) { + case PROP_SELECTION_MODE: + hdy_title_bar_set_selection_mode (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +hdy_title_bar_draw (GtkWidget *widget, + cairo_t *cr) +{ + GtkStyleContext *context; + + context = gtk_widget_get_style_context (widget); + /* GtkWidget draws nothing by default so we have to render the background + * explicitly for HdyTitleBar to render the typical titlebar background. + */ + gtk_render_background (context, + cr, + 0, 0, + gtk_widget_get_allocated_width (widget), + gtk_widget_get_allocated_height (widget)); + + return GTK_WIDGET_CLASS (hdy_title_bar_parent_class)->draw (widget, cr); +} + +/* This private method is prefixed by the class name because it will be a + * virtual method in GTK 4. + */ +static void +hdy_title_bar_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + GtkWidget *child; + gint parent_min, parent_nat; + gint css_width, css_height, css_min; + + child = gtk_bin_get_child (GTK_BIN (widget)); + + gtk_style_context_get (gtk_widget_get_style_context (widget), + gtk_widget_get_state_flags (widget), + "min-width", &css_width, + "min-height", &css_height, + NULL); + + if (orientation == GTK_ORIENTATION_HORIZONTAL) + css_min = css_width; + else + css_min = css_height; + + if (child) + if (orientation == GTK_ORIENTATION_HORIZONTAL) + if (for_size != 1) + gtk_widget_get_preferred_width_for_height (child, + MAX (for_size, css_height), + &parent_min, &parent_nat); + else + gtk_widget_get_preferred_width (child, &parent_min, &parent_nat); + else + if (for_size != 1) + gtk_widget_get_preferred_height_for_width (child, + MAX (for_size, css_width), + &parent_min, &parent_nat); + else + gtk_widget_get_preferred_height (child, &parent_min, &parent_nat); + else { + parent_min = 0; + parent_nat = 0; + } + + if (minimum) + *minimum = MAX (parent_min, css_min); + + if (natural) + *natural = MAX (parent_nat, css_min); + + if (minimum_baseline) + *minimum_baseline = -1; + + if (natural_baseline) + *natural_baseline = -1; +} + +static void +hdy_title_bar_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_title_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_title_bar_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + hdy_title_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum, natural, NULL, NULL); +} + +static void +hdy_title_bar_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + hdy_title_bar_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum, natural, NULL, NULL); +} + +static void +hdy_title_bar_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + hdy_title_bar_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, NULL, NULL); +} + +static void +hdy_title_bar_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + GtkAllocation clip; + + gtk_render_background_get_clip (gtk_widget_get_style_context (widget), + allocation->x, + allocation->y, + allocation->width, + allocation->height, + &clip); + + GTK_WIDGET_CLASS (hdy_title_bar_parent_class)->size_allocate (widget, allocation); + gtk_widget_set_clip (widget, &clip); +} + +static void +hdy_title_bar_class_init (HdyTitleBarClass *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 = hdy_title_bar_get_property; + object_class->set_property = hdy_title_bar_set_property; + + widget_class->draw = hdy_title_bar_draw; + widget_class->get_preferred_width = hdy_title_bar_get_preferred_width; + widget_class->get_preferred_width_for_height = hdy_title_bar_get_preferred_width_for_height; + widget_class->get_preferred_height = hdy_title_bar_get_preferred_height; + widget_class->get_preferred_height_for_width = hdy_title_bar_get_preferred_height_for_width; + widget_class->size_allocate = hdy_title_bar_size_allocate; + + /** + * HdyTitleBar:selection_mode: + * + * %TRUE if the title bar is in selection mode. + */ + props[PROP_SELECTION_MODE] = + g_param_spec_boolean ("selection-mode", + _("Selection mode"), + _("Whether or not the title bar is in selection mode"), + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_TITLE_BAR); + /* Adwaita states it expects a headerbar to be the top-level titlebar widget, + * so style-wise HdyTitleBar pretends to be one as its role is to be the + * top-level titlebar widget. + */ + gtk_widget_class_set_css_name (widget_class, "headerbar"); + gtk_container_class_handle_border_width (container_class); +} + +static void +hdy_title_bar_init (HdyTitleBar *self) +{ + GtkStyleContext *context; + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + /* Ensure the widget has the titlebar style class. */ + gtk_style_context_add_class (context, "titlebar"); + + g_signal_connect (self, "style-updated", G_CALLBACK (style_updated_cb), NULL); +} + +/** + * hdy_title_bar_new: + * + * Creates a new #HdyTitleBar. + * + * Returns: a new #HdyTitleBar + */ +GtkWidget * +hdy_title_bar_new (void) +{ + return g_object_new (HDY_TYPE_TITLE_BAR, NULL); +} diff --git a/subprojects/libhandy/src/hdy-title-bar.h b/subprojects/libhandy/src/hdy-title-bar.h new file mode 100644 index 0000000..275f4ea --- /dev/null +++ b/subprojects/libhandy/src/hdy-title-bar.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_TITLE_BAR (hdy_title_bar_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyTitleBar, hdy_title_bar, HDY, TITLE_BAR, GtkBin) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_title_bar_new (void); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_title_bar_get_selection_mode (HdyTitleBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_title_bar_set_selection_mode (HdyTitleBar *self, + gboolean selection_mode); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-types.h b/subprojects/libhandy/src/hdy-types.h new file mode 100644 index 0000000..56a3763 --- /dev/null +++ b/subprojects/libhandy/src/hdy-types.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +G_BEGIN_DECLS + +typedef struct _HdySwipeTracker HdySwipeTracker; + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-value-object.c b/subprojects/libhandy/src/hdy-value-object.c new file mode 100644 index 0000000..485d4b3 --- /dev/null +++ b/subprojects/libhandy/src/hdy-value-object.c @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2019 Red Hat Inc. + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> +#include <gobject/gvaluecollector.h> +#include "hdy-value-object.h" + +/** + * SECTION:hdy-value-object + * @short_description: An object representing a #GValue. + * @Title: HdyValueObject + * + * The #HdyValueObject object represents a #GValue, allowing it to be + * used with #GListModel. + * + * Since: 0.0.8 + */ + +struct _HdyValueObject +{ + GObject parent_instance; + + GValue value; +}; + +G_DEFINE_TYPE (HdyValueObject, hdy_value_object, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_VALUE, + N_PROPS +}; + +static GParamSpec *props [N_PROPS]; + +/** + * hdy_value_object_new: + * @value: the #GValue to store + * + * Create a new #HdyValueObject. + * + * Returns: a new #HdyValueObject + * Since: 0.0.8 + */ +HdyValueObject * +hdy_value_object_new (const GValue *value) +{ + return g_object_new (HDY_TYPE_VALUE_OBJECT, + "value", value, + NULL); +} + +/** + * hdy_value_object_new_collect: (skip) + * @type: the #GType of the value + * @...: the value to store + * + * Creates a new #HdyValueObject. This is a convenience method which uses + * the G_VALUE_COLLECT() macro internally. + * + * Returns: a new #HdyValueObject + * Since: 0.0.8 + */ +HdyValueObject* +hdy_value_object_new_collect (GType type, ...) +{ + g_auto(GValue) value = G_VALUE_INIT; + g_autofree gchar *error = NULL; + va_list var_args; + + va_start (var_args, type); + + G_VALUE_COLLECT_INIT (&value, type, var_args, 0, &error); + + va_end (var_args); + + if (error) + g_critical ("%s: %s", G_STRFUNC, error); + + return g_object_new (HDY_TYPE_VALUE_OBJECT, + "value", &value, + NULL); +} + +/** + * hdy_value_object_new_string: (skip) + * @string: (transfer none): the string to store + * + * Creates a new #HdyValueObject. This is a convenience method to create a + * #HdyValueObject that stores a string. + * + * Returns: a new #HdyValueObject + * Since: 0.0.8 + */ +HdyValueObject* +hdy_value_object_new_string (const gchar *string) +{ + g_auto(GValue) value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_STRING); + g_value_set_string (&value, string); + return hdy_value_object_new (&value); +} + +/** + * hdy_value_object_new_take_string: (skip) + * @string: (transfer full): the string to store + * + * Creates a new #HdyValueObject. This is a convenience method to create a + * #HdyValueObject that stores a string taking ownership of it. + * + * Returns: a new #HdyValueObject + * Since: 0.0.8 + */ +HdyValueObject* +hdy_value_object_new_take_string (gchar *string) +{ + g_auto(GValue) value = G_VALUE_INIT; + + g_value_init (&value, G_TYPE_STRING); + g_value_take_string (&value, string); + return hdy_value_object_new (&value); +} + +static void +hdy_value_object_finalize (GObject *object) +{ + HdyValueObject *self = HDY_VALUE_OBJECT (object); + + g_value_unset (&self->value); + + G_OBJECT_CLASS (hdy_value_object_parent_class)->finalize (object); +} + +static void +hdy_value_object_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyValueObject *self = HDY_VALUE_OBJECT (object); + + switch (prop_id) + { + case PROP_VALUE: + g_value_set_boxed (value, &self->value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_value_object_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyValueObject *self = HDY_VALUE_OBJECT (object); + GValue *real_value; + + switch (prop_id) + { + case PROP_VALUE: + /* construct only */ + real_value = g_value_get_boxed (value); + g_value_init (&self->value, real_value->g_type); + g_value_copy (real_value, &self->value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +hdy_value_object_class_init (HdyValueObjectClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = hdy_value_object_finalize; + object_class->get_property = hdy_value_object_get_property; + object_class->set_property = hdy_value_object_set_property; + + props[PROP_VALUE] = + g_param_spec_boxed ("value", C_("HdyValueObjectClass", "Value"), + C_("HdyValueObjectClass", "The contained value"), + G_TYPE_VALUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, + N_PROPS, + props); +} + +static void +hdy_value_object_init (HdyValueObject *self) +{ +} + +/** + * hdy_value_object_get_value: + * @value: the #HdyValueObject + * + * Return the contained value. + * + * Returns: (transfer none): the contained #GValue + * Since: 0.0.8 + */ +const GValue* +hdy_value_object_get_value (HdyValueObject *value) +{ + return &value->value; +} + +/** + * hdy_value_object_copy_value: + * @value: the #HdyValueObject + * @dest: #GValue with correct type to copy into + * + * Copy data from the contained #GValue into @dest. + * + * Since: 0.0.8 + */ +void +hdy_value_object_copy_value (HdyValueObject *value, + GValue *dest) +{ + g_value_copy (&value->value, dest); +} + +/** + * hdy_value_object_get_string: + * @value: the #HdyValueObject + * + * Returns the contained string if the value is of type #G_TYPE_STRING. + * + * Returns: (transfer none): the contained string + * Since: 0.0.8 + */ +const gchar* +hdy_value_object_get_string (HdyValueObject *value) +{ + return g_value_get_string (&value->value); +} + +/** + * hdy_value_object_dup_string: + * @value: the #HdyValueObject + * + * Returns a copy of the contained string if the value is of type + * #G_TYPE_STRING. + * + * Returns: (transfer full): a copy of the contained string + * Since: 0.0.8 + */ +gchar* +hdy_value_object_dup_string (HdyValueObject *value) +{ + return g_value_dup_string (&value->value); +} + diff --git a/subprojects/libhandy/src/hdy-value-object.h b/subprojects/libhandy/src/hdy-value-object.h new file mode 100644 index 0000000..b44f106 --- /dev/null +++ b/subprojects/libhandy/src/hdy-value-object.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 Red Hat Inc. + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gio/gio.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_VALUE_OBJECT (hdy_value_object_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyValueObject, hdy_value_object, HDY, VALUE_OBJECT, GObject) + +HDY_AVAILABLE_IN_ALL +HdyValueObject *hdy_value_object_new (const GValue *value); +HDY_AVAILABLE_IN_ALL +HdyValueObject *hdy_value_object_new_collect (GType type, + ...); +HDY_AVAILABLE_IN_ALL +HdyValueObject *hdy_value_object_new_string (const gchar *string); +HDY_AVAILABLE_IN_ALL +HdyValueObject *hdy_value_object_new_take_string (gchar *string); + +HDY_AVAILABLE_IN_ALL +const GValue* hdy_value_object_get_value (HdyValueObject *value); +HDY_AVAILABLE_IN_ALL +void hdy_value_object_copy_value (HdyValueObject *value, + GValue *dest); +HDY_AVAILABLE_IN_ALL +const gchar* hdy_value_object_get_string (HdyValueObject *value); +HDY_AVAILABLE_IN_ALL +gchar* hdy_value_object_dup_string (HdyValueObject *value); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-version.h.in b/subprojects/libhandy/src/hdy-version.h.in new file mode 100644 index 0000000..0cb923f --- /dev/null +++ b/subprojects/libhandy/src/hdy-version.h.in @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +/** + * SECTION:hdy-version + * @short_description: Handy version checking. + * + * Handy provides macros to check the version of the library at compile-time. + */ + +/** + * HDY_MAJOR_VERSION: + * + * Hdy major version component (e.g. 1 if %HDY_VERSION is 1.2.3) + */ +#define HDY_MAJOR_VERSION (@HDY_MAJOR_VERSION@) + +/** + * HDY_MINOR_VERSION: + * + * Hdy minor version component (e.g. 2 if %HDY_VERSION is 1.2.3) + */ +#define HDY_MINOR_VERSION (@HDY_MINOR_VERSION@) + +/** + * HDY_MICRO_VERSION: + * + * Hdy micro version component (e.g. 3 if %HDY_VERSION is 1.2.3) + */ +#define HDY_MICRO_VERSION (@HDY_MICRO_VERSION@) + +/** + * HDY_VERSION + * + * Hdy version. + */ +#define HDY_VERSION (@HDY_VERSION@) + +/** + * HDY_VERSION_S: + * + * Handy version, encoded as a string, useful for printing and + * concatenation. + */ +#define HDY_VERSION_S "@HDY_VERSION@" + +#define HDY_ENCODE_VERSION(major,minor,micro) \ + ((major) << 24 | (minor) << 16 | (micro) << 8) + +/** + * HDY_VERSION_HEX: + * + * Handy version, encoded as an hexadecimal number, useful for + * integer comparisons. + */ +#define HDY_VERSION_HEX \ + (HDY_ENCODE_VERSION (HDY_MAJOR_VERSION, HDY_MINOR_VERSION, HDY_MICRO_VERSION)) + +/** + * HDY_CHECK_VERSION: + * @major: required major version + * @minor: required minor version + * @micro: required micro version + * + * Compile-time version checking. Evaluates to %TRUE if the version + * of handy is greater than the required one. + */ +#define HDY_CHECK_VERSION(major,minor,micro) \ + (HDY_MAJOR_VERSION > (major) || \ + (HDY_MAJOR_VERSION == (major) && HDY_MINOR_VERSION > (minor)) || \ + (HDY_MAJOR_VERSION == (major) && HDY_MINOR_VERSION == (minor) && \ + HDY_MICRO_VERSION >= (micro))) + +#ifndef _HDY_EXTERN +#define _HDY_EXTERN extern +#endif + +#define HDY_AVAILABLE_IN_ALL _HDY_EXTERN diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.c b/subprojects/libhandy/src/hdy-view-switcher-bar.c new file mode 100644 index 0000000..111d3e6 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-bar.c @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-enums.h" +#include "hdy-view-switcher-bar.h" + +/** + * SECTION:hdy-view-switcher-bar + * @short_description: A view switcher action bar. + * @title: HdyViewSwitcherBar + * @See_also: #HdyViewSwitcher, #HdyViewSwitcherTitle + * + * An action bar letting you switch between multiple views offered by a + * #GtkStack, via an #HdyViewSwitcher. It is designed to be put at the bottom of + * a window and to be revealed only on really narrow windows e.g. on mobile + * phones. It can't be revealed if there are less than two pages. + * + * You can conveniently bind the #HdyViewSwitcherBar:reveal property to + * #HdyViewSwitcherTitle:title-visible to automatically reveal the view switcher + * bar when the title label is displayed in place of the view switcher. + * + * An example of the UI definition for a common use case: + * |[ + * <object class="GtkWindow"/> + * <child type="titlebar"> + * <object class="HdyHeaderBar"> + * <property name="centering-policy">strict</property> + * <child type="title"> + * <object class="HdyViewSwitcherTitle" + * id="view_switcher_title"> + * <property name="stack">stack</property> + * </object> + * </child> + * </object> + * </child> + * <child> + * <object class="GtkBox"> + * <child> + * <object class="GtkStack" id="stack"/> + * </child> + * <child> + * <object class="HdyViewSwitcherBar"> + * <property name="stack">stack</property> + * <property name="reveal" + * bind-source="view_switcher_title" + * bind-property="title-visible" + * bind-flags="sync-create"/> + * </object> + * </child> + * </object> + * </child> + * </object> + * ]| + * + * # CSS nodes + * + * #HdyViewSwitcherBar has a single CSS node with name viewswitcherbar. + * + * Since: 0.0.10 + */ + +enum { + PROP_0, + PROP_POLICY, + PROP_STACK, + PROP_REVEAL, + LAST_PROP, +}; + +struct _HdyViewSwitcherBar +{ + GtkBin parent_instance; + + GtkActionBar *action_bar; + GtkRevealer *revealer; + HdyViewSwitcher *view_switcher; + + HdyViewSwitcherPolicy policy; + gboolean reveal; +}; + +static GParamSpec *props[LAST_PROP]; + +G_DEFINE_TYPE (HdyViewSwitcherBar, hdy_view_switcher_bar, GTK_TYPE_BIN) + +static void +count_children_cb (GtkWidget *widget, + gint *count) +{ + (*count)++; +} + +static void +update_bar_revealed (HdyViewSwitcherBar *self) { + GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher); + gint count = 0; + + if (self->reveal && stack) + gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count); + + gtk_revealer_set_reveal_child (self->revealer, count > 1); +} + +static void +hdy_view_switcher_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyViewSwitcherBar *self = HDY_VIEW_SWITCHER_BAR (object); + + switch (prop_id) { + case PROP_POLICY: + g_value_set_enum (value, hdy_view_switcher_bar_get_policy (self)); + break; + case PROP_STACK: + g_value_set_object (value, hdy_view_switcher_bar_get_stack (self)); + break; + case PROP_REVEAL: + g_value_set_boolean (value, hdy_view_switcher_bar_get_reveal (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_view_switcher_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyViewSwitcherBar *self = HDY_VIEW_SWITCHER_BAR (object); + + switch (prop_id) { + case PROP_POLICY: + hdy_view_switcher_bar_set_policy (self, g_value_get_enum (value)); + break; + case PROP_STACK: + hdy_view_switcher_bar_set_stack (self, g_value_get_object (value)); + break; + case PROP_REVEAL: + hdy_view_switcher_bar_set_reveal (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_view_switcher_bar_class_init (HdyViewSwitcherBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = hdy_view_switcher_bar_get_property; + object_class->set_property = hdy_view_switcher_bar_set_property; + + /** + * HdyViewSwitcherBar:policy: + * + * The #HdyViewSwitcherPolicy the #HdyViewSwitcher 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_NARROW, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * HdyViewSwitcherBar:stack: + * + * The #GtkStack the #HdyViewSwitcher 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); + + /** + * HdyViewSwitcherBar:reveal: + * + * Whether the bar should be revealed or hidden. + * + * Since: 0.0.10 + */ + props[PROP_REVEAL] = + g_param_spec_boolean ("reveal", + _("Reveal"), + _("Whether the view switcher is revealed"), + FALSE, + 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, "viewswitcherbar"); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-view-switcher-bar.ui"); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherBar, action_bar); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherBar, view_switcher); +} + +static void +hdy_view_switcher_bar_init (HdyViewSwitcherBar *self) +{ + /* This must be initialized before the template so the embedded view switcher + * can pick up the correct default value. + */ + self->policy = HDY_VIEW_SWITCHER_POLICY_NARROW; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->revealer = GTK_REVEALER (gtk_bin_get_child (GTK_BIN (self->action_bar))); + update_bar_revealed (self); + gtk_revealer_set_transition_type (self->revealer, GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP); +} + +/** + * hdy_view_switcher_bar_new: + * + * Creates a new #HdyViewSwitcherBar widget. + * + * Returns: a new #HdyViewSwitcherBar + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_view_switcher_bar_new (void) +{ + return g_object_new (HDY_TYPE_VIEW_SWITCHER_BAR, NULL); +} + +/** + * hdy_view_switcher_bar_get_policy: + * @self: a #HdyViewSwitcherBar + * + * Gets the policy of @self. + * + * Returns: the policy of @self + * + * Since: 0.0.10 + */ +HdyViewSwitcherPolicy +hdy_view_switcher_bar_get_policy (HdyViewSwitcherBar *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), HDY_VIEW_SWITCHER_POLICY_NARROW); + + return self->policy; +} + +/** + * hdy_view_switcher_bar_set_policy: + * @self: a #HdyViewSwitcherBar + * @policy: the new policy + * + * Sets the policy of @self. + * + * Since: 0.0.10 + */ +void +hdy_view_switcher_bar_set_policy (HdyViewSwitcherBar *self, + HdyViewSwitcherPolicy policy) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (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_bar_get_stack: + * @self: a #HdyViewSwitcherBar + * + * Get the #GtkStack being controlled by the #HdyViewSwitcher. + * + * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set + * + * Since: 0.0.10 + */ +GtkStack * +hdy_view_switcher_bar_get_stack (HdyViewSwitcherBar *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), NULL); + + return hdy_view_switcher_get_stack (self->view_switcher); +} + +/** + * hdy_view_switcher_bar_set_stack: + * @self: a #HdyViewSwitcherBar + * @stack: (nullable): a #GtkStack + * + * Sets the #GtkStack to control. + * + * Since: 0.0.10 + */ +void +hdy_view_switcher_bar_set_stack (HdyViewSwitcherBar *self, + GtkStack *stack) +{ + GtkStack *previous_stack; + + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self)); + g_return_if_fail (stack == NULL || GTK_IS_STACK (stack)); + + previous_stack = hdy_view_switcher_get_stack (self->view_switcher); + + if (previous_stack == stack) + return; + + if (previous_stack) + g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_bar_revealed), self); + + hdy_view_switcher_set_stack (self->view_switcher, stack); + + if (stack) { + g_signal_connect_swapped (stack, "add", G_CALLBACK (update_bar_revealed), self); + g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_bar_revealed), self); + } + + update_bar_revealed (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]); +} + +/** + * hdy_view_switcher_bar_get_reveal: + * @self: a #HdyViewSwitcherBar + * + * Gets whether @self should be revealed or not. + * + * Returns: %TRUE if @self is revealed, %FALSE if not. + * + * Since: 0.0.10 + */ +gboolean +hdy_view_switcher_bar_get_reveal (HdyViewSwitcherBar *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), FALSE); + + return self->reveal; +} + +/** + * hdy_view_switcher_bar_set_reveal: + * @self: a #HdyViewSwitcherBar + * @reveal: %TRUE to reveal @self + * + * Sets whether @self should be revealed or not. + * + * Since: 0.0.10 + */ +void +hdy_view_switcher_bar_set_reveal (HdyViewSwitcherBar *self, + gboolean reveal) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self)); + + reveal = !!reveal; + + if (self->reveal == reveal) + return; + + self->reveal = reveal; + update_bar_revealed (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL]); +} diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.h b/subprojects/libhandy/src/hdy-view-switcher-bar.h new file mode 100644 index 0000000..be2db35 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-bar.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +#include "hdy-view-switcher.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyViewSwitcherBar, hdy_view_switcher_bar, HDY, VIEW_SWITCHER_BAR, GtkBin) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_view_switcher_bar_new (void); + +HDY_AVAILABLE_IN_ALL +HdyViewSwitcherPolicy hdy_view_switcher_bar_get_policy (HdyViewSwitcherBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_bar_set_policy (HdyViewSwitcherBar *self, + HdyViewSwitcherPolicy policy); + +HDY_AVAILABLE_IN_ALL +GtkStack *hdy_view_switcher_bar_get_stack (HdyViewSwitcherBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_bar_set_stack (HdyViewSwitcherBar *self, + GtkStack *stack); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_view_switcher_bar_get_reveal (HdyViewSwitcherBar *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_bar_set_reveal (HdyViewSwitcherBar *self, + gboolean reveal); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.ui b/subprojects/libhandy/src/hdy-view-switcher-bar.ui new file mode 100644 index 0000000..a2b1266 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-bar.ui @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyViewSwitcherBar" parent="GtkBin"> + <child> + <object class="GtkActionBar" id="action_bar"> + <property name="visible">True</property> + <child type="center"> + <object class="HdyViewSwitcher" id="view_switcher"> + <property name="margin-start">10</property> + <property name="margin-end">10</property> + <property name="narrow-ellipsize">end</property> + <property name="policy" bind-source="HdyViewSwitcherBar" bind-property="policy" bind-flags="sync-create|bidirectional" /> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-view-switcher-button-private.h b/subprojects/libhandy/src/hdy-view-switcher-button-private.h new file mode 100644 index 0000000..5f85c52 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-button-private.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_VIEW_SWITCHER_BUTTON (hdy_view_switcher_button_get_type()) + +G_DECLARE_FINAL_TYPE (HdyViewSwitcherButton, hdy_view_switcher_button, HDY, VIEW_SWITCHER_BUTTON, GtkRadioButton) + +GtkWidget *hdy_view_switcher_button_new (void); + +const gchar *hdy_view_switcher_button_get_icon_name (HdyViewSwitcherButton *self); +void hdy_view_switcher_button_set_icon_name (HdyViewSwitcherButton *self, + const gchar *icon_name); + +GtkIconSize hdy_view_switcher_button_get_icon_size (HdyViewSwitcherButton *self); +void hdy_view_switcher_button_set_icon_size (HdyViewSwitcherButton *self, + GtkIconSize icon_size); + +gboolean hdy_view_switcher_button_get_needs_attention (HdyViewSwitcherButton *self); +void hdy_view_switcher_button_set_needs_attention (HdyViewSwitcherButton *self, + gboolean needs_attention); + +const gchar *hdy_view_switcher_button_get_label (HdyViewSwitcherButton *self); +void hdy_view_switcher_button_set_label (HdyViewSwitcherButton *self, + const gchar *label); + +void hdy_view_switcher_button_set_narrow_ellipsize (HdyViewSwitcherButton *self, + PangoEllipsizeMode mode); + +void hdy_view_switcher_button_get_size (HdyViewSwitcherButton *self, + gint *h_min_width, + gint *h_nat_width, + gint *v_min_width, + gint *v_nat_width); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-view-switcher-button.c b/subprojects/libhandy/src/hdy-view-switcher-button.c new file mode 100644 index 0000000..20b57b0 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-button.c @@ -0,0 +1,536 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-view-switcher-button-private.h" + +/** + * PRIVATE:hdy-view-switcher-button + * @short_description: Button used in #HdyViewSwitcher. + * @title: HdyViewSwitcherButton + * @See_also: #HdyViewSwitcher + * @stability: Private + * + * #HdyViewSwitcherButton represents an application's view. It is designed to be + * used exclusively internally by #HdyViewSwitcher. + * + * Since: 0.0.10 + */ + +enum { + PROP_0, + PROP_ICON_SIZE, + PROP_ICON_NAME, + PROP_NEEDS_ATTENTION, + + /* Overridden properties */ + PROP_LABEL, + PROP_ORIENTATION, + + LAST_PROP = PROP_NEEDS_ATTENTION + 1, +}; + +struct _HdyViewSwitcherButton +{ + GtkRadioButton parent_instance; + + GtkBox *horizontal_box; + GtkImage *horizontal_image; + GtkLabel *horizontal_label_active; + GtkLabel *horizontal_label_inactive; + GtkStack *horizontal_label_stack; + GtkStack *stack; + GtkBox *vertical_box; + GtkImage *vertical_image; + GtkLabel *vertical_label_active; + GtkLabel *vertical_label_inactive; + GtkStack *vertical_label_stack; + + gchar *icon_name; + GtkIconSize icon_size; + gchar *label; + GtkOrientation orientation; +}; + +static GParamSpec *props[LAST_PROP]; + +G_DEFINE_TYPE_WITH_CODE (HdyViewSwitcherButton, hdy_view_switcher_button, GTK_TYPE_RADIO_BUTTON, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +static void +on_active_changed (HdyViewSwitcherButton *self) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + + if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self))) { + gtk_stack_set_visible_child (self->horizontal_label_stack, GTK_WIDGET (self->horizontal_label_active)); + gtk_stack_set_visible_child (self->vertical_label_stack, GTK_WIDGET (self->vertical_label_active)); + } else { + gtk_stack_set_visible_child (self->horizontal_label_stack, GTK_WIDGET (self->horizontal_label_inactive)); + gtk_stack_set_visible_child (self->vertical_label_stack, GTK_WIDGET (self->vertical_label_inactive)); + } +} + +static GtkOrientation +get_orientation (HdyViewSwitcherButton *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), GTK_ORIENTATION_HORIZONTAL); + + return self->orientation; +} + +static void +set_orientation (HdyViewSwitcherButton *self, + GtkOrientation orientation) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + + if (self->orientation == orientation) + return; + + self->orientation = orientation; + + gtk_stack_set_visible_child (self->stack, + GTK_WIDGET (self->orientation == GTK_ORIENTATION_VERTICAL ? + self->vertical_box : + self->horizontal_box)); +} + +static void +hdy_view_switcher_button_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object); + + switch (prop_id) { + case PROP_ICON_NAME: + g_value_set_string (value, hdy_view_switcher_button_get_icon_name (self)); + break; + case PROP_ICON_SIZE: + g_value_set_int (value, hdy_view_switcher_button_get_icon_size (self)); + break; + case PROP_NEEDS_ATTENTION: + g_value_set_boolean (value, hdy_view_switcher_button_get_needs_attention (self)); + break; + case PROP_LABEL: + g_value_set_string (value, hdy_view_switcher_button_get_label (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, get_orientation (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_view_switcher_button_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object); + + switch (prop_id) { + case PROP_ICON_NAME: + hdy_view_switcher_button_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_ICON_SIZE: + hdy_view_switcher_button_set_icon_size (self, g_value_get_int (value)); + break; + case PROP_NEEDS_ATTENTION: + hdy_view_switcher_button_set_needs_attention (self, g_value_get_boolean (value)); + break; + case PROP_LABEL: + hdy_view_switcher_button_set_label (self, g_value_get_string (value)); + break; + case PROP_ORIENTATION: + set_orientation (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_view_switcher_button_finalize (GObject *object) +{ + HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object); + + g_free (self->icon_name); + g_free (self->label); + + G_OBJECT_CLASS (hdy_view_switcher_button_parent_class)->finalize (object); +} + +static void +hdy_view_switcher_button_class_init (HdyViewSwitcherButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = hdy_view_switcher_button_get_property; + object_class->set_property = hdy_view_switcher_button_set_property; + object_class->finalize = hdy_view_switcher_button_finalize; + + g_object_class_override_property (object_class, + PROP_LABEL, + "label"); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + /** + * HdyViewSwitcherButton:icon-name: + * + * The icon name representing the view, or %NULL for no icon. + * + * Since: 0.0.10 + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + _("Icon Name"), + _("Icon name for image"), + "text-x-generic-symbolic", + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE); + + /** + * HdyViewSwitcherButton:icon-size: + * + * The icon size. + * + * Since: 0.0.10 + */ + props[PROP_ICON_SIZE] = + g_param_spec_int ("icon-size", + _("Icon Size"), + _("Symbolic size to use for named icon"), + 0, G_MAXINT, GTK_ICON_SIZE_BUTTON, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE); + + /** + * HdyViewSwitcherButton:needs-attention: + * + * Sets a flag specifying whether the view requires the user attention. This + * is used by the HdyViewSwitcher to change the appearance of the + * corresponding button when a view needs attention and it is not the current + * one. + * + * Since: 0.0.10 + */ + props[PROP_NEEDS_ATTENTION] = + g_param_spec_boolean ("needs-attention", + _("Needs attention"), + _("Hint the view needs attention"), + FALSE, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /* We probably should set the class's CSS name to "viewswitcherbutton" + * here, but it doesn't work because GtkCheckButton hardcodes it to "button" + * on instantiation, and the functions required to override it are private. + * In the meantime, we can use the "viewswitcher > button" CSS selector as + * a fairly safe fallback. + */ + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-view-switcher-button.ui"); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_box); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_image); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_active); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_inactive); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_stack); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, stack); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_box); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_image); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_active); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_inactive); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_stack); + gtk_widget_class_bind_template_callback (widget_class, on_active_changed); +} + +static void +hdy_view_switcher_button_init (HdyViewSwitcherButton *self) +{ + self->icon_size = GTK_ICON_SIZE_BUTTON; + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_stack_set_visible_child (GTK_STACK (self->stack), GTK_WIDGET (self->horizontal_box)); + + gtk_widget_set_focus_on_click (GTK_WIDGET (self), FALSE); + /* Make the button look like a regular button and not a radio button. */ + gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE); + + on_active_changed (self); +} + +/** + * hdy_view_switcher_button_new: + * + * Creates a new #HdyViewSwitcherButton widget. + * + * Returns: a new #HdyViewSwitcherButton + * + * Since: 0.0.10 + */ +GtkWidget * +hdy_view_switcher_button_new (void) +{ + return g_object_new (HDY_TYPE_VIEW_SWITCHER_BUTTON, NULL); +} + +/** + * hdy_view_switcher_button_get_icon_name: + * @self: a #HdyViewSwitcherButton + * + * Gets the icon name representing the view, or %NULL is no icon is set. + * + * Returns: (transfer none) (nullable): the icon name, or %NULL + * + * Since: 0.0.10 + **/ +const gchar * +hdy_view_switcher_button_get_icon_name (HdyViewSwitcherButton *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), NULL); + + return self->icon_name; +} + +/** + * hdy_view_switcher_button_set_icon_name: + * @self: a #HdyViewSwitcherButton + * @icon_name: (nullable): an icon name or %NULL + * + * Sets the icon name representing the view, or %NULL to disable the icon. + * + * Since: 0.0.10 + **/ +void +hdy_view_switcher_button_set_icon_name (HdyViewSwitcherButton *self, + const gchar *icon_name) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + + if (!g_strcmp0 (self->icon_name, icon_name)) + return; + + g_free (self->icon_name); + self->icon_name = g_strdup (icon_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + +/** + * hdy_view_switcher_button_get_icon_size: + * @self: a #HdyViewSwitcherButton + * + * Gets the icon size used by @self. + * + * Returns: the icon size used by @self + * + * Since: 0.0.10 + **/ +GtkIconSize +hdy_view_switcher_button_get_icon_size (HdyViewSwitcherButton *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), GTK_ICON_SIZE_INVALID); + + return self->icon_size; +} + +/** + * hdy_view_switcher_button_set_icon_size: + * @self: a #HdyViewSwitcherButton + * @icon_size: the new icon size + * + * Sets the icon size used by @self. + * + * Since: 0.0.10 + */ +void +hdy_view_switcher_button_set_icon_size (HdyViewSwitcherButton *self, + GtkIconSize icon_size) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + + if (self->icon_size == icon_size) + return; + + self->icon_size = icon_size; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_SIZE]); +} + +/** + * hdy_view_switcher_button_get_needs_attention: + * @self: a #HdyViewSwitcherButton + * + * Gets whether the view represented by @self requires the user attention. + * + * Returns: %TRUE if the view represented by @self requires the user attention, %FALSE otherwise + * + * Since: 0.0.10 + **/ +gboolean +hdy_view_switcher_button_get_needs_attention (HdyViewSwitcherButton *self) +{ + GtkStyleContext *context; + + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), FALSE); + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + return gtk_style_context_has_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); +} + +/** + * hdy_view_switcher_button_set_needs_attention: + * @self: a #HdyViewSwitcherButton + * @needs_attention: the new icon size + * + * Sets whether the view represented by @self requires the user attention. + * + * Since: 0.0.10 + */ +void +hdy_view_switcher_button_set_needs_attention (HdyViewSwitcherButton *self, + gboolean needs_attention) +{ + GtkStyleContext *context; + + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + + needs_attention = !!needs_attention; + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + if (gtk_style_context_has_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION) == needs_attention) + return; + + if (needs_attention) + gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); + else + gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION]); +} + +/** + * hdy_view_switcher_button_get_label: + * @self: a #HdyViewSwitcherButton + * + * Gets the label representing the view. + * + * Returns: (transfer none) (nullable): the label, or %NULL + * + * Since: 0.0.10 + **/ +const gchar * +hdy_view_switcher_button_get_label (HdyViewSwitcherButton *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), NULL); + + return self->label; +} + +/** + * hdy_view_switcher_button_set_label: + * @self: a #HdyViewSwitcherButton + * @label: (nullable): a label or %NULL + * + * Sets the label representing the view. + * + * Since: 0.0.10 + **/ +void +hdy_view_switcher_button_set_label (HdyViewSwitcherButton *self, + const gchar *label) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + + if (!g_strcmp0 (self->label, label)) + return; + + g_free (self->label); + self->label = g_strdup (label); + + g_object_notify (G_OBJECT (self), "label"); +} + +/** + * hdy_view_switcher_button_set_narrow_ellipsize: + * @self: a #HdyViewSwitcherButton + * @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_button_set_narrow_ellipsize (HdyViewSwitcherButton *self, + PangoEllipsizeMode mode) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self)); + g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END); + + gtk_label_set_ellipsize (self->vertical_label_active, mode); + gtk_label_set_ellipsize (self->vertical_label_inactive, mode); +} + +/** + * hdy_view_switcher_button_get_size: + * @self: a #HdyViewSwitcherButton + * @h_min_width: (out) (nullable): the minimum width when horizontal + * @h_nat_width: (out) (nullable): the natural width when horizontal + * @v_min_width: (out) (nullable): the minimum width when vertical + * @v_nat_width: (out) (nullable): the natural width when vertical + * + * Measure the size requests in both horizontal and vertical modes. + * + * Since: 0.0.10 + */ +void +hdy_view_switcher_button_get_size (HdyViewSwitcherButton *self, + gint *h_min_width, + gint *h_nat_width, + gint *v_min_width, + gint *v_nat_width) +{ + GtkStyleContext *context; + GtkStateFlags state; + GtkBorder border; + + /* gtk_widget_get_preferred_width() doesn't accept both its out parameters to + * be NULL, so we must have guards. + */ + if (h_min_width != NULL || h_nat_width != NULL) + gtk_widget_get_preferred_width (GTK_WIDGET (self->horizontal_box), h_min_width, h_nat_width); + if (v_min_width != NULL || v_nat_width != NULL) + gtk_widget_get_preferred_width (GTK_WIDGET (self->vertical_box), v_min_width, v_nat_width); + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + state = gtk_style_context_get_state (context); + gtk_style_context_get_border (context, state, &border); + if (h_min_width != NULL) + *h_min_width += border.left + border.right; + if (h_nat_width != NULL) + *h_nat_width += border.left + border.right; + if (v_min_width != NULL) + *v_min_width += border.left + border.right; + if (v_nat_width != NULL) + *v_nat_width += border.left + border.right; +} diff --git a/subprojects/libhandy/src/hdy-view-switcher-button.ui b/subprojects/libhandy/src/hdy-view-switcher-button.ui new file mode 100644 index 0000000..018b6cc --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-button.ui @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyViewSwitcherButton" parent="GtkRadioButton"> + <signal name="notify::active" handler="on_active_changed" after="yes"/> + <child> + <object class="GtkStack" id="stack"> + <property name="hhomogeneous">False</property> + <property name="transition-type">crossfade</property> + <property name="vhomogeneous">True</property> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="horizontal_box"> + <property name="halign">center</property> + <property name="orientation">horizontal</property> + <property name="spacing">8</property> + <property name="valign">center</property> + <property name="visible">True</property> + <style> + <class name="wide"/> + </style> + <child> + <object class="GtkImage" id="horizontal_image"> + <property name="icon-name" bind-source="HdyViewSwitcherButton" bind-property="icon-name" bind-flags="sync-create" /> + <property name="icon-size" bind-source="HdyViewSwitcherButton" bind-property="icon-size" bind-flags="sync-create" /> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkStack" id="horizontal_label_stack"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="horizontal_label_inactive"> + <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" /> + <property name="visible">True</property> + </object> + <packing> + <property name="name">inactive</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="horizontal_label_active"> + <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" /> + <property name="visible">True</property> + <style> + <class name="active"/> + </style> + </object> + <packing> + <property name="name">active</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="vertical_box"> + <property name="halign">center</property> + <property name="orientation">vertical</property> + <property name="spacing">4</property> + <property name="valign">center</property> + <property name="visible">True</property> + <style> + <class name="narrow"/> + </style> + <child> + <object class="GtkImage" id="vertical_image"> + <property name="icon-name" bind-source="HdyViewSwitcherButton" bind-property="icon-name" bind-flags="sync-create" /> + <property name="icon-size" bind-source="HdyViewSwitcherButton" bind-property="icon-size" bind-flags="sync-create" /> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkStack" id="vertical_label_stack"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="vertical_label_inactive"> + <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" /> + <property name="visible">True</property> + </object> + <packing> + <property name="name">inactive</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="vertical_label_active"> + <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" /> + <property name="visible">True</property> + <style> + <class name="active"/> + </style> + </object> + <packing> + <property name="name">active</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.c b/subprojects/libhandy/src/hdy-view-switcher-title.c new file mode 100644 index 0000000..39bdafa --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-title.c @@ -0,0 +1,600 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "hdy-view-switcher-title.h" +#include "hdy-squeezer.h" + +/** + * SECTION:hdy-view-switcher-title + * @short_description: A view switcher title. + * @title: HdyViewSwitcherTitle + * @See_also: #HdyHeaderBar, #HdyViewSwitcher, #HdyViewSwitcherBar + * + * A widget letting you switch between multiple views offered by a #GtkStack, + * via an #HdyViewSwitcher. It is designed to be used as the title widget of a + * #HdyHeaderBar, and will display the window's title when the window is too + * narrow to fit the view switcher e.g. on mobile phones, or if there are less + * than two views. + * + * You can conveniently bind the #HdyViewSwitcherBar:reveal property to + * #HdyViewSwitcherTitle:title-visible to automatically reveal the view switcher + * bar when the title label is displayed in place of the view switcher. + * + * An example of the UI definition for a common use case: + * |[ + * <object class="GtkWindow"/> + * <child type="titlebar"> + * <object class="HdyHeaderBar"> + * <property name="centering-policy">strict</property> + * <child type="title"> + * <object class="HdyViewSwitcherTitle" + * id="view_switcher_title"> + * <property name="stack">stack</property> + * </object> + * </child> + * </object> + * </child> + * <child> + * <object class="GtkBox"> + * <child> + * <object class="GtkStack" id="stack"/> + * </child> + * <child> + * <object class="HdyViewSwitcherBar"> + * <property name="stack">stack</property> + * <property name="reveal" + * bind-source="view_switcher_title" + * bind-property="title-visible" + * bind-flags="sync-create"/> + * </object> + * </child> + * </object> + * </child> + * </object> + * ]| + * + * # CSS nodes + * + * #HdyViewSwitcherTitle has a single CSS node with name viewswitchertitle. + * + * Since: 1.0 + */ + +enum { + PROP_0, + PROP_POLICY, + PROP_STACK, + PROP_TITLE, + PROP_SUBTITLE, + PROP_VIEW_SWITCHER_ENABLED, + PROP_TITLE_VISIBLE, + LAST_PROP, +}; + +struct _HdyViewSwitcherTitle +{ + GtkBin parent_instance; + + HdySqueezer *squeezer; + GtkLabel *subtitle_label; + GtkBox *title_box; + GtkLabel *title_label; + HdyViewSwitcher *view_switcher; + + gboolean view_switcher_enabled; +}; + +static GParamSpec *props[LAST_PROP]; + +G_DEFINE_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, GTK_TYPE_BIN) + +static void +update_subtitle_label (HdyViewSwitcherTitle *self) +{ + const gchar *subtitle = gtk_label_get_label (self->subtitle_label); + + gtk_widget_set_visible (GTK_WIDGET (self->subtitle_label), subtitle && subtitle[0]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +static void +count_children_cb (GtkWidget *widget, + gint *count) +{ + (*count)++; +} + +static void +update_view_switcher_visible (HdyViewSwitcherTitle *self) +{ + GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher); + gint count = 0; + + if (self->view_switcher_enabled && stack) + gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count); + + hdy_squeezer_set_child_enabled (self->squeezer, GTK_WIDGET (self->view_switcher), count > 1); +} + +static void +notify_squeezer_visible_child_cb (GObject *self) +{ + g_object_notify_by_pspec (self, props[PROP_TITLE_VISIBLE]); +} + +static void +hdy_view_switcher_title_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object); + + switch (prop_id) { + case PROP_POLICY: + g_value_set_enum (value, hdy_view_switcher_title_get_policy (self)); + break; + case PROP_STACK: + g_value_set_object (value, hdy_view_switcher_title_get_stack (self)); + break; + case PROP_TITLE: + g_value_set_string (value, hdy_view_switcher_title_get_title (self)); + break; + case PROP_SUBTITLE: + g_value_set_string (value, hdy_view_switcher_title_get_subtitle (self)); + break; + case PROP_VIEW_SWITCHER_ENABLED: + g_value_set_boolean (value, hdy_view_switcher_title_get_view_switcher_enabled (self)); + break; + case PROP_TITLE_VISIBLE: + g_value_set_boolean (value, hdy_view_switcher_title_get_title_visible (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_view_switcher_title_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object); + + switch (prop_id) { + case PROP_POLICY: + hdy_view_switcher_title_set_policy (self, g_value_get_enum (value)); + break; + case PROP_STACK: + hdy_view_switcher_title_set_stack (self, g_value_get_object (value)); + break; + case PROP_TITLE: + hdy_view_switcher_title_set_title (self, g_value_get_string (value)); + break; + case PROP_SUBTITLE: + hdy_view_switcher_title_set_subtitle (self, g_value_get_string (value)); + break; + case PROP_VIEW_SWITCHER_ENABLED: + hdy_view_switcher_title_set_view_switcher_enabled (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +hdy_view_switcher_title_dispose (GObject *object) { + HdyViewSwitcherTitle *self = (HdyViewSwitcherTitle *)object; + + if (self->view_switcher) { + GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher); + + if (stack) + g_signal_handlers_disconnect_by_func (stack, G_CALLBACK (update_view_switcher_visible), self); + } + + G_OBJECT_CLASS (hdy_view_switcher_title_parent_class)->dispose (object); +} + +static void +hdy_view_switcher_title_class_init (HdyViewSwitcherTitleClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = hdy_view_switcher_title_dispose; + object_class->get_property = hdy_view_switcher_title_get_property; + object_class->set_property = hdy_view_switcher_title_set_property; + + /** + * HdyViewSwitcherTitle:policy: + * + * The #HdyViewSwitcherPolicy the #HdyViewSwitcher should use to determine + * which mode to use. + * + * Since: 1.0 + */ + 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); + + /** + * HdyViewSwitcherTitle:stack: + * + * The #GtkStack the #HdyViewSwitcher controls. + * + * Since: 1.0 + */ + props[PROP_STACK] = + g_param_spec_object ("stack", + _("Stack"), + _("Stack"), + GTK_TYPE_STACK, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * HdyViewSwitcherTitle:title: + * + * The title of the #HdyViewSwitcher. + * + * Since: 1.0 + */ + props[PROP_TITLE] = + g_param_spec_string ("title", + _("Title"), + _("The title to display"), + NULL, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * HdyViewSwitcherTitle:subtitle: + * + * The subtitle of the #HdyViewSwitcher. + * + * Since: 1.0 + */ + props[PROP_SUBTITLE] = + g_param_spec_string ("subtitle", + _("Subtitle"), + _("The subtitle to display"), + NULL, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * HdyViewSwitcherTitle:view-switcher-enabled: + * + * Whether the bar should be revealed or hidden. + * + * Since: 1.0 + */ + props[PROP_VIEW_SWITCHER_ENABLED] = + g_param_spec_boolean ("view-switcher-enabled", + _("View switcher enabled"), + _("Whether the view switcher is enabled"), + TRUE, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * HdyViewSwitcherTitle:title-visible: + * + * Whether the bar should be revealed or hidden. + * + * Since: 1.0 + */ + props[PROP_TITLE_VISIBLE] = + g_param_spec_boolean ("title-visible", + _("Title visible"), + _("Whether the title label is visible"), + TRUE, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "viewswitchertitle"); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/handy/ui/hdy-view-switcher-title.ui"); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, squeezer); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, subtitle_label); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_box); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_label); + gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, view_switcher); + gtk_widget_class_bind_template_callback (widget_class, notify_squeezer_visible_child_cb); +} + +static void +hdy_view_switcher_title_init (HdyViewSwitcherTitle *self) +{ + /* This must be initialized before the template so the embedded view switcher + * can pick up the correct default value. + */ + self->view_switcher_enabled = TRUE; + + gtk_widget_init_template (GTK_WIDGET (self)); + + update_subtitle_label (self); + update_view_switcher_visible (self); +} + +/** + * hdy_view_switcher_title_new: + * + * Creates a new #HdyViewSwitcherTitle widget. + * + * Returns: a new #HdyViewSwitcherTitle + * + * Since: 1.0 + */ +HdyViewSwitcherTitle * +hdy_view_switcher_title_new (void) +{ + return g_object_new (HDY_TYPE_VIEW_SWITCHER_TITLE, NULL); +} + +/** + * hdy_view_switcher_title_get_policy: + * @self: a #HdyViewSwitcherTitle + * + * Gets the policy of @self. + * + * Returns: the policy of @self + * + * Since: 1.0 + */ +HdyViewSwitcherPolicy +hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), HDY_VIEW_SWITCHER_POLICY_NARROW); + + return hdy_view_switcher_get_policy (self->view_switcher); +} + +/** + * hdy_view_switcher_title_set_policy: + * @self: a #HdyViewSwitcherTitle + * @policy: the new policy + * + * Sets the policy of @self. + * + * Since: 1.0 + */ +void +hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle *self, + HdyViewSwitcherPolicy policy) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self)); + + if (hdy_view_switcher_get_policy (self->view_switcher) == policy) + return; + + hdy_view_switcher_set_policy (self->view_switcher, policy); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]); + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + +/** + * hdy_view_switcher_title_get_stack: + * @self: a #HdyViewSwitcherTitle + * + * Get the #GtkStack being controlled by the #HdyViewSwitcher. + * + * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set + * + * Since: 1.0 + */ +GtkStack * +hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL); + + return hdy_view_switcher_get_stack (self->view_switcher); +} + +/** + * hdy_view_switcher_title_set_stack: + * @self: a #HdyViewSwitcherTitle + * @stack: (nullable): a #GtkStack + * + * Sets the #GtkStack to control. + * + * Since: 1.0 + */ +void +hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self, + GtkStack *stack) +{ + GtkStack *previous_stack; + + g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self)); + g_return_if_fail (stack == NULL || GTK_IS_STACK (stack)); + + previous_stack = hdy_view_switcher_get_stack (self->view_switcher); + + if (previous_stack == stack) + return; + + if (previous_stack) + g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_view_switcher_visible), self); + + hdy_view_switcher_set_stack (self->view_switcher, stack); + + if (stack) { + g_signal_connect_swapped (stack, "add", G_CALLBACK (update_view_switcher_visible), self); + g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_view_switcher_visible), self); + } + + update_view_switcher_visible (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]); +} + +/** + * hdy_view_switcher_title_get_title: + * @self: a #HdyViewSwitcherTitle + * + * Gets the title of @self. See hdy_view_switcher_title_set_title(). + * + * Returns: (transfer none) (nullable): the title of @self, or %NULL. + * + * Since: 1.0 + */ +const gchar * +hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL); + + return gtk_label_get_label (self->title_label); +} + +/** + * hdy_view_switcher_title_set_title: + * @self: a #HdyViewSwitcherTitle + * @title: (nullable): a title, or %NULL + * + * Sets the title of @self. The title should give a user additional details. A + * good title should not include the application name. + * + * Since: 1.0 + */ +void +hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self, + const gchar *title) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self)); + + if (g_strcmp0 (gtk_label_get_label (self->title_label), title) == 0) + return; + + gtk_label_set_label (self->title_label, title); + gtk_widget_set_visible (GTK_WIDGET (self->title_label), title && title[0]); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +/** + * hdy_view_switcher_title_get_subtitle: + * @self: a #HdyViewSwitcherTitle + * + * Gets the subtitle of @self. See hdy_view_switcher_title_set_subtitle(). + * + * Returns: (transfer none) (nullable): the subtitle of @self, or %NULL. + * + * Since: 1.0 + */ +const gchar * +hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL); + + return gtk_label_get_label (self->subtitle_label); +} + +/** + * hdy_view_switcher_title_set_subtitle: + * @self: a #HdyViewSwitcherTitle + * @subtitle: (nullable): a subtitle, or %NULL + * + * Sets the subtitle of @self. The subtitle should give a user additional + * details. + * + * Since: 1.0 + */ +void +hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self, + const gchar *subtitle) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self)); + + if (g_strcmp0 (gtk_label_get_label (self->subtitle_label), subtitle) == 0) + return; + + gtk_label_set_label (self->subtitle_label, subtitle); + update_subtitle_label (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]); +} + +/** + * hdy_view_switcher_title_get_view_switcher_enabled: + * @self: a #HdyViewSwitcherTitle + * + * Gets whether @self's view switcher is enabled. + * + * See hdy_view_switcher_title_set_view_switcher_enabled(). + * + * Returns: %TRUE if the view switcher is enabled, %FALSE otherwise. + * + * Since: 1.0 + */ +gboolean +hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE); + + return self->view_switcher_enabled; +} + +/** + * hdy_view_switcher_title_set_view_switcher_enabled: + * @self: a #HdyViewSwitcherTitle + * @enabled: %TRUE to enable the view switcher, %FALSE to disable it + * + * Make @self enable or disable its view switcher. If it is disabled, the title + * will be displayed instead. This allows to programmatically and prematurely + * hide the view switcher of @self even if it fits in the available space. + * + * This can be used e.g. to ensure the view switcher is hidden below a certain + * window width, or any other constraint you find suitable. + * + * Since: 1.0 + */ +void +hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self, + gboolean enabled) +{ + g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self)); + + enabled = !!enabled; + + if (self->view_switcher_enabled == enabled) + return; + + self->view_switcher_enabled = enabled; + update_view_switcher_visible (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW_SWITCHER_ENABLED]); +} + +/** + * hdy_view_switcher_title_get_title_visible: + * @self: a #HdyViewSwitcherTitle + * + * Get whether the title label of @self is visible. + * + * Returns: %TRUE if the title label of @self is visible, %FALSE if not. + * + * Since: 1.0 + */ +gboolean +hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self) +{ + g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE); + + return hdy_squeezer_get_visible_child (self->squeezer) == (GtkWidget *) self->title_box; +} diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.h b/subprojects/libhandy/src/hdy-view-switcher-title.h new file mode 100644 index 0000000..2540396 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-title.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +#include "hdy-view-switcher.h" + +G_BEGIN_DECLS + +#define HDY_TYPE_VIEW_SWITCHER_TITLE (hdy_view_switcher_title_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, HDY, VIEW_SWITCHER_TITLE, GtkBin) + +HDY_AVAILABLE_IN_ALL +HdyViewSwitcherTitle *hdy_view_switcher_title_new (void); + +HDY_AVAILABLE_IN_ALL +HdyViewSwitcherPolicy hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle *self, + HdyViewSwitcherPolicy policy); + +HDY_AVAILABLE_IN_ALL +GtkStack *hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self, + GtkStack *stack); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self, + const gchar *title); + +HDY_AVAILABLE_IN_ALL +const gchar *hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self, + const gchar *subtitle); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self, + gboolean enabled); + +HDY_AVAILABLE_IN_ALL +gboolean hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.ui b/subprojects/libhandy/src/hdy-view-switcher-title.ui new file mode 100644 index 0000000..1c706bc --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher-title.ui @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyViewSwitcherTitle" parent="GtkBin"> + <child> + <object class="HdySqueezer" id="squeezer"> + <property name="transition-type">crossfade</property> + <property name="visible">True</property> + <property name="no-show-all">True</property> + <signal name="notify::visible-child" handler="notify_squeezer_visible_child_cb" swapped="yes"/> + <child> + <object class="HdyViewSwitcher" id="view_switcher"> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkBox" id="title_box"> + <property name="orientation">vertical</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="title_label"> + <property name="ellipsize">end</property> + <property name="halign">center</property> + <property name="wrap">False</property> + <property name="single-line-mode">True</property> + <property name="visible">True</property> + <property name="width-chars">5</property> + <style> + <class name="title"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="subtitle_label"> + <property name="ellipsize">end</property> + <property name="halign">center</property> + <property name="wrap">False</property> + <property name="single-line-mode">True</property> + <property name="visible">True</property> + <style> + <class name="subtitle"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/src/hdy-view-switcher.c b/subprojects/libhandy/src/hdy-view-switcher.c new file mode 100644 index 0000000..26c5bcf --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher.c @@ -0,0 +1,734 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * 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 <glib/gi18n-lib.h> + +#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]); +} diff --git a/subprojects/libhandy/src/hdy-view-switcher.h b/subprojects/libhandy/src/hdy-view-switcher.h new file mode 100644 index 0000000..3ec02f6 --- /dev/null +++ b/subprojects/libhandy/src/hdy-view-switcher.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 Zander Brown <zbrown@gnome.org> + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_VIEW_SWITCHER (hdy_view_switcher_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyViewSwitcher, hdy_view_switcher, HDY, VIEW_SWITCHER, GtkBin) + +typedef enum { + HDY_VIEW_SWITCHER_POLICY_AUTO, + HDY_VIEW_SWITCHER_POLICY_NARROW, + HDY_VIEW_SWITCHER_POLICY_WIDE, +} HdyViewSwitcherPolicy; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_view_switcher_new (void); + +HDY_AVAILABLE_IN_ALL +HdyViewSwitcherPolicy hdy_view_switcher_get_policy (HdyViewSwitcher *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_set_policy (HdyViewSwitcher *self, + HdyViewSwitcherPolicy policy); + +HDY_AVAILABLE_IN_ALL +PangoEllipsizeMode hdy_view_switcher_get_narrow_ellipsize (HdyViewSwitcher *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_set_narrow_ellipsize (HdyViewSwitcher *self, + PangoEllipsizeMode mode); + +HDY_AVAILABLE_IN_ALL +GtkStack *hdy_view_switcher_get_stack (HdyViewSwitcher *self); +HDY_AVAILABLE_IN_ALL +void hdy_view_switcher_set_stack (HdyViewSwitcher *self, + GtkStack *stack); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-window-handle-controller-private.h b/subprojects/libhandy/src/hdy-window-handle-controller-private.h new file mode 100644 index 0000000..0a19251 --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-handle-controller-private.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_WINDOW_HANDLE_CONTROLLER (hdy_window_handle_controller_get_type()) + +G_DECLARE_FINAL_TYPE (HdyWindowHandleController, hdy_window_handle_controller, HDY, WINDOW_HANDLE_CONTROLLER, GObject) + +HdyWindowHandleController *hdy_window_handle_controller_new (GtkWidget *widget); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-window-handle-controller.c b/subprojects/libhandy/src/hdy-window-handle-controller.c new file mode 100644 index 0000000..d668745 --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-handle-controller.c @@ -0,0 +1,515 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/. + */ + +/* Most of the file is based on bits of code from GtkWindow */ + +#include "config.h" + +#include "gtk-window-private.h" +#include "hdy-window-handle-controller-private.h" + +#include <glib/gi18n-lib.h> + +/** + * PRIVATE:hdy-window-handle-controller + * @short_description: An oblect that makes widgets behave like titlebars. + * @Title: HdyWindowHandleController + * @See_also: #HdyHeaderBar, #HdyWindowHandle + * @stability: Private + * + * When HdyWindowHandleController is added to the widget, dragging that widget + * will move the window, and right click, double click and middle click will be + * handled as if that widget was a titlebar. Currently it's used to implement + * these properties in #HdyWindowHandle and #HdyHeaderBar + * + * Since: 1.0 + */ + +struct _HdyWindowHandleController +{ + GObject parent; + + GtkWidget *widget; + GtkGesture *multipress_gesture; + GtkWidget *fallback_menu; + gboolean keep_above; +}; + +G_DEFINE_TYPE (HdyWindowHandleController, hdy_window_handle_controller, G_TYPE_OBJECT); + +static GtkWindow * +get_window (HdyWindowHandleController *self) +{ + GtkWidget *toplevel = gtk_widget_get_toplevel (self->widget); + + if (GTK_IS_WINDOW (toplevel)) + return GTK_WINDOW (toplevel); + + return NULL; +} + +static void +popup_menu_detach (GtkWidget *widget, + GtkMenu *menu) +{ + HdyWindowHandleController *self; + + self = g_object_steal_data (G_OBJECT (menu), "hdywindowhandlecontroller"); + + self->fallback_menu = NULL; +} + +static void +restore_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + GdkWindowState state; + + if (!window) + return; + + if (gtk_window_is_maximized (window)) { + gtk_window_unmaximize (window); + return; + } + + state = hdy_gtk_window_get_state (window); + + if (state & GDK_WINDOW_STATE_ICONIFIED) + gtk_window_deiconify (window); +} + +static void +move_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + + if (!window) + return; + + gtk_window_begin_move_drag (window, + 0, /* 0 means "use keyboard" */ + 0, 0, + GDK_CURRENT_TIME); +} + +static void +resize_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + + if (!window) + return; + + gtk_window_begin_resize_drag (window, + 0, + 0, /* 0 means "use keyboard" */ + 0, 0, + GDK_CURRENT_TIME); +} + +static void +minimize_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + + if (!window) + return; + + /* Turns out, we can't iconify a maximized window */ + if (gtk_window_is_maximized (window)) + gtk_window_unmaximize (window); + + gtk_window_iconify (window); +} + +static void +maximize_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + GdkWindowState state; + + if (!window) + return; + + state = hdy_gtk_window_get_state (window); + + if (state & GDK_WINDOW_STATE_ICONIFIED) + gtk_window_deiconify (window); + + gtk_window_maximize (window); +} + +static void +ontop_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + + if (!window) + return; + + /* + * FIXME: It will go out of sync if something else calls + * gtk_window_set_keep_above(), so we need to actually track it. + * For some reason this doesn't seem to be reflected in the + * window state. + */ + self->keep_above = !self->keep_above; + gtk_window_set_keep_above (window, self->keep_above); +} + +static void +close_window_cb (GtkMenuItem *menuitem, + HdyWindowHandleController *self) +{ + GtkWindow *window = get_window (self); + + if (!window) + return; + + gtk_window_close (window); +} + +static void +do_popup (HdyWindowHandleController *self, + GdkEventButton *event) +{ + GtkWindow *window = get_window (self); + GtkWidget *menuitem; + GdkWindowState state; + gboolean maximized, iconified, resizable; + GdkWindowTypeHint type_hint; + + if (!window) + return; + + if (gdk_window_show_window_menu (gtk_widget_get_window (GTK_WIDGET (window)), + (GdkEvent *) event)) + return; + + if (self->fallback_menu) + gtk_widget_destroy (self->fallback_menu); + + state = hdy_gtk_window_get_state (window); + + iconified = (state & GDK_WINDOW_STATE_ICONIFIED) == GDK_WINDOW_STATE_ICONIFIED; + maximized = gtk_window_is_maximized (window) && !iconified; + resizable = gtk_window_get_resizable (window); + type_hint = gtk_window_get_type_hint (window); + + self->fallback_menu = gtk_menu_new (); + gtk_style_context_add_class (gtk_widget_get_style_context (self->fallback_menu), + GTK_STYLE_CLASS_CONTEXT_MENU); + + /* We can't pass self to popup_menu_detach, so will have to use custom data */ + g_object_set_data (G_OBJECT (self->fallback_menu), + "hdywindowhandlecontroller", self); + + gtk_menu_attach_to_widget (GTK_MENU (self->fallback_menu), + self->widget, + popup_menu_detach); + + menuitem = gtk_menu_item_new_with_label (_("Restore")); + gtk_widget_show (menuitem); + /* "Restore" means "Unmaximize" or "Unminimize" + * (yes, some WMs allow window menu to be shown for minimized windows). + * Not restorable: + * - visible windows that are not maximized or minimized + * - non-resizable windows that are not minimized + * - non-normal windows + */ + if ((gtk_widget_is_visible (GTK_WIDGET (window)) && + !(maximized || iconified)) || + (!iconified && !resizable) || + type_hint != GDK_WINDOW_TYPE_HINT_NORMAL) + gtk_widget_set_sensitive (menuitem, FALSE); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (restore_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_menu_item_new_with_label (_("Move")); + gtk_widget_show (menuitem); + if (maximized || iconified) + gtk_widget_set_sensitive (menuitem, FALSE); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (move_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_menu_item_new_with_label (_("Resize")); + gtk_widget_show (menuitem); + if (!resizable || maximized || iconified) + gtk_widget_set_sensitive (menuitem, FALSE); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (resize_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_menu_item_new_with_label (_("Minimize")); + gtk_widget_show (menuitem); + if (iconified || + type_hint != GDK_WINDOW_TYPE_HINT_NORMAL) + gtk_widget_set_sensitive (menuitem, FALSE); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (minimize_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_menu_item_new_with_label (_("Maximize")); + gtk_widget_show (menuitem); + if (maximized || + !resizable || + type_hint != GDK_WINDOW_TYPE_HINT_NORMAL) + gtk_widget_set_sensitive (menuitem, FALSE); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (maximize_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_separator_menu_item_new (); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_check_menu_item_new_with_label (_("Always on Top")); + gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), self->keep_above); + if (maximized) + gtk_widget_set_sensitive (menuitem, FALSE); + gtk_widget_show (menuitem); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (ontop_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_separator_menu_item_new (); + gtk_widget_show (menuitem); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + + menuitem = gtk_menu_item_new_with_label (_("Close")); + gtk_widget_show (menuitem); + if (!gtk_window_get_deletable (window)) + gtk_widget_set_sensitive (menuitem, FALSE); + g_signal_connect (G_OBJECT (menuitem), "activate", + G_CALLBACK (close_window_cb), self); + gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem); + gtk_menu_popup_at_pointer (GTK_MENU (self->fallback_menu), (GdkEvent *) event); +} + +static gboolean +titlebar_action (HdyWindowHandleController *self, + const GdkEvent *event, + guint button) +{ + GtkSettings *settings; + g_autofree gchar *action = NULL; + GtkWindow *window = get_window (self); + + if (!window) + return FALSE; + + settings = gtk_widget_get_settings (GTK_WIDGET (window)); + + switch (button) { + case GDK_BUTTON_PRIMARY: + g_object_get (settings, "gtk-titlebar-double-click", &action, NULL); + break; + + case GDK_BUTTON_MIDDLE: + g_object_get (settings, "gtk-titlebar-middle-click", &action, NULL); + break; + + case GDK_BUTTON_SECONDARY: + g_object_get (settings, "gtk-titlebar-right-click", &action, NULL); + break; + + default: + g_assert_not_reached (); + } + + if (action == NULL) + return FALSE; + + if (g_str_equal (action, "none")) + return FALSE; + + if (g_str_has_prefix (action, "toggle-maximize")) { + /* + * gtk header bar won't show the maximize button if the following + * properties are not met, apply the same to title bar actions for + * consistency. + */ + if (gtk_window_get_resizable (window) && + gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL) + hdy_gtk_window_toggle_maximized (window); + + return TRUE; + } + + if (g_str_equal (action, "lower")) { + gdk_window_lower (gtk_widget_get_window (GTK_WIDGET (window))); + + return TRUE; + } + + if (g_str_equal (action, "minimize")) { + gdk_window_iconify (gtk_widget_get_window (GTK_WIDGET (window))); + + return TRUE; + } + + if (g_str_equal (action, "menu")) { + do_popup (self, (GdkEventButton*) event); + + return TRUE; + } + + g_warning ("Unsupported titlebar action %s", action); + + return FALSE; +} + +static void +pressed_cb (GtkGestureMultiPress *gesture, + gint n_press, + gdouble x, + gdouble y, + HdyWindowHandleController *self) +{ + GtkWidget *window = gtk_widget_get_toplevel (self->widget); + GdkEventSequence *sequence = + gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + const GdkEvent *event = + gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence); + guint button = + gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)); + + if (!event) + return; + + if (gdk_display_device_is_grabbed (gtk_widget_get_display (window), + gtk_gesture_get_device (GTK_GESTURE (gesture)))) + return; + + switch (button) { + case GDK_BUTTON_PRIMARY: + gdk_window_raise (gtk_widget_get_window (window)); + + if (n_press == 2) + titlebar_action (self, event, button); + + if (gtk_widget_has_grab (window)) + gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), + sequence, GTK_EVENT_SEQUENCE_CLAIMED); + + break; + + case GDK_BUTTON_SECONDARY: + if (titlebar_action (self, event, button)) + gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), + sequence, GTK_EVENT_SEQUENCE_CLAIMED); + + gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture)); + break; + + case GDK_BUTTON_MIDDLE: + if (titlebar_action (self, event, button)) + gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), + sequence, GTK_EVENT_SEQUENCE_CLAIMED); + break; + + default: + break; + } +} + +static void +hdy_window_handle_controller_finalize (GObject *object) +{ + HdyWindowHandleController *self = (HdyWindowHandleController *)object; + + self->widget = NULL; + g_clear_object (&self->multipress_gesture); + g_clear_object (&self->fallback_menu); + + G_OBJECT_CLASS (hdy_window_handle_controller_parent_class)->finalize (object); +} + +static void +hdy_window_handle_controller_class_init (HdyWindowHandleControllerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = hdy_window_handle_controller_finalize; +} + +static void +hdy_window_handle_controller_init (HdyWindowHandleController *self) +{ +} + +/** + * hdy_window_handle_controller_new: + * @widget: The widget to create a controller for + * + * Creates a new #HdyWindowHandleController for @widget. + * + * Returns: (transfer full): a newly created #HdyWindowHandleController + * + * Since: 1.0 + */ +HdyWindowHandleController * +hdy_window_handle_controller_new (GtkWidget *widget) +{ + HdyWindowHandleController *self; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + + self = g_object_new (HDY_TYPE_WINDOW_HANDLE_CONTROLLER, NULL); + + /* The object is intended to have the same life cycle as the widget, + * so we don't ref it. */ + self->widget = widget; + self->multipress_gesture = g_object_new (GTK_TYPE_GESTURE_MULTI_PRESS, + "widget", widget, + "button", 0, + NULL); + g_signal_connect_object (self->multipress_gesture, + "pressed", + G_CALLBACK (pressed_cb), + self, + 0); + + gtk_widget_add_events (widget, + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_BUTTON_MOTION_MASK | + GDK_TOUCH_MASK); + + gtk_style_context_add_class (gtk_widget_get_style_context (widget), + "windowhandle"); + + return self; +} diff --git a/subprojects/libhandy/src/hdy-window-handle.c b/subprojects/libhandy/src/hdy-window-handle.c new file mode 100644 index 0000000..30cc855 --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-handle.c @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-window-handle.h" +#include "hdy-window-handle-controller-private.h" + +/** + * SECTION:hdy-window-handle + * @short_description: A bin that acts like a titlebar. + * @Title: HdyWindowHandle + * @See_also: #HdyApplicationWindow, #HdyHeaderBar, #HdyWindow + * + * HdyWindowHandle is a #GtkBin subclass that can be dragged to move its + * #GtkWindow, and handles right click, middle click and double click as + * expected from a titlebar. This is particularly useful with #HdyWindow or + * #HdyApplicationWindow. + * + * It isn't necessary to use #HdyWindowHandle if you use #HdyHeaderBar. + * + * It can be safely nested or used in the actual window titlebar. + * + * # CSS nodes + * + * #HdyWindowHandle has a single CSS node with name windowhandle. + * + * Since: 1.0 + */ + +struct _HdyWindowHandle +{ + GtkEventBox parent_instance; + + HdyWindowHandleController *controller; +}; + +G_DEFINE_TYPE (HdyWindowHandle, hdy_window_handle, GTK_TYPE_EVENT_BOX) + +static void +hdy_window_handle_finalize (GObject *object) +{ + HdyWindowHandle *self = (HdyWindowHandle *)object; + + g_clear_object (&self->controller); + + G_OBJECT_CLASS (hdy_window_handle_parent_class)->finalize (object); +} + +static void +hdy_window_handle_class_init (HdyWindowHandleClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = hdy_window_handle_finalize; + + gtk_widget_class_set_css_name (widget_class, "windowhandle"); +} + +static void +hdy_window_handle_init (HdyWindowHandle *self) +{ + self->controller = hdy_window_handle_controller_new (GTK_WIDGET (self)); +} + +/** + * hdy_window_handle_new: + * + * Creates a new #HdyWindowHandle. + * + * Returns: (transfer full): a newly created #HdyWindowHandle + * + * Since: 1.0 + */ +GtkWidget * +hdy_window_handle_new (void) +{ + return g_object_new (HDY_TYPE_WINDOW_HANDLE, NULL); +} diff --git a/subprojects/libhandy/src/hdy-window-handle.h b/subprojects/libhandy/src/hdy-window-handle.h new file mode 100644 index 0000000..d7835f9 --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-handle.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_WINDOW_HANDLE (hdy_window_handle_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (HdyWindowHandle, hdy_window_handle, HDY, WINDOW_HANDLE, GtkEventBox) + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_window_handle_new (void); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-window-mixin-private.h b/subprojects/libhandy/src/hdy-window-mixin-private.h new file mode 100644 index 0000000..27ce713 --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-mixin-private.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_WINDOW_MIXIN (hdy_window_mixin_get_type()) + +G_DECLARE_FINAL_TYPE (HdyWindowMixin, hdy_window_mixin, HDY, WINDOW_MIXIN, GObject) + +HdyWindowMixin *hdy_window_mixin_new (GtkWindow *window, + GtkWindowClass *klass); + +void hdy_window_mixin_add (HdyWindowMixin *self, + GtkWidget *widget); +void hdy_window_mixin_remove (HdyWindowMixin *self, + GtkWidget *widget); +void hdy_window_mixin_forall (HdyWindowMixin *self, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data); + +gboolean hdy_window_mixin_draw (HdyWindowMixin *self, + cairo_t *cr); +void hdy_window_mixin_destroy (HdyWindowMixin *self); + +void hdy_window_mixin_buildable_add_child (HdyWindowMixin *self, + GtkBuilder *builder, + GObject *child, + const gchar *type); + +G_END_DECLS diff --git a/subprojects/libhandy/src/hdy-window-mixin.c b/subprojects/libhandy/src/hdy-window-mixin.c new file mode 100644 index 0000000..d55536c --- /dev/null +++ b/subprojects/libhandy/src/hdy-window-mixin.c @@ -0,0 +1,583 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-cairo-private.h" +#include "hdy-deck.h" +#include "hdy-nothing-private.h" +#include "hdy-window-mixin-private.h" + +typedef enum { + HDY_CORNER_TOP_LEFT, + HDY_CORNER_TOP_RIGHT, + HDY_CORNER_BOTTOM_LEFT, + HDY_CORNER_BOTTOM_RIGHT, + HDY_N_CORNERS, +} HdyCorner; + +/** + * PRIVATE:hdy-window-mixin + * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow + * @title: HdyWindowMixin + * @See_also: #HdyApplicationWindow, #HdyWindow + * @stability: Private + * + * The HdyWindowMixin object contains the implementation of the HdyWindow and + * HdyApplicationWindow classes, providing a way to make a GtkWindow subclass + * that has masked window corners on all sides and no titlebar by default, + * allowing for more freedom with how to handle the titlebar for applications. + * + * Since: 1.0 + */ + +struct _HdyWindowMixin +{ + GObject parent; + + GtkWindow *window; + GtkWindowClass *klass; + + GtkWidget *content; + GtkWidget *titlebar; + cairo_surface_t *masks[HDY_N_CORNERS]; + gint last_border_radius; + + GtkStyleContext *decoration_context; + GtkStyleContext *overlay_context; + + GtkWidget *child; +}; + +G_DEFINE_TYPE (HdyWindowMixin, hdy_window_mixin, G_TYPE_OBJECT) + +static GtkStyleContext * +create_child_context (HdyWindowMixin *self) +{ + GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + GtkStyleContext *child = gtk_style_context_new (); + + gtk_style_context_set_parent (child, parent); + gtk_style_context_set_screen (child, gtk_style_context_get_screen (parent)); + gtk_style_context_set_frame_clock (child, gtk_style_context_get_frame_clock (parent)); + + g_signal_connect_object (child, + "changed", + G_CALLBACK (gtk_widget_queue_draw), + self->window, + G_CONNECT_SWAPPED); + + return child; +} + +static void +update_child_context (HdyWindowMixin *self, + GtkStyleContext *context, + const gchar *name) +{ + g_autoptr (GtkWidgetPath) path = gtk_widget_path_new (); + GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + gint position; + + gtk_widget_path_append_for_widget (path, GTK_WIDGET (self->window)); + position = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET); + gtk_widget_path_iter_set_object_name (path, position, name); + + gtk_style_context_set_path (context, path); + gtk_style_context_set_state (context, gtk_style_context_get_state (parent)); +} + +static void +style_changed_cb (HdyWindowMixin *self) +{ + update_child_context (self, self->decoration_context, "decoration"); + update_child_context (self, self->overlay_context, "decoration-overlay"); +} + +static gboolean +window_state_event_cb (HdyWindowMixin *self, + GdkEvent *event, + GtkWidget *widget) +{ + style_changed_cb (self); + + return GDK_EVENT_PROPAGATE; +} + +static void +size_allocate_cb (HdyWindowMixin *self, + GtkAllocation *alloc) +{ + /* We don't want to allow any other titlebar */ + if (gtk_window_get_titlebar (self->window) != self->titlebar) + g_error ("gtk_window_set_titlebar() is not supported for HdyWindow"); +} + +static gboolean +is_fullscreen (HdyWindowMixin *self) +{ + GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self->window)); + + return !!(gdk_window_get_state (window) & GDK_WINDOW_STATE_FULLSCREEN); +} + +static gboolean +supports_client_shadow (HdyWindowMixin *self) +{ + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + + /* + * GtkWindow adds this when it can't draw proper decorations, e.g. on a + * non-composited WM on X11. This is documented, so we can rely on this + * instead of copying the (pretty extensive) check. + */ + return !gtk_style_context_has_class (context, "solid-csd"); +} + +static void +max_borders (GtkBorder *one, + GtkBorder *two) +{ + one->top = MAX (one->top, two->top); + one->right = MAX (one->right, two->right); + one->bottom = MAX (one->bottom, two->bottom); + one->left = MAX (one->left, two->left); +} + +static void +get_shadow_width (HdyWindowMixin *self, + GtkStyleContext *context, + GtkBorder *shadow_width) +{ + GtkStateFlags state; + GtkBorder margin = { 0 }; + GtkAllocation content_alloc, alloc; + GtkWidget *titlebar; + + *shadow_width = margin; + + if (!gtk_window_get_decorated (self->window)) + return; + + if (gtk_window_is_maximized (self->window) || + is_fullscreen (self)) + return; + + if (!gtk_widget_is_toplevel (GTK_WIDGET (self->window))) + return; + + state = gtk_style_context_get_state (context); + + gtk_style_context_get_margin (context, state, &margin); + + gtk_widget_get_allocation (GTK_WIDGET (self->window), &alloc); + gtk_widget_get_allocation (self->content, &content_alloc); + + titlebar = gtk_window_get_titlebar (self->window); + if (titlebar && gtk_widget_get_visible (titlebar)) { + GtkAllocation titlebar_alloc; + + gtk_widget_get_allocation (titlebar, &titlebar_alloc); + + content_alloc.y = titlebar_alloc.y; + content_alloc.height += titlebar_alloc.height; + } + + /* + * Since we can't get shadow extents the normal way, + * we have to compare window and content allocation instead. + */ + shadow_width->left = content_alloc.x - alloc.x; + shadow_width->right = alloc.width - content_alloc.width - content_alloc.x; + shadow_width->top = content_alloc.y - alloc.y; + shadow_width->bottom = alloc.height - content_alloc.height - content_alloc.y; + + max_borders (shadow_width, &margin); +} + +static void +create_masks (HdyWindowMixin *self, + cairo_t *cr, + gint border_radius) +{ + gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self->window)); + gdouble radius_correction = 0.5 / scale_factor; + gdouble r = border_radius - radius_correction; + gint i; + + for (i = 0; i < HDY_N_CORNERS; i++) + g_clear_pointer (&self->masks[i], cairo_surface_destroy); + + if (r <= 0) + return; + + for (i = 0; i < HDY_N_CORNERS; i++) { + g_autoptr (cairo_t) mask_cr = NULL; + + self->masks[i] = + cairo_surface_create_similar_image (cairo_get_target (cr), + CAIRO_FORMAT_A8, + border_radius * scale_factor, + border_radius * scale_factor); + + mask_cr = cairo_create (self->masks[i]); + + cairo_scale (mask_cr, scale_factor, scale_factor); + cairo_set_source_rgb (mask_cr, 0, 0, 0); + cairo_arc (mask_cr, + (i % 2 == 0) ? r : radius_correction, + (i / 2 == 0) ? r : radius_correction, + r, + 0, G_PI * 2); + cairo_fill (mask_cr); + } +} + +void +hdy_window_mixin_add (HdyWindowMixin *self, + GtkWidget *widget) +{ + if (GTK_IS_POPOVER (widget)) + GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window), + widget); + else { + g_return_if_fail (self->child == NULL); + + self->child = widget; + gtk_container_add (GTK_CONTAINER (self->content), widget); + } +} + +void +hdy_window_mixin_remove (HdyWindowMixin *self, + GtkWidget *widget) +{ + GtkWidget *titlebar = gtk_window_get_titlebar (self->window); + + if (widget == self->content || + widget == titlebar || + GTK_IS_POPOVER (widget)) + GTK_CONTAINER_CLASS (self->klass)->remove (GTK_CONTAINER (self->window), + widget); + else if (widget == self->child) { + self->child = NULL; + gtk_container_remove (GTK_CONTAINER (self->content), widget); + } +} + +void +hdy_window_mixin_forall (HdyWindowMixin *self, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + if (include_internals) { + GTK_CONTAINER_CLASS (self->klass)->forall (GTK_CONTAINER (self->window), + include_internals, + callback, + callback_data); + + return; + } + + if (self->child) + (*callback) (self->child, callback_data); +} + +typedef struct { + HdyWindowMixin *self; + cairo_t *cr; +} HdyWindowMixinDrawData; + +static void +draw_popover_cb (GtkWidget *child, + HdyWindowMixinDrawData *data) +{ + HdyWindowMixin *self = data->self; + GdkWindow *window; + cairo_t *cr = data->cr; + + if (child == self->content || + child == gtk_window_get_titlebar (self->window) || + !gtk_widget_get_visible (child) || + !gtk_widget_get_child_visible (child)) + return; + + window = gtk_widget_get_window (child); + + if (gtk_widget_get_has_window (child)) + window = gdk_window_get_parent (window); + + if (!gtk_cairo_should_draw_window (cr, window)) + return; + + gtk_container_propagate_draw (GTK_CONTAINER (self->window), child, cr); +} + +static inline void +mask_corner (HdyWindowMixin *self, + cairo_t *cr, + gint scale_factor, + gint corner, + gint x, + gint y) +{ + cairo_save (cr); + cairo_scale (cr, 1.0 / scale_factor, 1.0 / scale_factor); + cairo_mask_surface (cr, + self->masks[corner], + x * scale_factor, + y * scale_factor); + cairo_restore (cr); +} + +gboolean +hdy_window_mixin_draw (HdyWindowMixin *self, + cairo_t *cr) +{ + HdyWindowMixinDrawData data; + GtkWidget *widget = GTK_WIDGET (self->window); + GdkWindow *window = gtk_widget_get_window (widget); + + if (gtk_cairo_should_draw_window (cr, window)) { + GtkStyleContext *context; + gboolean should_mask_corners; + GdkRectangle clip = { 0 }; + gint width, height, x, y, w, h, r, scale_factor; + GtkWidget *titlebar; + g_autoptr (cairo_surface_t) surface = NULL; + g_autoptr (cairo_t) surface_cr = NULL; + GtkBorder shadow; + + /* Use the parent drawing unless we have a reason to use masking */ + if (!gtk_window_get_decorated (self->window) || + !supports_client_shadow (self) || + is_fullscreen (self)) + return GTK_WIDGET_CLASS (self->klass)->draw (GTK_WIDGET (self->window), cr); + + context = gtk_widget_get_style_context (widget); + + get_shadow_width (self, self->decoration_context, &shadow); + + width = gtk_widget_get_allocated_width (widget); + height = gtk_widget_get_allocated_height (widget); + + x = shadow.left; + y = shadow.top; + w = width - shadow.left - shadow.right; + h = height - shadow.top - shadow.bottom; + + gtk_style_context_get (context, + gtk_style_context_get_state (self->decoration_context), + GTK_STYLE_PROPERTY_BORDER_RADIUS, &r, + NULL); + + r = CLAMP (r, 0, MIN (w / 2, h / 2)); + + if (!gdk_cairo_get_clip_rectangle (cr, &clip)) { + clip.x = 0; + clip.y = 0; + clip.width = w; + clip.height = h; + } + + gtk_render_background (self->decoration_context, cr, x, y, w, h); + gtk_render_frame (self->decoration_context, cr, x, y, w, h); + + cairo_save (cr); + + scale_factor = gtk_widget_get_scale_factor (widget); + + if (r * scale_factor != self->last_border_radius) { + create_masks (self, cr, r); + self->last_border_radius = r * scale_factor; + } + + should_mask_corners = !gtk_window_is_maximized (self->window) && + r > 0 && + ((clip.x < x + r && clip.y < y + r) || + (clip.x < x + r && clip.y + clip.height > y + h - r) || + (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r) || + (clip.x + clip.width > x + w - r && clip.y < y + r)); + + + if (should_mask_corners) { + surface = gdk_window_create_similar_surface (window, + CAIRO_CONTENT_COLOR_ALPHA, + MAX (clip.width, 1), + MAX (clip.height, 1)); + surface_cr = cairo_create (surface); + cairo_surface_set_device_offset (surface, -clip.x * scale_factor, -clip.y * scale_factor); + } else { + surface_cr = cairo_reference (cr); + } + + if (!gtk_widget_get_app_paintable (widget)) { + gtk_render_background (context, surface_cr, x, y, w, h); + gtk_render_frame (context, surface_cr, x, y, w, h); + } + + titlebar = gtk_window_get_titlebar (self->window); + + gtk_container_propagate_draw (GTK_CONTAINER (self->window), self->content, surface_cr); + gtk_container_propagate_draw (GTK_CONTAINER (self->window), titlebar, surface_cr); + + gtk_render_background (self->overlay_context, surface_cr, x, y, w, h); + gtk_render_frame (self->overlay_context, surface_cr, x, y, w, h); + + if (should_mask_corners) { + cairo_set_source_surface (cr, surface, 0, 0); + + cairo_rectangle (cr, x + r, y, w - r * 2, r); + cairo_rectangle (cr, x + r, y + h - r, w - r * 2, r); + cairo_rectangle (cr, x, y + r, w, h - r * 2); + cairo_fill (cr); + + if (clip.x < x + r && clip.y < y + r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_TOP_LEFT, x, y); + + if (clip.x + clip.width > x + w - r && clip.y < y + r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_TOP_RIGHT, x + w - r, y); + + if (clip.x < x + r && clip.y + clip.height > y + h - r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_BOTTOM_LEFT, x, y + h - r); + + if (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r) + mask_corner (self, cr, scale_factor, + HDY_CORNER_BOTTOM_RIGHT, x + w - r, y + h - r); + + cairo_surface_flush (surface); + } + + cairo_restore (cr); + } + + data.self = self; + data.cr = cr; + gtk_container_forall (GTK_CONTAINER (self->window), + (GtkCallback) draw_popover_cb, + &data); + + return GDK_EVENT_PROPAGATE; +} + +void +hdy_window_mixin_destroy (HdyWindowMixin *self) +{ + if (self->titlebar) { + hdy_window_mixin_remove (self, self->titlebar); + self->titlebar = NULL; + } + + if (self->content) { + hdy_window_mixin_remove (self, self->content); + self->content = NULL; + self->child = NULL; + } + + GTK_WIDGET_CLASS (self->klass)->destroy (GTK_WIDGET (self->window)); +} + +void +hdy_window_mixin_buildable_add_child (HdyWindowMixin *self, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + GtkBuildable *buildable = GTK_BUILDABLE (self->window); + + if (!type) + gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child)); + else + GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type); +} + +static void +hdy_window_mixin_finalize (GObject *object) +{ + HdyWindowMixin *self = (HdyWindowMixin *)object; + gint i; + + for (i = 0; i < HDY_N_CORNERS; i++) + g_clear_pointer (&self->masks[i], cairo_surface_destroy); + g_clear_object (&self->decoration_context); + g_clear_object (&self->overlay_context); + + G_OBJECT_CLASS (hdy_window_mixin_parent_class)->finalize (object); +} + +static void +hdy_window_mixin_class_init (HdyWindowMixinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = hdy_window_mixin_finalize; +} + +static void +hdy_window_mixin_init (HdyWindowMixin *self) +{ +} + +HdyWindowMixin * +hdy_window_mixin_new (GtkWindow *window, + GtkWindowClass *klass) +{ + HdyWindowMixin *self; + GtkStyleContext *context; + + g_return_val_if_fail (GTK_IS_WINDOW (window), NULL); + g_return_val_if_fail (GTK_IS_WINDOW_CLASS (klass), NULL); + g_return_val_if_fail (GTK_IS_BUILDABLE (window), NULL); + + self = g_object_new (HDY_TYPE_WINDOW_MIXIN, NULL); + + self->window = window; + self->klass = klass; + + gtk_widget_add_events (GTK_WIDGET (window), GDK_STRUCTURE_MASK); + + g_signal_connect_object (window, + "style-updated", + G_CALLBACK (style_changed_cb), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (window, + "window-state-event", + G_CALLBACK (window_state_event_cb), + self, + G_CONNECT_SWAPPED | G_CONNECT_AFTER); + + g_signal_connect_object (window, + "size-allocate", + G_CALLBACK (size_allocate_cb), + self, + G_CONNECT_SWAPPED); + + self->decoration_context = create_child_context (self); + self->overlay_context = create_child_context (self); + + style_changed_cb (self); + + self->content = hdy_deck_new (); + gtk_widget_set_vexpand (self->content, TRUE); + gtk_widget_show (self->content); + GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window), + self->content); + + self->titlebar = hdy_nothing_new (); + gtk_widget_set_no_show_all (self->titlebar, TRUE); + gtk_window_set_titlebar (self->window, self->titlebar); + + context = gtk_widget_get_style_context (GTK_WIDGET (self->window)); + gtk_style_context_add_class (context, "unified"); + + return self; +} diff --git a/subprojects/libhandy/src/hdy-window.c b/subprojects/libhandy/src/hdy-window.c new file mode 100644 index 0000000..1a868d7 --- /dev/null +++ b/subprojects/libhandy/src/hdy-window.c @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include "hdy-window.h" +#include "hdy-window-mixin-private.h" + +/** + * SECTION:hdy-window + * @short_description: A freeform window. + * @title: HdyWindow + * @See_also: #HdyApplicationWindow, #HdyHeaderBar, #HdyWindowHandle + * + * The HdyWindow widget is a subclass of #GtkWindow which has no titlebar area + * and provides rounded corners on all sides, ensuring they can never be + * overlapped by the content. This makes it safe to use headerbars in the + * content area as follows: + * + * |[ + * <object class="HdyWindow"/> + * <child> + * <object class="GtkBox"> + * <property name="visible">True</property> + * <property name="orientation">vertical</property> + * <child> + * <object class="HdyHeaderBar"> + * <property name="visible">True</property> + * <property name="show-close-button">True</property> + * </object> + * </child> + * <child> + * ... + * </child> + * </object> + * </child> + * </object> + * ]| + * + * It's recommended to use #HdyHeaderBar with #HdyWindow, as unlike + * #GtkHeaderBar it remains draggable inside the window. Otherwise, + * #HdyWindowHandle can be used. + * + * #HdyWindow allows to easily implement titlebar autohiding by putting the + * headerbar inside a #GtkRevealer, and to show titlebar above content by + * putting it into a #GtkOverlay instead of #GtkBox. + * + * if the window has a #GtkGLArea, it may bring a slight performance regression + * when the window is not fullscreen, tiled or maximized. + * + * Using gtk_window_get_titlebar() and gtk_window_set_titlebar() is not + * supported and will result in a crash. + * + * # CSS nodes + * + * #HdyWindow has a main CSS node with the name window and style classes + * .background, .csd and .unified. + * + * The .solid-csd style class on the main node is used for client-side + * decorations without invisible borders. + * + * #HdyWindow also represents window states with the following + * style classes on the main node: .tiled, .maximized, .fullscreen. + * + * It contains the subnodes decoration for window shadow and/or border, + * decoration-overlay for the sheen on top of the window, widget.titlebar, and + * deck, which contains the child inside the window. + * + * Since: 1.0 + */ + +typedef struct +{ + HdyWindowMixin *mixin; +} HdyWindowPrivate; + +static void hdy_window_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (HdyWindow, hdy_window, GTK_TYPE_WINDOW, + G_ADD_PRIVATE (HdyWindow) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, hdy_window_buildable_init)) + +#define HDY_GET_WINDOW_MIXIN(obj) (((HdyWindowPrivate *) hdy_window_get_instance_private (HDY_WINDOW (obj)))->mixin) + +static void +hdy_window_add (GtkContainer *container, + GtkWidget *widget) +{ + hdy_window_mixin_add (HDY_GET_WINDOW_MIXIN (container), widget); +} + +static void +hdy_window_remove (GtkContainer *container, + GtkWidget *widget) +{ + hdy_window_mixin_remove (HDY_GET_WINDOW_MIXIN (container), widget); +} + +static void +hdy_window_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + hdy_window_mixin_forall (HDY_GET_WINDOW_MIXIN (container), + include_internals, + callback, + callback_data); +} + +static gboolean +hdy_window_draw (GtkWidget *widget, + cairo_t *cr) +{ + return hdy_window_mixin_draw (HDY_GET_WINDOW_MIXIN (widget), cr); +} + +static void +hdy_window_destroy (GtkWidget *widget) +{ + hdy_window_mixin_destroy (HDY_GET_WINDOW_MIXIN (widget)); +} + +static void +hdy_window_finalize (GObject *object) +{ + HdyWindow *self = (HdyWindow *)object; + HdyWindowPrivate *priv = hdy_window_get_instance_private (self); + + g_clear_object (&priv->mixin); + + G_OBJECT_CLASS (hdy_window_parent_class)->finalize (object); +} + +static void +hdy_window_class_init (HdyWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->finalize = hdy_window_finalize; + widget_class->draw = hdy_window_draw; + widget_class->destroy = hdy_window_destroy; + container_class->add = hdy_window_add; + container_class->remove = hdy_window_remove; + container_class->forall = hdy_window_forall; +} + +static void +hdy_window_init (HdyWindow *self) +{ + HdyWindowPrivate *priv = hdy_window_get_instance_private (self); + + priv->mixin = hdy_window_mixin_new (GTK_WINDOW (self), + GTK_WINDOW_CLASS (hdy_window_parent_class)); +} + +static void +hdy_window_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + hdy_window_mixin_buildable_add_child (HDY_GET_WINDOW_MIXIN (buildable), + builder, + child, + type); +} + +static void +hdy_window_buildable_init (GtkBuildableIface *iface) +{ + iface->add_child = hdy_window_buildable_add_child; +} + +/** + * hdy_window_new: + * + * Creates a new #HdyWindow. + * + * Returns: (transfer full): a newly created #HdyWindow + * + * Since: 1.0 + */ +GtkWidget * +hdy_window_new (void) +{ + return g_object_new (HDY_TYPE_WINDOW, + "type", GTK_WINDOW_TOPLEVEL, + NULL); +} diff --git a/subprojects/libhandy/src/hdy-window.h b/subprojects/libhandy/src/hdy-window.h new file mode 100644 index 0000000..51099cf --- /dev/null +++ b/subprojects/libhandy/src/hdy-window.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif + +#include "hdy-version.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_WINDOW (hdy_window_get_type()) + +HDY_AVAILABLE_IN_ALL +G_DECLARE_DERIVABLE_TYPE (HdyWindow, hdy_window, HDY, WINDOW, GtkWindow) + +struct _HdyWindowClass +{ + GtkWindowClass parent_class; + + /*< private >*/ + gpointer padding[4]; +}; + +HDY_AVAILABLE_IN_ALL +GtkWidget *hdy_window_new (void); + +G_END_DECLS diff --git a/subprojects/libhandy/src/icons/avatar-default-symbolic.svg b/subprojects/libhandy/src/icons/avatar-default-symbolic.svg new file mode 100644 index 0000000..ec0905d --- /dev/null +++ b/subprojects/libhandy/src/icons/avatar-default-symbolic.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <path d="M8 1a3 3 0 100 6 3 3 0 000-6zM6.5 8A4.49 4.49 0 002 12.5V14c0 1 1 1 1 1h10s1 0 1-1v-1.5A4.49 4.49 0 009.5 8z" style="marker:none" color="#bebebe" overflow="visible" fill="#2e3436"/> +</svg> diff --git a/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg b/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg new file mode 100644 index 0000000..78ab0be --- /dev/null +++ b/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <g color="#000" fill="#474747"> + <path d="M3.707 5.293L2.293 6.707 8 12.414l5.707-5.707-1.414-1.414L8 9.586z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/> + <path d="M13 6V5h1v1zM2 6V5h1v1z" style="marker:none" overflow="visible"/> + <path d="M2 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1zM12 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1z" style="marker:none" overflow="visible"/> + </g> +</svg> diff --git a/subprojects/libhandy/src/meson.build b/subprojects/libhandy/src/meson.build new file mode 100644 index 0000000..11d4100 --- /dev/null +++ b/subprojects/libhandy/src/meson.build @@ -0,0 +1,298 @@ +libhandy_header_subdir = package_subdir / package_api_name +libhandy_header_dir = get_option('includedir') / libhandy_header_subdir +libhandy_resources = gnome.compile_resources( + 'hdy-resources', + 'handy.gresources.xml', + + c_name: 'hdy', +) + +hdy_public_enum_headers = [ + 'hdy-deck.h', + 'hdy-header-bar.h', + 'hdy-header-group.h', + 'hdy-leaflet.h', + 'hdy-navigation-direction.h', + 'hdy-squeezer.h', + 'hdy-view-switcher.h', +] + +hdy_private_enum_headers = [ + 'hdy-stackable-box-private.h', +] + +version_data = configuration_data() +version_data.set('HDY_MAJOR_VERSION', handy_version_major) +version_data.set('HDY_MINOR_VERSION', handy_version_minor) +version_data.set('HDY_MICRO_VERSION', handy_version_micro) +version_data.set('HDY_VERSION', meson.project_version()) + +hdy_version_h = configure_file( + input: 'hdy-version.h.in', + output: 'hdy-version.h', + install_dir: libhandy_header_dir, + install: true, + configuration: version_data) + +libhandy_generated_headers = [ +] + +install_headers(['handy.h'], + subdir: libhandy_header_subdir) + +# Filled out in the subdirs +libhandy_public_headers = [] +libhandy_public_sources = [] +libhandy_private_sources = [] + +hdy_public_enums = gnome.mkenums('hdy-enums', + h_template: 'hdy-enums.h.in', + c_template: 'hdy-enums.c.in', + sources: hdy_public_enum_headers, + install_header: true, + install_dir: libhandy_header_dir, +) + +hdy_private_enums = gnome.mkenums('hdy-enums-private', + h_template: 'hdy-enums-private.h.in', + c_template: 'hdy-enums-private.c.in', + sources: hdy_private_enum_headers, + install_header: false, +) + +libhandy_public_sources += [hdy_public_enums[0]] +libhandy_private_sources += [hdy_private_enums[0]] +libhandy_generated_headers += [hdy_public_enums[1]] + +src_headers = [ + 'hdy-action-row.h', + 'hdy-animation.h', + 'hdy-application-window.h', + 'hdy-avatar.h', + 'hdy-carousel.h', + 'hdy-carousel-indicator-dots.h', + 'hdy-carousel-indicator-lines.h', + 'hdy-clamp.h', + 'hdy-combo-row.h', + 'hdy-deck.h', + 'hdy-deprecation-macros.h', + 'hdy-enum-value-object.h', + 'hdy-expander-row.h', + 'hdy-header-bar.h', + 'hdy-header-group.h', + 'hdy-keypad.h', + 'hdy-leaflet.h', + 'hdy-main.h', + 'hdy-navigation-direction.h', + 'hdy-preferences-group.h', + 'hdy-preferences-page.h', + 'hdy-preferences-row.h', + 'hdy-preferences-window.h', + 'hdy-search-bar.h', + 'hdy-squeezer.h', + 'hdy-swipe-group.h', + 'hdy-swipe-tracker.h', + 'hdy-swipeable.h', + 'hdy-title-bar.h', + 'hdy-types.h', + 'hdy-value-object.h', + 'hdy-view-switcher.h', + 'hdy-view-switcher-bar.h', + 'hdy-view-switcher-title.h', + 'hdy-window.h', + 'hdy-window-handle.h', +] + +sed = find_program('sed', required: true) +gen_public_types = find_program('gen-public-types.sh', required: true) + +libhandy_init_public_types = custom_target('hdy-public-types.c', + output: 'hdy-public-types.c', + input: [src_headers, libhandy_generated_headers], + command: [gen_public_types, '@INPUT@'], + capture: true, +) + +src_sources = [ + 'gtkprogresstracker.c', + 'gtk-window.c', + 'hdy-action-row.c', + 'hdy-animation.c', + 'hdy-application-window.c', + 'hdy-avatar.c', + 'hdy-carousel.c', + 'hdy-carousel-box.c', + 'hdy-carousel-indicator-dots.c', + 'hdy-carousel-indicator-lines.c', + 'hdy-clamp.c', + 'hdy-combo-row.c', + 'hdy-css.c', + 'hdy-deck.c', + 'hdy-enum-value-object.c', + 'hdy-expander-row.c', + 'hdy-header-bar.c', + 'hdy-header-group.c', + 'hdy-keypad-button.c', + 'hdy-keypad.c', + 'hdy-leaflet.c', + 'hdy-main.c', + 'hdy-navigation-direction.c', + 'hdy-nothing.c', + 'hdy-preferences-group.c', + 'hdy-preferences-page.c', + 'hdy-preferences-row.c', + 'hdy-preferences-window.c', + 'hdy-search-bar.c', + 'hdy-shadow-helper.c', + 'hdy-squeezer.c', + 'hdy-stackable-box.c', + 'hdy-swipe-group.c', + 'hdy-swipe-tracker.c', + 'hdy-swipeable.c', + 'hdy-title-bar.c', + 'hdy-value-object.c', + 'hdy-view-switcher.c', + 'hdy-view-switcher-bar.c', + 'hdy-view-switcher-button.c', + 'hdy-view-switcher-title.c', + 'hdy-window.c', + 'hdy-window-handle.c', + 'hdy-window-handle-controller.c', + 'hdy-window-mixin.c', +] + +libhandy_public_headers += files(src_headers) +libhandy_public_sources += files(src_sources) + +install_headers(src_headers, subdir: libhandy_header_subdir) + + +libhandy_sources = [ + libhandy_generated_headers, + libhandy_public_sources, + libhandy_private_sources, + libhandy_resources, + libhandy_init_public_types, +] + +glib_min_version = '>= 2.44' + +libhandy_deps = [ + dependency('glib-2.0', version: glib_min_version), + dependency('gio-2.0', version: glib_min_version), + dependency('gmodule-2.0', version: glib_min_version), + dependency('gtk+-3.0', version: '>= 3.24.1'), + cc.find_library('m', required: false), + cc.find_library('rt', required: false), +] + +libhandy_c_args = [ + '-DG_LOG_DOMAIN="Handy"', +] + +config_h = configuration_data() +config_h.set_quoted('GETTEXT_PACKAGE', 'libhandy') +config_h.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir')) + +# Symbol visibility +if target_system == 'windows' + config_h.set('DLL_EXPORT', true) + config_h.set('_HDY_EXTERN', '__declspec(dllexport) extern') + if cc.get_id() != 'msvc' + libhandy_c_args += ['-fvisibility=hidden'] + endif +else + config_h.set('_HDY_EXTERN', '__attribute__((visibility("default"))) extern') + libhandy_c_args += ['-fvisibility=hidden'] +endif + +configure_file( + output: 'config.h', + configuration: config_h, +) + +libhandy_link_args = [] +libhandy_symbols_file = 'libhandy.syms' + +# Check linker flags +ld_version_script_arg = '-Wl,--version-script,@0@/@1@'.format(meson.source_root(), + libhandy_symbols_file) +if cc.links('int main() { return 0; }', args : ld_version_script_arg, name : 'ld_supports_version_script') + libhandy_link_args += [ld_version_script_arg] +endif + +# set default libdir on win32 for libhandy target to keep MinGW compatibility +if target_system == 'windows' + handy_libdir = [true] +else + handy_libdir = libdir +endif + +libhandy = shared_library( + 'handy-' + apiversion, + libhandy_sources, + + soversion: soversion, + c_args: libhandy_c_args, + dependencies: libhandy_deps, + include_directories: [ root_inc, src_inc ], + install: true, + link_args: libhandy_link_args, + install_dir: handy_libdir, +) + +libhandy_dep = declare_dependency( + sources: libhandy_generated_headers, + dependencies: libhandy_deps, + link_with: libhandy, + include_directories: include_directories('.'), +) + +if introspection + + libhandy_gir_extra_args = [ + '--c-include=handy.h', + '--quiet', + '-DHANDY_COMPILATION', + ] + + libhandy_gir = gnome.generate_gir(libhandy, + sources: libhandy_generated_headers + libhandy_public_headers + libhandy_public_sources, + nsversion: apiversion, + namespace: 'Handy', + export_packages: package_api_name, + symbol_prefix: 'hdy', + identifier_prefix: 'Hdy', + link_with: libhandy, + includes: ['Gio-2.0', 'Gtk-3.0'], + install: true, + install_dir_gir: girdir, + install_dir_typelib: typelibdir, + extra_args: libhandy_gir_extra_args, + ) + + if get_option('vapi') + + libhandy_vapi = gnome.generate_vapi(package_api_name, + sources: libhandy_gir[0], + packages: [ 'gio-2.0', 'gtk+-3.0' ], + install: true, + install_dir: vapidir, + metadata_dirs: [ meson.current_source_dir() ], + ) + + endif +endif + +pkgg = import('pkgconfig') + +pkgg.generate( + libraries: [libhandy], + subdirs: libhandy_header_subdir, + version: meson.project_version(), + name: 'Handy', + filebase: package_api_name, + description: 'Handy Mobile widgets', + requires: 'gtk+-3.0', + install_dir: libdir / 'pkgconfig', +) diff --git a/subprojects/libhandy/src/themes/Adwaita-dark.css b/subprojects/libhandy/src/themes/Adwaita-dark.css new file mode 100644 index 0000000..e553fac --- /dev/null +++ b/subprojects/libhandy/src/themes/Adwaita-dark.css @@ -0,0 +1,197 @@ +/*************************** Check and Radio buttons * */ +row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; } + +row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; } + +row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; } + +row.expander { background-color: transparent; } + +row.expander list.nested > row { background-color: alpha(#353535, 0.5); border-color: alpha(#1b1b1b, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; } + +row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); } + +row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; } + +row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); } + +row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); } + +row.expander:checked image.expander-row-arrow:not(:disabled) { color: #15539e; } + +row.expander image.expander-row-arrow:disabled { color: #919190; } + +deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.24); } + +deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.2); } + +deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; } + +deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.05); } + +avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; } + +avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; } + +avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; } + +avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; } + +avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; } + +avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; } + +avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; } + +avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; } + +avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; } + +avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; } + +avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; } + +avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; } + +avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; } + +avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; } + +avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; } + +avatar.contrasted { color: #fff; } + +viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; } + +/*************************** Check and Radio buttons * */ +popover.combo list { min-width: 200px; } + +window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; } + +.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; } + +popover.combo { padding: 0px; } + +popover.combo list { border-style: none; background-color: transparent; } + +popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; } + +popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#1b1b1b, 0.5); } + +popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; } + +popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; } + +row.expander { padding: 0px; } + +row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; } + +row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; } + +keypad .digit { font-size: 200%; font-weight: bold; } + +keypad .letters { font-size: 70%; } + +keypad .symbol { font-size: 160%; } + +viewswitcher, viewswitcher button { margin: 0; padding: 0; } + +viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; } + +viewswitcher button:not(:checked):not(:hover) { background: transparent; } + +viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; } + +viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; } + +viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; } + +viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#353535)); } + +viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#1b1b1b, 1.15); } + +viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#1b1b1b, 1.15); } + +viewswitcher button:not(:checked):hover:backdrop { background-image: image(#353535); } + +headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#353535, 0.7), 0.99) 2px, alpha(#353535, 0.7)); } + +headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #1b1b1b; } + +headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #1b1b1b; } + +headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#353535); } + +viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; } + +viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; } + +viewswitcher button > stack > box.wide { padding: 8px 12px; } + +viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; } + +viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; } + +viewswitcher button > stack > box label.active { font-weight: bold; } + +viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; } + +viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; } + +viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; } + +viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; } + +viewswitcherbar actionbar > revealer > box { padding: 0; } + +list.content, list.content list { background-color: transparent; } + +list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#353535, #2d2d2d, 0.5); } + +list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); } + +list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; } + +list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); } + +list.content > row, list.content > row list > row { border-color: alpha(#1b1b1b, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; } + +list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; } + +button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#1b1b1b, 0.5); box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0); } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.065); } + +window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; } diff --git a/subprojects/libhandy/src/themes/Adwaita-dark.scss b/subprojects/libhandy/src/themes/Adwaita-dark.scss new file mode 100644 index 0000000..918f489 --- /dev/null +++ b/subprojects/libhandy/src/themes/Adwaita-dark.scss @@ -0,0 +1,5 @@ +$variant: 'dark'; +$high_contrast: false; + +@import 'colors'; +@import 'Adwaita-base'; diff --git a/subprojects/libhandy/src/themes/Adwaita.css b/subprojects/libhandy/src/themes/Adwaita.css new file mode 100644 index 0000000..acb7f27 --- /dev/null +++ b/subprojects/libhandy/src/themes/Adwaita.css @@ -0,0 +1,197 @@ +/*************************** Check and Radio buttons * */ +row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; } + +row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; } + +row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; } + +row.expander { background-color: transparent; } + +row.expander list.nested > row { background-color: alpha(#f6f5f4, 0.5); border-color: alpha(#cdc7c2, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; } + +row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); } + +row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; } + +row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); } + +row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); } + +row.expander:checked image.expander-row-arrow:not(:disabled) { color: #3584e4; } + +row.expander image.expander-row-arrow:disabled { color: #929595; } + +deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); } + +deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.05); } + +deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; } + +deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.2); } + +avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; } + +avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; } + +avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; } + +avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; } + +avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; } + +avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; } + +avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; } + +avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; } + +avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; } + +avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; } + +avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; } + +avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; } + +avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; } + +avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; } + +avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; } + +avatar.contrasted { color: #fff; } + +viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; } + +/*************************** Check and Radio buttons * */ +popover.combo list { min-width: 200px; } + +window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; } + +.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; } + +popover.combo { padding: 0px; } + +popover.combo list { border-style: none; background-color: transparent; } + +popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; } + +popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#cdc7c2, 0.5); } + +popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; } + +popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; } + +row.expander { padding: 0px; } + +row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; } + +row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; } + +keypad .digit { font-size: 200%; font-weight: bold; } + +keypad .letters { font-size: 70%; } + +keypad .symbol { font-size: 160%; } + +viewswitcher, viewswitcher button { margin: 0; padding: 0; } + +viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; } + +viewswitcher button:not(:checked):not(:hover) { background: transparent; } + +viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; } + +viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; } + +viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; } + +viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#f6f5f4)); } + +viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#cdc7c2, 1.15); } + +viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#cdc7c2, 1.15); } + +viewswitcher button:not(:checked):hover:backdrop { background-image: image(#f6f5f4); } + +headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#f6f5f4, 0.7), 0.96) 2px, alpha(#f6f5f4, 0.7)); } + +headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #cdc7c2; } + +headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #cdc7c2; } + +headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#f6f5f4); } + +viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; } + +viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; } + +viewswitcher button > stack > box.wide { padding: 8px 12px; } + +viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; } + +viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; } + +viewswitcher button > stack > box label.active { font-weight: bold; } + +viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; } + +viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; } + +viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; } + +viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; } + +viewswitcherbar actionbar > revealer > box { padding: 0; } + +list.content, list.content list { background-color: transparent; } + +list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#f6f5f4, #ffffff, 0.5); } + +list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); } + +list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; } + +list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); } + +list.content > row, list.content > row list > row { border-color: alpha(#cdc7c2, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; } + +list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; } + +button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#cdc7c2, 0.5); box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0.7); } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.34); } + +window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; } diff --git a/subprojects/libhandy/src/themes/Adwaita.scss b/subprojects/libhandy/src/themes/Adwaita.scss new file mode 100644 index 0000000..5ded9f6 --- /dev/null +++ b/subprojects/libhandy/src/themes/Adwaita.scss @@ -0,0 +1,5 @@ +$variant: 'light'; +$high_contrast: false; + +@import 'colors'; +@import 'Adwaita-base'; diff --git a/subprojects/libhandy/src/themes/HighContrast.css b/subprojects/libhandy/src/themes/HighContrast.css new file mode 100644 index 0000000..f1d1eda --- /dev/null +++ b/subprojects/libhandy/src/themes/HighContrast.css @@ -0,0 +1,197 @@ +/*************************** Check and Radio buttons * */ +row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; } + +row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; } + +row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; } + +row.expander { background-color: transparent; } + +row.expander list.nested > row { background-color: alpha(#fdfdfc, 0.5); border-color: alpha(#877b6e, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; } + +row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); } + +row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; } + +row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); } + +row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); } + +row.expander:checked image.expander-row-arrow:not(:disabled) { color: #1b6acb; } + +row.expander image.expander-row-arrow:disabled { color: #929495; } + +deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); } + +deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: #877b6e; } + +deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; } + +deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: transparent; } + +avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; } + +avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; } + +avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; } + +avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; } + +avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; } + +avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; } + +avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; } + +avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; } + +avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; } + +avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; } + +avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; } + +avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; } + +avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; } + +avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; } + +avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; } + +avatar.contrasted { color: #fff; } + +viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; } + +/*************************** Check and Radio buttons * */ +popover.combo list { min-width: 200px; } + +window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; } + +.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; } + +popover.combo { padding: 0px; } + +popover.combo list { border-style: none; background-color: transparent; } + +popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; } + +popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#877b6e, 0.5); } + +popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; } + +popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; } + +row.expander { padding: 0px; } + +row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; } + +row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; } + +keypad .digit { font-size: 200%; font-weight: bold; } + +keypad .letters { font-size: 70%; } + +keypad .symbol { font-size: 160%; } + +viewswitcher, viewswitcher button { margin: 0; padding: 0; } + +viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; } + +viewswitcher button:not(:checked):not(:hover) { background: transparent; } + +viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; } + +viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; } + +viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; } + +viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#fdfdfc)); } + +viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#877b6e, 1.15); } + +viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#877b6e, 1.15); } + +viewswitcher button:not(:checked):hover:backdrop { background-image: image(#fdfdfc); } + +headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#fdfdfc, 0.7), 0.96) 2px, alpha(#fdfdfc, 0.7)); } + +headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #877b6e; } + +headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #877b6e; } + +headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#fdfdfc); } + +viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; } + +viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; } + +viewswitcher button > stack > box.wide { padding: 8px 12px; } + +viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; } + +viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; } + +viewswitcher button > stack > box label.active { font-weight: bold; } + +viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; } + +viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; } + +viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; } + +viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; } + +viewswitcherbar actionbar > revealer > box { padding: 0; } + +list.content, list.content list { background-color: transparent; } + +list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#fdfdfc, #ffffff, 0.5); } + +list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); } + +list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; } + +list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); } + +list.content > row, list.content > row list > row { border-color: alpha(#877b6e, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; } + +list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; } + +button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#877b6e, 0.5); box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0.7); } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.34); } + +window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; } diff --git a/subprojects/libhandy/src/themes/HighContrast.scss b/subprojects/libhandy/src/themes/HighContrast.scss new file mode 100644 index 0000000..4456428 --- /dev/null +++ b/subprojects/libhandy/src/themes/HighContrast.scss @@ -0,0 +1,6 @@ +$variant: 'light'; +$high_contrast: true; + +@import 'colors'; +@import 'colors-hc'; +@import 'Adwaita-base'; diff --git a/subprojects/libhandy/src/themes/HighContrastInverse.css b/subprojects/libhandy/src/themes/HighContrastInverse.css new file mode 100644 index 0000000..fd5b01b --- /dev/null +++ b/subprojects/libhandy/src/themes/HighContrastInverse.css @@ -0,0 +1,197 @@ +/*************************** Check and Radio buttons * */ +row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; } + +row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; } + +row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; } + +row.expander { background-color: transparent; } + +row.expander list.nested > row { background-color: alpha(#303030, 0.5); border-color: alpha(#686868, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; } + +row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); } + +row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; } + +row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); } + +row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); } + +row.expander:checked image.expander-row-arrow:not(:disabled) { color: #0f3b71; } + +row.expander image.expander-row-arrow:disabled { color: #919191; } + +deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.24); } + +deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: #686868; } + +deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; } + +deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: transparent; } + +avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; } + +avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; } + +avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; } + +avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; } + +avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; } + +avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; } + +avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; } + +avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; } + +avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; } + +avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; } + +avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; } + +avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; } + +avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; } + +avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; } + +avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; } + +avatar.contrasted { color: #fff; } + +viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; } + +/*************************** Check and Radio buttons * */ +popover.combo list { min-width: 200px; } + +window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; } + +.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; } + +popover.combo { padding: 0px; } + +popover.combo list { border-style: none; background-color: transparent; } + +popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; } + +popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#686868, 0.5); } + +popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; } + +popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; } + +row.expander { padding: 0px; } + +row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; } + +row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; } + +keypad .digit { font-size: 200%; font-weight: bold; } + +keypad .letters { font-size: 70%; } + +keypad .symbol { font-size: 160%; } + +viewswitcher, viewswitcher button { margin: 0; padding: 0; } + +viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; } + +viewswitcher button:not(:checked):not(:hover) { background: transparent; } + +viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; } + +viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; } + +viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; } + +viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#303030)); } + +viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#686868, 1.15); } + +viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#686868, 1.15); } + +viewswitcher button:not(:checked):hover:backdrop { background-image: image(#303030); } + +headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#303030, 0.7), 0.99) 2px, alpha(#303030, 0.7)); } + +headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #686868; } + +headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #686868; } + +headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#303030); } + +viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; } + +viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; } + +viewswitcher button > stack > box.wide { padding: 8px 12px; } + +viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; } + +viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; } + +viewswitcher button > stack > box label.active { font-weight: bold; } + +viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; } + +viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; } + +viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; } + +viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; } + +viewswitcherbar actionbar > revealer > box { padding: 0; } + +list.content, list.content list { background-color: transparent; } + +list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#303030, #2d2d2d, 0.5); } + +list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); } + +list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; } + +list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); } + +list.content > row, list.content > row list > row { border-color: alpha(#686868, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; } + +list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; } + +list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; } + +list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; } + +button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#686868, 0.5); box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0); } + +window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; } + +window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.065); } + +window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; } diff --git a/subprojects/libhandy/src/themes/HighContrastInverse.scss b/subprojects/libhandy/src/themes/HighContrastInverse.scss new file mode 100644 index 0000000..a49c0e1 --- /dev/null +++ b/subprojects/libhandy/src/themes/HighContrastInverse.scss @@ -0,0 +1,6 @@ +$variant: 'dark'; +$high_contrast: true; + +@import 'colors'; +@import 'colors-hc'; +@import 'Adwaita-base'; diff --git a/subprojects/libhandy/src/themes/_Adwaita-base.scss b/subprojects/libhandy/src/themes/_Adwaita-base.scss new file mode 100644 index 0000000..cc0b754 --- /dev/null +++ b/subprojects/libhandy/src/themes/_Adwaita-base.scss @@ -0,0 +1,336 @@ +// Include base styling. +@import 'fallback-base'; +@import 'shared-base'; + +// HdyComboRow + +popover.combo { + padding: 0px; + + list { + border-style: none; + background-color: transparent; + + > row { + padding: 0px 12px 0px 12px; + min-height: 50px; + + &:not(:last-child) { + border-bottom: 1px solid hdyalpha($borders_color, 0.5) + } + + &:first-child { + @include rounded-border(top); + } + + &:last-child { + @include rounded-border(bottom); + } + } + } + + @each $border in top, bottom { + overshoot.#{$border} { + @include rounded-border($border); + } + } + + scrollbar.vertical { + padding-top: 2px; + padding-bottom: 2px; + + &:dir(ltr) { + @include rounded-border(right); + } + + &:dir(rtl) { + @include rounded-border(left); + } + } +} + +// HdyExpanderRow + +row.expander { + padding: 0px; + + image.expander-row-arrow { + @include margin-start(6px); + } +} + +// HdyKeypad + +keypad { + .digit { + font-size: 200%; + font-weight: bold; + } + + .letters { + font-size: 70%; + } + + .symbol { + font-size: 160%; + } +} + +// HdyViewSwitcher + +viewswitcher { + &, & button { + margin: 0; + padding: 0; + } + + button { + border-radius: 0; + border-top: 0; + border-bottom: 0; + box-shadow: none; + font-size: 1rem; + + &:not(:checked):not(:hover) { + background: transparent; + } + + &:not(:only-child):not(:last-child) { + border-right-width: 0px; + } + + &:not(only-child):first-child:not(:checked):not(:hover), + &:not(:checked):not(:hover) + button:not(:checked):not(:hover) { + border-left-color: transparent; + } + + &:not(only-child):last-child:not(:checked):not(:hover) { + border-right-color: transparent; + } + + &:not(:checked):hover:not(:backdrop) { + background-image: image(lighter($bg_color)); + } + + &:not(only-child):first-child:not(:checked):hover, + &:not(:checked):hover + button:not(:checked):not(:hover), + &:not(:checked):not(:hover) + button:not(:checked):hover { + border-left-color: shade($borders_color, 1.15); + } + + &:not(only-child):last-child:not(:checked):hover { + border-right-color: shade($borders_color, 1.15); + } + + &:not(:checked):hover:backdrop { + background-image: image($bg_color); + } + + // View switcher in a header bar + headerbar &:not(:checked) { + &:hover:not(:backdrop) { + // Reimplementation of $button_fill from Adwaita. The colors are made + // only 70% visible to avoid the highlight to be too strong. + $c: hdyalpha($bg_color, 0.7); + $button_fill: if($variant == 'light', linear-gradient(to top, shade($c, 0.96) 2px, $c), + linear-gradient(to top, shade($c, 0.99) 2px, $c)) !global; + background-image: $button_fill; + } + + &:not(only-child):first-child:hover, + &:hover + button:not(:checked):not(:hover), + &:not(:hover) + button:not(:checked):hover { + border-left-color: $borders_color; + } + + &:not(only-child):last-child:hover { + border-right-color: $borders_color; + } + + &:hover:backdrop { + background-image: image($bg_color); + } + } + + // View switcher button + > stack > box { + &.narrow { + font-size: 0.75rem; + padding-top: 7px; + padding-bottom: 5px; + + image, + label { + padding-left: 8px; + padding-right: 8px; + } + } + + &.wide { + padding: 8px 12px; + + label { + &:dir(ltr) { + padding-right: 7px; + } + + &:dir(rtl) { + padding-left: 7px; + } + } + } + + label.active { + font-weight: bold; + } + } + + &.needs-attention { + &:active > stack > box label, + &:checked > stack > box label { + animation: none; + background-image: none; + } + + > stack > box label { + animation: needs_attention 150ms ease-in; + background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); + background-size: 6px 6px, 6px 6px; + background-repeat: no-repeat; + background-position: right 0px, right 1px; + + &:backdrop { + background-size: 6px 6px, 0 0; + } + + &:dir(rtl) { + background-position: left 0px, left 1px; + } + } + } + } +} + +// HdyViewSwitcherBar + +viewswitcherbar actionbar > revealer > box { + padding: 0; +} + +// Content list + +list.content { + &, + list { + background-color: transparent; + } + + // Nested rows background + list.nested > row:not(:active) { + &:not(:hover):not(:selected), + &:hover:not(.activatable):not(:selected) { + background-color: hdymix($bg_color, $base_color, 0.5); + } + + &:hover.activatable:not(:selected) { + background-color: hdymix($fg_color, $base_color, 0.95); + } + } + + > row { + // Regular rows and expander header rows background + &:not(.expander):not(:active):not(:hover):not(:selected), + &:not(.expander):not(:active):hover:not(.activatable):not(:selected), + &.expander row.header:not(:active):not(:hover):not(:selected), + &.expander row.header:not(:active):hover:not(.activatable):not(:selected) { + background-color: $base_color; + } + + &:not(.expander):not(:active):hover.activatable:not(:selected), + &.expander row.header:not(:active):hover.activatable:not(:selected) { + background-color: hdymix($fg_color, $base_color, 0.95); + } + + &, + list > row { + border-color: hdyalpha($borders_color, 0.7); + border-style: solid; + transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + } + + // Top border + &:not(:last-child) { + border-width: 1px 1px 0px 1px; + } + + // Rounded top + &:first-child, + &.expander:first-child row.header, + &.expander:checked, + &.expander:checked row.header, + &.expander:checked + row, + &.expander:checked + row.expander row.header { + @include rounded-border(top); + } + + // Bottom border + &:last-child, + &.checked-expander-row-previous-sibling, + &.expander:checked { + border-width: 1px; + } + + // Rounded bottom + &:last-child, + &.checked-expander-row-previous-sibling, + &.expander:checked, + &.expander:not(:checked):last-child row.header, + &.expander:not(:checked).checked-expander-row-previous-sibling row.header, + &.expander.empty:checked row.header, + &.expander list.nested > row:last-child { + @include rounded-border(bottom); + } + + // Add space around expanded rows + &.expander:checked:not(:first-child), + &.expander:checked + row { + margin-top: 6px; + } + } +} + +// List button + +button.list-button:not(:active):not(:checked):not(:hover) { + background: none; + border: 1px solid hdyalpha($borders_color, 0.5); + box-shadow: none; +} + +// Unified window + +window.csd.unified:not(.solid-csd):not(.fullscreen) { + // Remove the sheen on headerbar... + headerbar { + box-shadow: inset 0 1px rgba(255, 255, 255, if($variant == 'light', 0.7, 0)); + + &.selection-mode { + box-shadow: none; + } + } + + // ...and add it on the window itself + > decoration-overlay { + // Use a white sheen instead of @borders, as it has to be neutral enough + // for any content and not just headerbar background + box-shadow: inset 0 1px rgba(255, 255, 255, if($variant == 'light', 0.34, 0.065)); + } + + &:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) { + &, + > decoration, + > decoration-overlay { + border-radius: 8px; + } + } +} diff --git a/subprojects/libhandy/src/themes/_definitions.scss b/subprojects/libhandy/src/themes/_definitions.scss new file mode 100644 index 0000000..ed427a4 --- /dev/null +++ b/subprojects/libhandy/src/themes/_definitions.scss @@ -0,0 +1,66 @@ +@import 'drawing'; + +@function hdyalpha($c, $a) { + @return unquote("alpha(#{$c}, #{$a})"); +} + +@function hdymix($c1, $c2, $r) { + @return unquote("mix(#{$c1}, #{$c2}, #{$r})"); +} + +$leaflet_dimming: rgba(0, 0, 0, if($variant == 'light', 0.12, 0.24)); +$leaflet_border: rgba(0, 0, 0, if($variant == 'light', 0.05, 0.2)); +$leaflet_outline: rgba(255, 255, 255, if($variant == 'light', 0.2, 0.05)); + +@if $high_contrast { + $leaflet_border: $borders_color; + $leaflet_outline: transparent; +} + +@mixin background-shadow($direction) { + background-image: + linear-gradient($direction, + rgba(0, 0, 0, if($variant == 'light', 0.05, 0.1)), + rgba(0, 0, 0, if($variant == 'light', 0.01, 0.02)) 40px, + rgba(0, 0, 0, 0) 56px), + linear-gradient($direction, + rgba(0, 0, 0, if($variant == 'light', 0.03, 0.06)), + rgba(0, 0, 0, if($variant == 'light', 0.01, 0.02)) 7px, + rgba(0, 0, 0, 0) 24px); +} + +// Makes the corners of the given border rounded. +// $border must be top, bottom, left, or right. +@mixin rounded-border($border) { + // The floors (top, bottom) and walls (left, right) of the corners matching + // $border. This is needed to easily form floor-wall pairs regardless of + // whether $border is a floor or a wall. + $corners: ( + 'top': (('top'), ('left', 'right')), + 'bottom': (('bottom'), ('left', 'right')), + 'left': (('top', 'bottom'), ('left')), + 'right': (('top', 'bottom'), ('right')), + ); + + @if not map-get($corners, $border) { + @error "Unknown border type: #{$border}"; + } + + // Loop through the floors and walls of the corners of $border. + @each $floor in nth(map-get($corners, $border), 1) { + @each $wall in nth(map-get($corners, $border), 2) { + border-#{$floor}-#{$wall}-radius: 8px; + -gtk-outline-#{$floor}-#{$wall}-radius: 7px; + } + } +} + +@mixin margin-start($margin) { + &:dir(ltr) { + margin-left: $margin; + } + + &:dir(rtl) { + margin-right: $margin; + } +} diff --git a/subprojects/libhandy/src/themes/_fallback-base.scss b/subprojects/libhandy/src/themes/_fallback-base.scss new file mode 100644 index 0000000..b821d95 --- /dev/null +++ b/subprojects/libhandy/src/themes/_fallback-base.scss @@ -0,0 +1,146 @@ +@import 'definitions'; + +// HdyActionRow + +row { + label.subtitle { + font-size: smaller; + opacity: 0.55; + text-shadow: none; + } + + > box.header { + margin-left: 12px; + margin-right: 12px; + min-height: 50px; + + > box.title { + margin-top: 8px; + margin-bottom: 8px; + } + } +} + +// HdyExpanderRow + +row.expander { + // Drop transparent background on expander rows to let nested rows handle it, + // avoiding double highlights. + background-color: transparent; + + list.nested > row { + background-color: hdyalpha($bg_color, 0.5); + border-color: hdyalpha($borders_color, 0.7); + border-style: solid; + border-width: 1px 0px 0px 0px; + } + + // HdyExpanderRow arrow rotation + + image.expander-row-arrow { + transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); + } + + &:checked image.expander-row-arrow { + -gtk-icon-transform: rotate(0turn); + } + + &:not(:checked) image.expander-row-arrow { + opacity: 0.55; + text-shadow: none; + + &:dir(ltr) { + -gtk-icon-transform: rotate(-0.25turn); + } + + &:dir(rtl) { + -gtk-icon-transform: rotate(0.25turn); + } + } + + &:checked image.expander-row-arrow:not(:disabled) { + color: $selected_bg_color; + } + + & image.expander-row-arrow:disabled { + color: $insensitive_fg_color; + } +} + +// Shadows + +deck, +leaflet { + > dimming { + background: $leaflet_dimming; + } + + > border { + min-width: 1px; + min-height: 1px; + background: $leaflet_border; + } + + > shadow { + min-width: 56px; + min-height: 56px; + + &.left { @include background-shadow(to right); } + &.right { @include background-shadow(to left); } + &.up { @include background-shadow(to bottom); } + &.down { @include background-shadow(to top); } + } + + > outline { + min-width: 1px; + min-height: 1px; + background: $leaflet_outline; + } +} + +// Avatar + +avatar { + border-radius: 9999px; + -gtk-outline-radius: 9999px; + font-weight: bold; + + // The list of colors to generate avatars. + // Each avatar color is represented by a font color, a gradient start color and a gradient stop color. + // There are 8 different colors for avtars in the list if you change the number of them you + // need to update the NUMBER_OF_COLORS in src/hdy-avatar.c. + // The 2D list has this form: ((font-color, gradient-top-color, gradient-bottom-color)). + $avatarcolorlist: ( + (#cfe1f5, #83b6ec, #337fdc), // blue + (#caeaf2, #7ad9f1, #0f9ac8), // cyan + (#cef8d8, #8de6b1, #29ae74), // green + (#e6f9d7, #b5e98a, #6ab85b), // lime + (#f9f4e1, #f8e359, #d29d09), // yellow + (#ffead1, #ffcb62, #d68400), // gold + (#ffe5c5, #ffa95a, #ed5b00), // orange + (#f8d2ce, #f78773, #e62d42), // raspberry + (#fac7de, #e973ab, #e33b6a), // magenta + (#e7c2e8, #cb78d4, #9945b5), // purple + (#d5d2f5, #9e91e8, #7a59ca), // violet + (#f2eade, #e3cf9c, #b08952), // beige + (#e5d6ca, #be916d, #785336), // brown + (#d8d7d3, #c0bfbc, #6e6d71), // gray + ); + + @for $i from 1 through length($avatarcolorlist) { + &.color#{$i} { + $avatarcolor: nth($avatarcolorlist, $i); + background-image: linear-gradient(nth($avatarcolor, 2), nth($avatarcolor, 3)); + color: nth($avatarcolor, 1); + } + } + + &.contrasted { color: #fff; } +} + +// HdyViewSwitcherTitle + +viewswitchertitle viewswitcher { + margin-left: 12px; + margin-right: 12px; +} diff --git a/subprojects/libhandy/src/themes/_shared-base.scss b/subprojects/libhandy/src/themes/_shared-base.scss new file mode 100644 index 0000000..934eeda --- /dev/null +++ b/subprojects/libhandy/src/themes/_shared-base.scss @@ -0,0 +1,21 @@ +@import 'definitions'; + +// HdyComboRow + +popover.combo list { + min-width: 200px; +} + +window.csd.unified:not(.solid-csd) { + // Since corners are masked, there's no need for round corners anymore + &, headerbar { + border-radius: 0; + } +} + +.windowhandle { + &, & * { + // This is the most reliable way to enable window dragging + -GtkWidget-window-dragging: true; + } +} diff --git a/subprojects/libhandy/src/themes/fallback.css b/subprojects/libhandy/src/themes/fallback.css new file mode 100644 index 0000000..8c1d89b --- /dev/null +++ b/subprojects/libhandy/src/themes/fallback.css @@ -0,0 +1,74 @@ +/*************************** Check and Radio buttons * */ +row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; } + +row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; } + +row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; } + +row.expander { background-color: transparent; } + +row.expander list.nested > row { background-color: alpha(#f6f5f4, 0.5); border-color: alpha(#cdc7c2, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; } + +row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } + +row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); } + +row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; } + +row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); } + +row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); } + +row.expander:checked image.expander-row-arrow:not(:disabled) { color: #3584e4; } + +row.expander image.expander-row-arrow:disabled { color: #929595; } + +deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); } + +deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.05); } + +deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; } + +deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); } + +deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.2); } + +avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; } + +avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; } + +avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; } + +avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; } + +avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; } + +avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; } + +avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; } + +avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; } + +avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; } + +avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; } + +avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; } + +avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; } + +avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; } + +avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; } + +avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; } + +avatar.contrasted { color: #fff; } + +viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; } diff --git a/subprojects/libhandy/src/themes/fallback.scss b/subprojects/libhandy/src/themes/fallback.scss new file mode 100644 index 0000000..d8a0985 --- /dev/null +++ b/subprojects/libhandy/src/themes/fallback.scss @@ -0,0 +1,5 @@ +$variant: 'light'; +$high_contrast: false; + +@import 'colors'; +@import 'fallback-base'; diff --git a/subprojects/libhandy/src/themes/parse-sass.sh b/subprojects/libhandy/src/themes/parse-sass.sh new file mode 100755 index 0000000..4238e88 --- /dev/null +++ b/subprojects/libhandy/src/themes/parse-sass.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +if [ ! "$(which sassc 2> /dev/null)" ]; then + echo sassc needs to be installed to generate the css. + exit 1 +fi + +if [ ! "$(which git 2> /dev/null)" ]; then + echo git needs to be installed to check GTK. + exit 1 +fi + +SASSC_OPT="-M -t compact" + +: ${GTK_SOURCE_PATH:="../../../gtk"} +: ${GTK_TAG:="3.24.21"} + +if [ ! -d "${GTK_SOURCE_PATH}/gtk/theme/Adwaita" ]; then + echo GTK sources not found at ${GTK_SOURCE_PATH}. + exit 1 +fi + +# > /dev/null makes pushd and popd silent. +pushd ${GTK_SOURCE_PATH} > /dev/null +GTK_CURRENT_TAG=`git describe --tags` +popd > /dev/null + +if [ "${GTK_CURRENT_TAG}" != "${GTK_TAG}" ]; then + echo GTK must be at tag ${GTK_TAG}. + exit 1 +fi + +sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \ + Adwaita.scss Adwaita.css +sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \ + Adwaita-dark.scss Adwaita-dark.css +sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \ + fallback.scss fallback.css +sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita -I${GTK_SOURCE_PATH}/gtk/theme/HighContrast \ + HighContrast.scss HighContrast.css +sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita -I${GTK_SOURCE_PATH}/gtk/theme/HighContrast \ + HighContrastInverse.scss HighContrastInverse.css +sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \ + shared.scss shared.css diff --git a/subprojects/libhandy/src/themes/shared.css b/subprojects/libhandy/src/themes/shared.css new file mode 100644 index 0000000..6bfd522 --- /dev/null +++ b/subprojects/libhandy/src/themes/shared.css @@ -0,0 +1,6 @@ +/*************************** Check and Radio buttons * */ +popover.combo list { min-width: 200px; } + +window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; } + +.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; } diff --git a/subprojects/libhandy/src/themes/shared.scss b/subprojects/libhandy/src/themes/shared.scss new file mode 100644 index 0000000..86f64b0 --- /dev/null +++ b/subprojects/libhandy/src/themes/shared.scss @@ -0,0 +1,5 @@ +$variant: 'light'; +$high_contrast: false; + +@import 'colors'; +@import 'shared-base'; |