diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 14:36:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 14:36:24 +0000 |
commit | 9b6d8e63db85c30007b463e91f91a791969fa83f (patch) | |
tree | 0899af51d73c1bf986f73ae39a03c4436083018a /subprojects | |
parent | Initial commit. (diff) | |
download | gnome-control-center-upstream.tar.xz gnome-control-center-upstream.zip |
Adding upstream version 1:3.38.4.upstream/1%3.38.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
310 files changed, 64653 insertions, 0 deletions
diff --git a/subprojects/gvc/README.md b/subprojects/gvc/README.md new file mode 100644 index 0000000..2fabe49 --- /dev/null +++ b/subprojects/gvc/README.md @@ -0,0 +1,12 @@ +# libgnome-volume-control + +libgnome-volume-control is a copy library that's supposed to be used as +a git sub-module. If your project uses some of libgnome-volume-control's +strings in a user-facing manner, don't forget to add those files to your +POTFILES.in for translation. + +## Projects using libgnome-volume-control + +- [gnome-shell](https://gitlab.gnome.org/GNOME/gnome-shell) +- [gnome-settings-daemon](https://gitlab.gnome.org/GNOME/gnome-settings-daemon) +- [gnome-control-center](https://gitlab.gnome.org/GNOME/gnome-control-center) diff --git a/subprojects/gvc/gvc-channel-map-private.h b/subprojects/gvc/gvc-channel-map-private.h new file mode 100644 index 0000000..3949de3 --- /dev/null +++ b/subprojects/gvc/gvc-channel-map-private.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_CHANNEL_MAP_PRIVATE_H +#define __GVC_CHANNEL_MAP_PRIVATE_H + +#include <glib-object.h> +#include <pulse/pulseaudio.h> + +G_BEGIN_DECLS + +GvcChannelMap * gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *map); +const pa_channel_map * gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map); + +void gvc_channel_map_volume_changed (GvcChannelMap *map, + const pa_cvolume *cv, + gboolean set); +const pa_cvolume * gvc_channel_map_get_cvolume (const GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_MAP_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-channel-map.c b/subprojects/gvc/gvc-channel-map.c new file mode 100644 index 0000000..bf4d737 --- /dev/null +++ b/subprojects/gvc/gvc-channel-map.c @@ -0,0 +1,247 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-channel-map.h" +#include "gvc-channel-map-private.h" + +struct GvcChannelMapPrivate +{ + pa_channel_map pa_map; + gboolean pa_volume_is_set; + pa_cvolume pa_volume; + gdouble extern_volume[NUM_TYPES]; /* volume, balance, fade, lfe */ + gboolean can_balance; + gboolean can_fade; +}; + +enum { + VOLUME_CHANGED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +static void gvc_channel_map_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcChannelMap, gvc_channel_map, G_TYPE_OBJECT) + +guint +gvc_channel_map_get_num_channels (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), 0); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return 0; + + return map->priv->pa_map.channels; +} + +const gdouble * +gvc_channel_map_get_volume (GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + map->priv->extern_volume[VOLUME] = (gdouble) pa_cvolume_max (&map->priv->pa_volume); + if (gvc_channel_map_can_balance (map)) + map->priv->extern_volume[BALANCE] = (gdouble) pa_cvolume_get_balance (&map->priv->pa_volume, &map->priv->pa_map); + else + map->priv->extern_volume[BALANCE] = 0; + if (gvc_channel_map_can_fade (map)) + map->priv->extern_volume[FADE] = (gdouble) pa_cvolume_get_fade (&map->priv->pa_volume, &map->priv->pa_map); + else + map->priv->extern_volume[FADE] = 0; + if (gvc_channel_map_has_lfe (map)) + map->priv->extern_volume[LFE] = (gdouble) pa_cvolume_get_position (&map->priv->pa_volume, &map->priv->pa_map, PA_CHANNEL_POSITION_LFE); + else + map->priv->extern_volume[LFE] = 0; + + return map->priv->extern_volume; +} + +gboolean +gvc_channel_map_can_balance (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return map->priv->can_balance; +} + +gboolean +gvc_channel_map_can_fade (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return map->priv->can_fade; +} + +const char * +gvc_channel_map_get_mapping (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return pa_channel_map_to_pretty_name (&map->priv->pa_map); +} + +/** + * gvc_channel_map_has_position: (skip) + * @map: + * @position: + * + * Returns: + */ +gboolean +gvc_channel_map_has_position (const GvcChannelMap *map, + pa_channel_position_t position) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE); + + return pa_channel_map_has_position (&(map->priv->pa_map), position); +} + +const pa_channel_map * +gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return &map->priv->pa_map; +} + +const pa_cvolume * +gvc_channel_map_get_cvolume (const GvcChannelMap *map) +{ + g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL); + + if (!pa_channel_map_valid(&map->priv->pa_map)) + return NULL; + + return &map->priv->pa_volume; +} + +static void +gvc_channel_map_class_init (GvcChannelMapClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = gvc_channel_map_finalize; + + signals [VOLUME_CHANGED] = + g_signal_new ("volume-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcChannelMapClass, volume_changed), + NULL, NULL, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); +} + +void +gvc_channel_map_volume_changed (GvcChannelMap *map, + const pa_cvolume *cv, + gboolean set) +{ + g_return_if_fail (GVC_IS_CHANNEL_MAP (map)); + g_return_if_fail (cv != NULL); + g_return_if_fail (pa_cvolume_compatible_with_channel_map(cv, &map->priv->pa_map)); + + if (pa_cvolume_equal(cv, &map->priv->pa_volume)) + return; + + map->priv->pa_volume = *cv; + + if (map->priv->pa_volume_is_set == FALSE) { + map->priv->pa_volume_is_set = TRUE; + return; + } + g_signal_emit (map, signals[VOLUME_CHANGED], 0, set); +} + +static void +gvc_channel_map_init (GvcChannelMap *map) +{ + map->priv = gvc_channel_map_get_instance_private (map); + map->priv->pa_volume_is_set = FALSE; +} + +static void +gvc_channel_map_finalize (GObject *object) +{ + GvcChannelMap *channel_map; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_CHANNEL_MAP (object)); + + channel_map = GVC_CHANNEL_MAP (object); + + g_return_if_fail (channel_map->priv != NULL); + + G_OBJECT_CLASS (gvc_channel_map_parent_class)->finalize (object); +} + +GvcChannelMap * +gvc_channel_map_new (void) +{ + GObject *map; + map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL); + return GVC_CHANNEL_MAP (map); +} + +static void +set_from_pa_map (GvcChannelMap *map, + const pa_channel_map *pa_map) +{ + g_assert (pa_channel_map_valid(pa_map)); + + map->priv->can_balance = pa_channel_map_can_balance (pa_map); + map->priv->can_fade = pa_channel_map_can_fade (pa_map); + + map->priv->pa_map = *pa_map; + pa_cvolume_set(&map->priv->pa_volume, pa_map->channels, PA_VOLUME_NORM); +} + +GvcChannelMap * +gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *pa_map) +{ + GObject *map; + map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL); + + set_from_pa_map (GVC_CHANNEL_MAP (map), pa_map); + + return GVC_CHANNEL_MAP (map); +} diff --git a/subprojects/gvc/gvc-channel-map.h b/subprojects/gvc/gvc-channel-map.h new file mode 100644 index 0000000..85c5772 --- /dev/null +++ b/subprojects/gvc/gvc-channel-map.h @@ -0,0 +1,73 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_CHANNEL_MAP_H +#define __GVC_CHANNEL_MAP_H + +#include <glib-object.h> +#include <gvc-pulseaudio-fake.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_CHANNEL_MAP (gvc_channel_map_get_type ()) +#define GVC_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMap)) +#define GVC_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass)) +#define GVC_IS_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_CHANNEL_MAP)) +#define GVC_IS_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_CHANNEL_MAP)) +#define GVC_CHANNEL_MAP_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass)) + +typedef struct GvcChannelMapPrivate GvcChannelMapPrivate; + +typedef struct +{ + GObject parent; + GvcChannelMapPrivate *priv; +} GvcChannelMap; + +typedef struct +{ + GObjectClass parent_class; + void (*volume_changed) (GvcChannelMap *channel_map, gboolean set); +} GvcChannelMapClass; + +enum { + VOLUME, + BALANCE, + FADE, + LFE, + NUM_TYPES +}; + +GType gvc_channel_map_get_type (void); + +GvcChannelMap * gvc_channel_map_new (void); +guint gvc_channel_map_get_num_channels (const GvcChannelMap *map); +const gdouble * gvc_channel_map_get_volume (GvcChannelMap *map); +gboolean gvc_channel_map_can_balance (const GvcChannelMap *map); +gboolean gvc_channel_map_can_fade (const GvcChannelMap *map); +gboolean gvc_channel_map_has_position (const GvcChannelMap *map, + pa_channel_position_t position); +#define gvc_channel_map_has_lfe(x) gvc_channel_map_has_position (x, PA_CHANNEL_POSITION_LFE) + +const char * gvc_channel_map_get_mapping (const GvcChannelMap *map); + +G_END_DECLS + +#endif /* __GVC_CHANNEL_MAP_H */ diff --git a/subprojects/gvc/gvc-mixer-card-private.h b/subprojects/gvc/gvc-mixer-card-private.h new file mode 100644 index 0000000..e190f7f --- /dev/null +++ b/subprojects/gvc/gvc-mixer-card-private.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008-2009 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CARD_PRIVATE_H +#define __GVC_MIXER_CARD_PRIVATE_H + +#include <pulse/pulseaudio.h> +#include "gvc-mixer-card.h" + +G_BEGIN_DECLS + +GvcMixerCard * gvc_mixer_card_new (pa_context *context, + guint index); +pa_context * gvc_mixer_card_get_pa_context (GvcMixerCard *card); + +G_END_DECLS + +#endif /* __GVC_MIXER_CARD_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-mixer-card.c b/subprojects/gvc/gvc-mixer-card.c new file mode 100644 index 0000000..93be4da --- /dev/null +++ b/subprojects/gvc/gvc-mixer-card.c @@ -0,0 +1,584 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * Copyright (C) 2009 Bastien Nocera + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-card.h" +#include "gvc-mixer-card-private.h" + +static guint32 card_serial = 1; + +struct GvcMixerCardPrivate +{ + pa_context *pa_context; + guint id; + guint index; + char *name; + char *icon_name; + char *profile; + char *target_profile; + char *human_profile; + GList *profiles; + pa_operation *profile_op; + GList *ports; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_PA_CONTEXT, + PROP_INDEX, + PROP_NAME, + PROP_ICON_NAME, + PROP_PROFILE, + PROP_HUMAN_PROFILE, +}; + +static void gvc_mixer_card_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerCard, gvc_mixer_card, G_TYPE_OBJECT) + +static guint32 +get_next_card_serial (void) +{ + guint32 serial; + + serial = card_serial++; + + if ((gint32)card_serial < 0) { + card_serial = 1; + } + + return serial; +} + +pa_context * +gvc_mixer_card_get_pa_context (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->pa_context; +} + +guint +gvc_mixer_card_get_index (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->index; +} + +guint +gvc_mixer_card_get_id (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0); + return card->priv->id; +} + +const char * +gvc_mixer_card_get_name (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->name; +} + +gboolean +gvc_mixer_card_set_name (GvcMixerCard *card, + const char *name) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + + g_free (card->priv->name); + card->priv->name = g_strdup (name); + g_object_notify (G_OBJECT (card), "name"); + + return TRUE; +} + +const char * +gvc_mixer_card_get_icon_name (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->icon_name; +} + +gboolean +gvc_mixer_card_set_icon_name (GvcMixerCard *card, + const char *icon_name) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + + g_free (card->priv->icon_name); + card->priv->icon_name = g_strdup (icon_name); + g_object_notify (G_OBJECT (card), "icon-name"); + + return TRUE; +} + +/** + * gvc_mixer_card_get_profile: (skip) + * @card: + * + * Returns: + */ +GvcMixerCardProfile * +gvc_mixer_card_get_profile (GvcMixerCard *card) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + g_return_val_if_fail (card->priv->profiles != NULL, NULL); + + for (l = card->priv->profiles; l != NULL; l = l->next) { + GvcMixerCardProfile *p = l->data; + if (g_str_equal (card->priv->profile, p->profile)) { + return p; + } + } + + g_assert_not_reached (); + + return NULL; +} + +gboolean +gvc_mixer_card_set_profile (GvcMixerCard *card, + const char *profile) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles != NULL, FALSE); + + g_free (card->priv->profile); + card->priv->profile = g_strdup (profile); + + g_free (card->priv->human_profile); + card->priv->human_profile = NULL; + + for (l = card->priv->profiles; l != NULL; l = l->next) { + GvcMixerCardProfile *p = l->data; + if (g_str_equal (card->priv->profile, p->profile)) { + card->priv->human_profile = g_strdup (p->human_profile); + break; + } + } + + g_object_notify (G_OBJECT (card), "profile"); + + return TRUE; +} + +static void +_pa_context_set_card_profile_by_index_cb (pa_context *context, + int success, + void *userdata) +{ + GvcMixerCard *card = GVC_MIXER_CARD (userdata); + + g_assert (card->priv->target_profile); + + if (success > 0) { + gvc_mixer_card_set_profile (card, card->priv->target_profile); + } else { + g_debug ("Failed to switch profile on '%s' from '%s' to '%s'", + card->priv->name, + card->priv->profile, + card->priv->target_profile); + } + g_free (card->priv->target_profile); + card->priv->target_profile = NULL; + + pa_operation_unref (card->priv->profile_op); + card->priv->profile_op = NULL; +} + +/** + * gvc_mixer_card_change_profile: + * @card: a #GvcMixerCard + * @profile: (allow-none): the profile to change to or %NULL. + * + * Change the profile in use on this card. + * + * Returns: %TRUE if profile successfully changed or already using this profile. + */ +gboolean +gvc_mixer_card_change_profile (GvcMixerCard *card, + const char *profile) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles != NULL, FALSE); + + /* Same profile, or already requested? */ + if (g_strcmp0 (card->priv->profile, profile) == 0) + return TRUE; + if (g_strcmp0 (profile, card->priv->target_profile) == 0) + return TRUE; + if (card->priv->profile_op != NULL) { + pa_operation_cancel (card->priv->profile_op); + pa_operation_unref (card->priv->profile_op); + card->priv->profile_op = NULL; + } + + if (card->priv->profile != NULL) { + g_free (card->priv->target_profile); + card->priv->target_profile = g_strdup (profile); + + card->priv->profile_op = pa_context_set_card_profile_by_index (card->priv->pa_context, + card->priv->index, + card->priv->target_profile, + _pa_context_set_card_profile_by_index_cb, + card); + + if (card->priv->profile_op == NULL) { + g_warning ("pa_context_set_card_profile_by_index() failed"); + return FALSE; + } + } else { + g_assert (card->priv->human_profile == NULL); + card->priv->profile = g_strdup (profile); + } + + return TRUE; +} + +/** + * gvc_mixer_card_get_profiles: + * + * Return value: (transfer none) (element-type GvcMixerCardProfile): + */ +const GList * +gvc_mixer_card_get_profiles (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->profiles; +} + +/** + * gvc_mixer_card_get_ports: + * + * Return value: (transfer none) (element-type GvcMixerCardPort): + */ +const GList * +gvc_mixer_card_get_ports (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + return card->priv->ports; +} + +/** + * gvc_mixer_card_profile_compare: + * + * Return value: 1 if @a has a higher priority, -1 if @b has a higher + * priority, 0 if @a and @b have the same priority. + */ +int +gvc_mixer_card_profile_compare (GvcMixerCardProfile *a, + GvcMixerCardProfile *b) +{ + if (a->priority == b->priority) + return 0; + if (a->priority > b->priority) + return 1; + return -1; +} + +/** + * gvc_mixer_card_set_profiles: + * @profiles: (transfer full) (element-type GvcMixerCardProfile): + */ +gboolean +gvc_mixer_card_set_profiles (GvcMixerCard *card, + GList *profiles) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->profiles == NULL, FALSE); + + card->priv->profiles = g_list_sort (profiles, (GCompareFunc) gvc_mixer_card_profile_compare); + + return TRUE; +} + +/** + * gvc_mixer_card_get_gicon: + * @card: + * + * Return value: (transfer full): + */ +GIcon * +gvc_mixer_card_get_gicon (GvcMixerCard *card) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL); + + if (card->priv->icon_name == NULL) + return NULL; + + return g_themed_icon_new_with_default_fallbacks (card->priv->icon_name); +} + +static void +free_port (GvcMixerCardPort *port) +{ + g_free (port->port); + g_free (port->human_port); + g_free (port->icon_name); + g_list_free (port->profiles); + + g_free (port); +} + +/** + * gvc_mixer_card_set_ports: + * @ports: (transfer full) (element-type GvcMixerCardPort): + */ +gboolean +gvc_mixer_card_set_ports (GvcMixerCard *card, + GList *ports) +{ + g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE); + g_return_val_if_fail (card->priv->ports == NULL, FALSE); + + g_list_free_full (card->priv->ports, (GDestroyNotify) free_port); + card->priv->ports = ports; + + return TRUE; +} + +static void +gvc_mixer_card_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerCard *self = GVC_MIXER_CARD (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + self->priv->pa_context = g_value_get_pointer (value); + break; + case PROP_INDEX: + self->priv->index = g_value_get_ulong (value); + break; + case PROP_ID: + self->priv->id = g_value_get_ulong (value); + break; + case PROP_NAME: + gvc_mixer_card_set_name (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_card_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_PROFILE: + gvc_mixer_card_set_profile (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_card_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerCard *self = GVC_MIXER_CARD (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + g_value_set_pointer (value, self->priv->pa_context); + break; + case PROP_INDEX: + g_value_set_ulong (value, self->priv->index); + break; + case PROP_ID: + g_value_set_ulong (value, self->priv->id); + break; + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->priv->icon_name); + break; + case PROP_PROFILE: + g_value_set_string (value, self->priv->profile); + break; + case PROP_HUMAN_PROFILE: + g_value_set_string (value, self->priv->human_profile); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_card_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerCard *self; + + object = G_OBJECT_CLASS (gvc_mixer_card_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_CARD (object); + + self->priv->id = get_next_card_serial (); + + return object; +} + +static void +gvc_mixer_card_class_init (GvcMixerCardClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructor = gvc_mixer_card_constructor; + gobject_class->finalize = gvc_mixer_card_finalize; + + gobject_class->set_property = gvc_mixer_card_set_property; + gobject_class->get_property = gvc_mixer_card_get_property; + + g_object_class_install_property (gobject_class, + PROP_INDEX, + g_param_spec_ulong ("index", + "Index", + "The index for this card", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_ID, + g_param_spec_ulong ("id", + "id", + "The id for this card", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_PA_CONTEXT, + g_param_spec_pointer ("pa-context", + "PulseAudio context", + "The PulseAudio context for this card", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Name to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_ICON_NAME, + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_PROFILE, + g_param_spec_string ("profile", + "Profile", + "Name of current profile for this card", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_HUMAN_PROFILE, + g_param_spec_string ("human-profile", + "Profile (Human readable)", + "Name of current profile for this card in human readable form", + NULL, + G_PARAM_READABLE)); +} + +static void +gvc_mixer_card_init (GvcMixerCard *card) +{ + card->priv = gvc_mixer_card_get_instance_private (card); +} + +GvcMixerCard * +gvc_mixer_card_new (pa_context *context, + guint index) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_CARD, + "index", index, + "pa-context", context, + NULL); + return GVC_MIXER_CARD (object); +} + +static void +free_profile (GvcMixerCardProfile *p) +{ + g_free (p->profile); + g_free (p->human_profile); + g_free (p->status); + g_free (p); +} + +static void +gvc_mixer_card_finalize (GObject *object) +{ + GvcMixerCard *mixer_card; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_CARD (object)); + + mixer_card = GVC_MIXER_CARD (object); + + g_return_if_fail (mixer_card->priv != NULL); + + g_free (mixer_card->priv->name); + mixer_card->priv->name = NULL; + + g_free (mixer_card->priv->icon_name); + mixer_card->priv->icon_name = NULL; + + g_free (mixer_card->priv->target_profile); + mixer_card->priv->target_profile = NULL; + + g_free (mixer_card->priv->profile); + mixer_card->priv->profile = NULL; + + g_free (mixer_card->priv->human_profile); + mixer_card->priv->human_profile = NULL; + + g_list_free_full (mixer_card->priv->profiles, (GDestroyNotify) free_profile); + mixer_card->priv->profiles = NULL; + + g_list_free_full (mixer_card->priv->ports, (GDestroyNotify) free_port); + mixer_card->priv->ports = NULL; + + G_OBJECT_CLASS (gvc_mixer_card_parent_class)->finalize (object); +} + diff --git a/subprojects/gvc/gvc-mixer-card.h b/subprojects/gvc/gvc-mixer-card.h new file mode 100644 index 0000000..814f8d4 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-card.h @@ -0,0 +1,102 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008-2009 Red Hat, Inc. + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CARD_H +#define __GVC_MIXER_CARD_H + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_CARD (gvc_mixer_card_get_type ()) +#define GVC_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CARD, GvcMixerCard)) +#define GVC_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CARD, GvcMixerCardClass)) +#define GVC_IS_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CARD)) +#define GVC_IS_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CARD)) +#define GVC_MIXER_CARD_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CARD, GvcMixerCardClass)) + +typedef struct GvcMixerCardPrivate GvcMixerCardPrivate; + +typedef struct +{ + GObject parent; + GvcMixerCardPrivate *priv; +} GvcMixerCard; + +typedef struct +{ + GObjectClass parent_class; + + /* vtable */ +} GvcMixerCardClass; + +typedef struct +{ + char *profile; + char *human_profile; + char *status; + guint priority; + guint n_sinks, n_sources; +} GvcMixerCardProfile; + +typedef struct +{ + char *port; + char *human_port; + char *icon_name; + guint priority; + gint available; + gint direction; + GList *profiles; +} GvcMixerCardPort; + +GType gvc_mixer_card_get_type (void); + +guint gvc_mixer_card_get_id (GvcMixerCard *card); +guint gvc_mixer_card_get_index (GvcMixerCard *card); +const char * gvc_mixer_card_get_name (GvcMixerCard *card); +const char * gvc_mixer_card_get_icon_name (GvcMixerCard *card); +GvcMixerCardProfile * gvc_mixer_card_get_profile (GvcMixerCard *card); +const GList * gvc_mixer_card_get_profiles (GvcMixerCard *card); +const GList * gvc_mixer_card_get_ports (GvcMixerCard *card); +gboolean gvc_mixer_card_change_profile (GvcMixerCard *card, + const char *profile); +GIcon * gvc_mixer_card_get_gicon (GvcMixerCard *card); + +int gvc_mixer_card_profile_compare (GvcMixerCardProfile *a, + GvcMixerCardProfile *b); + +/* private */ +gboolean gvc_mixer_card_set_name (GvcMixerCard *card, + const char *name); +gboolean gvc_mixer_card_set_icon_name (GvcMixerCard *card, + const char *name); +gboolean gvc_mixer_card_set_profile (GvcMixerCard *card, + const char *profile); +gboolean gvc_mixer_card_set_profiles (GvcMixerCard *card, + GList *profiles); +gboolean gvc_mixer_card_set_ports (GvcMixerCard *stream, + GList *ports); + +G_END_DECLS + +#endif /* __GVC_MIXER_CARD_H */ diff --git a/subprojects/gvc/gvc-mixer-control-private.h b/subprojects/gvc/gvc-mixer-control-private.h new file mode 100644 index 0000000..ac79975 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-control-private.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CONTROL_PRIVATE_H +#define __GVC_MIXER_CONTROL_PRIVATE_H + +#include <glib-object.h> +#include <pulse/pulseaudio.h> +#include "gvc-mixer-stream.h" +#include "gvc-mixer-card.h" + +G_BEGIN_DECLS + +pa_context * gvc_mixer_control_get_pa_context (GvcMixerControl *control); + +G_END_DECLS + +#endif /* __GVC_MIXER_CONTROL_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-mixer-control.c b/subprojects/gvc/gvc-mixer-control.c new file mode 100644 index 0000000..8b39080 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-control.c @@ -0,0 +1,3876 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2006-2008 Lennart Poettering + * Copyright (C) 2008 Sjoerd Simons <sjoerd@luon.net> + * Copyright (C) 2008 William Jon McCann + * Copyright (C) 2012 Conor Curran + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> +#include <pulse/glib-mainloop.h> +#include <pulse/ext-stream-restore.h> + +#ifdef HAVE_ALSA +#include <alsa/asoundlib.h> +#endif /* HAVE_ALSA */ + +#include "gvc-mixer-control.h" +#include "gvc-mixer-sink.h" +#include "gvc-mixer-source.h" +#include "gvc-mixer-sink-input.h" +#include "gvc-mixer-source-output.h" +#include "gvc-mixer-event-role.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-card-private.h" +#include "gvc-channel-map-private.h" +#include "gvc-mixer-control-private.h" +#include "gvc-mixer-ui-device.h" + +#define RECONNECT_DELAY 5 + +enum { + PROP_0, + PROP_NAME +}; + +struct GvcMixerControlPrivate +{ + pa_glib_mainloop *pa_mainloop; + pa_mainloop_api *pa_api; + pa_context *pa_context; + guint server_protocol_version; + int n_outstanding; + guint reconnect_id; + char *name; + + gboolean default_sink_is_set; + guint default_sink_id; + char *default_sink_name; + gboolean default_source_is_set; + guint default_source_id; + char *default_source_name; + + gboolean event_sink_input_is_set; + guint event_sink_input_id; + + GHashTable *all_streams; + GHashTable *sinks; /* fixed outputs */ + GHashTable *sources; /* fixed inputs */ + GHashTable *sink_inputs; /* routable output streams */ + GHashTable *source_outputs; /* routable input streams */ + GHashTable *clients; + GHashTable *cards; + + GvcMixerStream *new_default_sink_stream; /* new default sink stream, used in gvc_mixer_control_set_default_sink () */ + GvcMixerStream *new_default_source_stream; /* new default source stream, used in gvc_mixer_control_set_default_source () */ + + GHashTable *ui_outputs; /* UI visible outputs */ + GHashTable *ui_inputs; /* UI visible inputs */ + + /* When we change profile on a device that is not the server default sink, + * it will jump back to the default sink set by the server to prevent the + * audio setup from being 'outputless'. + * + * All well and good but then when we get the new stream created for the + * new profile how do we know that this is the intended default or selected + * device the user wishes to use. */ + guint profile_swapping_device_id; + +#ifdef HAVE_ALSA + int headset_card; + gboolean has_headsetmic; + gboolean has_headphonemic; + gboolean headset_plugged_in; + char *headphones_name; + char *headsetmic_name; + char *headphonemic_name; + char *internalspk_name; + char *internalmic_name; +#endif /* HAVE_ALSA */ + + GvcMixerControlState state; +}; + +enum { + STATE_CHANGED, + STREAM_ADDED, + STREAM_REMOVED, + STREAM_CHANGED, + CARD_ADDED, + CARD_REMOVED, + DEFAULT_SINK_CHANGED, + DEFAULT_SOURCE_CHANGED, + ACTIVE_OUTPUT_UPDATE, + ACTIVE_INPUT_UPDATE, + OUTPUT_ADDED, + INPUT_ADDED, + OUTPUT_REMOVED, + INPUT_REMOVED, + AUDIO_DEVICE_SELECTION_NEEDED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +static void gvc_mixer_control_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT) + +pa_context * +gvc_mixer_control_get_pa_context (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + return control->priv->pa_context; +} + +/** + * gvc_mixer_control_get_event_sink_input: + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_event_sink_input (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->event_sink_input_id)); + + return stream; +} + +static void +gvc_mixer_control_stream_restore_cb (pa_context *c, + GvcMixerStream *new_stream, + const pa_ext_stream_restore_info *info, + GvcMixerControl *control) +{ + pa_operation *o; + pa_ext_stream_restore_info new_info; + + if (new_stream == NULL) + return; + + new_info.name = info->name; + new_info.channel_map = info->channel_map; + new_info.volume = info->volume; + new_info.mute = info->mute; + + new_info.device = gvc_mixer_stream_get_name (new_stream); + + o = pa_ext_stream_restore_write (control->priv->pa_context, + PA_UPDATE_REPLACE, + &new_info, 1, + TRUE, NULL, NULL); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_write() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return; + } + + g_debug ("Changed default device for %s to %s", info->name, new_info.device); + + pa_operation_unref (o); +} + +static void +gvc_mixer_control_stream_restore_sink_cb (pa_context *c, + const pa_ext_stream_restore_info *info, + int eol, + void *userdata) +{ + GvcMixerControl *control = (GvcMixerControl *) userdata; + if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by")) + return; + gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control); +} + +static void +gvc_mixer_control_stream_restore_source_cb (pa_context *c, + const pa_ext_stream_restore_info *info, + int eol, + void *userdata) +{ + GvcMixerControl *control = (GvcMixerControl *) userdata; + if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by")) + return; + gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control); +} + +/** + * gvc_mixer_control_lookup_device_from_stream: + * @control: + * @stream: + * + * Returns: (transfer none): a #GvcUIDevice or %NULL + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + GList *devices, *d; + gboolean is_network_stream; + const GList *ports; + GvcMixerUIDevice *ret; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + + if (GVC_IS_MIXER_SOURCE (stream)) + devices = g_hash_table_get_values (control->priv->ui_inputs); + else + devices = g_hash_table_get_values (control->priv->ui_outputs); + + ret = NULL; + ports = gvc_mixer_stream_get_ports (stream); + is_network_stream = (ports == NULL); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerUIDevice *device = d->data; + guint stream_id = G_MAXUINT; + + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + + if (is_network_stream && + stream_id == gvc_mixer_stream_get_id (stream)) { + g_debug ("lookup device from stream - %s - it is a network_stream ", + gvc_mixer_ui_device_get_description (device)); + ret = device; + break; + } else if (!is_network_stream) { + const GvcMixerStreamPort *port; + port = gvc_mixer_stream_get_port (stream); + + if (stream_id == gvc_mixer_stream_get_id (stream) && + g_strcmp0 (gvc_mixer_ui_device_get_port (device), + port->port) == 0) { + g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'", + gvc_mixer_ui_device_get_description (device), + gvc_mixer_ui_device_get_port (device), + stream_id, + port->port, + gvc_mixer_stream_get_id (stream), + gvc_mixer_stream_get_description (stream)); + ret = device; + break; + } + } + } + + g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream)); + + g_list_free (devices); + + return ret; +} + +gboolean +gvc_mixer_control_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream) +{ + pa_operation *o; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_debug ("about to set default sink on server"); + o = pa_context_set_default_sink (control->priv->pa_context, + gvc_mixer_stream_get_name (stream), + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_sink() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + control->priv->new_default_sink_stream = stream; + g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream); + + o = pa_ext_stream_restore_read (control->priv->pa_context, + gvc_mixer_control_stream_restore_sink_cb, + control); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + return TRUE; +} + +gboolean +gvc_mixer_control_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream) +{ + GvcMixerUIDevice* input; + pa_operation *o; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + o = pa_context_set_default_source (control->priv->pa_context, + gvc_mixer_stream_get_name (stream), + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_source() failed"); + return FALSE; + } + + pa_operation_unref (o); + + control->priv->new_default_source_stream = stream; + g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream); + + o = pa_ext_stream_restore_read (control->priv->pa_context, + gvc_mixer_control_stream_restore_source_cb, + control); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + return FALSE; + } + + pa_operation_unref (o); + + /* source change successful, update the UI. */ + input = gvc_mixer_control_lookup_device_from_stream (control, stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + + return TRUE; +} + +/** + * gvc_mixer_control_get_default_sink: + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_default_sink (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + if (control->priv->default_sink_is_set) { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->default_sink_id)); + } else { + stream = NULL; + } + + return stream; +} + +/** + * gvc_mixer_control_get_default_source: + * @control: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_default_source (GvcMixerControl *control) +{ + GvcMixerStream *stream; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + if (control->priv->default_source_is_set) { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->default_source_id)); + } else { + stream = NULL; + } + + return stream; +} + +static gpointer +gvc_mixer_control_lookup_id (GHashTable *hash_table, + guint id) +{ + return g_hash_table_lookup (hash_table, + GUINT_TO_POINTER (id)); +} + +/** + * gvc_mixer_control_lookup_stream_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_lookup_stream_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->all_streams, id); +} + +/** + * gvc_mixer_control_lookup_card_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerCard * +gvc_mixer_control_lookup_card_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->cards, id); +} + +/** + * gvc_mixer_control_lookup_output_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_output_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id); +} + +/** + * gvc_mixer_control_lookup_input_id: + * @control: + * @id: + * + * Returns: (transfer none): + */ +GvcMixerUIDevice * +gvc_mixer_control_lookup_input_id (GvcMixerControl *control, + guint id) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id); +} + +/** + * gvc_mixer_control_get_stream_from_device: + * @control: + * @device: + * + * Returns: (transfer none): + */ +GvcMixerStream * +gvc_mixer_control_get_stream_from_device (GvcMixerControl *control, + GvcMixerUIDevice *device) +{ + gint stream_id; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + stream_id = gvc_mixer_ui_device_get_stream_id (device); + + if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) { + g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream"); + return NULL; + } + return gvc_mixer_control_lookup_stream_id (control, stream_id); +} + +/** + * gvc_mixer_control_change_profile_on_selected_device: + * @control: + * @device: + * @profile: (allow-none): Can be %NULL if any profile present on this port is okay + * + * Returns: This method will attempt to swap the profile on the card of + * the device with given profile name. If successfull it will set the + * preferred profile on that device so as we know the next time the user + * moves to that device it should have this profile active. + */ +gboolean +gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control, + GvcMixerUIDevice *device, + const gchar *profile) +{ + const gchar *best_profile; + GvcMixerCardProfile *current_profile; + GvcMixerCard *card; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + g_object_get (G_OBJECT (device), "card", &card, NULL); + current_profile = gvc_mixer_card_get_profile (card); + + if (current_profile) + best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile); + else + best_profile = profile; + + g_assert (best_profile); + + g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i", + profile ? profile : "(any)", best_profile, + gvc_mixer_card_get_name (card), + gvc_mixer_ui_device_get_stream_id (device)); + + g_debug ("default sink name = %s and default sink id %u", + control->priv->default_sink_name, + control->priv->default_sink_id); + + control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device); + + if (gvc_mixer_card_change_profile (card, best_profile)) { + gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile); + return TRUE; + } + return FALSE; +} + +/** + * gvc_mixer_control_change_output: + * @control: + * @output: + * This method is called from the UI when the user selects a previously unselected device. + * - Firstly it queries the stream from the device. + * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources) + * In the scenario of a NULL stream on the device + * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device. + * - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered + * from when we attempt to change profile we will know exactly what device to highlight on that stream. + * - It attempts to swap the profile on the card from that device and returns. + * - Next, it handles network or bluetooth streams that only require their stream to be made the default. + * - Next it deals with port changes so if the stream's active port is not the same as the port on the device + * it will attempt to change the port on that stream to be same as the device. If this fails it will return. + * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device. + */ +void +gvc_mixer_control_change_output (GvcMixerControl *control, + GvcMixerUIDevice* output) +{ + GvcMixerStream *stream; + GvcMixerStream *default_stream; + const GvcMixerStreamPort *active_port; + const gchar *output_port; + + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output)); + + g_debug ("control change output"); + + stream = gvc_mixer_control_get_stream_from_device (control, output); + if (stream == NULL) { + gvc_mixer_control_change_profile_on_selected_device (control, + output, NULL); + return; + } + + /* Handle a network sink as a portless or cardless device */ + if (!gvc_mixer_ui_device_has_ports (output)) { + g_debug ("Did we try to move to a software/bluetooth sink ?"); + if (gvc_mixer_control_set_default_sink (control, stream)) { + /* sink change was successful, update the UI.*/ + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } + else { + g_warning ("Failed to set default sink with stream from output %s", + gvc_mixer_ui_device_get_description (output)); + } + return; + } + + active_port = gvc_mixer_stream_get_port (stream); + output_port = gvc_mixer_ui_device_get_port (output); + /* First ensure the correct port is active on the sink */ + if (g_strcmp0 (active_port->port, output_port) != 0) { + g_debug ("Port change, switch to = %s", output_port); + if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) { + g_warning ("Could not change port !"); + return; + } + } + + default_stream = gvc_mixer_control_get_default_sink (control); + + /* Finally if we are not on the correct stream, swap over. */ + if (stream != default_stream) { + GvcMixerUIDevice* device; + + g_debug ("Attempting to swap over to stream %s ", + gvc_mixer_stream_get_description (stream)); + if (gvc_mixer_control_set_default_sink (control, stream)) { + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + /* If the move failed for some reason reset the UI. */ + device = gvc_mixer_control_lookup_device_from_stream (control, default_stream); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (device)); + } + } +} + + +/** + * gvc_mixer_control_change_input: + * @control: + * @input: + * This method is called from the UI when the user selects a previously unselected device. + * - Firstly it queries the stream from the device. + * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources) + * In the scenario of a NULL stream on the device + * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device. + * - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered + * from when we attempt to change profile we will know exactly what device to highlight on that stream. + * - It attempts to swap the profile on the card from that device and returns. + * - Next, it handles network or bluetooth streams that only require their stream to be made the default. + * - Next it deals with port changes so if the stream's active port is not the same as the port on the device + * it will attempt to change the port on that stream to be same as the device. If this fails it will return. + * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device. + */ +void +gvc_mixer_control_change_input (GvcMixerControl *control, + GvcMixerUIDevice* input) +{ + GvcMixerStream *stream; + GvcMixerStream *default_stream; + const GvcMixerStreamPort *active_port; + const gchar *input_port; + + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input)); + + stream = gvc_mixer_control_get_stream_from_device (control, input); + if (stream == NULL) { + gvc_mixer_control_change_profile_on_selected_device (control, + input, NULL); + return; + } + + /* Handle a network sink as a portless/cardless device */ + if (!gvc_mixer_ui_device_has_ports (input)) { + g_debug ("Did we try to move to a software/bluetooth source ?"); + if (! gvc_mixer_control_set_default_source (control, stream)) { + g_warning ("Failed to set default source with stream from input %s", + gvc_mixer_ui_device_get_description (input)); + } + return; + } + + active_port = gvc_mixer_stream_get_port (stream); + input_port = gvc_mixer_ui_device_get_port (input); + /* First ensure the correct port is active on the sink */ + if (g_strcmp0 (active_port->port, input_port) != 0) { + g_debug ("Port change, switch to = %s", input_port); + if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) { + g_warning ("Could not change port!"); + return; + } + } + + default_stream = gvc_mixer_control_get_default_source (control); + + /* Finally if we are not on the correct stream, swap over. */ + if (stream != default_stream) { + g_debug ("change-input - attempting to swap over to stream %s", + gvc_mixer_stream_get_description (stream)); + gvc_mixer_control_set_default_source (control, stream); + } +} + + +static void +listify_hash_values_hfunc (gpointer key, + gpointer value, + gpointer user_data) +{ + GSList **list = user_data; + + *list = g_slist_prepend (*list, value); +} + +static int +gvc_name_collate (const char *namea, + const char *nameb) +{ + if (nameb == NULL && namea == NULL) + return 0; + if (nameb == NULL) + return 1; + if (namea == NULL) + return -1; + + return g_utf8_collate (namea, nameb); +} + +static int +gvc_card_collate (GvcMixerCard *a, + GvcMixerCard *b) +{ + const char *namea; + const char *nameb; + + g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0); + g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0); + + namea = gvc_mixer_card_get_name (a); + nameb = gvc_mixer_card_get_name (b); + + return gvc_name_collate (namea, nameb); +} + +/** + * gvc_mixer_control_get_cards: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerCard): + */ +GSList * +gvc_mixer_control_get_cards (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->cards, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_card_collate); +} + +static int +gvc_stream_collate (GvcMixerStream *a, + GvcMixerStream *b) +{ + const char *namea; + const char *nameb; + + g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0); + g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0); + + namea = gvc_mixer_stream_get_name (a); + nameb = gvc_mixer_stream_get_name (b); + + return gvc_name_collate (namea, nameb); +} + +/** + * gvc_mixer_control_get_streams: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerStream): + */ +GSList * +gvc_mixer_control_get_streams (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->all_streams, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sinks: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSink): + */ +GSList * +gvc_mixer_control_get_sinks (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sinks, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sources: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSource): + */ +GSList * +gvc_mixer_control_get_sources (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sources, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_sink_inputs: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSinkInput): + */ +GSList * +gvc_mixer_control_get_sink_inputs (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->sink_inputs, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +/** + * gvc_mixer_control_get_source_outputs: + * @control: + * + * Returns: (transfer container) (element-type Gvc.MixerSourceOutput): + */ +GSList * +gvc_mixer_control_get_source_outputs (GvcMixerControl *control) +{ + GSList *retval; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL); + + retval = NULL; + g_hash_table_foreach (control->priv->source_outputs, + listify_hash_values_hfunc, + &retval); + return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate); +} + +static void +dec_outstanding (GvcMixerControl *control) +{ + if (control->priv->n_outstanding <= 0) { + return; + } + + if (--control->priv->n_outstanding <= 0) { + control->priv->state = GVC_STATE_READY; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY); + } +} + +GvcMixerControlState +gvc_mixer_control_get_state (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), GVC_STATE_CLOSED); + + return control->priv->state; +} + +static void +on_default_source_port_notify (GObject *object, + GParamSpec *pspec, + GvcMixerControl *control) +{ + char *port; + GvcMixerUIDevice *input; + + g_object_get (object, "port", &port, NULL); + input = gvc_mixer_control_lookup_device_from_stream (control, + GVC_MIXER_STREAM (object)); + + g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'", + port, + gvc_mixer_ui_device_get_description (input)); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + + g_free (port); +} + + +static void +_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint new_id; + + if (stream == NULL) { + control->priv->default_source_id = 0; + control->priv->default_source_is_set = FALSE; + g_signal_emit (control, + signals[DEFAULT_SOURCE_CHANGED], + 0, + PA_INVALID_INDEX); + return; + } + + new_id = gvc_mixer_stream_get_id (stream); + + if (control->priv->default_source_id != new_id) { + GvcMixerUIDevice *input; + control->priv->default_source_id = new_id; + control->priv->default_source_is_set = TRUE; + g_signal_emit (control, + signals[DEFAULT_SOURCE_CHANGED], + 0, + new_id); + + if (control->priv->default_source_is_set) { + g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control), + on_default_source_port_notify, + control); + } + + g_signal_connect (stream, + "notify::port", + G_CALLBACK (on_default_source_port_notify), + control); + + input = gvc_mixer_control_lookup_device_from_stream (control, stream); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_INPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (input)); + } +} + +static void +on_default_sink_port_notify (GObject *object, + GParamSpec *pspec, + GvcMixerControl *control) +{ + char *port; + GvcMixerUIDevice *output; + + g_object_get (object, "port", &port, NULL); + + output = gvc_mixer_control_lookup_device_from_stream (control, + GVC_MIXER_STREAM (object)); + if (output != NULL) { + g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s", + port, + gvc_mixer_ui_device_get_description (output)); + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } + g_free (port); +} + +static void +_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint new_id; + + if (stream == NULL) { + /* Don't tell front-ends about an unset default + * sink if it's already unset */ + if (control->priv->default_sink_is_set == FALSE) + return; + control->priv->default_sink_id = 0; + control->priv->default_sink_is_set = FALSE; + g_signal_emit (control, + signals[DEFAULT_SINK_CHANGED], + 0, + PA_INVALID_INDEX); + return; + } + + new_id = gvc_mixer_stream_get_id (stream); + + if (control->priv->default_sink_id != new_id) { + GvcMixerUIDevice *output; + if (control->priv->default_sink_is_set) { + g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control), + on_default_sink_port_notify, + control); + } + + control->priv->default_sink_id = new_id; + + control->priv->default_sink_is_set = TRUE; + g_signal_emit (control, + signals[DEFAULT_SINK_CHANGED], + 0, + new_id); + + g_signal_connect (stream, + "notify::port", + G_CALLBACK (on_default_sink_port_notify), + control); + + output = gvc_mixer_control_lookup_device_from_stream (control, stream); + + g_debug ("active_sink change"); + + g_signal_emit (G_OBJECT (control), + signals[ACTIVE_OUTPUT_UPDATE], + 0, + gvc_mixer_ui_device_get_id (output)); + } +} + +static gboolean +_stream_has_name (gpointer key, + GvcMixerStream *stream, + const char *name) +{ + const char *t_name; + + t_name = gvc_mixer_stream_get_name (stream); + + if (t_name != NULL + && name != NULL + && strcmp (t_name, name) == 0) { + return TRUE; + } + + return FALSE; +} + +static GvcMixerStream * +find_stream_for_name (GvcMixerControl *control, + const char *name) +{ + GvcMixerStream *stream; + + stream = g_hash_table_find (control->priv->all_streams, + (GHRFunc)_stream_has_name, + (char *)name); + return stream; +} + +static void +update_default_source_from_name (GvcMixerControl *control, + const char *name) +{ + gboolean changed = FALSE; + + if ((control->priv->default_source_name == NULL + && name != NULL) + || (control->priv->default_source_name != NULL + && name == NULL) + || (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) { + changed = TRUE; + } + + if (changed) { + GvcMixerStream *stream; + + g_free (control->priv->default_source_name); + control->priv->default_source_name = g_strdup (name); + + stream = find_stream_for_name (control, name); + _set_default_source (control, stream); + } +} + +static void +update_default_sink_from_name (GvcMixerControl *control, + const char *name) +{ + gboolean changed = FALSE; + + if ((control->priv->default_sink_name == NULL + && name != NULL) + || (control->priv->default_sink_name != NULL + && name == NULL) + || (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) { + changed = TRUE; + } + + if (changed) { + GvcMixerStream *stream; + g_free (control->priv->default_sink_name); + control->priv->default_sink_name = g_strdup (name); + + stream = find_stream_for_name (control, name); + _set_default_sink (control, stream); + } +} + +static void +update_server (GvcMixerControl *control, + const pa_server_info *info) +{ + if (info->default_source_name != NULL) { + update_default_source_from_name (control, info->default_source_name); + } + if (info->default_sink_name != NULL) { + g_debug ("update server"); + update_default_sink_from_name (control, info->default_sink_name); + } +} + +static void +remove_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + guint id; + + g_object_ref (stream); + + id = gvc_mixer_stream_get_id (stream); + + if (id == control->priv->default_sink_id) { + _set_default_sink (control, NULL); + } else if (id == control->priv->default_source_id) { + _set_default_source (control, NULL); + } + + g_hash_table_remove (control->priv->all_streams, + GUINT_TO_POINTER (id)); + g_signal_emit (G_OBJECT (control), + signals[STREAM_REMOVED], + 0, + gvc_mixer_stream_get_id (stream)); + g_object_unref (stream); +} + +static void +add_stream (GvcMixerControl *control, + GvcMixerStream *stream) +{ + g_hash_table_insert (control->priv->all_streams, + GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)), + stream); + g_signal_emit (G_OBJECT (control), + signals[STREAM_ADDED], + 0, + gvc_mixer_stream_get_id (stream)); +} + +/* This method will match individual stream ports against its corresponding device + * It does this by: + * - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream + * and the port-name on the device is the same as the streamport-name. + * This should always find a match and is used exclusively by sync_devices(). + */ +static gboolean +match_stream_with_devices (GvcMixerControl *control, + GvcMixerStreamPort *stream_port, + GvcMixerStream *stream) +{ + GList *devices, *d; + guint stream_card_id; + guint stream_id; + gboolean in_possession = FALSE; + + stream_id = gvc_mixer_stream_get_id (stream); + stream_card_id = gvc_mixer_stream_get_card_index (stream); + + devices = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerUIDevice *device; + gint device_stream_id; + gchar *device_port_name; + gchar *origin; + gchar *description; + GvcMixerCard *card; + guint card_id; + + device = d->data; + g_object_get (G_OBJECT (device), + "stream-id", &device_stream_id, + "card", &card, + "origin", &origin, + "description", &description, + "port-name", &device_port_name, + NULL); + + card_id = gvc_mixer_card_get_index (card); + + g_debug ("Attempt to match_stream update_with_existing_outputs - Try description : '%s', origin : '%s', device port name : '%s', card : %p, AGAINST stream port: '%s', sink card id %i", + description, + origin, + device_port_name, + card, + stream_port->port, + stream_card_id); + + if (stream_card_id == card_id && + g_strcmp0 (device_port_name, stream_port->port) == 0) { + g_debug ("Match device with stream: We have a match with description: '%s', origin: '%s', cached already with device id %u, so set stream id to %i", + description, + origin, + gvc_mixer_ui_device_get_id (device), + stream_id); + + g_object_set (G_OBJECT (device), + "stream-id", (gint)stream_id, + NULL); + in_possession = TRUE; + } + + g_free (device_port_name); + g_free (origin); + g_free (description); + + if (in_possession == TRUE) + break; + } + + g_list_free (devices); + return in_possession; +} + +/* + * This method attempts to match a sink or source with its relevant UI device. + * GvcMixerStream can represent both a sink or source. + * Using static card port introspection implies that we know beforehand what + * outputs and inputs are available to the user. + * But that does not mean that all of these inputs and outputs are available to be used. + * For instance we might be able to see that there is a HDMI port available but if + * we are on the default analog stereo output profile there is no valid sink for + * that HDMI device. We first need to change profile and when update_sink() is called + * only then can we match the new hdmi sink with its corresponding device. + * + * Firstly it checks to see if the incoming stream has no ports. + * - If a stream has no ports but has a valid card ID (bluetooth), it will attempt + * to match the device with the stream using the card id. + * - If a stream has no ports and no valid card id, it goes ahead and makes a new + * device (software/network devices are only detectable at the sink/source level) + * If the stream has ports it will match each port against the stream using match_stream_with_devices(). + * + * This method should always find a match. + */ +static void +sync_devices (GvcMixerControl *control, + GvcMixerStream* stream) +{ + /* Go through ports to see what outputs can be created. */ + const GList *stream_ports; + const GList *n = NULL; + gboolean is_output = !GVC_IS_MIXER_SOURCE (stream); + gint stream_port_count = 0; + + stream_ports = gvc_mixer_stream_get_ports (stream); + + if (stream_ports == NULL) { + GvcMixerUIDevice *device; + /* Bluetooth, no ports but a valid card */ + if (gvc_mixer_stream_get_card_index (stream) != PA_INVALID_INDEX) { + GList *devices, *d; + gboolean in_possession = FALSE; + + devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *card; + guint card_id; + + device = d->data; + + g_object_get (G_OBJECT (device), + "card", &card, + NULL); + card_id = gvc_mixer_card_get_index (card); + g_debug ("sync devices, device description - '%s', device card id - %i, stream description - %s, stream card id - %i", + gvc_mixer_ui_device_get_description (device), + card_id, + gvc_mixer_stream_get_description (stream), + gvc_mixer_stream_get_card_index (stream)); + if (card_id == gvc_mixer_stream_get_card_index (stream)) { + in_possession = TRUE; + break; + } + } + g_list_free (devices); + + if (!in_possession) { + g_warning ("Couldn't match the portless stream (with card) - '%s' is it an input ? -> %i, streams card id -> %i", + gvc_mixer_stream_get_description (stream), + GVC_IS_MIXER_SOURCE (stream), + gvc_mixer_stream_get_card_index (stream)); + return; + } + + g_object_set (G_OBJECT (device), + "stream-id", (gint)gvc_mixer_stream_get_id (stream), + "description", gvc_mixer_stream_get_description (stream), + "origin", "", /*Leave it empty for these special cases*/ + "port-name", NULL, + "port-available", TRUE, + NULL); + } else { /* Network sink/source has no ports and no card. */ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "stream-id", (gint)gvc_mixer_stream_get_id (stream), + "description", gvc_mixer_stream_get_description (stream), + "origin", "", /* Leave it empty for these special cases */ + "port-name", NULL, + "port-available", TRUE, + NULL); + device = GVC_MIXER_UI_DEVICE (object); + + g_hash_table_insert (is_output ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)), + g_object_ref (device)); + + } + g_signal_emit (G_OBJECT (control), + signals[is_output ? OUTPUT_ADDED : INPUT_ADDED], + 0, + gvc_mixer_ui_device_get_id (device)); + + return; + } + + /* Go ahead and make sure to match each port against a previously created device */ + for (n = stream_ports; n != NULL; n = n->next) { + + GvcMixerStreamPort *stream_port; + stream_port = n->data; + stream_port_count ++; + + if (match_stream_with_devices (control, stream_port, stream)) + continue; + + g_warning ("Sync_devices: Failed to match stream id: %u, description: '%s', origin: '%s'", + gvc_mixer_stream_get_id (stream), + stream_port->human_port, + gvc_mixer_stream_get_description (stream)); + } +} + +static void +set_icon_name_from_proplist (GvcMixerStream *stream, + pa_proplist *l, + const char *default_icon_name) +{ + const char *t; + + if ((t = pa_proplist_gets (l, PA_PROP_DEVICE_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_WINDOW_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ICON_NAME))) { + goto finish; + } + + if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) { + + if (strcmp (t, "video") == 0 || + strcmp (t, "phone") == 0) { + goto finish; + } + + if (strcmp (t, "music") == 0) { + t = "audio"; + goto finish; + } + + if (strcmp (t, "game") == 0) { + t = "applications-games"; + goto finish; + } + + if (strcmp (t, "event") == 0) { + t = "dialog-information"; + goto finish; + } + } + + t = default_icon_name; + + finish: + gvc_mixer_stream_set_icon_name (stream, t); +} + +static GvcMixerStreamState +translate_pa_state (pa_sink_state_t state) { + switch (state) { + case PA_SINK_RUNNING: + return GVC_STREAM_STATE_RUNNING; + case PA_SINK_IDLE: + return GVC_STREAM_STATE_IDLE; + case PA_SINK_SUSPENDED: + return GVC_STREAM_STATE_SUSPENDED; + case PA_SINK_INIT: + case PA_SINK_INVALID_STATE: + case PA_SINK_UNLINKED: + default: + return GVC_STREAM_STATE_INVALID; + } +} + +/* + * Called when anything changes with a sink. + */ +static void +update_sink (GvcMixerControl *control, + const pa_sink_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + GvcChannelMap *map; + char map_buff[PA_CHANNEL_MAP_SNPRINT_MAX]; + + pa_channel_map_snprint (map_buff, PA_CHANNEL_MAP_SNPRINT_MAX, &info->channel_map); +#if 1 + g_debug ("Updating sink: index=%u name='%s' description='%s' map='%s'", + info->index, + info->name, + info->description, + map_buff); +#endif + + map = NULL; + is_new = FALSE; + stream = g_hash_table_lookup (control->priv->sinks, + GUINT_TO_POINTER (info->index)); + + if (stream == NULL) { + GList *list = NULL; + guint i; + + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_sink_new (control->priv->pa_context, + info->index, + map); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerStreamPort *port; + + port = g_slice_new0 (GvcMixerStreamPort); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + port->available = info->ports[i]->available != PA_PORT_AVAILABLE_NO; + + list = g_list_prepend (list, port); + } + gvc_mixer_stream_set_ports (stream, list); + + g_object_unref (map); + is_new = TRUE; + + } else if (gvc_mixer_stream_is_running (stream)) { + /* Ignore events if volume changes are outstanding */ + g_debug ("Ignoring event, volume changes are outstanding"); + return; + } + + max_volume = pa_cvolume_max (&info->volume); + gvc_mixer_stream_set_name (stream, info->name); + gvc_mixer_stream_set_card_index (stream, info->card); + gvc_mixer_stream_set_description (stream, info->description); + set_icon_name_from_proplist (stream, info->proplist, "audio-card"); + gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR)); + gvc_mixer_stream_set_sysfs_path (stream, pa_proplist_gets (info->proplist, "sysfs.path")); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SINK_DECIBEL_VOLUME)); + gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume); + gvc_mixer_stream_set_state (stream, translate_pa_state (info->state)); + + /* Messy I know but to set the port everytime regardless of whether it has changed will cost us a + * port change notify signal which causes the frontend to resync. + * Only update the UI when something has changed. */ + if (info->active_port != NULL) { + if (is_new) + gvc_mixer_stream_set_port (stream, info->active_port->name); + else { + const GvcMixerStreamPort *active_port; + active_port = gvc_mixer_stream_get_port (stream); + if (active_port == NULL || + g_strcmp0 (active_port->port, info->active_port->name) != 0) { + g_debug ("update sink - apparently a port update"); + gvc_mixer_stream_set_port (stream, info->active_port->name); + } + } + } + + if (is_new) { + g_debug ("update sink - is new"); + + g_hash_table_insert (control->priv->sinks, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + /* Always sink on a new stream to able to assign the right stream id + * to the appropriate outputs (multiple potential outputs per stream). */ + sync_devices (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } + + /* + * When we change profile on a device that is not the server default sink, + * it will jump back to the default sink set by the server to prevent the audio setup from being 'outputless'. + * All well and good but then when we get the new stream created for the new profile how do we know + * that this is the intended default or selected device the user wishes to use. + * This is messy but it's the only reliable way that it can be done without ripping the whole thing apart. + */ + if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) { + GvcMixerUIDevice *dev = NULL; + dev = gvc_mixer_control_lookup_output_id (control, control->priv->profile_swapping_device_id); + if (dev != NULL) { + /* now check to make sure this new stream is the same stream just matched and set on the device object */ + if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) { + g_debug ("Looks like we profile swapped on a non server default sink"); + gvc_mixer_control_set_default_sink (control, stream); + control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID; + } + } + } + + if (control->priv->default_sink_name != NULL + && info->name != NULL + && strcmp (control->priv->default_sink_name, info->name) == 0) { + _set_default_sink (control, stream); + } + + if (map == NULL) + map = (GvcChannelMap *) gvc_mixer_stream_get_channel_map (stream); + + gvc_channel_map_volume_changed (map, &info->volume, FALSE); +} + +static void +update_source (GvcMixerControl *control, + const pa_source_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + +#if 1 + g_debug ("Updating source: index=%u name='%s' description='%s'", + info->index, + info->name, + info->description); +#endif + + /* completely ignore monitors, they're not real sources */ + if (info->monitor_of_sink != PA_INVALID_INDEX) { + return; + } + + is_new = FALSE; + + stream = g_hash_table_lookup (control->priv->sources, + GUINT_TO_POINTER (info->index)); + if (stream == NULL) { + GList *list = NULL; + guint i; + GvcChannelMap *map; + + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_source_new (control->priv->pa_context, + info->index, + map); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerStreamPort *port; + + port = g_slice_new0 (GvcMixerStreamPort); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + list = g_list_prepend (list, port); + } + gvc_mixer_stream_set_ports (stream, list); + + g_object_unref (map); + is_new = TRUE; + } else if (gvc_mixer_stream_is_running (stream)) { + /* Ignore events if volume changes are outstanding */ + g_debug ("Ignoring event, volume changes are outstanding"); + return; + } + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, info->name); + gvc_mixer_stream_set_card_index (stream, info->card); + gvc_mixer_stream_set_description (stream, info->description); + set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone"); + gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR)); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SOURCE_DECIBEL_VOLUME)); + gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume); + g_debug ("update source"); + + if (info->active_port != NULL) { + if (is_new) + gvc_mixer_stream_set_port (stream, info->active_port->name); + else { + const GvcMixerStreamPort *active_port; + active_port = gvc_mixer_stream_get_port (stream); + if (active_port == NULL || + g_strcmp0 (active_port->port, info->active_port->name) != 0) { + g_debug ("update source - apparently a port update"); + gvc_mixer_stream_set_port (stream, info->active_port->name); + } + } + } + + if (is_new) { + g_hash_table_insert (control->priv->sources, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + sync_devices (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } + + if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) { + GvcMixerUIDevice *dev = NULL; + + dev = gvc_mixer_control_lookup_input_id (control, control->priv->profile_swapping_device_id); + + if (dev != NULL) { + /* now check to make sure this new stream is the same stream just matched and set on the device object */ + if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) { + g_debug ("Looks like we profile swapped on a non server default source"); + gvc_mixer_control_set_default_source (control, stream); + control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID; + } + } + } + if (control->priv->default_source_name != NULL + && info->name != NULL + && strcmp (control->priv->default_source_name, info->name) == 0) { + _set_default_source (control, stream); + } +} + +static void +set_is_event_stream_from_proplist (GvcMixerStream *stream, + pa_proplist *l) +{ + const char *t; + gboolean is_event_stream; + + is_event_stream = FALSE; + + if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) { + if (g_str_equal (t, "event")) + is_event_stream = TRUE; + } + + gvc_mixer_stream_set_is_event_stream (stream, is_event_stream); +} + +static void +set_application_id_from_proplist (GvcMixerStream *stream, + pa_proplist *l) +{ + const char *t; + + if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ID))) { + gvc_mixer_stream_set_application_id (stream, t); + } +} + +static void +update_sink_input (GvcMixerControl *control, + const pa_sink_input_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + const char *name; + +#if 0 + g_debug ("Updating sink input: index=%u name='%s' client=%u sink=%u", + info->index, + info->name, + info->client, + info->sink); +#endif + + is_new = FALSE; + + stream = g_hash_table_lookup (control->priv->sink_inputs, + GUINT_TO_POINTER (info->index)); + if (stream == NULL) { + GvcChannelMap *map; + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_sink_input_new (control->priv->pa_context, + info->index, + map); + g_object_unref (map); + is_new = TRUE; + } else if (gvc_mixer_stream_is_running (stream)) { + /* Ignore events if volume changes are outstanding */ + g_debug ("Ignoring event, volume changes are outstanding"); + return; + } + + max_volume = pa_cvolume_max (&info->volume); + + name = (const char *)g_hash_table_lookup (control->priv->clients, + GUINT_TO_POINTER (info->client)); + gvc_mixer_stream_set_name (stream, name); + gvc_mixer_stream_set_description (stream, info->name); + + set_application_id_from_proplist (stream, info->proplist); + set_is_event_stream_from_proplist (stream, info->proplist); + set_icon_name_from_proplist (stream, info->proplist, "applications-multimedia"); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + gvc_mixer_stream_set_is_virtual (stream, info->client == PA_INVALID_INDEX); + + if (is_new) { + g_hash_table_insert (control->priv->sink_inputs, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } +} + +static void +update_source_output (GvcMixerControl *control, + const pa_source_output_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + const char *name; + +#if 1 + g_debug ("Updating source output: index=%u name='%s' client=%u source=%u", + info->index, + info->name, + info->client, + info->source); +#endif + + is_new = FALSE; + stream = g_hash_table_lookup (control->priv->source_outputs, + GUINT_TO_POINTER (info->index)); + if (stream == NULL) { + GvcChannelMap *map; + map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map); + stream = gvc_mixer_source_output_new (control->priv->pa_context, + info->index, + map); + g_object_unref (map); + is_new = TRUE; + } + + name = (const char *)g_hash_table_lookup (control->priv->clients, + GUINT_TO_POINTER (info->client)); + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, name); + gvc_mixer_stream_set_description (stream, info->name); + set_application_id_from_proplist (stream, info->proplist); + set_is_event_stream_from_proplist (stream, info->proplist); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone"); + + if (is_new) { + g_hash_table_insert (control->priv->source_outputs, + GUINT_TO_POINTER (info->index), + g_object_ref (stream)); + add_stream (control, stream); + } else { + g_signal_emit (G_OBJECT (control), + signals[STREAM_CHANGED], + 0, + gvc_mixer_stream_get_id (stream)); + } +} + +static void +update_client (GvcMixerControl *control, + const pa_client_info *info) +{ +#if 1 + g_debug ("Updating client: index=%u name='%s'", + info->index, + info->name); +#endif + g_hash_table_insert (control->priv->clients, + GUINT_TO_POINTER (info->index), + g_strdup (info->name)); +} + +static char * +card_num_streams_to_status (guint sinks, + guint sources) +{ + char *sinks_str; + char *sources_str; + char *ret; + + if (sinks == 0 && sources == 0) { + /* translators: + * The device has been disabled */ + return g_strdup (_("Disabled")); + } + if (sinks == 0) { + sinks_str = NULL; + } else { + /* translators: + * The number of sound outputs on a particular device */ + sinks_str = g_strdup_printf (ngettext ("%u Output", + "%u Outputs", + sinks), + sinks); + } + if (sources == 0) { + sources_str = NULL; + } else { + /* translators: + * The number of sound inputs on a particular device */ + sources_str = g_strdup_printf (ngettext ("%u Input", + "%u Inputs", + sources), + sources); + } + if (sources_str == NULL) + return sinks_str; + if (sinks_str == NULL) + return sources_str; + ret = g_strdup_printf ("%s / %s", sinks_str, sources_str); + g_free (sinks_str); + g_free (sources_str); + return ret; +} + +/* + * A utility method to gather which card profiles are relevant to the port . + */ +static GList * +determine_profiles_for_port (pa_card_port_info *port, + GList* card_profiles) +{ + guint i; + GList *supported_profiles = NULL; + GList *p; + for (i = 0; i < port->n_profiles; i++) { + for (p = card_profiles; p != NULL; p = p->next) { + GvcMixerCardProfile *prof; + prof = p->data; + if (g_strcmp0 (port->profiles[i]->name, prof->profile) == 0) + supported_profiles = g_list_append (supported_profiles, prof); + } + } + g_debug ("%i profiles supported on port %s", + g_list_length (supported_profiles), + port->description); + return g_list_sort (supported_profiles, (GCompareFunc) gvc_mixer_card_profile_compare); +} + +static gboolean +is_card_port_an_output (GvcMixerCardPort* port) +{ + return port->direction == PA_DIRECTION_OUTPUT ? TRUE : FALSE; +} + +/* + * This method will create a ui device for the given port. + */ +static void +create_ui_device_from_port (GvcMixerControl* control, + GvcMixerCardPort* port, + GvcMixerCard* card) +{ + GvcMixerUIDeviceDirection direction; + GObject *object; + GvcMixerUIDevice *uidevice; + gboolean available = port->available != PA_PORT_AVAILABLE_NO; + + direction = (is_card_port_an_output (port) == TRUE) ? UIDeviceOutput : UIDeviceInput; + + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "type", (guint)direction, + "card", card, + "port-name", port->port, + "description", port->human_port, + "origin", gvc_mixer_card_get_name (card), + "port-available", available, + "icon-name", port->icon_name, + NULL); + + uidevice = GVC_MIXER_UI_DEVICE (object); + gvc_mixer_ui_device_set_profiles (uidevice, port->profiles); + + g_hash_table_insert (is_card_port_an_output (port) ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (uidevice)), + uidevice); + + + if (available) { + g_signal_emit (G_OBJECT (control), + signals[is_card_port_an_output (port) ? OUTPUT_ADDED : INPUT_ADDED], + 0, + gvc_mixer_ui_device_get_id (uidevice)); + } + + g_debug ("create_ui_device_from_port, direction %u, description '%s', origin '%s', port available %i", + direction, + port->human_port, + gvc_mixer_card_get_name (card), + available); +} + +/* + * This method will match up GvcMixerCardPorts with existing devices. + * A match is achieved if the device's card-id and the port's card-id are the same + * && the device's port-name and the card-port's port member are the same. + * A signal is then sent adding or removing that device from the UI depending on the availability of the port. + */ +static void +match_card_port_with_existing_device (GvcMixerControl *control, + GvcMixerCardPort *card_port, + GvcMixerCard *card, + gboolean available) +{ + GList *d; + GList *devices; + GvcMixerUIDevice *device; + gboolean is_output = is_card_port_an_output (card_port); + + devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *device_card; + gchar *device_port_name; + + device = d->data; + g_object_get (G_OBJECT (device), + "card", &device_card, + "port-name", &device_port_name, + NULL); + + if (g_strcmp0 (card_port->port, device_port_name) == 0 && + device_card == card) { + g_debug ("Found the relevant device %s, update its port availability flag to %i, is_output %i", + device_port_name, + available, + is_output); + g_object_set (G_OBJECT (device), + "port-available", available, NULL); + g_signal_emit (G_OBJECT (control), + is_output ? signals[available ? OUTPUT_ADDED : OUTPUT_REMOVED] : signals[available ? INPUT_ADDED : INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } + g_free (device_port_name); + } + + g_list_free (devices); +} + +static void +create_ui_device_from_card (GvcMixerControl *control, + GvcMixerCard *card) +{ + GObject *object; + GvcMixerUIDevice *in; + GvcMixerUIDevice *out; + const GList *profiles; + + /* For now just create two devices and presume this device is multi directional + * Ensure to remove both on card removal (available to false by default) */ + profiles = gvc_mixer_card_get_profiles (card); + + g_debug ("Portless card just registered - %i", gvc_mixer_card_get_index (card)); + + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "type", UIDeviceInput, + "description", gvc_mixer_card_get_name (card), + "origin", "", /* Leave it empty for these special cases */ + "port-name", NULL, + "port-available", FALSE, + "card", card, + NULL); + in = GVC_MIXER_UI_DEVICE (object); + gvc_mixer_ui_device_set_profiles (in, profiles); + + g_hash_table_insert (control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (in)), + g_object_ref (in)); + object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE, + "type", UIDeviceOutput, + "description", gvc_mixer_card_get_name (card), + "origin", "", /* Leave it empty for these special cases */ + "port-name", NULL, + "port-available", FALSE, + "card", card, + NULL); + out = GVC_MIXER_UI_DEVICE (object); + gvc_mixer_ui_device_set_profiles (out, profiles); + + g_hash_table_insert (control->priv->ui_outputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (out)), + g_object_ref (out)); +} + +#ifdef HAVE_ALSA +typedef struct { + char *port_name_to_set; + guint32 headset_card; +} PortStatusData; + +static void +port_status_data_free (PortStatusData *data) +{ + if (data == NULL) + return; + g_free (data->port_name_to_set); + g_free (data); +} + +/* + We need to re-enumerate sources and sinks every time the user makes a choice, + because they can change due to use interaction in other software (or policy + changes inside PulseAudio). Enumeration means PulseAudio will do a series of + callbacks, one for every source/sink. + Set the port when we find the correct source/sink. + */ + +static void +sink_info_cb (pa_context *c, + const pa_sink_info *i, + int eol, + void *userdata) +{ + PortStatusData *data = userdata; + pa_operation *o; + guint j; + const char *s; + + if (eol != 0) { + port_status_data_free (data); + return; + } + + if (i->card != data->headset_card) + return; + + s = data->port_name_to_set; + + if (i->active_port && + strcmp (i->active_port->name, s) == 0) + return; + + for (j = 0; j < i->n_ports; j++) + if (strcmp (i->ports[j]->name, s) == 0) + break; + + if (j >= i->n_ports) + return; + + o = pa_context_set_sink_port_by_index (c, i->index, s, NULL, NULL); + g_clear_pointer (&o, pa_operation_unref); +} + +static void +source_info_cb (pa_context *c, + const pa_source_info *i, + int eol, + void *userdata) +{ + PortStatusData *data = userdata; + pa_operation *o; + guint j; + const char *s; + + if (eol != 0) { + port_status_data_free (data); + return; + } + + if (i->card != data->headset_card) + return; + + s = data->port_name_to_set; + + for (j = 0; j < i->n_ports; j++) { + if (g_str_equal (i->ports[j]->name, s)) { + o = pa_context_set_default_source (c, + i->name, + NULL, + NULL); + if (o == NULL) { + g_warning ("pa_context_set_default_source() failed"); + return; + } + } + } + + if (i->active_port && strcmp (i->active_port->name, s) == 0) + return; + + for (j = 0; j < i->n_ports; j++) + if (strcmp (i->ports[j]->name, s) == 0) + break; + + if (j >= i->n_ports) + return; + + o = pa_context_set_source_port_by_index(c, i->index, s, NULL, NULL); + g_clear_pointer (&o, pa_operation_unref); +} + +static void +gvc_mixer_control_set_port_status_for_headset (GvcMixerControl *control, + guint id, + const char *port_name, + gboolean is_output) +{ + pa_operation *o; + PortStatusData *data; + + if (port_name == NULL) + return; + + data = g_new0 (PortStatusData, 1); + data->port_name_to_set = g_strdup (port_name); + data->headset_card = id; + + if (is_output) + o = pa_context_get_sink_info_list (control->priv->pa_context, sink_info_cb, data); + else + o = pa_context_get_source_info_list (control->priv->pa_context, source_info_cb, data); + + g_clear_pointer (&o, pa_operation_unref); +} +#endif /* HAVE_ALSA */ + +static void +free_priv_port_names (GvcMixerControl *control) +{ +#ifdef HAVE_ALSA + g_clear_pointer (&control->priv->headphones_name, g_free); + g_clear_pointer (&control->priv->headsetmic_name, g_free); + g_clear_pointer (&control->priv->headphonemic_name, g_free); + g_clear_pointer (&control->priv->internalspk_name, g_free); + g_clear_pointer (&control->priv->internalmic_name, g_free); +#endif +} + +void +gvc_mixer_control_set_headset_port (GvcMixerControl *control, + guint id, + GvcHeadsetPortChoice choice) +{ + g_return_if_fail (GVC_IS_MIXER_CONTROL (control)); + +#ifdef HAVE_ALSA + switch (choice) { + case GVC_HEADSET_PORT_CHOICE_HEADPHONES: + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE); + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalmic_name, FALSE); + break; + case GVC_HEADSET_PORT_CHOICE_HEADSET: + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE); + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headsetmic_name, FALSE); + break; + case GVC_HEADSET_PORT_CHOICE_MIC: + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalspk_name, TRUE); + gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphonemic_name, FALSE); + break; + case GVC_HEADSET_PORT_CHOICE_NONE: + default: + g_assert_not_reached (); + } +#else + g_warning ("BUG: libgnome-volume-control compiled without ALSA support"); +#endif /* HAVE_ALSA */ +} + +#ifdef HAVE_ALSA +typedef struct { + const pa_card_port_info *headphones; + const pa_card_port_info *headsetmic; + const pa_card_port_info *headphonemic; + const pa_card_port_info *internalmic; + const pa_card_port_info *internalspk; +} headset_ports; + +/* + In PulseAudio without ucm, ports will show up with the following names: + Headphones - analog-output-headphones + Headset mic - analog-input-headset-mic (was: analog-input-microphone-headset) + Jack in mic-in mode - analog-input-headphone-mic (was: analog-input-microphone) + + However, since regular mics also show up as analog-input-microphone, + we need to check for certain controls on alsa mixer level too, to know + if we deal with a separate mic jack, or a multi-function jack with a + mic-in mode (also called "headphone mic"). + We check for the following names: + + Headphone Mic Jack - indicates headphone and mic-in mode share the same jack, + i e, not two separate jacks. Hardware cannot distinguish between a + headphone and a mic. + Headset Mic Phantom Jack - indicates headset jack where hardware can not + distinguish between headphones and headsets + Headset Mic Jack - indicates headset jack where hardware can distinguish + between headphones and headsets. There is no use popping up a dialog in + this case, unless we already need to do this for the mic-in mode. + + From the PA_PROCOTOL_VERSION=34, The device_port structure adds 2 members + availability_group and type, with the help of these 2 members, we could + consolidate the port checking and port setting for non-ucm and with-ucm + cases. +*/ + +#define HEADSET_PORT_SET(dst, src) \ + do { \ + if (!(dst) || (dst)->priority < (src)->priority) \ + dst = src; \ + } while (0) + +#define GET_PORT_NAME(x) (x ? g_strdup (x->name) : NULL) + +static headset_ports * +get_headset_ports (GvcMixerControl *control, + const pa_card_info *c) +{ + headset_ports *h; + guint i; + + h = g_new0 (headset_ports, 1); + + for (i = 0; i < c->n_ports; i++) { + pa_card_port_info *p = c->ports[i]; + if (control->priv->server_protocol_version < 34) { + if (g_str_equal (p->name, "analog-output-headphones")) + h->headphones = p; + else if (g_str_equal (p->name, "analog-input-headset-mic")) + h->headsetmic = p; + else if (g_str_equal (p->name, "analog-input-headphone-mic")) + h->headphonemic = p; + else if (g_str_equal (p->name, "analog-input-internal-mic")) + h->internalmic = p; + else if (g_str_equal (p->name, "analog-output-speaker")) + h->internalspk = p; + } else { +#if (PA_PROTOCOL_VERSION >= 34) + /* in the first loop, set only headphones */ + /* the microphone ports are assigned in the second loop */ + if (p->type == PA_DEVICE_PORT_TYPE_HEADPHONES) { + if (p->availability_group) + HEADSET_PORT_SET (h->headphones, p); + } else if (p->type == PA_DEVICE_PORT_TYPE_SPEAKER) { + HEADSET_PORT_SET (h->internalspk, p); + } else if (p->type == PA_DEVICE_PORT_TYPE_MIC) { + if (!p->availability_group) + HEADSET_PORT_SET (h->internalmic, p); + } +#else + g_warning_once ("libgnome-volume-control running against PulseAudio %u, " + "but compiled against older %d, report a bug to your distribution", + control->priv->server_protocol_version, + PA_PROTOCOL_VERSION); +#endif + } + } + +#if (PA_PROTOCOL_VERSION >= 34) + if (h->headphones && (control->priv->server_protocol_version >= 34)) { + for (i = 0; i < c->n_ports; i++) { + pa_card_port_info *p = c->ports[i]; + if (g_strcmp0(h->headphones->availability_group, p->availability_group)) + continue; + if (p->direction != PA_DIRECTION_INPUT) + continue; + if (p->type == PA_DEVICE_PORT_TYPE_HEADSET) + HEADSET_PORT_SET (h->headsetmic, p); + else if (p->type == PA_DEVICE_PORT_TYPE_MIC) + HEADSET_PORT_SET (h->headphonemic, p); + } + } +#endif + + return h; +} + +static gboolean +verify_alsa_card (int cardindex, + gboolean *headsetmic, + gboolean *headphonemic) +{ + char *ctlstr; + snd_hctl_t *hctl; + snd_ctl_elem_id_t *id; + int err; + + *headsetmic = FALSE; + *headphonemic = FALSE; + + ctlstr = g_strdup_printf ("hw:%i", cardindex); + if ((err = snd_hctl_open (&hctl, ctlstr, 0)) < 0) { + g_warning ("snd_hctl_open failed: %s", snd_strerror(err)); + g_free (ctlstr); + return FALSE; + } + g_free (ctlstr); + + if ((err = snd_hctl_load (hctl)) < 0) { + g_warning ("snd_hctl_load failed: %s", snd_strerror(err)); + snd_hctl_close (hctl); + return FALSE; + } + + snd_ctl_elem_id_alloca (&id); + + snd_ctl_elem_id_clear (id); + snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name (id, "Headphone Mic Jack"); + if (snd_hctl_find_elem (hctl, id)) + *headphonemic = TRUE; + + snd_ctl_elem_id_clear (id); + snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name (id, "Headset Mic Phantom Jack"); + if (snd_hctl_find_elem (hctl, id)) + *headsetmic = TRUE; + + if (*headphonemic) { + snd_ctl_elem_id_clear (id); + snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD); + snd_ctl_elem_id_set_name (id, "Headset Mic Jack"); + if (snd_hctl_find_elem (hctl, id)) + *headsetmic = TRUE; + } + + snd_hctl_close (hctl); + return *headsetmic || *headphonemic; +} + +static void +check_audio_device_selection_needed (GvcMixerControl *control, + const pa_card_info *info) +{ + headset_ports *h; + gboolean start_dialog, stop_dialog; + + start_dialog = FALSE; + stop_dialog = FALSE; + h = get_headset_ports (control, info); + + if (!h->headphones || + (!h->headsetmic && !h->headphonemic)) { + /* Not a headset jack */ + goto out; + } + + if (control->priv->headset_card != (int) info->index) { + int cardindex; + gboolean hsmic = TRUE; + gboolean hpmic = TRUE; + const char *s; + + s = pa_proplist_gets (info->proplist, "alsa.card"); + if (!s) + goto out; + + cardindex = strtol (s, NULL, 10); + if (cardindex == 0 && strcmp(s, "0") != 0) + goto out; + + if (control->priv->server_protocol_version < 34) { + if (!verify_alsa_card(cardindex, &hsmic, &hpmic)) + goto out; + } + + control->priv->headset_card = info->index; + control->priv->has_headsetmic = hsmic && h->headsetmic; + control->priv->has_headphonemic = hpmic && h->headphonemic; + } else { + start_dialog = (h->headphones->available != PA_PORT_AVAILABLE_NO) && !control->priv->headset_plugged_in; + stop_dialog = (h->headphones->available == PA_PORT_AVAILABLE_NO) && control->priv->headset_plugged_in; + } + + control->priv->headset_plugged_in = h->headphones->available != PA_PORT_AVAILABLE_NO; + free_priv_port_names (control); + control->priv->headphones_name = GET_PORT_NAME(h->headphones); + control->priv->headsetmic_name = GET_PORT_NAME(h->headsetmic); + control->priv->headphonemic_name = GET_PORT_NAME(h->headphonemic); + control->priv->internalspk_name = GET_PORT_NAME(h->internalspk); + control->priv->internalmic_name = GET_PORT_NAME(h->internalmic); + + if (!start_dialog && + !stop_dialog) + goto out; + + if (stop_dialog) { + g_signal_emit (G_OBJECT (control), + signals[AUDIO_DEVICE_SELECTION_NEEDED], + 0, + info->index, + FALSE, + GVC_HEADSET_PORT_CHOICE_NONE); + } else { + GvcHeadsetPortChoice choices; + + choices = GVC_HEADSET_PORT_CHOICE_HEADPHONES; + if (control->priv->has_headsetmic) + choices |= GVC_HEADSET_PORT_CHOICE_HEADSET; + if (control->priv->has_headphonemic) + choices |= GVC_HEADSET_PORT_CHOICE_MIC; + + g_signal_emit (G_OBJECT (control), + signals[AUDIO_DEVICE_SELECTION_NEEDED], + 0, + info->index, + TRUE, + choices); + } + +out: + g_free (h); +} +#endif /* HAVE_ALSA */ + +/* + * At this point we can determine all devices available to us (besides network 'ports') + * This is done by the following: + * + * - gvc_mixer_card and gvc_mixer_card_ports are created and relevant setters are called. + * - First it checks to see if it's a portless card. Bluetooth devices are portless AFAIHS. + * If so it creates two devices, an input and an output. + * - If it's a 'normal' card with ports it will create a new ui-device or + * synchronise port availability with the existing device cached for that port on this card. */ + +static void +update_card (GvcMixerControl *control, + const pa_card_info *info) +{ + const GList *card_ports = NULL; + const GList *m = NULL; + GvcMixerCard *card; + gboolean is_new = FALSE; +#if 1 + guint i; + const char *key; + void *state; + + g_debug ("Updating card %s (index: %u driver: %s):", + info->name, info->index, info->driver); + + for (i = 0; i < info->n_profiles; i++) { + struct pa_card_profile_info pi = info->profiles[i]; + gboolean is_default; + + is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0); + g_debug ("\tProfile '%s': %d sources %d sinks%s", + pi.name, pi.n_sources, pi.n_sinks, + is_default ? " (Current)" : ""); + } + state = NULL; + key = pa_proplist_iterate (info->proplist, &state); + while (key != NULL) { + g_debug ("\tProperty: '%s' = '%s'", + key, pa_proplist_gets (info->proplist, key)); + key = pa_proplist_iterate (info->proplist, &state); + } +#endif + card = g_hash_table_lookup (control->priv->cards, + GUINT_TO_POINTER (info->index)); + if (card == NULL) { + GList *profile_list = NULL; + GList *port_list = NULL; + + for (i = 0; i < info->n_profiles; i++) { + GvcMixerCardProfile *profile; + struct pa_card_profile_info pi = info->profiles[i]; + + profile = g_new0 (GvcMixerCardProfile, 1); + profile->profile = g_strdup (pi.name); + profile->human_profile = g_strdup (pi.description); + profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources); + profile->n_sinks = pi.n_sinks; + profile->n_sources = pi.n_sources; + profile->priority = pi.priority; + profile_list = g_list_prepend (profile_list, profile); + } + card = gvc_mixer_card_new (control->priv->pa_context, + info->index); + gvc_mixer_card_set_profiles (card, profile_list); + + for (i = 0; i < info->n_ports; i++) { + GvcMixerCardPort *port; + port = g_new0 (GvcMixerCardPort, 1); + port->port = g_strdup (info->ports[i]->name); + port->human_port = g_strdup (info->ports[i]->description); + port->priority = info->ports[i]->priority; + port->available = info->ports[i]->available; + port->direction = info->ports[i]->direction; + port->icon_name = g_strdup (pa_proplist_gets (info->ports[i]->proplist, "device.icon_name")); + port->profiles = determine_profiles_for_port (info->ports[i], profile_list); + port_list = g_list_prepend (port_list, port); + } + gvc_mixer_card_set_ports (card, port_list); + is_new = TRUE; + } + + gvc_mixer_card_set_name (card, pa_proplist_gets (info->proplist, "device.description")); + gvc_mixer_card_set_icon_name (card, pa_proplist_gets (info->proplist, "device.icon_name")); + gvc_mixer_card_set_profile (card, info->active_profile->name); + + if (is_new) { + g_hash_table_insert (control->priv->cards, + GUINT_TO_POINTER (info->index), + card); + } + + card_ports = gvc_mixer_card_get_ports (card); + + if (card_ports == NULL && is_new) { + g_debug ("Portless card just registered - %s", gvc_mixer_card_get_name (card)); + create_ui_device_from_card (control, card); + } + + for (m = card_ports; m != NULL; m = m->next) { + GvcMixerCardPort *card_port; + card_port = m->data; + if (is_new) + create_ui_device_from_port (control, card_port, card); + else { + for (i = 0; i < info->n_ports; i++) { + if (g_strcmp0 (card_port->port, info->ports[i]->name) == 0) { + if ((card_port->available == PA_PORT_AVAILABLE_NO) != (info->ports[i]->available == PA_PORT_AVAILABLE_NO)) { + card_port->available = info->ports[i]->available; + g_debug ("sync port availability on card %i, card port name '%s', new available value %i", + gvc_mixer_card_get_index (card), + card_port->port, + card_port->available); + match_card_port_with_existing_device (control, + card_port, + card, + card_port->available != PA_PORT_AVAILABLE_NO); + } + } + } + } + } + +#ifdef HAVE_ALSA + check_audio_device_selection_needed (control, info); +#endif /* HAVE_ALSA */ + + g_signal_emit (G_OBJECT (control), + signals[CARD_ADDED], + 0, + info->index); +} + +static void +_pa_context_get_sink_info_cb (pa_context *context, + const pa_sink_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Sink callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_sink (control, i); +} + +static void +_pa_context_get_source_info_cb (pa_context *context, + const pa_source_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Source callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_source (control, i); +} + +static void +_pa_context_get_sink_input_info_cb (pa_context *context, + const pa_sink_input_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Sink input callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_sink_input (control, i); +} + +static void +_pa_context_get_source_output_info_cb (pa_context *context, + const pa_source_output_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Source output callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_source_output (control, i); +} + +static void +_pa_context_get_client_info_cb (pa_context *context, + const pa_client_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) { + return; + } + + g_warning ("Client callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_client (control, i); +} + +static void +_pa_context_get_card_info_by_index_cb (pa_context *context, + const pa_card_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + if (pa_context_errno (context) == PA_ERR_NOENTITY) + return; + + g_warning ("Card callback failure"); + return; + } + + if (eol > 0) { + dec_outstanding (control); + return; + } + + update_card (control, i); +} + +static void +_pa_context_get_server_info_cb (pa_context *context, + const pa_server_info *i, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (i == NULL) { + g_warning ("Server info callback failure"); + return; + } + g_debug ("get server info"); + update_server (control, i); + dec_outstanding (control); +} + +static void +remove_event_role_stream (GvcMixerControl *control) +{ + g_debug ("Removing event role"); +} + +static void +update_event_role_stream (GvcMixerControl *control, + const pa_ext_stream_restore_info *info) +{ + GvcMixerStream *stream; + gboolean is_new; + pa_volume_t max_volume; + + if (strcmp (info->name, "sink-input-by-media-role:event") != 0) { + return; + } + +#if 0 + g_debug ("Updating event role: name='%s' device='%s'", + info->name, + info->device); +#endif + + is_new = FALSE; + + if (!control->priv->event_sink_input_is_set) { + pa_channel_map pa_map; + GvcChannelMap *map; + + pa_map.channels = 1; + pa_map.map[0] = PA_CHANNEL_POSITION_MONO; + map = gvc_channel_map_new_from_pa_channel_map (&pa_map); + + stream = gvc_mixer_event_role_new (control->priv->pa_context, + info->device, + map); + control->priv->event_sink_input_id = gvc_mixer_stream_get_id (stream); + control->priv->event_sink_input_is_set = TRUE; + + is_new = TRUE; + } else { + stream = g_hash_table_lookup (control->priv->all_streams, + GUINT_TO_POINTER (control->priv->event_sink_input_id)); + } + + max_volume = pa_cvolume_max (&info->volume); + + gvc_mixer_stream_set_name (stream, _("System Sounds")); + gvc_mixer_stream_set_icon_name (stream, "emblem-system-symbolic"); + gvc_mixer_stream_set_volume (stream, (guint)max_volume); + gvc_mixer_stream_set_is_muted (stream, info->mute); + + if (is_new) { + add_stream (control, stream); + } +} + +static void +_pa_ext_stream_restore_read_cb (pa_context *context, + const pa_ext_stream_restore_info *i, + int eol, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + if (eol < 0) { + g_debug ("Failed to initialized stream_restore extension: %s", + pa_strerror (pa_context_errno (context))); + remove_event_role_stream (control); + return; + } + + if (eol > 0) { + dec_outstanding (control); + /* If we don't have an event stream to restore, then + * set one up with a default 100% volume */ + if (!control->priv->event_sink_input_is_set) { + pa_ext_stream_restore_info info; + + memset (&info, 0, sizeof(info)); + info.name = "sink-input-by-media-role:event"; + info.volume.channels = 1; + info.volume.values[0] = PA_VOLUME_NORM; + update_event_role_stream (control, &info); + } + return; + } + + update_event_role_stream (control, i); +} + +static void +_pa_ext_stream_restore_subscribe_cb (pa_context *context, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + pa_operation *o; + + o = pa_ext_stream_restore_read (context, + _pa_ext_stream_restore_read_cb, + control); + if (o == NULL) { + g_warning ("pa_ext_stream_restore_read() failed"); + return; + } + + pa_operation_unref (o); +} + +static void +req_update_server_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + o = pa_context_get_server_info (control->priv->pa_context, + _pa_context_get_server_info_cb, + control); + if (o == NULL) { + g_warning ("pa_context_get_server_info() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_client_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_client_info_list (control->priv->pa_context, + _pa_context_get_client_info_cb, + control); + } else { + o = pa_context_get_client_info (control->priv->pa_context, + index, + _pa_context_get_client_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_client_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_card (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_card_info_list (control->priv->pa_context, + _pa_context_get_card_info_by_index_cb, + control); + } else { + o = pa_context_get_card_info_by_index (control->priv->pa_context, + index, + _pa_context_get_card_info_by_index_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_card_info_by_index() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_sink_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_sink_info_list (control->priv->pa_context, + _pa_context_get_sink_info_cb, + control); + } else { + o = pa_context_get_sink_info_by_index (control->priv->pa_context, + index, + _pa_context_get_sink_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_sink_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_source_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_source_info_list (control->priv->pa_context, + _pa_context_get_source_info_cb, + control); + } else { + o = pa_context_get_source_info_by_index(control->priv->pa_context, + index, + _pa_context_get_source_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_source_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_sink_input_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_sink_input_info_list (control->priv->pa_context, + _pa_context_get_sink_input_info_cb, + control); + } else { + o = pa_context_get_sink_input_info (control->priv->pa_context, + index, + _pa_context_get_sink_input_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_sink_input_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +req_update_source_output_info (GvcMixerControl *control, + int index) +{ + pa_operation *o; + + if (index < 0) { + o = pa_context_get_source_output_info_list (control->priv->pa_context, + _pa_context_get_source_output_info_cb, + control); + } else { + o = pa_context_get_source_output_info (control->priv->pa_context, + index, + _pa_context_get_source_output_info_cb, + control); + } + + if (o == NULL) { + g_warning ("pa_context_get_source_output_info_list() failed"); + return; + } + pa_operation_unref (o); +} + +static void +remove_client (GvcMixerControl *control, + guint index) +{ + g_hash_table_remove (control->priv->clients, + GUINT_TO_POINTER (index)); +} + +static void +remove_card (GvcMixerControl *control, + guint index) +{ + + GList *devices, *d; + + devices = g_list_concat (g_hash_table_get_values (control->priv->ui_inputs), + g_hash_table_get_values (control->priv->ui_outputs)); + + for (d = devices; d != NULL; d = d->next) { + GvcMixerCard *card; + GvcMixerUIDevice *device = d->data; + + g_object_get (G_OBJECT (device), "card", &card, NULL); + + if (gvc_mixer_card_get_index (card) == index) { + g_signal_emit (G_OBJECT (control), + signals[gvc_mixer_ui_device_is_output (device) ? OUTPUT_REMOVED : INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + g_debug ("Card removal remove device %s", + gvc_mixer_ui_device_get_description (device)); + g_hash_table_remove (gvc_mixer_ui_device_is_output (device) ? control->priv->ui_outputs : control->priv->ui_inputs, + GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device))); + } + } + + g_list_free (devices); + + g_hash_table_remove (control->priv->cards, + GUINT_TO_POINTER (index)); + + g_signal_emit (G_OBJECT (control), + signals[CARD_REMOVED], + 0, + index); +} + +static void +remove_sink (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + GvcMixerUIDevice *device; + + g_debug ("Removing sink: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sinks, + GUINT_TO_POINTER (index)); + if (stream == NULL) + return; + + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + + if (device != NULL) { + gvc_mixer_ui_device_invalidate_stream (device); + if (!gvc_mixer_ui_device_has_ports (device)) { + g_signal_emit (G_OBJECT (control), + signals[OUTPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + GList *devices, *d; + + devices = g_hash_table_get_values (control->priv->ui_outputs); + + for (d = devices; d != NULL; d = d->next) { + guint stream_id = GVC_MIXER_UI_DEVICE_INVALID; + device = d->data; + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + if (stream_id == gvc_mixer_stream_get_id (stream)) + gvc_mixer_ui_device_invalidate_stream (device); + } + + g_list_free (devices); + } + } + + g_hash_table_remove (control->priv->sinks, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +remove_source (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + GvcMixerUIDevice *device; + + g_debug ("Removing source: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sources, + GUINT_TO_POINTER (index)); + if (stream == NULL) + return; + + device = gvc_mixer_control_lookup_device_from_stream (control, stream); + + if (device != NULL) { + gvc_mixer_ui_device_invalidate_stream (device); + if (!gvc_mixer_ui_device_has_ports (device)) { + g_signal_emit (G_OBJECT (control), + signals[INPUT_REMOVED], + 0, + gvc_mixer_ui_device_get_id (device)); + } else { + GList *devices, *d; + + devices = g_hash_table_get_values (control->priv->ui_inputs); + + for (d = devices; d != NULL; d = d->next) { + guint stream_id = GVC_MIXER_UI_DEVICE_INVALID; + device = d->data; + g_object_get (G_OBJECT (device), + "stream-id", &stream_id, + NULL); + if (stream_id == gvc_mixer_stream_get_id (stream)) + gvc_mixer_ui_device_invalidate_stream (device); + } + + g_list_free (devices); + } + } + + g_hash_table_remove (control->priv->sources, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +remove_sink_input (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + + g_debug ("Removing sink input: index=%u", index); + + stream = g_hash_table_lookup (control->priv->sink_inputs, + GUINT_TO_POINTER (index)); + if (stream == NULL) { + return; + } + g_hash_table_remove (control->priv->sink_inputs, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +remove_source_output (GvcMixerControl *control, + guint index) +{ + GvcMixerStream *stream; + + g_debug ("Removing source output: index=%u", index); + + stream = g_hash_table_lookup (control->priv->source_outputs, + GUINT_TO_POINTER (index)); + if (stream == NULL) { + return; + } + g_hash_table_remove (control->priv->source_outputs, + GUINT_TO_POINTER (index)); + + remove_stream (control, stream); +} + +static void +_pa_context_subscribe_cb (pa_context *context, + pa_subscription_event_type_t t, + uint32_t index, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) { + case PA_SUBSCRIPTION_EVENT_SINK: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_sink (control, index); + } else { + req_update_sink_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_source (control, index); + } else { + req_update_source_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SINK_INPUT: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_sink_input (control, index); + } else { + req_update_sink_input_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_source_output (control, index); + } else { + req_update_source_output_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_CLIENT: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_client (control, index); + } else { + req_update_client_info (control, index); + } + break; + + case PA_SUBSCRIPTION_EVENT_SERVER: + req_update_server_info (control, index); + break; + + case PA_SUBSCRIPTION_EVENT_CARD: + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) { + remove_card (control, index); + } else { + req_update_card (control, index); + } + break; + default: + break; + } +} + +static void +gvc_mixer_control_ready (GvcMixerControl *control) +{ + pa_operation *o; + + pa_context_set_subscribe_callback (control->priv->pa_context, + _pa_context_subscribe_cb, + control); + o = pa_context_subscribe (control->priv->pa_context, + (pa_subscription_mask_t) + (PA_SUBSCRIPTION_MASK_SINK| + PA_SUBSCRIPTION_MASK_SOURCE| + PA_SUBSCRIPTION_MASK_SINK_INPUT| + PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT| + PA_SUBSCRIPTION_MASK_CLIENT| + PA_SUBSCRIPTION_MASK_SERVER| + PA_SUBSCRIPTION_MASK_CARD), + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_subscribe() failed"); + return; + } + pa_operation_unref (o); + + req_update_server_info (control, -1); + req_update_card (control, -1); + req_update_client_info (control, -1); + req_update_sink_info (control, -1); + req_update_source_info (control, -1); + req_update_sink_input_info (control, -1); + req_update_source_output_info (control, -1); + + control->priv->server_protocol_version = pa_context_get_server_protocol_version (control->priv->pa_context); + + control->priv->n_outstanding = 6; + + /* This call is not always supported */ + o = pa_ext_stream_restore_read (control->priv->pa_context, + _pa_ext_stream_restore_read_cb, + control); + if (o != NULL) { + pa_operation_unref (o); + control->priv->n_outstanding++; + + pa_ext_stream_restore_set_subscribe_cb (control->priv->pa_context, + _pa_ext_stream_restore_subscribe_cb, + control); + + o = pa_ext_stream_restore_subscribe (control->priv->pa_context, + 1, + NULL, + NULL); + if (o != NULL) { + pa_operation_unref (o); + } + + } else { + g_debug ("Failed to initialized stream_restore extension: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + } +} + +static void +gvc_mixer_new_pa_context (GvcMixerControl *self) +{ + pa_proplist *proplist; + + g_return_if_fail (self); + g_return_if_fail (!self->priv->pa_context); + + proplist = pa_proplist_new (); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_NAME, + self->priv->name); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_ID, + "org.gnome.VolumeControl"); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_ICON_NAME, + "multimedia-volume-control"); + pa_proplist_sets (proplist, + PA_PROP_APPLICATION_VERSION, + PACKAGE_VERSION); + + self->priv->pa_context = pa_context_new_with_proplist (self->priv->pa_api, NULL, proplist); + + pa_proplist_free (proplist); + g_assert (self->priv->pa_context); +} + +static void +remove_all_streams (GvcMixerControl *control, GHashTable *hash_table) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, hash_table); + while (g_hash_table_iter_next (&iter, &key, &value)) { + remove_stream (control, value); + g_hash_table_iter_remove (&iter); + } +} + +static gboolean +idle_reconnect (gpointer data) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (data); + GHashTableIter iter; + gpointer key, value; + + g_return_val_if_fail (control, FALSE); + + if (control->priv->pa_context) { + pa_context_unref (control->priv->pa_context); + control->priv->pa_context = NULL; + control->priv->server_protocol_version = 0; + gvc_mixer_new_pa_context (control); + } + + remove_all_streams (control, control->priv->sinks); + remove_all_streams (control, control->priv->sources); + remove_all_streams (control, control->priv->sink_inputs); + remove_all_streams (control, control->priv->source_outputs); + + g_hash_table_iter_init (&iter, control->priv->clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_iter_remove (&iter); + + gvc_mixer_control_open (control); /* cannot fail */ + + control->priv->reconnect_id = 0; + return FALSE; +} + +static void +_pa_context_state_cb (pa_context *context, + void *userdata) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (userdata); + + switch (pa_context_get_state (context)) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: + gvc_mixer_control_ready (control); + break; + + case PA_CONTEXT_FAILED: + control->priv->state = GVC_STATE_FAILED; + g_signal_emit (control, signals[STATE_CHANGED], 0, GVC_STATE_FAILED); + if (control->priv->reconnect_id == 0) + control->priv->reconnect_id = g_timeout_add_seconds (RECONNECT_DELAY, idle_reconnect, control); + break; + + case PA_CONTEXT_TERMINATED: + default: + /* FIXME: */ + break; + } +} + +gboolean +gvc_mixer_control_open (GvcMixerControl *control) +{ + int res; + + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (control->priv->pa_context != NULL, FALSE); + g_return_val_if_fail (pa_context_get_state (control->priv->pa_context) == PA_CONTEXT_UNCONNECTED, FALSE); + + pa_context_set_state_callback (control->priv->pa_context, + _pa_context_state_cb, + control); + + control->priv->state = GVC_STATE_CONNECTING; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CONNECTING); + res = pa_context_connect (control->priv->pa_context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFAIL, NULL); + if (res < 0) { + g_warning ("Failed to connect context: %s", + pa_strerror (pa_context_errno (control->priv->pa_context))); + } + + return res; +} + +gboolean +gvc_mixer_control_close (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE); + g_return_val_if_fail (control->priv->pa_context != NULL, FALSE); + + pa_context_disconnect (control->priv->pa_context); + + control->priv->state = GVC_STATE_CLOSED; + g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CLOSED); + return TRUE; +} + +static void +gvc_mixer_control_dispose (GObject *object) +{ + GvcMixerControl *control = GVC_MIXER_CONTROL (object); + + if (control->priv->reconnect_id != 0) { + g_source_remove (control->priv->reconnect_id); + control->priv->reconnect_id = 0; + } + + if (control->priv->pa_context != NULL) { + pa_context_unref (control->priv->pa_context); + control->priv->pa_context = NULL; + } + + if (control->priv->default_source_name != NULL) { + g_free (control->priv->default_source_name); + control->priv->default_source_name = NULL; + } + if (control->priv->default_sink_name != NULL) { + g_free (control->priv->default_sink_name); + control->priv->default_sink_name = NULL; + } + + if (control->priv->pa_mainloop != NULL) { + pa_glib_mainloop_free (control->priv->pa_mainloop); + control->priv->pa_mainloop = NULL; + } + + if (control->priv->all_streams != NULL) { + g_hash_table_destroy (control->priv->all_streams); + control->priv->all_streams = NULL; + } + + if (control->priv->sinks != NULL) { + g_hash_table_destroy (control->priv->sinks); + control->priv->sinks = NULL; + } + if (control->priv->sources != NULL) { + g_hash_table_destroy (control->priv->sources); + control->priv->sources = NULL; + } + if (control->priv->sink_inputs != NULL) { + g_hash_table_destroy (control->priv->sink_inputs); + control->priv->sink_inputs = NULL; + } + if (control->priv->source_outputs != NULL) { + g_hash_table_destroy (control->priv->source_outputs); + control->priv->source_outputs = NULL; + } + if (control->priv->clients != NULL) { + g_hash_table_destroy (control->priv->clients); + control->priv->clients = NULL; + } + if (control->priv->cards != NULL) { + g_hash_table_destroy (control->priv->cards); + control->priv->cards = NULL; + } + if (control->priv->ui_outputs != NULL) { + g_hash_table_destroy (control->priv->ui_outputs); + control->priv->ui_outputs = NULL; + } + if (control->priv->ui_inputs != NULL) { + g_hash_table_destroy (control->priv->ui_inputs); + control->priv->ui_inputs = NULL; + } + + free_priv_port_names (control); + G_OBJECT_CLASS (gvc_mixer_control_parent_class)->dispose (object); +} + +static void +gvc_mixer_control_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerControl *self = GVC_MIXER_CONTROL (object); + + switch (prop_id) { + case PROP_NAME: + g_free (self->priv->name); + self->priv->name = g_value_dup_string (value); + g_object_notify (G_OBJECT (self), "name"); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_control_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerControl *self = GVC_MIXER_CONTROL (object); + + switch (prop_id) { + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + + +static GObject * +gvc_mixer_control_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerControl *self; + + object = G_OBJECT_CLASS (gvc_mixer_control_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_CONTROL (object); + + gvc_mixer_new_pa_context (self); + self->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID; + + return object; +} + +static void +gvc_mixer_control_class_init (GvcMixerControlClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = gvc_mixer_control_constructor; + object_class->dispose = gvc_mixer_control_dispose; + object_class->finalize = gvc_mixer_control_finalize; + object_class->set_property = gvc_mixer_control_set_property; + object_class->get_property = gvc_mixer_control_get_property; + + g_object_class_install_property (object_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Name to display for this mixer control", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + + signals [STATE_CHANGED] = + g_signal_new ("state-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, state_changed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [STREAM_ADDED] = + g_signal_new ("stream-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, stream_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [STREAM_REMOVED] = + g_signal_new ("stream-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, stream_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [STREAM_CHANGED] = + g_signal_new ("stream-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, stream_changed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [AUDIO_DEVICE_SELECTION_NEEDED] = + g_signal_new ("audio-device-selection-needed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_UINT); + signals [CARD_ADDED] = + g_signal_new ("card-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, card_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [CARD_REMOVED] = + g_signal_new ("card-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, card_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [DEFAULT_SINK_CHANGED] = + g_signal_new ("default-sink-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, default_sink_changed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [DEFAULT_SOURCE_CHANGED] = + g_signal_new ("default-source-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, default_source_changed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [ACTIVE_OUTPUT_UPDATE] = + g_signal_new ("active-output-update", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, active_output_update), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [ACTIVE_INPUT_UPDATE] = + g_signal_new ("active-input-update", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, active_input_update), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [OUTPUT_ADDED] = + g_signal_new ("output-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, output_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [INPUT_ADDED] = + g_signal_new ("input-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, input_added), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [OUTPUT_REMOVED] = + g_signal_new ("output-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, output_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + signals [INPUT_REMOVED] = + g_signal_new ("input-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GvcMixerControlClass, input_removed), + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); +} + + +static void +gvc_mixer_control_init (GvcMixerControl *control) +{ + control->priv = gvc_mixer_control_get_instance_private (control); + + control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ()); + g_assert (control->priv->pa_mainloop); + + control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop); + g_assert (control->priv->pa_api); + + control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref); + + control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free); + +#ifdef HAVE_ALSA + control->priv->headset_card = -1; +#endif /* HAVE_ALSA */ + + control->priv->state = GVC_STATE_CLOSED; +} + +static void +gvc_mixer_control_finalize (GObject *object) +{ + GvcMixerControl *mixer_control; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_CONTROL (object)); + + mixer_control = GVC_MIXER_CONTROL (object); + g_free (mixer_control->priv->name); + mixer_control->priv->name = NULL; + + g_return_if_fail (mixer_control->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_control_parent_class)->finalize (object); +} + +GvcMixerControl * +gvc_mixer_control_new (const char *name) +{ + GObject *control; + control = g_object_new (GVC_TYPE_MIXER_CONTROL, + "name", name, + NULL); + return GVC_MIXER_CONTROL (control); +} + +gdouble +gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0); + return (gdouble) PA_VOLUME_NORM; +} + +gdouble +gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control) +{ + g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0); + return (gdouble) PA_VOLUME_UI_MAX; +} diff --git a/subprojects/gvc/gvc-mixer-control.h b/subprojects/gvc/gvc-mixer-control.h new file mode 100644 index 0000000..8137849 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-control.h @@ -0,0 +1,155 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_CONTROL_H +#define __GVC_MIXER_CONTROL_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" +#include "gvc-mixer-card.h" +#include "gvc-mixer-ui-device.h" + +G_BEGIN_DECLS + +typedef enum +{ + GVC_STATE_CLOSED, + GVC_STATE_READY, + GVC_STATE_CONNECTING, + GVC_STATE_FAILED +} GvcMixerControlState; + +typedef enum +{ + GVC_HEADSET_PORT_CHOICE_NONE = 0, + GVC_HEADSET_PORT_CHOICE_HEADPHONES = 1 << 0, + GVC_HEADSET_PORT_CHOICE_HEADSET = 1 << 1, + GVC_HEADSET_PORT_CHOICE_MIC = 1 << 2 +} GvcHeadsetPortChoice; + +#define GVC_TYPE_MIXER_CONTROL (gvc_mixer_control_get_type ()) +#define GVC_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControl)) +#define GVC_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass)) +#define GVC_IS_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CONTROL)) +#define GVC_IS_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CONTROL)) +#define GVC_MIXER_CONTROL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass)) + +typedef struct GvcMixerControlPrivate GvcMixerControlPrivate; + +typedef struct +{ + GObject parent; + GvcMixerControlPrivate *priv; +} GvcMixerControl; + +typedef struct +{ + GObjectClass parent_class; + + void (*state_changed) (GvcMixerControl *control, + GvcMixerControlState new_state); + void (*stream_added) (GvcMixerControl *control, + guint id); + void (*stream_changed) (GvcMixerControl *control, + guint id); + void (*stream_removed) (GvcMixerControl *control, + guint id); + void (*card_added) (GvcMixerControl *control, + guint id); + void (*card_removed) (GvcMixerControl *control, + guint id); + void (*default_sink_changed) (GvcMixerControl *control, + guint id); + void (*default_source_changed) (GvcMixerControl *control, + guint id); + void (*active_output_update) (GvcMixerControl *control, + guint id); + void (*active_input_update) (GvcMixerControl *control, + guint id); + void (*output_added) (GvcMixerControl *control, + guint id); + void (*input_added) (GvcMixerControl *control, + guint id); + void (*output_removed) (GvcMixerControl *control, + guint id); + void (*input_removed) (GvcMixerControl *control, + guint id); + void (*audio_device_selection_needed) + (GvcMixerControl *control, + guint id, + gboolean show_dialog, + GvcHeadsetPortChoice choices); +} GvcMixerControlClass; + +GType gvc_mixer_control_get_type (void); + +GvcMixerControl * gvc_mixer_control_new (const char *name); + +gboolean gvc_mixer_control_open (GvcMixerControl *control); +gboolean gvc_mixer_control_close (GvcMixerControl *control); + +GSList * gvc_mixer_control_get_cards (GvcMixerControl *control); +GSList * gvc_mixer_control_get_streams (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sinks (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sources (GvcMixerControl *control); +GSList * gvc_mixer_control_get_sink_inputs (GvcMixerControl *control); +GSList * gvc_mixer_control_get_source_outputs (GvcMixerControl *control); + +GvcMixerStream * gvc_mixer_control_lookup_stream_id (GvcMixerControl *control, + guint id); +GvcMixerCard * gvc_mixer_control_lookup_card_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_output_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_input_id (GvcMixerControl *control, + guint id); +GvcMixerUIDevice * gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control, + GvcMixerStream *stream); + +GvcMixerStream * gvc_mixer_control_get_default_sink (GvcMixerControl *control); +GvcMixerStream * gvc_mixer_control_get_default_source (GvcMixerControl *control); +GvcMixerStream * gvc_mixer_control_get_event_sink_input (GvcMixerControl *control); + +gboolean gvc_mixer_control_set_default_sink (GvcMixerControl *control, + GvcMixerStream *stream); +gboolean gvc_mixer_control_set_default_source (GvcMixerControl *control, + GvcMixerStream *stream); + +gdouble gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control); +gdouble gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control); +void gvc_mixer_control_change_output (GvcMixerControl *control, + GvcMixerUIDevice* output); +void gvc_mixer_control_change_input (GvcMixerControl *control, + GvcMixerUIDevice* input); +GvcMixerStream* gvc_mixer_control_get_stream_from_device (GvcMixerControl *control, + GvcMixerUIDevice *device); +gboolean gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control, + GvcMixerUIDevice *device, + const gchar* profile); + +void gvc_mixer_control_set_headset_port (GvcMixerControl *control, + guint id, + GvcHeadsetPortChoice choices); + +GvcMixerControlState gvc_mixer_control_get_state (GvcMixerControl *control); + +G_END_DECLS + +#endif /* __GVC_MIXER_CONTROL_H */ diff --git a/subprojects/gvc/gvc-mixer-event-role.c b/subprojects/gvc/gvc-mixer-event-role.c new file mode 100644 index 0000000..9f5e26a --- /dev/null +++ b/subprojects/gvc/gvc-mixer-event-role.c @@ -0,0 +1,228 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> +#include <pulse/ext-stream-restore.h> + +#include "gvc-mixer-event-role.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerEventRolePrivate +{ + char *device; +}; + +enum +{ + PROP_0, + PROP_DEVICE +}; + +static void gvc_mixer_event_role_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerEventRole, gvc_mixer_event_role, GVC_TYPE_MIXER_STREAM) + +static gboolean +update_settings (GvcMixerEventRole *role, + gboolean is_muted, + gpointer *op) +{ + pa_operation *o; + const GvcChannelMap *map; + pa_context *context; + pa_ext_stream_restore_info info; + + map = gvc_mixer_stream_get_channel_map (GVC_MIXER_STREAM(role)); + + info.volume = *gvc_channel_map_get_cvolume(map); + info.name = "sink-input-by-media-role:event"; + info.channel_map = *gvc_channel_map_get_pa_channel_map(map); + info.device = role->priv->device; + info.mute = is_muted; + + context = gvc_mixer_stream_get_pa_context (GVC_MIXER_STREAM (role)); + + o = pa_ext_stream_restore_write (context, + PA_UPDATE_REPLACE, + &info, + 1, + TRUE, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_ext_stream_restore_write() failed"); + return FALSE; + } + + if (op != NULL) + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_event_role_push_volume (GvcMixerStream *stream, gpointer *op) +{ + return update_settings (GVC_MIXER_EVENT_ROLE (stream), + gvc_mixer_stream_get_is_muted (stream), op); +} + +static gboolean +gvc_mixer_event_role_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + /* Apply change straight away so that we don't get a race with + * gvc_mixer_event_role_push_volume(). + * See https://bugs.freedesktop.org/show_bug.cgi?id=51413 */ + gvc_mixer_stream_set_is_muted (stream, is_muted); + return update_settings (GVC_MIXER_EVENT_ROLE (stream), + is_muted, NULL); +} + +static gboolean +gvc_mixer_event_role_set_device (GvcMixerEventRole *role, + const char *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_EVENT_ROLE (role), FALSE); + + g_free (role->priv->device); + role->priv->device = g_strdup (device); + g_object_notify (G_OBJECT (role), "device"); + + return TRUE; +} + +static void +gvc_mixer_event_role_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object); + + switch (prop_id) { + case PROP_DEVICE: + gvc_mixer_event_role_set_device (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_event_role_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object); + + switch (prop_id) { + case PROP_DEVICE: + g_value_set_string (value, self->priv->device); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_event_role_class_init (GvcMixerEventRoleClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_event_role_finalize; + object_class->set_property = gvc_mixer_event_role_set_property; + object_class->get_property = gvc_mixer_event_role_get_property; + + stream_class->push_volume = gvc_mixer_event_role_push_volume; + stream_class->change_is_muted = gvc_mixer_event_role_change_is_muted; + + g_object_class_install_property (object_class, + PROP_DEVICE, + g_param_spec_string ("device", + "Device", + "Device", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); +} + +static void +gvc_mixer_event_role_init (GvcMixerEventRole *event_role) +{ + event_role->priv = gvc_mixer_event_role_get_instance_private (event_role); + +} + +static void +gvc_mixer_event_role_finalize (GObject *object) +{ + GvcMixerEventRole *mixer_event_role; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_EVENT_ROLE (object)); + + mixer_event_role = GVC_MIXER_EVENT_ROLE (object); + + g_return_if_fail (mixer_event_role->priv != NULL); + + g_free (mixer_event_role->priv->device); + + G_OBJECT_CLASS (gvc_mixer_event_role_parent_class)->finalize (object); +} + +/** + * gvc_mixer_event_role_new: (skip) + * @context: + * @device: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_event_role_new (pa_context *context, + const char *device, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_EVENT_ROLE, + "pa-context", context, + "index", 0, + "device", device, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-event-role.h b/subprojects/gvc/gvc-mixer-event-role.h new file mode 100644 index 0000000..ab4c509 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-event-role.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_EVENT_ROLE_H +#define __GVC_MIXER_EVENT_ROLE_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_EVENT_ROLE (gvc_mixer_event_role_get_type ()) +#define GVC_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRole)) +#define GVC_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass)) +#define GVC_IS_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_EVENT_ROLE)) +#define GVC_IS_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_EVENT_ROLE)) +#define GVC_MIXER_EVENT_ROLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass)) + +typedef struct GvcMixerEventRolePrivate GvcMixerEventRolePrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerEventRolePrivate *priv; +} GvcMixerEventRole; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerEventRoleClass; + +GType gvc_mixer_event_role_get_type (void); + +GvcMixerStream * gvc_mixer_event_role_new (pa_context *context, + const char *device, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_EVENT_ROLE_H */ diff --git a/subprojects/gvc/gvc-mixer-sink-input.c b/subprojects/gvc/gvc-mixer-sink-input.c new file mode 100644 index 0000000..a359daf --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink-input.c @@ -0,0 +1,159 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-sink-input.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSinkInputPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_sink_input_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSinkInput, gvc_mixer_sink_input, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_sink_input_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_input_volume (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_input_volume() failed"); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_sink_input_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_input_mute (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_input_mute_by_index() failed"); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_sink_input_class_init (GvcMixerSinkInputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_sink_input_finalize; + + stream_class->push_volume = gvc_mixer_sink_input_push_volume; + stream_class->change_is_muted = gvc_mixer_sink_input_change_is_muted; +} + +static void +gvc_mixer_sink_input_init (GvcMixerSinkInput *sink_input) +{ + sink_input->priv = gvc_mixer_sink_input_get_instance_private (sink_input); +} + +static void +gvc_mixer_sink_input_finalize (GObject *object) +{ + GvcMixerSinkInput *mixer_sink_input; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SINK_INPUT (object)); + + mixer_sink_input = GVC_MIXER_SINK_INPUT (object); + + g_return_if_fail (mixer_sink_input->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_sink_input_parent_class)->finalize (object); +} + +/** + * gvc_mixer_sink_input_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_sink_input_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SINK_INPUT, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-sink-input.h b/subprojects/gvc/gvc-mixer-sink-input.h new file mode 100644 index 0000000..17bf127 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink-input.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SINK_INPUT_H +#define __GVC_MIXER_SINK_INPUT_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SINK_INPUT (gvc_mixer_sink_input_get_type ()) +#define GVC_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInput)) +#define GVC_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass)) +#define GVC_IS_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK_INPUT)) +#define GVC_IS_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK_INPUT)) +#define GVC_MIXER_SINK_INPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass)) + +typedef struct GvcMixerSinkInputPrivate GvcMixerSinkInputPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSinkInputPrivate *priv; +} GvcMixerSinkInput; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSinkInputClass; + +GType gvc_mixer_sink_input_get_type (void); + +GvcMixerStream * gvc_mixer_sink_input_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SINK_INPUT_H */ diff --git a/subprojects/gvc/gvc-mixer-sink.c b/subprojects/gvc/gvc-mixer-sink.c new file mode 100644 index 0000000..a6115c6 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink.c @@ -0,0 +1,189 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-sink.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSinkPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_sink_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSink, gvc_mixer_sink, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_sink_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + /* set the volume */ + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_volume_by_index (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_sink_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_mute_by_index (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static gboolean +gvc_mixer_sink_change_port (GvcMixerStream *stream, + const char *port) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_sink_port_by_index (context, + index, + port, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_sink_port_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_sink_class_init (GvcMixerSinkClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_sink_finalize; + + stream_class->push_volume = gvc_mixer_sink_push_volume; + stream_class->change_port = gvc_mixer_sink_change_port; + stream_class->change_is_muted = gvc_mixer_sink_change_is_muted; +} + +static void +gvc_mixer_sink_init (GvcMixerSink *sink) +{ + sink->priv = gvc_mixer_sink_get_instance_private (sink); +} + +static void +gvc_mixer_sink_finalize (GObject *object) +{ + GvcMixerSink *mixer_sink; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SINK (object)); + + mixer_sink = GVC_MIXER_SINK (object); + + g_return_if_fail (mixer_sink->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_sink_parent_class)->finalize (object); +} + +/** + * gvc_mixer_sink_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_sink_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) + +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SINK, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-sink.h b/subprojects/gvc/gvc-mixer-sink.h new file mode 100644 index 0000000..3fbe291 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-sink.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SINK_H +#define __GVC_MIXER_SINK_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SINK (gvc_mixer_sink_get_type ()) +#define GVC_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK, GvcMixerSink)) +#define GVC_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass)) +#define GVC_IS_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK)) +#define GVC_IS_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK)) +#define GVC_MIXER_SINK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass)) + +typedef struct GvcMixerSinkPrivate GvcMixerSinkPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSinkPrivate *priv; +} GvcMixerSink; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSinkClass; + +GType gvc_mixer_sink_get_type (void); + +GvcMixerStream * gvc_mixer_sink_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SINK_H */ diff --git a/subprojects/gvc/gvc-mixer-source-output.c b/subprojects/gvc/gvc-mixer-source-output.c new file mode 100644 index 0000000..c4a275a --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source-output.c @@ -0,0 +1,160 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-source-output.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSourceOutputPrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_source_output_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSourceOutput, gvc_mixer_source_output, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_source_output_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + cv = gvc_channel_map_get_cvolume(map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_output_volume (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_output_volume() failed"); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_source_output_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_output_mute (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_output_mute_by_index() failed"); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_source_output_class_init (GvcMixerSourceOutputClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_source_output_finalize; + + stream_class->push_volume = gvc_mixer_source_output_push_volume; + stream_class->change_is_muted = gvc_mixer_source_output_change_is_muted; +} + +static void +gvc_mixer_source_output_init (GvcMixerSourceOutput *source_output) +{ + source_output->priv = gvc_mixer_source_output_get_instance_private (source_output); + +} + +static void +gvc_mixer_source_output_finalize (GObject *object) +{ + GvcMixerSourceOutput *mixer_source_output; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SOURCE_OUTPUT (object)); + + mixer_source_output = GVC_MIXER_SOURCE_OUTPUT (object); + + g_return_if_fail (mixer_source_output->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_source_output_parent_class)->finalize (object); +} + +/** + * gvc_mixer_source_output_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_source_output_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SOURCE_OUTPUT, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-source-output.h b/subprojects/gvc/gvc-mixer-source-output.h new file mode 100644 index 0000000..4d9a6d6 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source-output.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SOURCE_OUTPUT_H +#define __GVC_MIXER_SOURCE_OUTPUT_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SOURCE_OUTPUT (gvc_mixer_source_output_get_type ()) +#define GVC_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutput)) +#define GVC_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass)) +#define GVC_IS_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT)) +#define GVC_IS_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE_OUTPUT)) +#define GVC_MIXER_SOURCE_OUTPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass)) + +typedef struct GvcMixerSourceOutputPrivate GvcMixerSourceOutputPrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSourceOutputPrivate *priv; +} GvcMixerSourceOutput; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSourceOutputClass; + +GType gvc_mixer_source_output_get_type (void); + +GvcMixerStream * gvc_mixer_source_output_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SOURCE_OUTPUT_H */ diff --git a/subprojects/gvc/gvc-mixer-source.c b/subprojects/gvc/gvc-mixer-source.c new file mode 100644 index 0000000..434eec3 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source.c @@ -0,0 +1,189 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-source.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" + +struct GvcMixerSourcePrivate +{ + gpointer dummy; +}; + +static void gvc_mixer_source_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSource, gvc_mixer_source, GVC_TYPE_MIXER_STREAM) + +static gboolean +gvc_mixer_source_push_volume (GvcMixerStream *stream, gpointer *op) +{ + pa_operation *o; + guint index; + const GvcChannelMap *map; + pa_context *context; + const pa_cvolume *cv; + + index = gvc_mixer_stream_get_index (stream); + + map = gvc_mixer_stream_get_channel_map (stream); + + /* set the volume */ + cv = gvc_channel_map_get_cvolume (map); + + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_volume_by_index (context, + index, + cv, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + *op = o; + + return TRUE; +} + +static gboolean +gvc_mixer_source_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_mute_by_index (context, + index, + is_muted, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static gboolean +gvc_mixer_source_change_port (GvcMixerStream *stream, + const char *port) +{ + pa_operation *o; + guint index; + pa_context *context; + + index = gvc_mixer_stream_get_index (stream); + context = gvc_mixer_stream_get_pa_context (stream); + + o = pa_context_set_source_port_by_index (context, + index, + port, + NULL, + NULL); + + if (o == NULL) { + g_warning ("pa_context_set_source_port_by_index() failed: %s", pa_strerror(pa_context_errno(context))); + return FALSE; + } + + pa_operation_unref(o); + + return TRUE; +} + +static void +gvc_mixer_source_class_init (GvcMixerSourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass); + + object_class->finalize = gvc_mixer_source_finalize; + + stream_class->push_volume = gvc_mixer_source_push_volume; + stream_class->change_is_muted = gvc_mixer_source_change_is_muted; + stream_class->change_port = gvc_mixer_source_change_port; +} + +static void +gvc_mixer_source_init (GvcMixerSource *source) +{ + source->priv = gvc_mixer_source_get_instance_private (source); +} + +static void +gvc_mixer_source_finalize (GObject *object) +{ + GvcMixerSource *mixer_source; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_SOURCE (object)); + + mixer_source = GVC_MIXER_SOURCE (object); + + g_return_if_fail (mixer_source->priv != NULL); + G_OBJECT_CLASS (gvc_mixer_source_parent_class)->finalize (object); +} + +/** + * gvc_mixer_source_new: (skip) + * @context: + * @index: + * @channel_map: + * + * Returns: + */ +GvcMixerStream * +gvc_mixer_source_new (pa_context *context, + guint index, + GvcChannelMap *channel_map) + +{ + GObject *object; + + object = g_object_new (GVC_TYPE_MIXER_SOURCE, + "pa-context", context, + "index", index, + "channel-map", channel_map, + NULL); + + return GVC_MIXER_STREAM (object); +} diff --git a/subprojects/gvc/gvc-mixer-source.h b/subprojects/gvc/gvc-mixer-source.h new file mode 100644 index 0000000..bdffe8c --- /dev/null +++ b/subprojects/gvc/gvc-mixer-source.h @@ -0,0 +1,57 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_SOURCE_H +#define __GVC_MIXER_SOURCE_H + +#include <glib-object.h> +#include "gvc-mixer-stream.h" + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_SOURCE (gvc_mixer_source_get_type ()) +#define GVC_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSource)) +#define GVC_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass)) +#define GVC_IS_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE)) +#define GVC_IS_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE)) +#define GVC_MIXER_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass)) + +typedef struct GvcMixerSourcePrivate GvcMixerSourcePrivate; + +typedef struct +{ + GvcMixerStream parent; + GvcMixerSourcePrivate *priv; +} GvcMixerSource; + +typedef struct +{ + GvcMixerStreamClass parent_class; +} GvcMixerSourceClass; + +GType gvc_mixer_source_get_type (void); + +GvcMixerStream * gvc_mixer_source_new (pa_context *context, + guint index, + GvcChannelMap *channel_map); + +G_END_DECLS + +#endif /* __GVC_MIXER_SOURCE_H */ diff --git a/subprojects/gvc/gvc-mixer-stream-private.h b/subprojects/gvc/gvc-mixer-stream-private.h new file mode 100644 index 0000000..b97ecf5 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-stream-private.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_STREAM_PRIVATE_H +#define __GVC_MIXER_STREAM_PRIVATE_H + +#include <glib-object.h> + +#include "gvc-channel-map.h" + +G_BEGIN_DECLS + +pa_context * gvc_mixer_stream_get_pa_context (GvcMixerStream *stream); + +G_END_DECLS + +#endif /* __GVC_MIXER_STREAM_PRIVATE_H */ diff --git a/subprojects/gvc/gvc-mixer-stream.c b/subprojects/gvc/gvc-mixer-stream.c new file mode 100644 index 0000000..c324900 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-stream.c @@ -0,0 +1,1090 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 William Jon McCann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gi18n-lib.h> + +#include <pulse/pulseaudio.h> + +#include "gvc-mixer-stream.h" +#include "gvc-mixer-stream-private.h" +#include "gvc-channel-map-private.h" +#include "gvc-enum-types.h" + +static guint32 stream_serial = 1; + +struct GvcMixerStreamPrivate +{ + pa_context *pa_context; + guint id; + guint index; + guint card_index; + GvcChannelMap *channel_map; + char *name; + char *description; + char *application_id; + char *icon_name; + char *form_factor; + char *sysfs_path; + gboolean is_muted; + gboolean can_decibel; + gboolean is_event_stream; + gboolean is_virtual; + pa_volume_t base_volume; + pa_operation *change_volume_op; + char *port; + char *human_port; + GList *ports; + GvcMixerStreamState state; +}; + +enum +{ + PROP_0, + PROP_ID, + PROP_PA_CONTEXT, + PROP_CHANNEL_MAP, + PROP_INDEX, + PROP_NAME, + PROP_DESCRIPTION, + PROP_APPLICATION_ID, + PROP_ICON_NAME, + PROP_FORM_FACTOR, + PROP_SYSFS_PATH, + PROP_VOLUME, + PROP_DECIBEL, + PROP_IS_MUTED, + PROP_CAN_DECIBEL, + PROP_IS_EVENT_STREAM, + PROP_IS_VIRTUAL, + PROP_CARD_INDEX, + PROP_PORT, + PROP_STATE, +}; + +static void gvc_mixer_stream_finalize (GObject *object); + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GvcMixerStream, gvc_mixer_stream, G_TYPE_OBJECT) + +static void +free_port (GvcMixerStreamPort *p) +{ + g_free (p->port); + g_free (p->human_port); + g_slice_free (GvcMixerStreamPort, p); +} + +static GvcMixerStreamPort * +dup_port (GvcMixerStreamPort *p) +{ + GvcMixerStreamPort *m; + + m = g_slice_new (GvcMixerStreamPort); + + *m = *p; + m->port = g_strdup (p->port); + m->human_port = g_strdup (p->human_port); + + return m; +} + +G_DEFINE_BOXED_TYPE (GvcMixerStreamPort, gvc_mixer_stream_port, dup_port, free_port) + +static guint32 +get_next_stream_serial (void) +{ + guint32 serial; + + serial = stream_serial++; + + if ((gint32)stream_serial < 0) { + stream_serial = 1; + } + + return serial; +} + +pa_context * +gvc_mixer_stream_get_pa_context (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->pa_context; +} + +guint +gvc_mixer_stream_get_index (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->index; +} + +guint +gvc_mixer_stream_get_id (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + return stream->priv->id; +} + +const GvcChannelMap * +gvc_mixer_stream_get_channel_map (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->channel_map; +} + +/** + * gvc_mixer_stream_get_volume: + * @stream: + * + * Returns: (type guint32): + */ +pa_volume_t +gvc_mixer_stream_get_volume (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]; +} + +gdouble +gvc_mixer_stream_get_decibel (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return pa_sw_volume_to_dB( + (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]); +} + +/** + * gvc_mixer_stream_set_volume: + * @stream: + * @volume: (type guint32): + * + * Returns: + */ +gboolean +gvc_mixer_stream_set_volume (GvcMixerStream *stream, + pa_volume_t volume) +{ + pa_cvolume cv; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map); + pa_cvolume_scale(&cv, volume); + + if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) { + gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE); + g_object_notify (G_OBJECT (stream), "volume"); + return TRUE; + } + + return FALSE; +} + +gboolean +gvc_mixer_stream_set_decibel (GvcMixerStream *stream, + gdouble db) +{ + pa_cvolume cv; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map); + pa_cvolume_scale(&cv, pa_sw_volume_from_dB(db)); + + if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) { + gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE); + g_object_notify (G_OBJECT (stream), "volume"); + } + + return TRUE; +} + +gboolean +gvc_mixer_stream_get_is_muted (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return stream->priv->is_muted; +} + +gboolean +gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return stream->priv->can_decibel; +} + +gboolean +gvc_mixer_stream_set_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (is_muted != stream->priv->is_muted) { + stream->priv->is_muted = is_muted; + g_object_notify (G_OBJECT (stream), "is-muted"); + } + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream, + gboolean can_decibel) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (can_decibel != stream->priv->can_decibel) { + stream->priv->can_decibel = can_decibel; + g_object_notify (G_OBJECT (stream), "can-decibel"); + } + + return TRUE; +} + +const char * +gvc_mixer_stream_get_name (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->name; +} + +const char * +gvc_mixer_stream_get_description (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->description; +} + +gboolean +gvc_mixer_stream_set_name (GvcMixerStream *stream, + const char *name) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->name); + stream->priv->name = g_strdup (name); + g_object_notify (G_OBJECT (stream), "name"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_description (GvcMixerStream *stream, + const char *description) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->description); + stream->priv->description = g_strdup (description); + g_object_notify (G_OBJECT (stream), "description"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_is_event_stream (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + return stream->priv->is_event_stream; +} + +gboolean +gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream, + gboolean is_event_stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->is_event_stream = is_event_stream; + g_object_notify (G_OBJECT (stream), "is-event-stream"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_is_virtual (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + return stream->priv->is_virtual; +} + +gboolean +gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream, + gboolean is_virtual) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->is_virtual = is_virtual; + g_object_notify (G_OBJECT (stream), "is-virtual"); + + return TRUE; +} + +const char * +gvc_mixer_stream_get_application_id (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->application_id; +} + +gboolean +gvc_mixer_stream_set_application_id (GvcMixerStream *stream, + const char *application_id) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->application_id); + stream->priv->application_id = g_strdup (application_id); + g_object_notify (G_OBJECT (stream), "application-id"); + + return TRUE; +} + +static void +on_channel_map_volume_changed (GvcChannelMap *channel_map, + gboolean set, + GvcMixerStream *stream) +{ + if (set == TRUE) + gvc_mixer_stream_push_volume (stream); + + g_object_notify (G_OBJECT (stream), "volume"); +} + +static gboolean +gvc_mixer_stream_set_channel_map (GvcMixerStream *stream, + GvcChannelMap *channel_map) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (channel_map != NULL) { + g_object_ref (channel_map); + } + + if (stream->priv->channel_map != NULL) { + g_signal_handlers_disconnect_by_func (stream->priv->channel_map, + on_channel_map_volume_changed, + stream); + g_object_unref (stream->priv->channel_map); + } + + stream->priv->channel_map = channel_map; + + if (stream->priv->channel_map != NULL) { + g_signal_connect (stream->priv->channel_map, + "volume-changed", + G_CALLBACK (on_channel_map_volume_changed), + stream); + + g_object_notify (G_OBJECT (stream), "channel-map"); + } + + return TRUE; +} + +const char * +gvc_mixer_stream_get_icon_name (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->icon_name; +} + +const char * +gvc_mixer_stream_get_form_factor (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->form_factor; +} + +const char * +gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->sysfs_path; +} + +/** + * gvc_mixer_stream_get_gicon: + * @stream: a #GvcMixerStream + * + * Returns: (transfer full): a new #GIcon + */ +GIcon * +gvc_mixer_stream_get_gicon (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + if (stream->priv->icon_name == NULL) + return NULL; + return g_themed_icon_new_with_default_fallbacks (stream->priv->icon_name); +} + +gboolean +gvc_mixer_stream_set_icon_name (GvcMixerStream *stream, + const char *icon_name) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->icon_name); + stream->priv->icon_name = g_strdup (icon_name); + g_object_notify (G_OBJECT (stream), "icon-name"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_form_factor (GvcMixerStream *stream, + const char *form_factor) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->form_factor); + stream->priv->form_factor = g_strdup (form_factor); + g_object_notify (G_OBJECT (stream), "form-factor"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream, + const char *sysfs_path) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + g_free (stream->priv->sysfs_path); + stream->priv->sysfs_path = g_strdup (sysfs_path); + g_object_notify (G_OBJECT (stream), "sysfs-path"); + + return TRUE; +} + +/** + * gvc_mixer_stream_get_base_volume: + * @stream: + * + * Returns: (type guint32): + */ +pa_volume_t +gvc_mixer_stream_get_base_volume (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0); + + return stream->priv->base_volume; +} + +/** + * gvc_mixer_stream_set_base_volume: + * @stream: + * @base_volume: (type guint32): + * + * Returns: + */ +gboolean +gvc_mixer_stream_set_base_volume (GvcMixerStream *stream, + pa_volume_t base_volume) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->base_volume = base_volume; + + return TRUE; +} + +const GvcMixerStreamPort * +gvc_mixer_stream_get_port (GvcMixerStream *stream) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + g_return_val_if_fail (stream->priv->ports != NULL, NULL); + + for (l = stream->priv->ports; l != NULL; l = l->next) { + GvcMixerStreamPort *p = l->data; + if (g_strcmp0 (stream->priv->port, p->port) == 0) { + return p; + } + } + + g_assert_not_reached (); + + return NULL; +} + +gboolean +gvc_mixer_stream_set_port (GvcMixerStream *stream, + const char *port) +{ + GList *l; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + g_return_val_if_fail (stream->priv->ports != NULL, FALSE); + + g_free (stream->priv->port); + stream->priv->port = g_strdup (port); + + g_free (stream->priv->human_port); + stream->priv->human_port = NULL; + + for (l = stream->priv->ports; l != NULL; l = l->next) { + GvcMixerStreamPort *p = l->data; + if (g_str_equal (stream->priv->port, p->port)) { + stream->priv->human_port = g_strdup (p->human_port); + break; + } + } + + g_object_notify (G_OBJECT (stream), "port"); + + return TRUE; +} + +gboolean +gvc_mixer_stream_change_port (GvcMixerStream *stream, + const char *port) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + return GVC_MIXER_STREAM_GET_CLASS (stream)->change_port (stream, port); +} + +/** + * gvc_mixer_stream_get_ports: + * + * Return value: (transfer none) (element-type GvcMixerStreamPort): + */ +const GList * +gvc_mixer_stream_get_ports (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL); + return stream->priv->ports; +} + +gboolean +gvc_mixer_stream_set_state (GvcMixerStream *stream, + GvcMixerStreamState state) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->state != state) { + stream->priv->state = state; + g_object_notify (G_OBJECT (stream), "state"); + } + + return TRUE; +} + +GvcMixerStreamState +gvc_mixer_stream_get_state (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), GVC_STREAM_STATE_INVALID); + return stream->priv->state; +} + +static int +sort_ports (GvcMixerStreamPort *a, + GvcMixerStreamPort *b) +{ + if (a->priority == b->priority) + return 0; + if (a->priority > b->priority) + return 1; + return -1; +} + +/** + * gvc_mixer_stream_set_ports: + * @ports: (transfer full) (element-type GvcMixerStreamPort): + */ +gboolean +gvc_mixer_stream_set_ports (GvcMixerStream *stream, + GList *ports) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + g_return_val_if_fail (stream->priv->ports == NULL, FALSE); + + stream->priv->ports = g_list_sort (ports, (GCompareFunc) sort_ports); + + return TRUE; +} + +guint +gvc_mixer_stream_get_card_index (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), PA_INVALID_INDEX); + return stream->priv->card_index; +} + +gboolean +gvc_mixer_stream_set_card_index (GvcMixerStream *stream, + guint card_index) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + stream->priv->card_index = card_index; + g_object_notify (G_OBJECT (stream), "card-index"); + + return TRUE; +} + +static void +gvc_mixer_stream_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerStream *self = GVC_MIXER_STREAM (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + self->priv->pa_context = g_value_get_pointer (value); + break; + case PROP_INDEX: + self->priv->index = g_value_get_ulong (value); + break; + case PROP_ID: + self->priv->id = g_value_get_ulong (value); + break; + case PROP_CHANNEL_MAP: + gvc_mixer_stream_set_channel_map (self, g_value_get_object (value)); + break; + case PROP_NAME: + gvc_mixer_stream_set_name (self, g_value_get_string (value)); + break; + case PROP_DESCRIPTION: + gvc_mixer_stream_set_description (self, g_value_get_string (value)); + break; + case PROP_APPLICATION_ID: + gvc_mixer_stream_set_application_id (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_stream_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_FORM_FACTOR: + gvc_mixer_stream_set_form_factor (self, g_value_get_string (value)); + break; + case PROP_SYSFS_PATH: + gvc_mixer_stream_set_sysfs_path (self, g_value_get_string (value)); + break; + case PROP_VOLUME: + gvc_mixer_stream_set_volume (self, g_value_get_ulong (value)); + break; + case PROP_DECIBEL: + gvc_mixer_stream_set_decibel (self, g_value_get_double (value)); + break; + case PROP_IS_MUTED: + gvc_mixer_stream_set_is_muted (self, g_value_get_boolean (value)); + break; + case PROP_IS_EVENT_STREAM: + gvc_mixer_stream_set_is_event_stream (self, g_value_get_boolean (value)); + break; + case PROP_IS_VIRTUAL: + gvc_mixer_stream_set_is_virtual (self, g_value_get_boolean (value)); + break; + case PROP_CAN_DECIBEL: + gvc_mixer_stream_set_can_decibel (self, g_value_get_boolean (value)); + break; + case PROP_PORT: + gvc_mixer_stream_set_port (self, g_value_get_string (value)); + break; + case PROP_STATE: + gvc_mixer_stream_set_state (self, g_value_get_enum (value)); + break; + case PROP_CARD_INDEX: + self->priv->card_index = g_value_get_long (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gvc_mixer_stream_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerStream *self = GVC_MIXER_STREAM (object); + + switch (prop_id) { + case PROP_PA_CONTEXT: + g_value_set_pointer (value, self->priv->pa_context); + break; + case PROP_INDEX: + g_value_set_ulong (value, self->priv->index); + break; + case PROP_ID: + g_value_set_ulong (value, self->priv->id); + break; + case PROP_CHANNEL_MAP: + g_value_set_object (value, self->priv->channel_map); + break; + case PROP_NAME: + g_value_set_string (value, self->priv->name); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->priv->description); + break; + case PROP_APPLICATION_ID: + g_value_set_string (value, self->priv->application_id); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->priv->icon_name); + break; + case PROP_FORM_FACTOR: + g_value_set_string (value, self->priv->form_factor); + break; + case PROP_SYSFS_PATH: + g_value_set_string (value, self->priv->sysfs_path); + break; + case PROP_VOLUME: + g_value_set_ulong (value, + pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map))); + break; + case PROP_DECIBEL: + g_value_set_double (value, + pa_sw_volume_to_dB(pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map)))); + break; + case PROP_IS_MUTED: + g_value_set_boolean (value, self->priv->is_muted); + break; + case PROP_IS_EVENT_STREAM: + g_value_set_boolean (value, self->priv->is_event_stream); + break; + case PROP_IS_VIRTUAL: + g_value_set_boolean (value, self->priv->is_virtual); + break; + case PROP_CAN_DECIBEL: + g_value_set_boolean (value, self->priv->can_decibel); + break; + case PROP_PORT: + g_value_set_string (value, self->priv->port); + break; + case PROP_STATE: + g_value_set_enum (value, self->priv->state); + break; + case PROP_CARD_INDEX: + g_value_set_long (value, self->priv->card_index); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_stream_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerStream *self; + + object = G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_STREAM (object); + + self->priv->id = get_next_stream_serial (); + + return object; +} + +static gboolean +gvc_mixer_stream_real_change_port (GvcMixerStream *stream, + const char *port) +{ + return FALSE; +} + +static gboolean +gvc_mixer_stream_real_push_volume (GvcMixerStream *stream, gpointer *op) +{ + return FALSE; +} + +static gboolean +gvc_mixer_stream_real_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + return FALSE; +} + +gboolean +gvc_mixer_stream_push_volume (GvcMixerStream *stream) +{ + pa_operation *op; + gboolean ret; + + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->is_event_stream != FALSE) + return TRUE; + + g_debug ("Pushing new volume to stream '%s' (%s)", + stream->priv->description, stream->priv->name); + + ret = GVC_MIXER_STREAM_GET_CLASS (stream)->push_volume (stream, (gpointer *) &op); + if (ret) { + if (stream->priv->change_volume_op != NULL) + pa_operation_unref (stream->priv->change_volume_op); + stream->priv->change_volume_op = op; + } + return ret; +} + +gboolean +gvc_mixer_stream_change_is_muted (GvcMixerStream *stream, + gboolean is_muted) +{ + gboolean ret; + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + ret = GVC_MIXER_STREAM_GET_CLASS (stream)->change_is_muted (stream, is_muted); + return ret; +} + +gboolean +gvc_mixer_stream_is_running (GvcMixerStream *stream) +{ + g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE); + + if (stream->priv->change_volume_op == NULL) + return FALSE; + + if ((pa_operation_get_state(stream->priv->change_volume_op) == PA_OPERATION_RUNNING)) + return TRUE; + + pa_operation_unref(stream->priv->change_volume_op); + stream->priv->change_volume_op = NULL; + + return FALSE; +} + +static void +gvc_mixer_stream_class_init (GvcMixerStreamClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->constructor = gvc_mixer_stream_constructor; + gobject_class->finalize = gvc_mixer_stream_finalize; + gobject_class->set_property = gvc_mixer_stream_set_property; + gobject_class->get_property = gvc_mixer_stream_get_property; + + klass->push_volume = gvc_mixer_stream_real_push_volume; + klass->change_port = gvc_mixer_stream_real_change_port; + klass->change_is_muted = gvc_mixer_stream_real_change_is_muted; + + g_object_class_install_property (gobject_class, + PROP_INDEX, + g_param_spec_ulong ("index", + "Index", + "The index for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_ID, + g_param_spec_ulong ("id", + "id", + "The id for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_CHANNEL_MAP, + g_param_spec_object ("channel-map", + "channel map", + "The channel map for this stream", + GVC_TYPE_CHANNEL_MAP, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_PA_CONTEXT, + g_param_spec_pointer ("pa-context", + "PulseAudio context", + "The PulseAudio context for this stream", + G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY)); + g_object_class_install_property (gobject_class, + PROP_VOLUME, + g_param_spec_ulong ("volume", + "Volume", + "The volume for this stream", + 0, G_MAXULONG, 0, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_DECIBEL, + g_param_spec_double ("decibel", + "Decibel", + "The decibel level for this stream", + -G_MAXDOUBLE, G_MAXDOUBLE, 0, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + + g_object_class_install_property (gobject_class, + PROP_NAME, + g_param_spec_string ("name", + "Name", + "Name to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_DESCRIPTION, + g_param_spec_string ("description", + "Description", + "Description to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_APPLICATION_ID, + g_param_spec_string ("application-id", + "Application identifier", + "Application identifier for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_ICON_NAME, + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_FORM_FACTOR, + g_param_spec_string ("form-factor", + "Form Factor", + "Device form factor for this stream, as reported by PulseAudio", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_SYSFS_PATH, + g_param_spec_string ("sysfs-path", + "Sysfs path", + "Sysfs path for the device associated with this stream", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_IS_MUTED, + g_param_spec_boolean ("is-muted", + "is muted", + "Whether stream is muted", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_CAN_DECIBEL, + g_param_spec_boolean ("can-decibel", + "can decibel", + "Whether stream volume can be converted to decibel units", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_IS_EVENT_STREAM, + g_param_spec_boolean ("is-event-stream", + "is event stream", + "Whether stream's role is to play an event", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_IS_VIRTUAL, + g_param_spec_boolean ("is-virtual", + "is virtual stream", + "Whether the stream is virtual", + FALSE, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); + g_object_class_install_property (gobject_class, + PROP_PORT, + g_param_spec_string ("port", + "Port", + "The name of the current port for this stream", + NULL, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_STATE, + g_param_spec_enum ("state", + "State", + "The current state of this stream", + GVC_TYPE_MIXER_STREAM_STATE, + GVC_STREAM_STATE_INVALID, + G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_CARD_INDEX, + g_param_spec_long ("card-index", + "Card index", + "The index of the card for this stream", + PA_INVALID_INDEX, G_MAXLONG, PA_INVALID_INDEX, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); +} + +static void +gvc_mixer_stream_init (GvcMixerStream *stream) +{ + stream->priv = gvc_mixer_stream_get_instance_private (stream); +} + +static void +gvc_mixer_stream_finalize (GObject *object) +{ + GvcMixerStream *mixer_stream; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_IS_MIXER_STREAM (object)); + + mixer_stream = GVC_MIXER_STREAM (object); + + g_return_if_fail (mixer_stream->priv != NULL); + + g_object_unref (mixer_stream->priv->channel_map); + mixer_stream->priv->channel_map = NULL; + + g_free (mixer_stream->priv->name); + mixer_stream->priv->name = NULL; + + g_free (mixer_stream->priv->description); + mixer_stream->priv->description = NULL; + + g_free (mixer_stream->priv->application_id); + mixer_stream->priv->application_id = NULL; + + g_free (mixer_stream->priv->icon_name); + mixer_stream->priv->icon_name = NULL; + + g_free (mixer_stream->priv->form_factor); + mixer_stream->priv->form_factor = NULL; + + g_free (mixer_stream->priv->sysfs_path); + mixer_stream->priv->sysfs_path = NULL; + + g_free (mixer_stream->priv->port); + mixer_stream->priv->port = NULL; + + g_free (mixer_stream->priv->human_port); + mixer_stream->priv->human_port = NULL; + + g_list_free_full (mixer_stream->priv->ports, (GDestroyNotify) free_port); + mixer_stream->priv->ports = NULL; + + if (mixer_stream->priv->change_volume_op) { + pa_operation_unref(mixer_stream->priv->change_volume_op); + mixer_stream->priv->change_volume_op = NULL; + } + + G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->finalize (object); +} diff --git a/subprojects/gvc/gvc-mixer-stream.h b/subprojects/gvc/gvc-mixer-stream.h new file mode 100644 index 0000000..586ec75 --- /dev/null +++ b/subprojects/gvc/gvc-mixer-stream.h @@ -0,0 +1,146 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_MIXER_STREAM_H +#define __GVC_MIXER_STREAM_H + +#include <glib-object.h> +#include "gvc-pulseaudio-fake.h" +#include "gvc-channel-map.h" +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_STREAM (gvc_mixer_stream_get_type ()) +#define GVC_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStream)) +#define GVC_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass)) +#define GVC_IS_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_STREAM)) +#define GVC_IS_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_STREAM)) +#define GVC_MIXER_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass)) + +typedef struct GvcMixerStreamPrivate GvcMixerStreamPrivate; + +typedef struct +{ + GObject parent; + GvcMixerStreamPrivate *priv; +} GvcMixerStream; + +typedef struct +{ + GObjectClass parent_class; + + /* vtable */ + gboolean (*push_volume) (GvcMixerStream *stream, + gpointer *operation); + gboolean (*change_is_muted) (GvcMixerStream *stream, + gboolean is_muted); + gboolean (*change_port) (GvcMixerStream *stream, + const char *port); +} GvcMixerStreamClass; + +typedef struct +{ + char *port; + char *human_port; + guint priority; + gboolean available; +} GvcMixerStreamPort; + +typedef enum +{ + GVC_STREAM_STATE_INVALID, + GVC_STREAM_STATE_RUNNING, + GVC_STREAM_STATE_IDLE, + GVC_STREAM_STATE_SUSPENDED +} GvcMixerStreamState; + +GType gvc_mixer_stream_port_get_type (void) G_GNUC_CONST; +GType gvc_mixer_stream_get_type (void) G_GNUC_CONST; + +guint gvc_mixer_stream_get_index (GvcMixerStream *stream); +guint gvc_mixer_stream_get_id (GvcMixerStream *stream); +const GvcChannelMap *gvc_mixer_stream_get_channel_map(GvcMixerStream *stream); +const GvcMixerStreamPort *gvc_mixer_stream_get_port (GvcMixerStream *stream); +const GList * gvc_mixer_stream_get_ports (GvcMixerStream *stream); +gboolean gvc_mixer_stream_change_port (GvcMixerStream *stream, + const char *port); + +pa_volume_t gvc_mixer_stream_get_volume (GvcMixerStream *stream); +gdouble gvc_mixer_stream_get_decibel (GvcMixerStream *stream); +gboolean gvc_mixer_stream_push_volume (GvcMixerStream *stream); +pa_volume_t gvc_mixer_stream_get_base_volume (GvcMixerStream *stream); + +gboolean gvc_mixer_stream_get_is_muted (GvcMixerStream *stream); +gboolean gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream); +gboolean gvc_mixer_stream_change_is_muted (GvcMixerStream *stream, + gboolean is_muted); +gboolean gvc_mixer_stream_is_running (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_name (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_icon_name (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_form_factor (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream); +GIcon * gvc_mixer_stream_get_gicon (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_description (GvcMixerStream *stream); +const char * gvc_mixer_stream_get_application_id (GvcMixerStream *stream); +gboolean gvc_mixer_stream_is_event_stream (GvcMixerStream *stream); +gboolean gvc_mixer_stream_is_virtual (GvcMixerStream *stream); +guint gvc_mixer_stream_get_card_index (GvcMixerStream *stream); +GvcMixerStreamState gvc_mixer_stream_get_state (GvcMixerStream *stream); + +/* private */ +gboolean gvc_mixer_stream_set_volume (GvcMixerStream *stream, + pa_volume_t volume); +gboolean gvc_mixer_stream_set_decibel (GvcMixerStream *stream, + gdouble db); +gboolean gvc_mixer_stream_set_is_muted (GvcMixerStream *stream, + gboolean is_muted); +gboolean gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream, + gboolean can_decibel); +gboolean gvc_mixer_stream_set_name (GvcMixerStream *stream, + const char *name); +gboolean gvc_mixer_stream_set_description (GvcMixerStream *stream, + const char *description); +gboolean gvc_mixer_stream_set_icon_name (GvcMixerStream *stream, + const char *name); +gboolean gvc_mixer_stream_set_form_factor (GvcMixerStream *stream, + const char *form_factor); +gboolean gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream, + const char *sysfs_path); +gboolean gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream, + gboolean is_event_stream); +gboolean gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream, + gboolean is_event_stream); +gboolean gvc_mixer_stream_set_application_id (GvcMixerStream *stream, + const char *application_id); +gboolean gvc_mixer_stream_set_base_volume (GvcMixerStream *stream, + pa_volume_t base_volume); +gboolean gvc_mixer_stream_set_port (GvcMixerStream *stream, + const char *port); +gboolean gvc_mixer_stream_set_ports (GvcMixerStream *stream, + GList *ports); +gboolean gvc_mixer_stream_set_card_index (GvcMixerStream *stream, + guint card_index); +gboolean gvc_mixer_stream_set_state (GvcMixerStream *stream, + GvcMixerStreamState state); + +G_END_DECLS + +#endif /* __GVC_MIXER_STREAM_H */ diff --git a/subprojects/gvc/gvc-mixer-ui-device.c b/subprojects/gvc/gvc-mixer-ui-device.c new file mode 100644 index 0000000..f7dd33e --- /dev/null +++ b/subprojects/gvc/gvc-mixer-ui-device.c @@ -0,0 +1,741 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * gvc-mixer-ui-device.c + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * Copyright (C) 2012 David Henningsson, Canonical Ltd. <david.henningsson@canonical.com> + * + * gvc-mixer-ui-device.c is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * gvc-mixer-ui-device.c is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "gvc-mixer-ui-device.h" +#include "gvc-mixer-card.h" + +struct GvcMixerUIDevicePrivate +{ + gchar *first_line_desc; + gchar *second_line_desc; + + GvcMixerCard *card; + gchar *port_name; + char *icon_name; + guint stream_id; + guint id; + gboolean port_available; + + /* These two lists contain pointers to GvcMixerCardProfile objects. Those objects are owned by GvcMixerCard. * + * TODO: Do we want to add a weak reference to the GvcMixerCard for this reason? */ + GList *supported_profiles; /* all profiles supported by this port.*/ + GList *profiles; /* profiles to be added to combobox, subset of supported_profiles. */ + GvcMixerUIDeviceDirection type; + gboolean disable_profile_swapping; + gchar *user_preferred_profile; +}; + +enum +{ + PROP_0, + PROP_DESC_LINE_1, + PROP_DESC_LINE_2, + PROP_CARD, + PROP_PORT_NAME, + PROP_STREAM_ID, + PROP_UI_DEVICE_TYPE, + PROP_PORT_AVAILABLE, + PROP_ICON_NAME, +}; + +static void gvc_mixer_ui_device_finalize (GObject *object); + +static void gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device, + const char *icon_name); + +G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerUIDevice, gvc_mixer_ui_device, G_TYPE_OBJECT) + +static guint32 +get_next_output_serial (void) +{ + static guint32 output_serial = 1; + guint32 serial; + + serial = output_serial++; + + if ((gint32)output_serial < 0) + output_serial = 1; + + return serial; +} + +static void +gvc_mixer_ui_device_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object); + + switch (property_id) { + case PROP_DESC_LINE_1: + g_value_set_string (value, self->priv->first_line_desc); + break; + case PROP_DESC_LINE_2: + g_value_set_string (value, self->priv->second_line_desc); + break; + case PROP_CARD: + g_value_set_pointer (value, self->priv->card); + break; + case PROP_PORT_NAME: + g_value_set_string (value, self->priv->port_name); + break; + case PROP_STREAM_ID: + g_value_set_uint (value, self->priv->stream_id); + break; + case PROP_UI_DEVICE_TYPE: + g_value_set_uint (value, (guint)self->priv->type); + break; + case PROP_PORT_AVAILABLE: + g_value_set_boolean (value, self->priv->port_available); + break; + case PROP_ICON_NAME: + g_value_set_string (value, gvc_mixer_ui_device_get_icon_name (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gvc_mixer_ui_device_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object); + + switch (property_id) { + case PROP_DESC_LINE_1: + g_free (self->priv->first_line_desc); + self->priv->first_line_desc = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - 1st line: %s", + self->priv->first_line_desc); + break; + case PROP_DESC_LINE_2: + g_free (self->priv->second_line_desc); + self->priv->second_line_desc = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - 2nd line: %s", + self->priv->second_line_desc); + break; + case PROP_CARD: + self->priv->card = g_value_get_pointer (value); + g_debug ("gvc-mixer-output-set-property - card: %p", + self->priv->card); + break; + case PROP_PORT_NAME: + g_free (self->priv->port_name); + self->priv->port_name = g_value_dup_string (value); + g_debug ("gvc-mixer-output-set-property - card port name: %s", + self->priv->port_name); + break; + case PROP_STREAM_ID: + self->priv->stream_id = g_value_get_uint (value); + g_debug ("gvc-mixer-output-set-property - sink/source id: %i", + self->priv->stream_id); + break; + case PROP_UI_DEVICE_TYPE: + self->priv->type = (GvcMixerUIDeviceDirection) g_value_get_uint (value); + break; + case PROP_PORT_AVAILABLE: + self->priv->port_available = g_value_get_boolean (value); + g_debug ("gvc-mixer-output-set-property - port available %i, value passed in %i", + self->priv->port_available, g_value_get_boolean (value)); + break; + case PROP_ICON_NAME: + gvc_mixer_ui_device_set_icon_name (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static GObject * +gvc_mixer_ui_device_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_params) +{ + GObject *object; + GvcMixerUIDevice *self; + + object = G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->constructor (type, n_construct_properties, construct_params); + + self = GVC_MIXER_UI_DEVICE (object); + self->priv->id = get_next_output_serial (); + self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID; + return object; +} + +static void +gvc_mixer_ui_device_init (GvcMixerUIDevice *device) +{ + device->priv = gvc_mixer_ui_device_get_instance_private (device); +} + +static void +gvc_mixer_ui_device_dispose (GObject *object) +{ + GvcMixerUIDevice *device; + + g_return_if_fail (object != NULL); + g_return_if_fail (GVC_MIXER_UI_DEVICE (object)); + + device = GVC_MIXER_UI_DEVICE (object); + + g_clear_pointer (&device->priv->port_name, g_free); + g_clear_pointer (&device->priv->icon_name, g_free); + g_clear_pointer (&device->priv->first_line_desc, g_free); + g_clear_pointer (&device->priv->second_line_desc, g_free); + g_clear_pointer (&device->priv->profiles, g_list_free); + g_clear_pointer (&device->priv->supported_profiles, g_list_free); + g_clear_pointer (&device->priv->user_preferred_profile, g_free); + + G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->dispose (object); +} + +static void +gvc_mixer_ui_device_finalize (GObject *object) +{ + G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->finalize (object); +} + +static void +gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS (klass); + GParamSpec *pspec; + + object_class->constructor = gvc_mixer_ui_device_constructor; + object_class->dispose = gvc_mixer_ui_device_dispose; + object_class->finalize = gvc_mixer_ui_device_finalize; + object_class->set_property = gvc_mixer_ui_device_set_property; + object_class->get_property = gvc_mixer_ui_device_get_property; + + pspec = g_param_spec_string ("description", + "Description construct prop", + "Set first line description", + "no-name-set", + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_DESC_LINE_1, pspec); + + pspec = g_param_spec_string ("origin", + "origin construct prop", + "Set second line description name", + "no-name-set", + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_DESC_LINE_2, pspec); + + pspec = g_param_spec_pointer ("card", + "Card from pulse", + "Set/Get card", + G_PARAM_READWRITE); + + g_object_class_install_property (object_class, PROP_CARD, pspec); + + pspec = g_param_spec_string ("port-name", + "port-name construct prop", + "Set port-name", + NULL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PORT_NAME, pspec); + + pspec = g_param_spec_uint ("stream-id", + "stream id assigned by gvc-stream", + "Set/Get stream id", + 0, + G_MAXUINT, + GVC_MIXER_UI_DEVICE_INVALID, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_STREAM_ID, pspec); + + pspec = g_param_spec_uint ("type", + "ui-device type", + "determine whether its an input and output", + 0, 1, 0, G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_UI_DEVICE_TYPE, pspec); + + pspec = g_param_spec_boolean ("port-available", + "available", + "determine whether this port is available", + FALSE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PORT_AVAILABLE, pspec); + + pspec = g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to display for this card", + NULL, + G_PARAM_READWRITE|G_PARAM_CONSTRUCT); + g_object_class_install_property (object_class, PROP_ICON_NAME, pspec); +} + +/* Removes the part of the string that starts with skip_prefix + * ie. corresponding to the other direction. + * Normally either "input:" or "output:" + * + * Example: if given the input string "output:hdmi-stereo+input:analog-stereo" and + * skip_prefix "input:", the resulting string is "output:hdmi-stereo". + * + * The returned string must be freed with g_free(). + */ +static gchar * +get_profile_canonical_name (const gchar *profile_name, const gchar *skip_prefix) +{ + gchar *result = NULL; + gchar **s; + guint i; + + /* optimisation for the simple case. */ + if (strstr (profile_name, skip_prefix) == NULL) + return g_strdup (profile_name); + + s = g_strsplit (profile_name, "+", 0); + for (i = 0; i < g_strv_length (s); i++) { + if (g_str_has_prefix (s[i], skip_prefix)) + continue; + if (result == NULL) + result = g_strdup (s[i]); + else { + gchar *c = g_strdup_printf("%s+%s", result, s[i]); + g_free(result); + result = c; + } + } + + g_strfreev(s); + + if (!result) + return g_strdup("off"); + + return result; +} + +const gchar * +gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, const gchar *profile) +{ + const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:"; + gchar *target_cname = get_profile_canonical_name (profile, skip_prefix); + GList *l; + gchar *result = NULL; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + g_return_val_if_fail (profile != NULL, NULL); + + for (l = device->priv->profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + if (strcmp (canonical_name, target_cname) == 0) + result = p->profile; + g_free (canonical_name); + } + + g_free (target_cname); + g_debug ("Matching profile for '%s' is '%s'", profile, result ? result : "(null)"); + return result; +} + + +static void +add_canonical_names_of_profiles (GvcMixerUIDevice *device, + const GList *in_profiles, + GHashTable *added_profiles, + const gchar *skip_prefix, + gboolean only_canonical) +{ + const GList *l; + + for (l = in_profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + g_debug ("The canonical name for '%s' is '%s'", p->profile, canonical_name); + + /* Have we already added the canonical version of this profile? */ + if (g_hash_table_contains (added_profiles, canonical_name)) { + g_free (canonical_name); + continue; + } + + if (only_canonical && strcmp (p->profile, canonical_name) != 0) { + g_free (canonical_name); + continue; + } + + g_free (canonical_name); + + /* https://bugzilla.gnome.org/show_bug.cgi?id=693654 + * Don't add a profile that will make the UI device completely disappear */ + if (p->n_sinks == 0 && p->n_sources == 0) + continue; + + g_debug ("Adding profile to combobox: '%s' - '%s'", p->profile, p->human_profile); + g_hash_table_insert (added_profiles, g_strdup (p->profile), p); + device->priv->profiles = g_list_append (device->priv->profiles, p); + } +} + +/** + * gvc_mixer_ui_device_set_profiles: + * @in_profiles: (element-type Gvc.MixerCardProfile): a list of GvcMixerCardProfile + * + * Assigns value to + * - device->priv->profiles (profiles to be added to combobox) + * - device->priv->supported_profiles (all profiles of this port) + * - device->priv->disable_profile_swapping (whether to show the combobox) + * + * This method attempts to reduce the list of profiles visible to the user by figuring out + * from the context of that device (whether it's an input or an output) what profiles + * actually provide an alternative. + * + * It does this by the following. + * - It ignores off profiles. + * - It takes the canonical name of the profile. That name is what you get when you + * ignore the other direction. + * - In the first iteration, it only adds the names of canonical profiles - i e + * when the other side is turned off. + * - Normally the first iteration covers all cases, but sometimes (e g bluetooth) + * it doesn't, so add other profiles whose canonical name isn't already added + * in a second iteration. + */ +void +gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device, + const GList *in_profiles) +{ + GHashTable *added_profiles; + const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:"; + + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + + g_debug ("Set profiles for '%s'", gvc_mixer_ui_device_get_description(device)); + + if (in_profiles == NULL) + return; + + device->priv->supported_profiles = g_list_copy ((GList*) in_profiles); + + added_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + /* Run two iterations: First, add profiles which are canonical themselves, + * Second, add profiles for which the canonical name is not added already. */ + + add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, TRUE); + add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, FALSE); + + /* TODO: Consider adding the "Off" profile here */ + + device->priv->disable_profile_swapping = g_hash_table_size (added_profiles) <= 1; + g_hash_table_destroy (added_profiles); +} + +/** + * gvc_mixer_ui_device_get_best_profile: + * @selected: (allow-none): The selected profile or its canonical name or %NULL for any profile + * @current: The currently selected profile + * + * Returns: (transfer none): a profile name, valid as long as the UI device profiles are. + */ +const gchar * +gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device, + const gchar *selected, + const gchar *current) +{ + GList *candidates, *l; + const gchar *result; + const gchar *skip_prefix; + gchar *canonical_name_selected; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + g_return_val_if_fail (current != NULL, NULL); + + if (device->priv->type == UIDeviceInput) + skip_prefix = "output:"; + else + skip_prefix = "input:"; + + /* First make a list of profiles acceptable to switch to */ + canonical_name_selected = NULL; + if (selected) + canonical_name_selected = get_profile_canonical_name (selected, skip_prefix); + + candidates = NULL; + for (l = device->priv->supported_profiles; l != NULL; l = l->next) { + gchar *canonical_name; + GvcMixerCardProfile* p = l->data; + canonical_name = get_profile_canonical_name (p->profile, skip_prefix); + if (!canonical_name_selected || strcmp (canonical_name, canonical_name_selected) == 0) { + candidates = g_list_append (candidates, p); + g_debug ("Candidate for profile switching: '%s'", p->profile); + } + g_free (canonical_name); + } + + if (!candidates) { + g_warning ("No suitable profile candidates for '%s'", selected ? selected : "(null)"); + g_free (canonical_name_selected); + return current; + } + + /* 1) Maybe we can skip profile switching altogether? */ + result = NULL; + for (l = candidates; (result == NULL) && (l != NULL); l = l->next) { + GvcMixerCardProfile* p = l->data; + if (strcmp (current, p->profile) == 0) + result = p->profile; + } + + /* 2) Try to keep the other side unchanged if possible */ + if (result == NULL) { + guint prio = 0; + const gchar *skip_prefix_reverse = device->priv->type == UIDeviceInput ? "input:" : "output:"; + gchar *current_reverse = get_profile_canonical_name (current, skip_prefix_reverse); + for (l = candidates; l != NULL; l = l->next) { + gchar *p_reverse; + GvcMixerCardProfile* p = l->data; + p_reverse = get_profile_canonical_name (p->profile, skip_prefix_reverse); + g_debug ("Comparing '%s' (from '%s') with '%s', prio %d", p_reverse, p->profile, current_reverse, p->priority); + if (strcmp (p_reverse, current_reverse) == 0 && (!result || p->priority > prio)) { + result = p->profile; + prio = p->priority; + } + g_free (p_reverse); + } + g_free (current_reverse); + } + + /* 3) All right, let's just pick the profile with highest priority. + * TODO: We could consider asking a GUI question if this stops streams + * in the other direction */ + if (result == NULL) { + guint prio = 0; + for (l = candidates; l != NULL; l = l->next) { + GvcMixerCardProfile* p = l->data; + if ((p->priority > prio) || !result) { + result = p->profile; + prio = p->priority; + } + } + } + + g_list_free (candidates); + g_free (canonical_name_selected); + return result; +} + +const gchar * +gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device) +{ + GvcMixerCardProfile *profile; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + if (device->priv->card == NULL) { + g_warning ("Device did not have an appropriate card"); + return NULL; + } + + profile = gvc_mixer_card_get_profile (device->priv->card); + return gvc_mixer_ui_device_get_matching_profile (device, profile->profile); +} + +gboolean +gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return device->priv->disable_profile_swapping; +} + +/** + * gvc_mixer_ui_device_get_profiles: + * @device: + * + * Returns: (transfer none) (element-type Gvc.MixerCardProfile): + */ +GList* +gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->profiles; +} + +/** + * gvc_mixer_ui_device_get_supported_profiles: + * @device: + * + * Returns: (transfer none) (element-type Gvc.MixerCardProfile): + */ +GList* +gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->supported_profiles; +} + +guint +gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0); + + return device->priv->id; +} + +guint +gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0); + + return device->priv->stream_id; +} + +void +gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *self) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (self)); + + self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID; +} + +const gchar * +gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->first_line_desc; +} + +const char * +gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + if (device->priv->icon_name) + return device->priv->icon_name; + + if (device->priv->card) + return gvc_mixer_card_get_icon_name (device->priv->card); + + return NULL; +} + +static void +gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device, + const char *icon_name) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + + g_free (device->priv->icon_name); + device->priv->icon_name = g_strdup (icon_name); + g_object_notify (G_OBJECT (device), "icon-name"); +} + + +/** + * gvc_mixer_ui_device_get_gicon: + * @device: + * + * Returns: (transfer full): + */ +GIcon * +gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device) +{ + const char *icon_name; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + icon_name = gvc_mixer_ui_device_get_icon_name (device); + + if (icon_name != NULL) + return g_themed_icon_new_with_default_fallbacks (icon_name); + else + return NULL; +} + +const gchar * +gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->second_line_desc; +} + +const gchar* +gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->user_preferred_profile; +} + +const gchar * +gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device) +{ + GList *last; + GvcMixerCardProfile *profile; + + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + last = g_list_last (device->priv->supported_profiles); + profile = last->data; + + return profile->profile; +} + +void +gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device, + const gchar *profile) +{ + g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device)); + g_return_if_fail (profile != NULL); + + g_free (device->priv->user_preferred_profile); + device->priv->user_preferred_profile = g_strdup (profile); +} + +const gchar * +gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL); + + return device->priv->port_name; +} + +gboolean +gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return (device->priv->port_name != NULL); +} + +gboolean +gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device) +{ + g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE); + + return (device->priv->type == UIDeviceOutput); +} diff --git a/subprojects/gvc/gvc-mixer-ui-device.h b/subprojects/gvc/gvc-mixer-ui-device.h new file mode 100644 index 0000000..69095cb --- /dev/null +++ b/subprojects/gvc/gvc-mixer-ui-device.h @@ -0,0 +1,85 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com> + * + * This is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * gvc-mixer-ui-device.h is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _GVC_MIXER_UI_DEVICE_H_ +#define _GVC_MIXER_UI_DEVICE_H_ + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GVC_TYPE_MIXER_UI_DEVICE (gvc_mixer_ui_device_get_type ()) +#define GVC_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDevice)) +#define GVC_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass)) +#define GVC_IS_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GVC_TYPE_MIXER_UI_DEVICE)) +#define GVC_IS_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GVC_TYPE_MIXER_UI_DEVICE)) +#define GVC_MIXER_UI_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass)) + +#define GVC_MIXER_UI_DEVICE_INVALID 0 + +typedef struct GvcMixerUIDevicePrivate GvcMixerUIDevicePrivate; + +typedef struct +{ + GObjectClass parent_class; +} GvcMixerUIDeviceClass; + +typedef struct +{ + GObject parent_instance; + GvcMixerUIDevicePrivate *priv; +} GvcMixerUIDevice; + +typedef enum +{ + UIDeviceInput, + UIDeviceOutput, +} GvcMixerUIDeviceDirection; + +GType gvc_mixer_ui_device_get_type (void) G_GNUC_CONST; + +guint gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device); +guint gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device); +GIcon * gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device, + const gchar *selected, + const gchar *current); +const gchar * gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device); +const gchar * gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, + const gchar *profile); +const gchar * gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device); +const gchar * gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device); +GList * gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device); +GList * gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device); +void gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device, + const GList *in_profiles); +void gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device, + const gchar *profile); +void gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device); +gboolean gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device); + +G_END_DECLS + +#endif /* _GVC_MIXER_UI_DEVICE_H_ */ diff --git a/subprojects/gvc/gvc-pulseaudio-fake.h b/subprojects/gvc/gvc-pulseaudio-fake.h new file mode 100644 index 0000000..92a41b6 --- /dev/null +++ b/subprojects/gvc/gvc-pulseaudio-fake.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef __GVC_PULSEAUDIO_FAKE_H +#define __GVC_PULSEAUDIO_FAKE_H + +#ifndef PA_API_VERSION +#define pa_channel_position_t int +#define pa_volume_t guint32 +#define pa_context gpointer +#endif /* PA_API_VERSION */ + +#endif /* __GVC_PULSEAUDIO_FAKE_H */ diff --git a/subprojects/gvc/libgnome-volume-control.doap b/subprojects/gvc/libgnome-volume-control.doap new file mode 100644 index 0000000..2fcc8e1 --- /dev/null +++ b/subprojects/gvc/libgnome-volume-control.doap @@ -0,0 +1,32 @@ +<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns:gnome="http://api.gnome.org/doap-extensions#" + xmlns="http://usefulinc.com/ns/doap#"> + + <name xml:lang="en">libgnome-volume-control</name> + <shortdesc xml:lang="en">GObject layer for PulseAudio</shortdesc> + <description> + This library contains code to access PulseAudio using a GObject + based library, shared between gnome-control-center, gnome-settings-daemon + and gnome-shell. It is not API stable, and it is meant to be used + as a submodule. + </description> + + <!-- <category rdf:resource="http://api.gnome.org/doap-extensions#desktop" /> --> + + <maintainer> + <foaf:Person> + <foaf:name>Giovanni Campagna</foaf:name> + <foaf:mbox rdf:resource="mailto:scampa.giovanni@gmail.com" /> + <gnome:userid>gcampagna</gnome:userid> + </foaf:Person> + </maintainer> + <maintainer> + <foaf:Person> + <foaf:name>Bastien Nocera</foaf:name> + <foaf:mbox rdf:resource="mailto:hadess@hadess.net" /> + <gnome:userid>hadess</gnome:userid> + </foaf:Person> + </maintainer> +</Project> diff --git a/subprojects/gvc/meson.build b/subprojects/gvc/meson.build new file mode 100644 index 0000000..a1a2af5 --- /dev/null +++ b/subprojects/gvc/meson.build @@ -0,0 +1,137 @@ +project('gvc', 'c', + meson_version: '>= 0.42.0', + default_options: ['static=true'] +) + +assert(meson.is_subproject(), 'This project is only intended to be used as a subproject!') + +gnome = import('gnome') + +pkglibdir = get_option('pkglibdir') +pkgdatadir = get_option('pkgdatadir') + +cdata = configuration_data() +cdata.set_quoted('GETTEXT_PACKAGE', get_option('package_name')) +cdata.set_quoted('PACKAGE_VERSION', get_option('package_version')) + +libgvc_gir_headers = [ + 'gvc-channel-map.h', + 'gvc-mixer-card.h', + 'gvc-mixer-control.h', + 'gvc-mixer-event-role.h', + 'gvc-mixer-sink.h', + 'gvc-mixer-sink-input.h', + 'gvc-mixer-source.h', + 'gvc-mixer-source-output.h', + 'gvc-mixer-stream.h', + 'gvc-mixer-ui-device.h' +] + +libgvc_enums = gnome.mkenums_simple('gvc-enum-types', + sources: libgvc_gir_headers +) + +libgvc_gir_sources = [ + 'gvc-channel-map.c', + 'gvc-mixer-card.c', + 'gvc-mixer-control.c', + 'gvc-mixer-event-role.c', + 'gvc-mixer-sink.c', + 'gvc-mixer-sink-input.c', + 'gvc-mixer-source.c', + 'gvc-mixer-source-output.c', + 'gvc-mixer-stream.c', + 'gvc-mixer-ui-device.c' +] + +libgvc_no_gir_sources = [ + 'gvc-mixer-card-private.h', + 'gvc-mixer-stream-private.h', + 'gvc-channel-map-private.h', + 'gvc-mixer-control-private.h', + 'gvc-pulseaudio-fake.h' +] + +libgvc_deps = [ + dependency('gio-2.0'), + dependency('gobject-2.0'), + dependency('libpulse', version: '>= 12.99.3'), + dependency('libpulse-mainloop-glib') +] + +enable_alsa = get_option('alsa') +if enable_alsa + libgvc_deps += dependency('alsa') +endif +cdata.set('HAVE_ALSA', enable_alsa) + +enable_static = get_option('static') +enable_introspection = get_option('introspection') + +assert(not enable_static or not enable_introspection, 'Currently meson requires a shared library for building girs.') +assert(enable_static or pkglibdir != '', 'Installing shared library, but pkglibdir is unset!') + +c_args = ['-DG_LOG_DOMAIN="Gvc"'] + +if enable_introspection + c_args += '-DWITH_INTROSPECTION' +endif + +if enable_static + libgvc_static = static_library('gvc', + sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums, + dependencies: libgvc_deps, + c_args: c_args + ) + + libgvc = libgvc_static +else + if pkglibdir == '' + error('Installing shared library, but pkglibdir is unset!') + endif + + libgvc_shared = shared_library('gvc', + sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums, + dependencies: libgvc_deps, + c_args: c_args, + install_dir: pkglibdir, + install: true + ) + + libgvc = libgvc_shared +endif + +if enable_introspection + assert(pkgdatadir != '', 'Installing introspection, but pkgdatadir is unset!') + + libgvc_gir = gnome.generate_gir(libgvc, + sources: libgvc_gir_sources + libgvc_gir_headers + libgvc_enums, + nsversion: '1.0', + namespace: 'Gvc', + includes: ['Gio-2.0', 'GObject-2.0'], + extra_args: ['-DWITH_INTROSPECTION', '--quiet'], + install_dir_gir: pkgdatadir, + install_dir_typelib: pkglibdir, + install: true + ) +endif + +if enable_alsa + executable('test-audio-device-selection', + sources: 'test-audio-device-selection.c', + link_with: libgvc, + dependencies: libgvc_deps, + c_args: c_args + ) +endif + +libgvc_dep = declare_dependency( + link_with: libgvc, + include_directories: include_directories('.'), + dependencies: libgvc_deps +) + +configure_file( + output: 'config.h', + configuration: cdata +) diff --git a/subprojects/gvc/meson_options.txt b/subprojects/gvc/meson_options.txt new file mode 100644 index 0000000..38513e3 --- /dev/null +++ b/subprojects/gvc/meson_options.txt @@ -0,0 +1,41 @@ +option('package_name', + type: 'string', + value: '', + description: 'The value for the GETTEXT_PACKAGE define.' +) + +option('package_version', + type: 'string', + value: '', + description: 'The value for the PACKAGE_VERSION define.' +) + +option('pkglibdir', + type: 'string', + value: '', + description: 'The private directory the shared library/typelib will be installed into.' +) + +option('pkgdatadir', + type: 'string', + value: '', + description: 'The private directory the gir file will be installed into.' +) + +option('alsa', + type: 'boolean', + value: true, + description: 'Build ALSA support.' +) + +option('static', + type: 'boolean', + value: false, + description: 'Build as a static library.' +) + +option('introspection', + type: 'boolean', + value: false, + description: 'Build gobject-introspection support' +) diff --git a/subprojects/gvc/test-audio-device-selection.c b/subprojects/gvc/test-audio-device-selection.c new file mode 100644 index 0000000..8195f9d --- /dev/null +++ b/subprojects/gvc/test-audio-device-selection.c @@ -0,0 +1,84 @@ + +#include <stdio.h> +#include <locale.h> +#include <pulse/pulseaudio.h> +#include "gvc-mixer-control.h" + +#define MAX_ATTEMPTS 3 + +typedef struct { + GvcHeadsetPortChoice choice; + const char *name; +} AudioSelectionChoice; + +static AudioSelectionChoice audio_selection_choices[] = { + { GVC_HEADSET_PORT_CHOICE_HEADPHONES, "headphones" }, + { GVC_HEADSET_PORT_CHOICE_HEADSET, "headset" }, + { GVC_HEADSET_PORT_CHOICE_MIC, "microphone" }, +}; + +static void +audio_selection_needed (GvcMixerControl *volume, + guint id, + gboolean show_dialog, + GvcHeadsetPortChoice choices, + gpointer user_data) +{ + const char *args[G_N_ELEMENTS (audio_selection_choices) + 1]; + guint i, n; + int response = -1; + + if (!show_dialog) { + g_print ("--- Audio selection not needed anymore for id %d\n", id); + return; + } + + n = 0; + for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) { + if (choices & audio_selection_choices[i].choice) + args[n++] = audio_selection_choices[i].name; + } + args[n] = NULL; + + g_print ("+++ Audio selection needed for id %d\n", id); + g_print (" Choices are:\n"); + for (i = 0; args[i] != NULL; i++) + g_print (" %d. %s\n", i + 1, args[i]); + + for (i = 0; response < 0 && i < MAX_ATTEMPTS; i++) { + int res; + + g_print ("What is your choice?\n"); + if (scanf ("%d", &res) == 1 && + res > 0 && + res < (int) g_strv_length ((char **) args)) { + response = res; + break; + } + } + + gvc_mixer_control_set_headset_port (volume, + id, + audio_selection_choices[response - 1].choice); +} + +int main (int argc, char **argv) +{ + GMainLoop *loop; + GvcMixerControl *volume; + + setlocale (LC_ALL, ""); + + loop = g_main_loop_new (NULL, FALSE); + + volume = gvc_mixer_control_new ("GNOME Volume Control test"); + g_signal_connect (volume, + "audio-device-selection-needed", + G_CALLBACK (audio_selection_needed), + NULL); + gvc_mixer_control_open (volume); + + g_main_loop_run (loop); + + return 0; +} diff --git a/subprojects/libhandy/.dir-locals.el b/subprojects/libhandy/.dir-locals.el new file mode 100644 index 0000000..0d066d3 --- /dev/null +++ b/subprojects/libhandy/.dir-locals.el @@ -0,0 +1,8 @@ +( + (c-mode . ( + (c-file-style . "linux") + (indent-tabs-mode . nil) + (c-basic-offset . 2) + )) +) + diff --git a/subprojects/libhandy/.editorconfig b/subprojects/libhandy/.editorconfig new file mode 100644 index 0000000..70827f6 --- /dev/null +++ b/subprojects/libhandy/.editorconfig @@ -0,0 +1,38 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[meson.build] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.{c,h,h.in}] +indent_size = 2 +tab_size = 2 +indent_style = space +max_line_length = 80 + +[*.css] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.xml] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.json] +indent_size = 2 +tab_size = 2 +indent_style = space + +[NEWS] +indent_size = 2 +tab_size = 2 +indent_style = space +max_line_length = 72 diff --git a/subprojects/libhandy/AUTHORS b/subprojects/libhandy/AUTHORS new file mode 100644 index 0000000..e061b0d --- /dev/null +++ b/subprojects/libhandy/AUTHORS @@ -0,0 +1,9 @@ +Adrien Plazas <adrien.plazas@puri.sm> +Bob Ham <bob.ham@puri.sm> +Dorota Czaplejewicz <dorota.czaplejewicz@puri.sm> +Guido Günther <agx@sigxcpu.org> +Heather Ellsworth <heather.ellsworth@puri.sm> +Julian Richen <julian@richen.io> +Julian Sparber <julian@sparber.net> +Sebastien Lafargue <slafargue@gnome.org> +Zander Brown <zbrown@gnome.org> diff --git a/subprojects/libhandy/COPYING b/subprojects/libhandy/COPYING new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/subprojects/libhandy/COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/subprojects/libhandy/HACKING.md b/subprojects/libhandy/HACKING.md new file mode 100644 index 0000000..c0cd1be --- /dev/null +++ b/subprojects/libhandy/HACKING.md @@ -0,0 +1,335 @@ +Building +======== +For build instructions see the README.md + +Pull requests +============= +Before filing a pull request run the tests: + +```sh +ninja -C _build test +``` + +Use descriptive commit messages, see + + https://wiki.gnome.org/Git/CommitMessages + +and check + + https://wiki.openstack.org/wiki/GitCommitMessages + +for good examples. + +Coding Style +============ +We mostly use kernel style but + +* Use spaces, never tabs +* Use 2 spaces for indentation + +GTK style function argument indentation +---------------------------------------- +Use GTK style function argument indentation. It's harder for renames but it's +what GNOME upstream projects do. + +*Good*: + +```c +static gboolean +key_press_event_cb (GtkWidget *widget, + GdkEvent *event, + gpointer data) +``` + +*Bad*: + +```c +static gboolean +key_press_event_cb (GtkWidget *widget, GdkEvent *event, gpointer data) +``` + + +Braces +------ +Everything besides functions and structs have the opening curly brace on the same line. + +*Good*: + +```c +if (i < 0) { + ... +} +``` + +*Bad*: + +```c +if (i < 0) +{ + ... +} +``` + +Single line `if` or `else` statements don't need braces but if either `if` or +`else` have braces both get them: + +*Good*: + +```c +if (i < 0) + i++; +else + i--; +``` + +```c +if (i < 0) { + i++; + j++; +} else { + i--; +} +``` + +```c +if (i < 0) { + i++; +} else { + i--; + j--; +} +``` + +*Bad*: + +```c +if (i < 0) { + i++; +} else { + i--; +} +``` + +```c +if (i < 0) { + i++; + j++; +} else + i--; +``` + +```c +if (i < 0) + i++; +else { + i--; + j--; +} +``` + +Function calls have a space between function name and invocation: + +*Good*: + +```c +visible_child_name = gtk_stack_get_visible_child_name (GTK_STACK (self->stack)); +``` + +*Bad*: + +```c +visible_child_name = gtk_stack_get_visible_child_name(GTK_STACK(self->stack)); +``` + + +Header Inclusion Guards +----------------------- +Guard header inclusion with `#pragma once` rather than the traditional +`#ifndef`-`#define`-`#endif` trio. + +Internal headers (for consistency, whether they need to be installed or not) +should contain the following guard to prevent users from directly including +them: +```c +#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION) +#error "Only <handy.h> can be included directly." +#endif +``` + +Only after these should you include headers. + + +Signals +------- +Prefix signal enum names with *SIGNAL_*. + +*Good*: + +```c +enum { + SIGNAL_SUBMITTED = 0, + SIGNAL_DELETED, + SIGNAL_SYMBOL_CLICKED, + SIGNAL_LAST_SIGNAL, +}; +``` + +Also note that the last element ends with a comma to reduce diff noise when +adding further signals. + + +Properties +---------- +Prefix property enum names with *PROP_*. + +*Good*: + +```c +enum { + PROP_0 = 0, + PROP_NUMBER, + PROP_SHOW_ACTION_BUTTONS, + PROP_COLUMN_SPACING, + PROP_ROW_SPACING, + PROP_RELIEF, + PROP_LAST_PROP, +}; +``` + +Also note that the last element ends with a comma to reduce diff noise when +adding further properties. + +Comment style +------------- +In comments use full sentences with proper capitalization and punctuation. + +*Good*: + +```c +/* Make sure we don't overflow. */ +``` + +*Bad:* + +```c +/* overflow check */ +``` + + +Callbacks +--------- +Signal callbacks have a *_cb* suffix. + +*Good*: + +```c +g_signal_connect(self, "clicked", G_CALLBACK (button_clicked_cb), NULL); +``` + +*Bad*: + +```c +g_signal_connect(self, "clicked", G_CALLBACK (handle_button_clicked), NULL); +``` + + +Static functions +---------------- +Static functions don't need the class prefix. E.g. with a type foo_bar: + +*Good*: + +```c +static void +grab_focus_cb (HdyDialer *dialer, + gpointer unused) +``` + +*Bad*: + +```c +static void +hdy_dialer_grab_focus_cb (HdyDialer *dialer, + gpointer unused) +``` + +Note however that virtual methods like +*<class_name>_{init,constructed,finalize,dispose}* do use the class prefix. +These functions are usually never called directly but only assigned once in +*<class_name>_constructed* so the longer name is kind of acceptable. This also +helps to distinguish virtual methods from regular private methods. + +Self argument +------------- +The first argument is usually the object itself so call it *self*. E.g. for a +non public function: + +*Good*: + +```c +static gboolean +expire_cb (FooButton *self) +{ + g_return_val_if_fail (BAR_IS_FOO_BUTTON (self), FALSE); + ... + return FALSE; +} +``` + +And for a public function: + +*Good*: + +```c +gint +foo_button_get_state (FooButton *self) +{ + FooButtonPrivate *priv = bar_foo_get_instance_private(self); + + g_return_val_if_fail (BAR_IS_FOO_BUTTON (self), -1); + return priv->state; +} +``` + +User interface files +-------------------- +User interface files should end in *.ui*. If there are multiple ui +files put them in a ui/ subdirectory below the sources +(e.g. *src/ui/main-window.ui*). + +### Properties +Use minus signs instead of underscores in property names: + +*Good*: + +```xml +<property name="margin-start">12</property> +``` + +*Bad*: + +```xml +<property name="margin_start">12</property> +``` + +Automatic cleanup +----------------- +It's recommended to use `g_auto()`, `g_autoptr()`, `g_autofree()` for +automatic resource cleanup when possible. + +*Good*: + +```c +g_autoptr(GdkPixbuf) sigterm = pixbuf = gtk_icon_info_load_icon (info, NULL); +``` + +*Bad*: + +```c +GdkPixbuf *pixbuf = gtk_icon_info_load_icon (info, NULL); +... +g_object_unref (pixbuf); +``` + +Using the above is fine since libhandy doesn't target any older glib versions +or non GCC/Clang compilers at the moment. diff --git a/subprojects/libhandy/NEWS b/subprojects/libhandy/NEWS new file mode 100644 index 0000000..c335a1e --- /dev/null +++ b/subprojects/libhandy/NEWS @@ -0,0 +1,228 @@ +============== +Version 0.90.0 +============== + +- Stop requiring the HANDY_USE_UNSTABLE_API guard. +- Stop transforming close buttons into back buttons for dialogs on the + desktop. +- Give some nice default and minimum sizes to HdyPreferencesWindow. +- HdyCarousel: + - Add HdyCarouselIndicatorDots and HdyCarouselIndicatorLines. + - Drop the indicator-style, indicator-spacing, and center-content + properties. +- Revamp the colors of HdyAvatar and augment its colors number to 14. +- Set the default column and row spacing of HdyKeypad to 6 pixels. +- Don't present an arrow and a popover in HdyComboRow when its model has + less than 2 items. +- Support CSS sizing properties for HdySqueezer and HdyViewSwitcher. +- Drop the icon-size properties of HdyViewSwitcher, HdyViewSwitcherTitle + and HdyViewSwitcherBar. +- Give some horizontal margins to the view switcher of + HdyViewSwitcherTitle via CSS. +- Add all files back to tarballs except the debian directory. + +============== +Version 0.85.0 +============== + +- HdyAvatar: + - Add the icon-name property to allow setting a different default icon + than avatar-default-symbolic. + - Ship avatar-default-symbolic as a resource to ensure it's there. + This shouldn't affect icon themes already offering it. + - Check the icon exists before using it to avoid a crash. +- HdyDeck and HdyLeaflet: + - Allow dragging the higher sibling only from the border where it + sits, rather than from the anywhere on the currently visible child, + reinforcing spatialization. + - Add the get_child_by_name() methods. +- HdyLeaflet: + - Rename the 'allow-visible' child property into 'navigatable'. +- HdySwipeable: + - Add a navigation direction param and a gesture type param to + get_swipe_area(). +- HdyPreferencesWindow: + - Allow presenting a subpage over the window via the new + present_subpage() and close_subpage() methods. + - Add the 'can-swipe-back' property to allow closing a subpage via a + back swipe gesture. + - Exclude untitled rows as well as invisible pages, groups, and rows + from the search results. +- HdyKeypad: + - Replace the 'show-symbols' property by 'letters-visible'. + - Replace the 'only-digits' property by 'symbols-visible', whose + boolean meaning is inverted. + - Replace the 'left-action' property by 'start-action'. + - Replace the 'right-action' property by 'end-action'. + - Make the 'entry' property declare it uses the GtkEntry type rather + than GtkWidget. +- HdySqeezer: + - Add the 'xalign' and 'yalign' properties to help aligning the + children during transitions. +- HdyViewSwitcherTitle: + - Set the 'policy' property default to 'auto' as in HdyViewSwitcher. +- HdyTitleBar: + - Fix an accidental mix of natural and minimum sizes in measure(). +- Harden the ABI by making symbols implicitly private and explicitly + public. +- Translation updates: + - Romanian + - Ukrainian + +============== +Version 0.84.0 +============== + +- HdyHeaderGroup: + - Replace GtkHeaderBar as the child type by HdyHeaderGroupChild, and + adjust the matching accessors. HdyHeaderGroupChild can hold a + GtkHeaderBar, a HdyHeaderBar, and a HdyHeaderGroup, allowing to nest + header groups. + - Replace the 'focus' property by the 'decorate-all' property. + - Add the update-decoration-layouts signal, used when nesting header + groups. +- HdyHeaderBar: + - Slight size request fix. +- Use the window node's radius instead of the decoration node's one to + mask HdyWindow and HdyApplicationWindow. +- Make HdyAvatar, HdyHeaderGroup, HdySqueezer, HdyViewSwitcher, + HdyViewSwitcherBar, HdyViewSwitcherTitle, and HdyWindowHandle final. +- Replace usage of (allow-none) by (nullable) or (optional). +- Translation updates: + - Ukrainian + +============== +Version 0.83.0 +============== + +- Initialization: + - Add hdy_init() back, with a different prototype. See its + documentation to know how to use it. + - Drop initializing the library via a constructor as it was causing + many issues. + - Drop the now useless Python override. + - Directly update themes on changes. +- Add HdySwipeTracker. +- HdySwipeable: + - Drop the begin_swipe(), update_swipe(), end_swipe() and get_range() + virtual methods + - Add the get_swipe_tracker() and get_swipe_area() virtual methods. + - Add the …_switch_child(), …_emit_child_switched(), + …_get_swipe_tracker(), …_get_distance(), …_get_snap_points(), + …_get_progress(), …_get_cancel_progress(), and …_get_swipe_area() + functions. + - Make implementing get_snap_points() mandatory by dropping its + default implementation, compensating the disparition of get_range(). + - Rename the switch-child signal to child-switched to avoid a naming + collision with the switch_child() method. +- HdyDeck and HdyLeaflet: + - Add an outline to shadows to make them slightly more contrasted yet + subtle. + - Make shadows work over OpenGL content. + - Cache shadows for child transitions. + - Stop drawing invisible shadows when no transition is running. + - Rewrite the transition code to give a window to all children, fixing + numerous issues. +- HdyExpanderRow: + - Add hdy_expander_row_add_prefix(). +- Add libhandy.syms back to tarballs as it was mistakenly removed. +- Translation updates: + - Polish + - Spanish + +============== +Version 0.82.0 +============== + +- Unblacklist run.in for inclusion into the tarball, fixing the build. +- HdyClamp: + - Rename HdyColumn as HdyClamp. + - Make it implement GtkOrientable. + - Rename its properties from maximum-width to maximum-size, and + linear-growth-width to tightening-threshold. + - Rename the style classes it sets on itself from .narrow, .medium and + .wide style to .small, .medium and .large. + - Set the default value of maximum-size to 600, and of + tightening-threshold to 400. + - Notify when changing size properties, and guard non-changes. +- HdyCarousel, HdyDeck and HdyLeaflet: + - Move the swipe tracker event handling to the bubble phase, giving + the priority to the inner widget. +- HdyDeck: + - Avoid some useless allocation computations. +- HdyLeaflet: + - Don't count children of size 0 to compute the fold state. + - Don't fold when there is only 1 visible size. +- HdySwipeable: + - Add the missing direct header inclusion guard. +- HdyWindow and HdyApplicationWindow: + - Implement destroy() to correctly destroy the internal widgets. +- Drop hdy_list_box_separator_header(). +- Don't install Glade files outside prefix. +- Update the project description. +- Translation updates: + - Spanish + - Ukrainian + +============== +Version 0.81.0 +============== + +- Migrated the project to https://gitlab.gnome.org/GNOME/libhandy/. + - Archived the project at https://source.puri.sm/Librem5/libhandy/. + - Updated URLs and email addresses across the project. + - Switch the CI to use GNOME's. + - Build and publish the nightly reference manual via GitLab Pages at + https://gnome.pages.gitlab.gnome.org/libhandy/. +- Make the reference manual and the Glade catalog parallel-installable + with libhandy 0.0. +- Add a Python override to ensure the library is initialized on import. +- Themes: + - Add the HighContrast theme. + - Split the shared theme into the fallback theme whose style can be + overridden by other themes, and the shared theme whose style + overrides the themes. + - Move window corners from the shared theme to Adwaita, so elementary + can do what they want. + - Make the leaflet and deck drop shadows darker for dark variants and + HighContrast, to ensure it's visible. + - Drop the .h4 fallback to avoid conflicts with .heading. Themes are + now expected to implement .heading, or optionally .h4. + - Fix list.preferences nested list bottom corner rounding issues. +- CSS support: + - Account for the CSS box-shadow property when clipping in HdyAvatar, + HdyHeaderBar, and HdyTitleBar. + - Support the CSS min-width and min-height properties in HdyHeaderBar, + and HdyTitleBar. +- HdyDeck and HdyLeaflet: + - Add *_get_adjacent_child() to get the child a swipe or a call to + *_navigate() would present. + - Don't skip the swipes with a 0 (child for leaflet) transition + duration. + - Correctly cancel transitions when the duration is 0 or the + transition is NONE. +- HdyCarousel: + - Allow mouse drag by default. + - Add the 'reveal-duration' property. + - Animate child addition and deletion. +- HdyExpanderRow: + - Move switch to the left of the arrow. + - Add hdy_expander_row_add_action_widget() and the 'action' child type + to allow adding widgets before the arrow and the switch. +- HdyHeaderBar: + - Add the .titlebar style class by default. +- HdyKeypad: + - Make it inherit from GtkBin instead of GtkGrid, contain one instead. + - Add spacing properties to set the grid's spacing. + - Don't make it visible by default. +- HdyPreferencesGroup: + - Use the .heading style class for the title in addition to .h4. +- HdyPreferencesWindow: + - Make clicking search rows work again. +- HdySwipeable: + - Add the get_distance(), get_range(), get_snap_points(), + get_progress(), and get_cancel_progress() virtual methods. +- HdyViewSwitcherTitle: + - Remove the useless has-subtitle property. + - Prevent gtk_widget_show_all() from modifying its internal state. + - Make dispose() reentrant. diff --git a/subprojects/libhandy/README.md b/subprojects/libhandy/README.md new file mode 100644 index 0000000..0eb6cbe --- /dev/null +++ b/subprojects/libhandy/README.md @@ -0,0 +1,71 @@ +# Handy +[![Pipeline status](https://gitlab.gnome.org/GNOME/libhandy/badges/master/build.svg)](https://gitlab.gnome.org/GNOME/libhandy/commits/master) +[![Code coverage](https://gitlab.gnome.org/GNOME/libhandy/badges/master/coverage.svg)](https://gitlab.gnome.org/GNOME/libhandy/commits/master) + +The aim of the Handy library is to help with developing UI for mobile devices +using GTK/GNOME. + +## License + +libhandy is licensed under the LGPL-2.1+. + +## Build dependencies + +To build libhandy you need to first install the build-deps defined by [the debian/control file](https://gitlab.gnome.org/GNOME/libhandy/blob/master/debian/control#L6). + +If you are running a Debian based distribution, you can easily install all those the dependencies making use of the following command + +```sh +sudo apt-get build-dep . +``` + +## Building + +We use the Meson (and thereby Ninja) build system for libhandy. The quickest +way to get going is to do the following: + +```sh +meson . _build +ninja -C _build +ninja -C _build install +``` + +For build options see [meson_options.txt](./meson_options.txt). E.g. to enable documentation: + +```sh +meson . _build -Dgtk_doc=true +ninja -C _build libhandy-doc +``` + +## Usage + +There's a C example: + +```sh +_build/examples/example +``` + +and one in Python. When running from the built source tree it +needs several environment variables so use \_build/run to set them: + +```sh +_build/run examples/example.py +``` + +### Glade + +To be able to use Handy's widgets in the glade interface designer without +installing the library use: + +```sh +_build/run glade +``` + +## Documentation + +The documentation can be found online +[here](https://gnome.pages.gitlab.gnome.org/libhandy). + +## Getting in touch + +Matrix room: [#libhandy:talk.puri.sm](https://gnome.element.io/#/room/#libhandy:talk.puri.sm) diff --git a/subprojects/libhandy/data/leak-suppress.txt b/subprojects/libhandy/data/leak-suppress.txt new file mode 100644 index 0000000..f44eb82 --- /dev/null +++ b/subprojects/libhandy/data/leak-suppress.txt @@ -0,0 +1,5 @@ +# Use via environment variable LSAN_OPTIONS=suppressions=data/leak-suppress.txt +# Ignore fontconfig reported leaks. It's caches cause false positives. +leak:libfontconfig.so.1 +# https://gitlab.gnome.org/GNOME/gtk/merge_requests/823 +leak:gtk_header_bar_set_decoration_layout diff --git a/subprojects/libhandy/data/packaging/rpm/libhandy.spec b/subprojects/libhandy/data/packaging/rpm/libhandy.spec new file mode 100644 index 0000000..739b751 --- /dev/null +++ b/subprojects/libhandy/data/packaging/rpm/libhandy.spec @@ -0,0 +1,60 @@ +%global _vpath_srcdir %{name} + +Name: libhandy +Version: 0.90.0 +Release: 1%{?dist} +Summary: A library full of GTK widgets for mobile phones + +License: LGPLv2+ +Url: https://gitlab.gnome.org/GNOME/libhandy +Source0: https://gitlab.gnome.org/GNOME/libhandy/archive/master.tar.gz + +BuildRequires: gcc +BuildRequires: gobject-introspection +BuildRequires: gtk-doc +BuildRequires: meson >= 0.40.1 +BuildRequires: pkgconfig(gio-2.0) +BuildRequires: pkgconfig(gladeui-2.0) +BuildRequires: pkgconfig(glib-2.0) +BuildRequires: pkgconfig(gmodule-2.0) +BuildRequires: pkgconfig(gtk+-3.0) +BuildRequires: pkgconf-pkg-config +BuildRequires: vala + +%description +%{summary}. + +%package devel +Summary: Development libraries, headers, and documentation for %{name} +Requires: libhandy = %{version}-%{release} + +%description devel +%{summary}. + +%prep +%setup -c -q + +%build +%meson -Dexamples=false -Dgtk_doc=true +%meson_build + +%install +%meson_install + +%files +%{_libdir}/libhandy*.so.* +%{_libdir}/girepository-1.0/Handy*.typelib + +%files devel +%{_includedir}/libhandy* +%{_libdir}/libhandy*.so +%{_libdir}/pkgconfig/libhandy*.pc +%{_datadir}/gir-1.0/Handy*.gir +%{_datadir}/glade/catalogs/libhandy.xml +%{_datadir}/vala/vapi/libhandy*.deps +%{_datadir}/vala/vapi/libhandy*.vapi +%{_datadir}/gtk-doc + +%changelog +* Fri May 18 2018 Julian Richen <julian@richen.io> - 0.0.0-1 +- Update to 0.0.0-1 diff --git a/subprojects/libhandy/debian/README.source b/subprojects/libhandy/debian/README.source new file mode 100644 index 0000000..90e1f00 --- /dev/null +++ b/subprojects/libhandy/debian/README.source @@ -0,0 +1,29 @@ +This package is maintained with git-buildpackage(1). It follows DEP-14 +for branch naming (e.g. using debian/sid for the current version +in Debian unstable). + +It uses pristine-tar(1) to store enough information in git to generate +bit identical tarballs when building the package without having +downloaded an upstream tarball first. + +When working with patches it is recommended to use "gbp pq import" to +import the patches, modify the source and then use "gbp pq export +--commit" to commit the modifications. + +The changelog is generated using "gbp dch" so if you submit any +changes don't bother to add changelog entries but rather provide +a nice git commit message that can then end up in the changelog. + +It is recommended to build the package with pbuilder using: + + gbp buildpackage --git-pbuilder + +For information on how to set up a pbuilder environment see the +git-pbuilder(1) manpage. In short: + + DIST=sid git-pbuilder create + gbp clone <project-url> + cd <project> + gbp buildpackage --git-pbuilder + + -- Guido Günther <agx@sigxcpu.org>, Wed, 2 Dec 2015 18:51:15 +0100 diff --git a/subprojects/libhandy/debian/changelog b/subprojects/libhandy/debian/changelog new file mode 100644 index 0000000..60c5911 --- /dev/null +++ b/subprojects/libhandy/debian/changelog @@ -0,0 +1,1433 @@ +libhandy-1 (0.90.0) amber-phone; urgency=medium + + * New upstream release + + -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 07 Aug 2020 13:23:50 +0200 + +libhandy-1 (0.85.0) amber-phone; urgency=medium + + * New upstream release + + -- Adrien Plazas <adrien.plazas@puri.sm> Thu, 30 Jul 2020 08:51:53 +0200 + +libhandy-1 (0.84.0) amber-phone; urgency=medium + + * New upstream release + + -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 17 Jul 2020 13:13:25 +0200 + +libhandy-1 (0.83.0) amber-phone; urgency=medium + + * New upstream release + + -- Adrien Plazas <adrien.plazas@puri.sm> Thu, 02 Jul 2020 09:37:18 +0200 + +libhandy-1 (0.82.0) amber-phone; urgency=medium + + * New upstream release + + -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 19 Jun 2020 09:30:31 +0200 + +libhandy-1 (0.81.0) amber-phone; urgency=medium + + * New upstream release + + -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 05 Jun 2020 13:08:25 +0200 + +libhandy-1 (0.80.0) amber-phone; urgency=medium + + [ Guido Günther ] + * Bump API version to 1.0.0. + + [ Adrien Plazas ] + * Release libhandy 0.80.0. + * meson: Don't allow to build as a static library. + * meson: Fix disabling the Glade catalog. + * meson: Separate public and private enums. + * doc: Add the Handy 0.0 to Handy 1 migration guide. + * Document CSS nodes and style classes. + * Rename HdyPaginator into HdyCarousel. + Aslo rename HdyPaginatorBox into HdyCarouselBox. + * Remove the deprecated widgets. + Remove HdyArrows, HdyDialer, HdyDialerButton, and HdyDialerCycleButton. + * Drop HdyDialog. + It was deemed not the right way to implement the features we want from it. + * Drop HdyFold. + It has been replaced by a boolean. + * Drop the hdy prefix from CSS node names. + This matches what Adwaita does and will help better blend with it. + * Drop UTF-8 string functions. + They were unused and are not core to what libhandy wants to offer. + * Add HdyViewSwitcherTitle. + A view switcher designed to be used as a window title. + * action-row: Widget overhaul. + Drop the ability to add widgets below, and remove the 'action' buildable + child type and hdy_action_row_add_action(), instead widgets are appended at + the end of the row. + Add the 'activated' signal, and automatically make the row activatable when + is is given an activatable widget. + Define the sizes with CSS, style the title and subtitle with CSS, and rename + the .row-header style class to .header. + * column: Add the .narrow, .medium and .wide style classes. + Allow to easily update style based on the column's clamping state. + * column: Ensure the column is at least as wide as its child. + * combo-row: Make it activatable when it has a bound model. + * combo-row: Popover style overhaul and add a checkmark to the selected row. + * expander-row: Widget overhaul. + Completely redesign the widget. Also prevent gtk_widget_show_all(). + * flatpak: Update the example command name and drop useless build options. + * glade: Support Glade 3.36. + Make the catalog support both 3.24 and 3.36, and add a Glade+libhandy + flatpak manifest which uses glade 3.36. + * icons: Add hdy-expander-arrow-symbolic. + * leaflet: Add the .folded and .unfolded style classes. + Allow to easily update style based on the leaflet's fold state. + * leaflet: Default to the 'over' transition type. + This better match the expected behavior or a leaflet. + * leaflet: Avoid divisions by 0, don't implement the unused GtkBuildableIface + and drop the deprecated transition props and types. + * main: Automatically init libhandy. + Add a library constructor to init on startup and remove hdy_init(). + Initialize the global styles and icons when the main loop starts. + * preferences-window: Add the 'search-enabled' property. + * preferences-window: Hide filtered-out rows. + * preferences-window: Use HdyViewSwitcherTitle. + * style: Style overhaul. + Use SASS to implement the styles, offer both a shared base theme used as + a fallback and an Adwaita-specific theme, and offer a dark variant. + Ship pre-compiled CSS and dynamically load it depending on the theme. + * style: Add the button.list-button CSS style class. + * style: Add the list.preferences CSS style class. + * swipe-tracker: Fix a leak in …_confirm_swipe(). + * swipe-tracker: Use g_clear_pointer() where possible. + * view-switcher-bar: Document relation with HdyViewSwitcherTitle. + * view-switcher-bar: Don't reveal for less than two pages. + * debian: Use HdyKeypad in the Python GI test. + * examples: Add a dark theme toggle button. + * examples: Add a HdyDeck example. + * examples: Bind the switch-rows to their switches. + * examples: Don't set action rows as unactivatable. + * examples: Drop setting the header group focus. + * examples: Give its own header to the search bar demo. + * examples: Make the radio buttons non-focusable. + * examples: Make the resource path match the app ID. + * examples: Put the right header bar in a GtkStack. + * examples: Use HdyViewSwitcherTitle. + * examples: Use the button.list-button CSS style class. + * examples: Vertically align the radio buttons. + + [ Alexander Mikhaylenko ] + * Add HdyDeck. + A swipeable container widget allowing to stack widgets. + * Add HdyNavigationDirection. + * Add HdyStackableBox. + A private object easing the implementation of HdyDeck and HdyLeaflet. + * Add HdyWindow. + A free-form window widget with rounded corners. + * Add HdyApplicationWindow. + A free-form application window widget with rounded corners. + * Add HdyWindowHandle. + A bin widget allowing to control a window like with a titlebar. + * Add HdyNothing. + A private empty widget, easing the implementation of free-form window types. + * Add HdyWindowMixin. + A private object easing the implementation of free-form window types. + * Add HdyWindowHandleController. + A private object allowing a widget to control a window like with a titlebar. + * action-row: Don't allow adding null prefixes, and implement remove(). + * column: Queue resize after changing maximum width. + * expander-row: Fix forall(), and implement remove(). + * glade: Properly support all public widgets and objects. + The glade catalog has been overhauled, fixing support of widgets and objects + already included in the catalog, and adding the newly added ones. + * gtk-window: Add hdy_gtk_window_get_state(). + * header-bar: Add a window handle controller. + Also make it register its own window to get the needed events. + * header-bar: Remove some unused variables. + * Introduce hdy-cairo-private.h. + This helps automatically cleanup up Cairo objects. + * leaflet: Add a function for moving back/forward programmatically. + * leaflet: Allow hdy_leaflet_navigate() regardless of swipe properties. + * leaflet: Document visible child functions. + * leaflet: Fix hdy_leaflet_get_can_swipe_forward() docs and some typos. + * leaflet: Make HdyLeaflet a wrapper around HdyStackableBox. + * leaflet: Remove 'todo' vfunc. + * main: Don't use G_SOURCE_FUNC() macro. + * preferences-group: Implement remove(). + * preferences-group: Subclass GtkBin instead of GtkBox. + * preferences-page: Implement remove() and forall(). + * preferences-page: Subclass GtkBin instead of GtkScrolledWindow. + * preferences-window: Add .titlebar to the headerbar. + * preferences-window: Erase search terms after hiding search bar. + * preferences-window: Implement remove() and forall(). + * preferences-window: Name signal callbacks _cb. + * preferences-window: Port to HdyWindow. + * preferences-window: Use crossfade transition. + * preferences-window: Use GDK_EVENT_* constants. + * preferences-window: Use gtk_search_entry_handle_event(). + * shadow-helper: Don't set style context parent. + * stackable-box: Avoid use-after-free in remove(). + * stackable-box: Check is visible child exists in folded mode + * stackable-box: Disconnect the signal handler after removing a child + * stackable-box: Fix a typo in a comment + * stackable-box: Only count allow-visible=true children for index + * stackable-box: Only hide last visible child when folded + * stackable-box: Remove an extra line in a doc comment + * stackable-box: Skip mode transitions for deck + * stackable-box: Unset last_visible_child after removing or hiding + * swipeable: Use HdyNavigationDirection for begin_swipe() direction + * swipe-tracker: Fix crash in confirm_swipe(). + * swipe-tracker: Reject drags in window's draggable areas. + * view-switcher: Extend bin instead of box. + * view-switcher-title: Unset stack before destroying. + * glade: List all the missing public widgets. + List HdyApplicationWindow, HdyAvatar, HdyDeck, HdyKeypad, + HdyViewSwitcherTitle, HdyWindow, and HdyWindowHandle. + * example: Add a HdyWindow demo. + * example: Add .titlebar to all headerbars. + * example: Don't leave an empty autoptr declaration. + * example: Fix a typo on the HdyWindow page. + * example: Fix leaflet/deck typos. + * example: Make the "Go to the next page" row activatable. + * example: Port main window to HdyApplicationWindow. + * example: Port view switcher window to HdyWindow. + * example: Stop using "fold" HdyLeaflet property. + * example: Use a menu model for primary menu. + * example: Use HdyDeck in complex dialog demo. + * example: Use hdy_leaflet_navigate() for back button and clicking rows. + + [ Ujjwal Kumar ] + * preferences-window: Cancel search from keyboard. + * Return GtkWidget* with _new(). + * Coding style fixes. + * doc: Tell about widget constructor changes. + * example: Resize demo window. + * example: Replace deprecated Dialer with Keypad in example.py. + * example: Add some spacing between widgets. + + [ Julian Sparber ] + * Add HdyAvatar. + A widget to visually represent a contact. + + [ Felix Pojtinger ] + * doc: Add macOS build instructions. + + [ louib ] + * Fix acknowledge typo in build doc. + * Adding new example apps using libhandy. + + [ Alberto Fanjul ] + * glade: Adapt to Glade 3.36 API changes. + + -- Adrien Plazas <adrien.plazas@puri.sm> Tue, 19 May 2020 09:45:02 +0200 + +libhandy (0.0.13) amber-phone; urgency=medium + + [ Alexander Mikhaylenko ] + * paginator-box: Stop using gtk_widget_set_child_visible() + This function is meant for widgets that don't need to be mapped along with + parent widget, not for scrolled out widgets. Additionally, using it causes + strange side effects with GtkOverlay window z-ordering. Stop using it and + instead track visiblity manually. Also, clarify the code a bit. + * leaflet: Correctly handle 0 duration for swipe snap-back + * swipe-tracker: Don't animate when the distance is 0. + Usually it makes sense to restrict the minimum animation duration. However, + if the progress already matches the end progress, it just causes a delay, + so skip it completely. + + [ Julian Sparber ] + * Keypad: Do not show allow typing + when only_digits is true. + The keypad shouldn't allow typing or show + when only_digits + is set to true. Therefore this adds the correct behavior. + + [ Guido Günther ] + * Release libhandy 0.0.13 + + -- Guido Günther <agx@sigxcpu.org> Fri, 27 Dec 2019 12:22:18 +0100 + +libhandy (0.0.12) experimental; urgency=medium + + [ Zander Brown ] + * build: Don't install glade catalogue when used as submodule + + [ Alexander Mikhaylenko ] + * swipe-tracker: Grab widget during the gesture + * swipe-tracker: Animate when canceled. + There are some cases where not animating the canceled gesture looks + awkward. For example, when tapping a paginator while it animates. + * swipe-tracker: Don't add GDK_ALL_EVENTS_MASK. + That was a debugging leftover. + * header-group: Fix a leftover GtkSizeGroup mention + * paginator: Delegate hdy_paginator_scroll_to() to scroll_to_full() + This will help to avoid duplicating code in later commits. + * paginator-box: Add hdy_paginator_box_get_nth_child() + * doc: Add 0.0.12 index + * Add HdySwipeable. + A common interface that swipeable widgets should implement and that + HdySwipeGroup and HdySwipeTracker will use. + * paginator: Implement HdySwipeable + * swipe-tracker: Port to HdySwipeable. + Use a HdySwipeable instead of GtkWidget. Remove 'begin', 'update' and 'end' + signals and instead call HdySwipeable methods. + * Add HdySwipeGroup. + An object that allows to synchronize swipe animations of multiple widgets. + This can be used to sync widgets between headerbar and window content area. + * tests: Add HdySwipeGroup test + * glade: Support HdySwipeGroup. + Do the same thing as for HdyHeaderGroup. + * leaflet: Fix the folding sliding children padding. + Sets the children padding of the folding sliding animation depending on + the surface they'll be drawn on. + This doesn't change a thing for the sliding animation, but this will + avoid the children to be moved when snapshotting them, which is needed + for the over and under animations — which will be added in the next + commit — to work correctly. + * leaflet: Only clip visible area during transitions. + Adjust width and height of the clip rectangle to avoid drawing areas + outside of the widget. + * Introduce HdyShadowHelper. + This will be used in the following commits to add shadows to HeyLeaflet + transitions. + * leaflet: Dim bottom children during transitions. + Draw a dimming layer and a drop shadow over bottom child during 'over' and + 'under' mode and child transitions. + The dimming, shadow and border styles are defined in CSS. The current style + is based on the similar animation in WebKit. + * swipe-tracker: Reduce base distance for vertical swipes. + Use 300px instead of 400px, otherwise it can be hard to use on small + touchpads. + * paginator-box: Adjust index when removing pages. + Prevent jumping when removing pages to the left of the current one. + * paginator: Support discrete scrolling. + Support scrolling on devices like mice. Switch a page when a scroll event + arrives and add a delay to prevent too fast scrolling. + Use animation duration as a delay, but don't let it go below 250ms, mainly + to ensure it still works with animations disabled. + Fixes https://source.puri.sm/Librem5/libhandy/issues/155 + * swipe-tracker: Stop handling trackpoint. + Handle it like discrete scrolling instead. + * leaflet: Mention replacements in deprecations. + Have more useful warnings. + * leaflet: Mark child-transition and mode-transition as deprecated. + Properties are deprecated too, not just accessors. + * leaflet: Ignore deprecations for transition type acccessor declarations. + Since enums are deprecated now, these declarations trigger warnings in + modules that use libhandy. Since these functions are already deprecated + anyway, silence these warnings. + * deprecation-macros: Stop referencing nonexistent macros. + G_DEPRECATED_* and G_DEPRECATED_*_FOR aren't a thing. + * swipe-tracker: Make dragging touch-only. + Since HdyPaginator has mouse scrolling now, there's no need to have + dragging available on non-touch devices, so drop it. + * paginator-box: Wrap children into child info structs. + This will allow to carry additional data for them later. + * paginator-box: Put children into their own GdkWindows. + This allows to stop doing size allocation on each frame, and will allow + to implement drawing cache in the next commit. + * paginator-box: Implement drawing cache. + Keep a Cairo surface for each child. Paint children onto their surfaces, + then compose the final image. Instead of painting the whole children, + track invalidations and paint only changed parts. This means most paginator + redraws don't involve any child redraws. This should significantly speed + up scrolling when children are expensive to draw. + * paginator-box: Add animation-stopped signal. + This will be used in the next commit to add page-changed signal to + HdyPaginator. + * paginator: Add page-changed signal. + Allows to know when the current page has changed, this can be used to + implement "infinite scrolling" by connecting to this signal and amending + the pages. + * leaflet: Allocate last visible child during child transitions. + Fixes one cause of https://source.puri.sm/Librem5/libhandy/issues/85 + * keypad: Immediately assign g_autoptrs to NULL. + Avoid compile-time warnings. + * paginator-box: Create window with correct dimensions. + It doesn't matter because it gets overridden later, but still fix it. + * example: Remove leftover adjustments. + See aa7a4eca68d8c75ff6347202c90515c5aea30c64 + * paginator-box: Fix hdy_paginator_box_get_nth_child() + Return the actual widget, not child info struct. + A leftover from 710bcaacb97bdfac6061726a77665235279d4fe6 + * leaflet: Use provided duration for child transitions. + Actually use the value from the function argument. + * swipeable: Provide swipe direction when preparing. + This will allow to restrict the swipe to only one direction for leaflet. + * swipeable: Distinguish direct and indirect swipes. + Add "direct" parameter to hdy_paginator_begin_swipe() and the corresponding + vfunc, providing a way to tell apart swipes started via HdySwipeGroup sync. + This will be used to have leaflet in headerbar that's not swipeable, but + can still animate along with leaflet in content area. + * swipe-tracker: Skip swipes in wrong direction. + Prevent swiping if the direction doesn't match tracker orientation. This + allows to have GtkScrolledArea inside or around swipeable widgets without + swipes taking over scrolling. + * leaflet: Add allow-visible child property. + This will be used to prevent swiping to widgets such as separators. + * leaflet: Add properties for controlling swipes. + This will allow to selectively enable back and/or forward swipes for + HdyLeaflet. By default swipes are disabled. + * leaflet: Implement back/forward swipe gesture. + Implement HdySwipeable and use HdySwipeTracker to detect back/forward + swipes. + Use can-swipe-back and can-swipe-forward properties for controlling swipes, + and use allow-visible child property to exclude certain widgets, such as + separators, from the gesture. + Multiple leaflets can be synced via HdySwipeGroup. + * example: Enable back swipe in the leaflet. + Set can-swipe-back=true on the content leaflet, allow-visible=false for + separators and use HdySwipeGroup for syncing leaflets rather than binding + visible child name. + * leaflet: Queue relayout after child transition ends. + Prevents close button from occasionally disappearing after swipes. + * swipe-tracker: Add 'allow-mouse-drag' property + * paginator: Add 'allow-mouse-drag' property. + Usually we don't want this, because there's scrolling. However, phosh + still needs this for lockscreen, hence optionally allow it. + * paginator-box: Register window before setting parent. + Prevents newly created widgets from reusing parent's window. + Fixes a regression from e6a477492de6cc4d5107147b9724980ffd7343ea + Fixes https://source.puri.sm/Librem5/libhandy/issues/165 + * swipeable: Fix signal names for docs + * swipe-group: Don't escape tag names for docs + * leaflet: Deprecate old transition type properties. + They did already have the deprecated flag, but weren't shown as deprecated + in docs. + * Update @See_also for swipeable widgets. + Mention HdyLeaflet in HdySwipeable, HdySwipeGroup and HdySwipeTracker. + + [ louib ] + * Fix typo in README. + * Remove casts requiring increased alignment. + Some casts were increasing the required alignment in + callbacks, raising warnings when compiled on arm with gcc. + + [ Guido Günther ] + * Add deprecation macros. + The macros are libhandy internal (should not be used in application + code) and are as such marked with a '_'. This also makes gtk-doc + happy since it treats it as a public symbol otherwise. + * Deprecate all hdy-dialer{-cycle}-button api. + It's considered HdyDialer internal API + * HdyDialer: Remove excessive '*' + * build: Install new header file. + Fixes: ac94e649aac540c1ecaa9df98364049e182605cc + * Release libhandy 0.0.12 + + [ Adrien Plazas ] + * leaflet: Clip children when drawing unfolded. + This will clip children to ensure they don't get drawn on or under the + visible child, which will allow to create mode transition animations + where other children appear to be drawn under the visible child. + * leaflet: Clip the end surface when drawing folded. + This will clip the end surface to ensure it doesn't get drawn on or + under the visible child, which will allow to create mode transition + animations where other children appear to be drawn under the visible + child. + * leaflet: Add the over and under mode transition animations. + This allows the mode transition animation to match the semantic of the + over and under child transitions. + * leaflet: Unify the transition types. + Add the HdyLeafletTransitionType enumeration and the transition-type + property to define both the mode and child transitions, as having them + different makes no sense and could lead to spatialization issues. + This new type doesn't offer a crossfade transition on purpose as it was + deemed inappropriate for the leaflet, for which the position of the + children is inherently important. + This also deprecates the two previous properties and their respective + types. + Fixes https://source.puri.sm/Librem5/libhandy/issues/92. + * leaflet: Remove the over and under mode transitions. + There is no point in adding enum values and deprecating them in the same + version, so let's just remove them. The animations are still available + via the newly added HdyLeafletTransitionType type and the + transition-type property, so this also encourages migrating to the new + API. + * examples: Add a Leaflet page. + This adds a page to demo the leaflet transitions, drops usage of the + deprecated leaflet transition types and properties, and defaults to the + 'over' transition to demo it and its shadow effect. + * Deprecate HdyArrows. + As far as we know, nothing uses it anymore and it's not part of our + latest designs. + Fixes https://source.puri.sm/Librem5/libhandy/issues/126. + * examples: Drop the Arrows page. + HdyArrows is now deprecated, so we don't want to promote it. + * leaflet: Drop some old TODOs. + We just don't need them anymore. + * leaflet: Add Alexander Mikhaylenko's copyright. + His work on this class is far from negligeable, let's reflect that in + the copyright. + * view-switcher-button: Fix the action bar hover style. + This makes the buttons out of a header bar slightly lighter when hovered + and the window is focused. Previously they were the same color as the + unfocused buttons and the action bar, making them look less good and + harder to use. + Fixes https://source.puri.sm/Librem5/libhandy/issues/147. + + [ Julian Sparber ] + * Keypad: Add a general keypad. + This is based on HdyDialer, but with more flexible API. + The new Keypad allows to set a custom Widget to the left/right + lower corner, replacing the original widget. + The Keypad extents directly GtkGrid which exposes all grid properties. + It also allows to replace/change every button in the Keypad, just like + in GtkGrid. + It also adds a GtkEntry which can be used as the focus widget, + it has the key-press-event already connected and it grabs focus once + it's mapped. The Entry isn't part of the keypad, it's just a + convenienced way to create a Entry, you would expect to use with a + keypad. + * Tests: add keypad tests + * Docs: add docs and demo for keypad + * Dialer: deprecate hdydialer + * HdyDialer: Remove it from the demo. + Remove the dialer from the demo since it's deprecated. + * HdyDialer: Deprecate objects related to dialer. + HdyDialerButton, HdyDialerCycleButton and HdyDialer objects where not + deprecated, only there methods were. + + [ Oliver Galvin ] + * README: minor punctuation fixes, and update Fractal URL to GNOME namespace + * docs: Consistently use full sentences in short descriptions. + * docs: Add sections about building and bundling to the 'Compiling with + libhandy' page, and generally tidy the page. * docs: Update copyright + year range. + * meson: fix configure-time warning - Use the 'pie' kwarg instead of passing + '-fpie' manually. Also bump Meson to 0.49.0, when the pie kwarg was added. + * meson: Tidy build files. Use / operator (added in Meson 0.49.0) instead of + join_paths. Use package_api_name variable to avoid repetition. + * style: Remove odd tabs as per 'Coding Style' in HACKING.md, and fix typo. + + [ Ting-Wei Lan ] + * keypad: Fix compilation error for clang. + Function hdy_keypad_button_get_digit is declared to return 'char' in + src/hdy-keypad-button-private.h but defined to return 'const char' in + src/hdy-keypad-button.c. This is not allowed by clang. Since it is + unusual to mark a return value itself as const, just drop const here. + + -- Guido Günther <agx@sigxcpu.org> Thu, 12 Dec 2019 09:49:04 +0100 + +libhandy (0.0.11) experimental; urgency=medium + + [ Adrien Plazas ] + * dialer: Work around GtkGrid row homogeneity. + Puts the buttons into a vertical size group rather than making the rows + homogeneous. This prevents a bug from GtkGrid to make the buttons too + tall when the action buttons are hidden. + * dialog: Don't warn if the titlebar isn't a GtkHeaderBar. + Using another widget is perfectly valid, so we should just return + instead. + * dialog: Refactor the transient-for workaround. + This will make introducing new properties simpler. + * dialog: Add the narrow property. + * header-bar: Show a back button in a narrow HdyDialog. + If a header bar is in a narrow HdyDialog, it will display a back button + at its start in place of its usual window decorations. + * examples: Add a complex HdyDialog example. + This shows how to use HdyHeaderBar and HdyDialog to create a more + complex adaptive dialog. + * header-bar: Show a back button on small non-sovereign windows. + This will show the back button not only in small HdyDialog but in all + small windows that are not sovereign. + * meson: Set the log domain. + This makes the log messages from libhandy look like `Handy-Debug: …` + rather than `** Debug: …`, making them easier to distinguish. + * README.md: Update the documentation URL. + It's on the developer.puri.sm now. + * Add animation helpers. + Add various animation helpers to avoid coyping them around. + * squeezer: Support animation disablement. + This will animate the child transitions only if animations are enabled. + * preferences-group: Use the h4 style class. + Use the h4 style class instead of hardcoding the bold style for the + preferences group title, and implement a fallback making the font bold. + This is needed by elementary to use their own style. + * animation: Make some functions public. + This makes hdy_get_enable_animations() and hdy_ease_out_cubic() public. + * view-switcher-button: Don't make transparent on hover. + This doesn't make the background transparent when hovering and apply the + same style as non-hovered buttons on hovered buttons in a headerbar. + + [ Gabriele Musco ] + * Added Unifydmin to Python 3 examples + * Add HydraPaper to Python 3 examples + + [ Ting-Wei Lan ] + * Don't require GNU sed + + [ Jeremy Bicha ] + * Debian packaging improvements + + [ Guido Günther ] + * debian: Ship example program and files + * Release libhandy 0.0.11 + + [ Alexander Mikhaylenko ] + * search-bar: Hide start and end boxes instead of close button. + * glade: Update catalog dtd. + * Add new HdySwipeTracker widget. + This will be used to implement swipes in new widgets. + * Add new HdyPaginator widget. + Display set of pages with swipe based navigation. + + [ David Boddie ] + * Deploy documentation for the master branch + + [ Michael Catanzaro ] + * glade: Don't install glade files outside build prefix. + + -- Guido Günther <agx@sigxcpu.org> Tue, 27 Aug 2019 12:50:01 +0200 + +libhandy (0.0.10) experimental; urgency=medium + + [ Adrien Plazas ] + * .editorconfig: Add CSS + * arrows: Refresh HdyArrowsDirection docs. + This moves the HdyArrowsDirection documentation to the C file and + removes the final period from the values definitions, like for all other + enums documentations. + * docs: Add section for new symbols in 0.0.10 + * view-switcher: Fix stack children callbacks. + This fixes the callbacks when a child is added or removed from the view + switcher's stack. + * view-switcher-button: Make an active button's label bold. + This makes the view switcher easier to read. + It uses multiple labels with or without the specific style rather than a + single label with the style toggled on and off to ensure the size + requests don't change depending on whether the button is active or not. + * leaflet: Synchronize paired notifications. + This ensures users can't react to a visible child change notification or + a fold change notification before we finish emitting all related + notifications. + * Add HdySqueezer. + This can be used to automatically hide a widget like a HdyViewSwitcher + in a header bar when there is not enough space for it and show a title + label instead. + Fixes https://source.puri.sm/Librem5/libhandy/issues/100 + * examples: Use a HdySqueezer. + Use a HdySqueezer in the view switcher window to show either the view + switcher in the header bar, or a window title and a view switcher bar + depending on the window's width. + * view-switcher-button: Allow to elipsize in narrow mode. + This will be used to let HdyViewSwitcherBar reach even narrower widths. + * view-switcher: Allow to elipsize in narrow mode. + This will be used to let HdyViewSwitcherBar reach even narrower widths. + * view-switcher-bar: Ellipsize in narrow mode. + This lets HdyViewSwitcherBar reach even narrower widths. + * view-switcher-button: Use buttons borders in size. + When computing the size of the button, take the button's border into + account. + Fixes https://source.puri.sm/Librem5/libhandy/issues/108 + * view-switcher-bar: Sort properties by alphabetical order. + This fixes a code style error and will avoid to propagate it as the file + gets edited. + * view-switcher-bar: Add margins. + Add margings around the view switcher to better match the mockups. + * view-switcher: Define a minimum natural width. + This prevents the buttons from looking terribly narrow in a wide bar by + making them request a minimum good looking natural size. + * Add HdyPreferencesRow. + This will be used as the base row for the preferences window, offering + data needed to search a preference entry. + * action-row: Extend HdyPreferencesRow. + This allows to use HdyActionRow and its derivatives as preferences rows. + * Add HdyPreferencesGroup. + This will be used to group preferences rows as a coherent group in a + preferences page. + * Add HdyPreferencesPage. + This will be used to group preferences as pages in a preferences window. + * Add HdyPreferencesWindow. + This allows to easily create searchable preferences windows. + Fixes https://source.puri.sm/Librem5/libhandy/issues/101 + * examples: Add a HdyPreferencesWindow example + * Add private GtkWindow functions. + Add the private GtkWindow functions _gtk_window_toggle_maximized() + and gtk_window_get_icon_for_size() which will be used in the next commit + by HdyHeaderBar. + * Add HdyHeaderBar. + Fork GtkHeaderBar to help fixing shortcomings caused by adaptive designs + or coming from GtkHeaderBar itself as features are not accepted into GTK + 3 anymore. + Fixes https://source.puri.sm/Librem5/libhandy/issues/102 + * examples: Use HdyHeaderBar in the View Switcher page. + This correctly centers the view switcher and demoes HdyHeaderBar. + * view-switcher: Recommend to use a HdyHeaderBar. + This will help users of HdyViewSwitcher to know how to make it look + good in a header bar. + * examples: Drop un unused signal connection. + This avoids a run time warning. + * docs: Add images for HdyViewSwitcher and HdyViewSwitcherBar + * preferences-window: Strictly center the header bar. + This makes the header bar's widgets look better by ensuring they are + always centered, even if it means they will be narrower. + * conbo-row: Make the popover relative to the arrow. + Consistently point to the arrow rather than sometimes to the arrow and + sometimes to the invisible box containing the current value. + * combo-row: Add HdyComboRowGetName. + Replace HdyComboRowCreateLabelData by HdyComboRowGetName and keep a + reference to in the combo row to allow accessing it externally. It will + be needed to automatically handle converting the value into a name to + display as the subtitle of the row. + * combo-row: Add the use-subtitle property. + Allow to display the current value as the subtitle rather than at the + end of the row. + Fixes https://source.puri.sm/Librem5/libhandy/issues/95 + * header-bar: Render margins and borders. + Fixes https://source.puri.sm/Librem5/libhandy/issues/121 + + [ Zander Brown ] + * Add HdyViewSwitcherButton. + This will be used in the next commit by HdyViewSwitcher. + * Add HdyViewSwitcher. + This more modern and adaptive take on GtkStackSwitcher helps building + adaptive UIs for phones. + Fixes https://source.puri.sm/Librem5/libhandy/issues/64 + * Add HdyViewSwitcherBar. + This action bar offers a HdyViewSwitcher and is designed to be put at + the bottom of windows. It is designed to be revealed when the header bar + doesn't have enough room to fit a HdyViewSwitcher, helping the windows + to reach narrower widths. + * examples: Add the View Switcher page. + This example presents a HdyViewSwitcher and a HdyViewSwitcherBar in + their own window. Currently both are visible at the same time, a later + commit should make only one visible at a time, depending on the + available width. + + [ Aearil ] + * Update components list for the external projects in the README + + [ Mohammed Sadiq ] + * dialog: Fix typos in documentation + * demo-window: Fix typo in property name + + [ Oliver Galvin ] + * Change GTK+ to GTK + * Fix a few typos and grammatical mistakes + * Expand the visual overview. + Add more widgets and a comparison of HdyDialog + + [ Guido Günther ] + * Release libhandy 0.0.10 + * HACKING: + - Properly end emphasis + - Document extra space after function calls + * ci improvements + - Split doc build to different stage + - Split out unit tests to different stage + - Drop coverage on Fedora. It's not evaulated anyway + - Split out build commands + - Drop tests from static build + - Move Debian package to packaging stage + * gitlab-ci: Archive the build debs + * HdyArrows: + - Fix obvious documentation errors + - Only redraw widget if visible + - Don't emit notify signals on unchanged properties + - Redraw arrows on property changes + * HdyDemoWindow: Don't schedule arrow redraws + * Add suppression for ASAN + * tests-dialer: cleanups + * HdyDialer: Make show_action_buttons match the initial property default + + -- Guido Günther <agx@sigxcpu.org> Wed, 12 Jun 2019 17:23:21 +0200 + +libhandy (0.0.9) experimental; urgency=medium + + [ Benjamin Berg ] + * glade: Mark ActionRow properties as translatable/icon. + Without this, it is impossible to set the translatable flag in glade, + making it hard to create proper UI definitions. + + [ Bastien Nocera ] + * Use correct i18n include. + From the Internationalization section of the GLib API docs: + In order to use these macros in an application, you must include + <glib/gi18n.h>. For use in a library, you must include <glib/gi18n-lib.h> + after defining the GETTEXT_PACKAGE macro suitably for your library + * Fix broken translations in all libhandy applications. + Translations in all the applications using libhandy would be broken + after a call to hdy_init() as it changed the default gettext translation + domain. + See https://gitlab.gnome.org/GNOME/gnome-control-center/issues/393 + + [ Adrien Plazas ] + * examples: Update the Flatpak command. + The command should changed with the demo application name. + * leaflet: Improve the slide child transition description. + This makes the slide child transition description match the one of the + slide mode transition one. + * action-row: Upcast self to check the activated row. + Upcast the HdyActionRow rather than downcasting the activated row to + compare their pointers. This prevents error messages when a sibbling row + that isn't a HdyActionRow is activated. Also use a simple cast rather + than a safe cast as it is there only to please the compiler and is + useless for a pointer comparison and it's faster. + * Drop 'dialer' from the UI resources path. + This makes the UI file paths more correct and simpler. + * leaflet: Add hdy_leaflet_stop_child_transition() + This makes the code clearer by encapsulating child mode transition + cancellation into its own function. + * leaflet: Factorize bin window move and resize. + This ensures we move or resize it consistently. + * leaflet: Move the bin window on child transition cancellation. + This avoids the children to be drawn out of place when a mode transition + is triggered while a child transition was ongoing. + Fixes https://source.puri.sm/Librem5/libhandy/issues/93 + * Add HDY_STYLE_PROVIDER_PRIORITY. + Add and use HDY_STYLE_PROVIDER_PRIORITY to help ensuring custom styling + is applied consistently and correctly accross all the library. + * expander-row: Move the custom style to a resource. + This makes the code cleaner, easier to read, and simnpler to modify. + * combo-row: Move the custom style to a resource. + This makes the code cleaner, easier to read, and simnpler to modify. + * expander-row: Add the expanded property. + This can be used to reveal external widgets depending on the state of + the row. + + [ Guido Günther ] + * debian: Test GObject introspection. + This makes sure we have the typelib file installed correctly. + * debian/tests: Drop API version from include. + This makes sure we respect pkg-config's findings. + * examples: Add API version to demo name. + This makes different versions co-installable. + * build: Don't hardcode API version + * Release libhandy 0.0.9 + + -- Guido Günther <agx@sigxcpu.org> Thu, 07 Mar 2019 12:37:34 +0100 + +libhandy (0.0.8) experimental; urgency=medium + + [ Adrien Plazas ] + * examples: Use the "frame" stylesheet on listboxes. + This avoids using GtkFrame where it's not relevant and shows the + example. + * examples: Refactor the Dialer panel. + This makes it more in line with the other panels. + * examples: Refactor the Arrows panel. + This makes it more in line with the other panels. + * examples: Fix the Lists panel column width. + We were accidentally using the widths from the Column panel. + * examples: Fix a typo + * action-row: Add the row-header style class to the header box. + This will allow to style the row's header separately. + * expander-row: Add the expander style class. + This will allow to style the row's padding appropriately to be used as + an expander. + * README.md: Add GNOME Settings and GNOME Web to users + * meson: Don't install if it's a static subproject + * title-bar: Drop useless definitions and inclusions. + These were copy and paste errors. + * README.md: Add gnome-bluetooth as a user + * examples: Rename the example program to handy-demo. + This also renames the type and files to match the new name. + Fixes https://source.puri.sm/Librem5/libhandy/issues/81 + * meson: Fix the examples option description. + Fixes https://source.puri.sm/Librem5/libhandy/issues/82 + * expander-row: Animate the arrow rotation. + Because we can! + * leaflet: Support RTL languages when unfolded. + Fixes https://source.puri.sm/Librem5/libhandy/issues/86 + + [ Benjamin Berg ] + * Add -s -noreset to xvfb-run calls. + Xvfb will close when the last client exists, which may be the cause of + sporadic test failures. Add -s -noreset to the command line to prevent + this from happening. + * combo-row: Fix memory leak + g_list_model_get_item returns a referenced GObject which needs to be + unref'ed. + * combo-row: Fix memory leak in set_for_enum + * value-object: Add an object to stuff a GValue into a GListModel. + This is useful to store arbitrary (but simple) values inside a + HdyComboRow. + * example: Use value object rather. + The code was storing strings in labels, just to extract them again. + Also, the code was leaking the labels as g_list_store_insert does not + sink the reference of the passed object. + * tests: Add tests for HdyValueObject + * action-row: Destroy the contained widget. + The GtkBox that contains everything is an internal child which must be + destroyed explicitly. + + [ Guido Günther ] + * run.in: Set GLADE_MODULE_SEARCH_PATH as well. + This makes sure we're using the freshly built module when running + from the source tree. + * Release libhandy 0.0.8 + + [ Pellegrino Prevete ] + * README: added Daty to example apps + * build: Force default libdir location for libhandy target on Windows to + keep MinGW compatibility + + [ Alexander Mikhaylenko ] + * leaflet: Add missing check for moving child window. + Prevent child window from moving in transitions that don't require it, + instead just resize it. + Fixes https://source.puri.sm/Librem5/libhandy/issues/80 + * leaflet: Drop commented out 'under' child transition. + It's going to be replaced with the actual implementation in the next + commit. + * leaflet: Make 'over' child transition symmetric. + Implement 'under' child transition animation, use it for 'over' for right + and down directions, matching 'over' description. + Fixes https://source.puri.sm/Librem5/libhandy/issues/79 + * leaflet: Add 'under' child transition. + Use same animations as 'over', but with reversed directions. + Documentation descriptions by Adrien Plazas. + Fixes https://source.puri.sm/Librem5/libhandy/issues/84 + * leaflet: Clip bottom child during child transitions. + Prevents bottom child from being visible through the top one during 'over' + and 'under' child transitions. + + [ maxice8 ] + * meson: pass -DHANDY_COMPILATION to GIR compiler. + Fixes cross compilation of GIR in Void Linux. + + -- Guido Günther <agx@sigxcpu.org> Fri, 15 Feb 2019 11:27:35 +0100 + +libhandy (0.0.7) experimental; urgency=medium + + [ Adrien Plazas ] + * glade: Add row widgets to the widget classes. They are missing and don't + appear in Glade. + * glade: Add that HdySearchBar. It's in libhandy since 0.0.6 + * action-row: Handle show_all() + This avoids an empty image, an empty subtitle and an empty prefixes box + to be visible when calling show_all(), as they are handled by the row + itself. + * action-row: Add the Since annotation to properties + * example: Make the row with no action non-activatable + * tests: Init libhandy. + This ensures we run the test the same way applications are expected to + run libhandy. + * docs: Add section for new symbols in 0.0.7 + * action-row: Add the activatable-widget property. + This allows to bind the activation of the row by a click or a mnemonic + to the activation of a widget. + * action-row: Chain up the parent dispose method + * combo-row: Release the model on dispose. + This avoids errors when trying to disconnect signals on finalization. + * combo-box: Rename selected_position to selecxted_index. + This will better match the name for its accessors which will be added in + the next commit. + * combo-row: Add the selected-index property. + This allows to access the selected item. + * main: Explicitely load the resources in hdy_init() + This is mandatory to use resources of a static version of libhandy, and + is hence mandatory to allow to build libhandy as a static library. + * meson: Bump Meson to 0.47.0. + This is required to use the feature option type in the next commit. + * meson: Make introspection and the Glade catalog features. + This avoids having to disable them when their dependencies aren't + available and it will allow to disable them properly when libhandy will + be allowed to be built as a static library in the next commit. + * meson: Allow to build as a static library. + This also disables the Glade catalog as it doesn't work with a static + libhandy. + * action-row: Drop pointers to internals on destruction. + This avoids crashes when trying to access pointers to already dropped + widgets. + Fixes https://source.puri.sm/Librem5/libhandy/issues/69 + * expander-row: Drop pointers to internals on destruction. + This avoids crashes when trying to access pointers to already dropped + widgets. + Fixes https://source.puri.sm/Librem5/libhandy/issues/69 + * examples: Make the Dialog section look nicer. + This improves the spacing, adds and icon and adds a description to the + Dialog section. + * dialog: Close when pressing the back button. + Close the dialog instead of destroying it when clicking the back button. + This is the same behavior as when pressing escape or clicking the close + button and allows the dialog to be reused as some applications like to + do. + Fixes https://source.puri.sm/Librem5/libhandy/issues/70 + + [ louib ] + * Add GNOME Contacts as example + + [ Guido Günther ] + * HdyComboRow: Don't use g_autoptr for GEnumClass + g_autoptr for GEnumClass was added post 2.56, so using it makes it + harder for people to package for distros. Not using g_autoptr there + doesn't make the code much less readable. + * HdyDialer: Don't use class method slot for 'delete' + We used the one of 'submit' so far due to a c'n'p error. (Closes: #67) + * HdyComboRow: hdy_combo_row_get_model: Add missing scope annotation + * gitlab-ci: Build static library. + The library build is sufficiently different that we want to run the + build and tests. + * Release libhandy 0.0.7 + + [ David Cordero ] + * Update documentation regarding build dependencies + + [ Zander Brown ] + * Implement HdyDialog, an adaptive GtkDialog + https://source.puri.sm/Librem5/libhandy/issues/52 + * example: Add to example application. + Silly simple demo of HdyDialog. + + [ Benjamin Berg ] + * combo-row: Rework selected-index property setting and notification. + The notify::selected-index signal was not selected in most cases. Rework + the selection handling to ensure that it is always emited when it changes + or if the module is replaced. + Also fixed are a few checks on whether the selection index is valid. + + -- Guido Günther <agx@sigxcpu.org> Fri, 18 Jan 2019 14:38:30 +0100 + +libhandy (0.0.6) experimental; urgency=medium + + [ Adrien Plazas ] + * Set relevant ATK roles. + This will help the widgets to be more accessible. + * doc: Rephrase the unstability ack section. + Rephrase the documentation explaining how to include libhandy in a way + that could include other languages such as Vala. + * doc: Document the unstability ack for Vala + * Guard header inclusions with #pragma once. + This standardizes the header inclusion guards to #pragma once, which + shouldn't be a problem as we already were using it in some files. + * hacking: Document header inclusion guard preferences + * example: Disable more libhandy options in Flatpak. + Disable generation of the GObject Introspection files, the VAPI and the + tests in the example Flatpak as they are not used by it. + * arrow: Use a measure() method. + This will simplify porting to GTK+ 4. + * column: Use a measure() method. + This will simplify porting to GTK+ 4. + * dialer-button: Use a measure() method. + This will simplify porting to GTK+ 4. + * leaflet: Use a measure() method. + This will simplify porting to GTK+ 4. + * init: Make the arguments optional. + Annotate the arguments of hdy_init() with (optional) to specify that NULL + arguments are valid. This also replaces the deprecated (allow-none) by + (nullable) to specify that the array pointed at by argv can be NULL. + * init: Document that libhandy can't be reinitialized + * Normalize and document private header guards + * Add HdySearchBar. + This is similar to GtkSearchBar except it allows the central widget + (typically a GtkEntry) to fill all the available space. This is needed to + manage an entry's width via a HdyColumn, letting the entry (and by + extention the search bar) have a limited maximum width while allowing it to + shrink to accomodate smaller windows. + * example: Add the 'Search bar' page. + This adds a demo of HdySearchBar. + * example: Put the content in a scrolled window. + This ensures the example can fit windows of any height. This also makes + the stack containing the content non vertically homogeneous so the + scrollbar appears only on examples needing it, while keeping it + horizontally homogeneous for to keep when the leaflets will be folded + consistent. + * build: Set the shared object install directory. + This is required for Meson subprojects to work as intended. + * build: Do not install hdy-public-types.c. + There is no point in installing this generated C file. + * leaflet: Allow editing the children list when looping through it. + This avoids potential crashes when destroying a leaflet and this avoids + leaks as not all children where looped through as the children list was + edited while being looped through when destroying the leaflet. This fixes + https://source.puri.sm/Librem5/libhandy/issues/42. + * Add hdy_list_box_separator_header() + This list box header update function is commonly used by many applications + and is going to be used by HdyComboRow which is going to be added to + libhandy later.This makes it available for everyone. + * examples: Use hdy_list_box_separator_header() + This makes the code simpler. + * Add HdyActionRow. + This implements a very commonly used list box row pattern and make it + convenient to use. It is going to be used as the base class for many + other commonly used row types. + * examples: Use HdyRow. + This makes the code simpler and demoes the widget. + * Add HdyExpanderRow + * Add HdyEnumValueObject. + This will be used in the next commit to use enumeration values in a + GListModel. + * Add HdyComboRow + * examples: Add the Lists page. + This page presents GtkListBox related widgets like HdyRow and its + descendants. + * examples: Put the scrolled window in the end pane size group. + This fixes the fold synchronization of the leaflets in the example + application's window. + + [ Guido Günther ] + * hdy-enums: Make build reproducible. + Use @basename@ instead of @filename@ since the later uses the full + path which varies across builds. + * HACKING: Clarify braces in if-else. + Document common practice in the other files. + * spec: Sort dependencies + * spec: Build-depend on libgladeui-2.0 + * gitlab-ci: Deduplicate tags + * gitlab-ci: Build on Fedora as well. + This gives us more confidence that we build succesfully and without + warnings on an OS much used by GNOME developers. It also makes sure we + validate the spec file. + * gitlab-ci: Switch to clang-tools + clang-3.9 does not contain scan-build anymore. + * HdyHeaderGroup: Cleanup references to header bars in dispose. + The dispose heandler is meant to break refs to other objects, not + finalize. + * HdyHeaderGroup: Disconnect from header bar's signals during dispose. The + header bars might still emit signals which leads to CRITICALS or actual + crashes. Fixes parts of #56 + * docs: Add section for new symbols in 0.0.6 + * Annotate APIs new in 0.0.6 + * Release libhandy 0.0.6 + + [ Alexander Mikhaylenko ] + * init: Add (transfer none) to argv parameter. + This allows to call the function from Vala more easily. + * header-group: Ref itself instead of header bars. + When adding a header bar, ref the header group and connect to 'destroy' + signal of the header bar. When a header bar is destroyed or + hdy_header_group_remove_header_bar() is called, unref the header bar and + remove it from the list. + This way, a non-empty header group is only destroyed after every header + bar it contains has been removed from the group or destroyed. + Fixes #56 + * Revert "HdyHeaderGroup: Disconnect from header bar's signals during + dispose" + Since commit c5bf27d44022bdfa94b3f560aac8c22115e06363 header bars are + destroyed before header group, so when destroying the header group, the + list of header bars is always empty, so there's nothing to unref anymore. + Reverts commit 14e5fc7b923440a99c3a62635cf895e73c5a49cd. + + [ tallero ] + * build: Don't use -fstack-protector-strong on mingw64. + This unbreaks compilation on that platform. (Closes: #64) + + -- Guido Günther <agx@sigxcpu.org> Mon, 17 Dec 2018 16:26:19 +0100 + +libhandy (0.0.5) experimental; urgency=medium + + [ Guido Günther ] + * Release libhandy 0.0.5 + * meson: Properly depend on the generated headers. + This fixes dependency problems with the generated headers such as + https://arm01.puri.sm/job/debs/job/deb-libhandy-buster-armhf/263/console + See + http://mesonbuild.com/Wrap-best-practices-and-tips.html#eclare-generated-headers-explicitly + * debian: Make sure we create a strict shlibs file libhandy's ABI changes a + lot so make sure we generate dependencies that always require the upstream + version built against. + * debian: Mark buil-deps for tests as <!nocheck> + * gitlab-ci: Deduplicate before_script + * gitlab-ci: Build with clang (scan-build) as well. + We currently don't fail on warnings: + https://github.com/mesonbuild/meson/issues/4334 + * HdyLeaflet: Remove unused initializations spotted by clang + * doc: Add that virtual methods carry the class prefix (Closes: #53) + * docs: Add libhandy users. This allows to find in uses examples easily. + * docs: Mention meson as well. Fewer and fewer GNOME projects use + autotools. + * docs: Drop package_ver_str from include path. We add this in the + pkg-config file so no need to specify it again. + * Add i18n infrastructure + * Add hdy_init() This initializes i18n. (Closes: #36) + * meson: Depend on glib that supports g_auto*. Related to #33 + * HACKING: document using g_auto* is o.k. (Closes: #33) + * HACKING: Use syntax highlighting. + * Drop Jenkinsfile. We run in gitlab-ci now + * build: Detect if ld supports a version script. This is e.g. not the case + for Clang on OSX. (Closes: #58) + + [ Jeremy Bicha ] + * debian: Have libhandy-0.0-dev depend on libgtk-3-dev (Closes: #910384) + * debian: Use dh --with gir so that gir1.2-handy gets its dependencies set + correctly + * debian: Simplify debian/rules. + + [ Adrien Plazas ] + * example: Drop Glade support in flatpak build. + * main: Init public GObject types in hdy_init() This will avoid our users to + manually ensure libhandy widget types are loaded before using them in + GtkBuilder templates. + Fixes https://source.puri.sm/Librem5/libhandy/issues/20 + * dialer: Descend from GtkBin directly. + Makes HdyDialer descend from GtkBin directly rather than from + GtkEventBox. GtkEventBox will be dropped in GTK+ 4 and brings no + functionality to HdyDialer. + * example: Rename margin-left/right to margin-start/end. + Left and right margin names are not RTL friendly and will be dropped in + GTK+ 4. + * HACKING.md: Rename margin-left to margin-start. + Left and right margin names are not RTL friendly and will be dropped in + GTK+ 4. + * titlebar: Fix a mention of HdyLeaflet in the docs + * example: Do not access event fields. + This is needed to port to GTK+ 4 as these fields will be private. + * dialer: Do not access event fields. + This is needed to port to GTK+ 4 as these fields will be private. + + [ Alexander Mikhaylenko ] + * example: Remove styles present in GTK+ 3.24.1. + Libhandy requires `gtk+-3.0 >= 3.24.1` anyway, so these styles aren't + necessary, and also break upstream `.devel` style. + + [ Jan Tojnar ] + * Use pkg-config for obtaining glade catalogdir + + -- Guido Günther <agx@sigxcpu.org> Wed, 07 Nov 2018 11:17:14 +0100 + +libhandy (0.0.4) experimental; urgency=medium + + [ Mohammed Sadiq ] + * dialer-button: Fix emitting signal. + As the properties where set to not explicitly fire ::notify, no + signals where emitted. Let it be not explicit so that + the signal will be emitted on change + * ci: Enable code coverage. + GitLab pages isn't supported now. So simply store the artifacts. + * README: Add build and coverage status images + * dialer: Handle delete button long press. + Make the delete button clear the whole user input on long press + + [ Alexander Mikhaylenko ] + * example: Remove sidebar border less aggressively. + Applying the style to every element inside 'stacksidebar' also removes + border from unrelated elements such as scrollbars. Hence only remove it + from lists. + + [ Adrien Plazas ] + * leaflet: Add the folded property. + This is a boolean equivalent of the fold property, it is a needed + convenience as is can be used in GtkBuilder declarations while the fold + property is more convenient to use from C as it enables stronger typing. + * example: Bind back and close buttons visibility to fold. + Directly bind whether the back button and the close button are visible + to whether the headerbar is folded. + * Add HdyHeaderGroup + * example: Use a HdyHeaderGroup. + This automatically updates the headerbars' window decoration layout. + * dialer-button: Replace digit and letters by symbols. + Unify the digit and the letters of a dialer button as its symbols. This + allows to make the code simpler by limiting the number of special cases. + digit. This also handles Unicode characters. + * dialer-cycle-button: Don't make the secondary label dim. + This helps making it clear that these symbols are available, contrary to + the dim ones from a regular dialer button. + * dialer-button: Make the secondary label smaller. + Makes the secondary text smaller to better match the mockups for Calls. + * Add CSS names to the widgets + * leaflet: Document the fold and folded properties + * dialer: Set buttons checked instead setting relief. + When digit keys are pressed, check the buttons state to checked rather + than changing the relief. + * dialer: Add the relief property. + This allows to set the relief of the dialer buttons. + * header-group: Drop forgotten log. + This was accidentally left in. + Fixes https://source.puri.sm/Librem5/libhandy/issues/47 + * example: Let the Column panel reach narrower widths. + Readjust the column widget's margins and ellipsize its labels to let it + reach narrower widths. + * example: Separate the listbox items + * example: Let the Dialer panel reach narrower widths. + Put the dialer into a column rather than forcing its width to let it + reach narrower widths. + * example: Enlarge the dialer label. + This makes the dialed number more readable. + * example: Let the Welcome panel reach narrower widths. + Let the welcome panel's labels wrap to let it reach narrower widths. + * header-group: Sanitize the decoration layout. + Checks whether the decoration layout is NULL, empty or has at least one + colon. + Fixes https://source.puri.sm/Librem5/libhandy/issues/44 + * header-group: Better handle references of header bars. + Take a reference when adding a header bar, release them on destruction + and don't take extra references on the focused child. This avoids using + pointers to dead header bars or to leak them. + * header-group: Fix the type of the focus property. + This also fixes the types of the accessor functions. + Fixes https://source.puri.sm/Librem5/libhandy/issues/46 + * header-group: Fix the docs of the focus property. + This also improves the documentation of its accessor functions. + * header-group: Guard the focused header bar setter. + Better guard the focused header bar setter by checking that the set + header bar actually is one and is part of the group. + * meson: Require GTK+ 3.24.1 or newer. + GTK+ 3.24.1 received style fixes required for HdyTitleBar to work as + expected. + + [ Felix Pojtinger ] + * docs: Format README to enable syntax highlighting. + This also adds code fences and blanks around headers. + + [ Guido Günther ] + * Depend on generated headers. + If tests or examples are built early we want that hdy-enums.h is alread + there. + * docs: Add HdyFold. + This makes sure it can be linked to by HdyLeaflet. + * HdyLealflet: Use glib-mkenums. + This makes the enums clickable in the HdyLeaflet documentation + and makes the code smaller. + * HdyFold: Use glib-mkenums. + This makes the enums clickable in the HdyLeaflet documentation, + HdyFold usable in GtkBuild and makes the code smaller. + * HdyHeaderGroup: Document hdy_group_set_focus() + This makes newer newer Gir scanner happy (and is a good thing anyway). + * debian: Update shared library symbols + * d/rules: Set a proper locale for the tests. + * Check the debian package build during CI as well. + This make sure we notice build breackage before it hits Jenkins to build + the official debs. + * ci: Fail on gtkdoc warnings. + Gitlab seems to get confused by the '!' expression so use if instead. + * tests: Test hdy_header_group_{get,set}_focus + * HdyDialer: Apply 'keypad' style class. + This applies the 'keypad' style class to both the keypad itself and its + buttons. This allows to style the buttons and the keypad in the + application. + * glade: Verify catalog data via xmllint + * debian: Add dependenies for running xmllint. + This also makes sure we have it available during CI + * HdyHeaderGroup: Allow to get and remove the headerbars + * debian: Add new symbols + * glade: Add a module so we can handle HdyHeaderGroup + * run: Add glade lib to LD_LIBRARY_PATH. + This makes it simple to test the built version. + * Move glade catalog from data/ to glade/ + Given that there will be more complex widgets lets keep the catalog + and module together. + * glade: Use a custom DTD. + Glades DTD is not up to date. Use a custom copy until this is fixed + upstream: + https://gitlab.gnome.org/GNOME/glade/merge_requests/11 + We do this as a separate commit so we can revert it once upstream glade is + fixed. + * glade: Support HdyHeaderGroup (Closes: #38) + * debian: Ship glade module + + -- Guido Günther <agx@sigxcpu.org> Fri, 05 Oct 2018 18:32:42 +0200 + +libhandy (0.0.3) experimental; urgency=medium + + [ Adrien Plazas ] + * New HdyTitleBar widget. This coupled with a transparent headerbar + background can work around graphical glitches when animation header bars + in a leaflet. + * column: Add the linear-growth-width property + * glade: Fix the generic name of HdyArrows + * flatpak: Switch the runtime of the example to master. + * column: Add a missing break statement. + * leaflet: Hide children on transition end only when folded. + * leaflet: Init mode transition positions to the final values. + * example: Always show a close button. + * example: Load custom style from CSS resource + * example: Draw the right color for sidebar separators. + * example: Use separators to separate the panels. + * leaflet: Start the child transition only when folded. + + [ Christopher Davis ] + * Add HdyColumn to libhandy.xml for glade. + + [ Heather Ellsworth ] + * Add issue template + + [ Jordan Petridis ] + * leaflet: initialize a variable. + + [ Guido Günther ] + * HdyButton: Chain up to parent on finalize + * gitlab-ci: Fail on compile warnings + * meson: Warn about possible uninitialized variables + * HdyLeaflet: Fix two uninitialized variables + * Update list of compiler warnings from phosh + and fix the fallout. + + -- Guido Günther <agx@sigxcpu.org> Wed, 12 Sep 2018 12:03:54 +0200 + +libhandy (0.0.2) experimental; urgency=medium + + [ Guido Günther ] + * Use source.puri.sm instead of code.puri.sm. + * Add AUTHORS file + * gitlab-ci: Build on Debian buster using provided build-deps. + * arrows: test object construction + * Multiple gtk-doc fixes + * docs: Abort on warnings. + * DialerButton: free letters + + [ Adrien Plazas ] + * dialer: Make the grid visible and forbid show all. + * example: Drop usage of show_all() + * dialer: Add column-spacing and row-spacing props. + * example: Change the grid's spacing and minimum size request. + * flatpak: Allow access to the dconf config dir. + * Replace phone-dial-symbolic by call-start-symbolic. + * column: Fix height for width request. + + -- Guido Günther <agx@sigxcpu.org> Wed, 18 Jul 2018 13:12:10 +0200 + +libhandy (0.0.1) experimental; urgency=medium + + [ Guido Günther ] + * Release 0.0.1 + + [ Adrien Plazas ] + * Add HdyColumn widget + + -- Guido Günther <agx@sigxcpu.org> Sat, 09 Jun 2018 09:12:06 +0200 + +libhandy (0.0~git20180517) unstable; urgency=medium + + * Add an arrows widget. + The widget prints a number of arrows one by one to indicate a sliding + direction. Number of arrows and animation duration are configurable. + * Add symbols file + + -- Guido Günther <agx@sigxcpu.org> Thu, 17 May 2018 15:51:01 +0200 + +libhandy (0.0~git20180429) unstable; urgency=medium + + [ Guido Günther ] + * New git snapshot + * HdyDialer: Emit symbol-clicked signal. This signal is emitted when a + symbol button (numbers or '#' or '*') is clicked. + * HdyDialer: Emit signal when delete button was clicked. + * dialer: Make it simple to clear the stored number. + This also makes sure we don't send multiple number changed events + when nothing changed. + * dialer: Delay number notify. On button press send out the number changed + signal at the very end so listeners can process the button event prior to + the number update event. + + [ Adrien Plazas ] + * leaflet: Refactor homogeneity. + This makes factorizes the homogeneity functions of HdyLeaflet to make + the code a bit shorter. + * build: Add '--c-include=handy.h' GIR options back. + This is necessary for introspection to know the header file to use. + * dialer: Check params of the 'number' prop accessors. + Sanitize the parameters of the 'number' property accessor. This will + warn or misusages of the API at runtime and avoid potential crashes. + * dialer: Style cleanup of the 'number' prop accessors. + Use gchar instead of char, use GNOME style pointer spacing and name the + number parameter 'number'. This is all cosmetic but will make the code + look a bit more GNOME-like. + * example: Drop hardcoded default window size. + This avoid overridding with the one we set in the the .ui file of the + window. + * example: Move window title to .ui file. + This avoid hardcoding values when we can put them in the UI description. + * example-window: Make the default size more phone-like + + [ Bob Ham ] + * dialer: Add "show-action-buttons" property. + Add a new boolean "show-action-buttons" property that specifies + whether the submit and delete buttons are displayed. + + -- Guido Günther <agx@sigxcpu.org> Sun, 29 Apr 2018 12:01:58 +0200 + +libhandy (0.0~git20180402) unstable; urgency=medium + + * Initial release + + -- Guido Günther <agx@sigxcpu.org> Mon, 02 Apr 2018 12:17:44 +0200 diff --git a/subprojects/libhandy/debian/control b/subprojects/libhandy/debian/control new file mode 100644 index 0000000..b414420 --- /dev/null +++ b/subprojects/libhandy/debian/control @@ -0,0 +1,79 @@ +Source: libhandy-1 +Section: libs +Priority: optional +Maintainer: Guido Günther <agx@sigxcpu.org> +Build-Depends: + debhelper-compat (= 12), + dh-sequence-gir, + gtk-doc-tools, + libgirepository1.0-dev, + libgladeui-dev, + libglib2.0-doc, + libgnome-desktop-3-dev, + libgtk-3-doc, + libgtk-3-dev, + libxml2-utils, + meson, + pkg-config, + valac (>= 0.20), +# to run the tests + xvfb <!nocheck>, + xauth <!nocheck>, +Standards-Version: 4.1.3 +Homepage: https://gitlab.gnome.org/GNOME/libhandy +Vcs-Browser: https://salsa.debian.org/DebianOnMobile-team/libhandy +Vcs-Git: https://salsa.debian.org/DebianOnMobile-team/libhandy.git + +Package: libhandy-1-0 +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + ${shlibs:Depends}, +Description: Library with GTK widgets for mobile phones + libhandy provides GTK widgets and GObjects to ease developing + applications for mobile phones. + . + This package contains the shared library. + +Package: libhandy-1-dev +Architecture: any +Multi-Arch: same +Section: libdevel +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + gir1.2-handy-1 (= ${binary:Version}), + libhandy-1-0 (= ${binary:Version}), + libgtk-3-dev, +Recommends: pkg-config +Description: Development files for libhandy + libhandy provides GTK widgets and GObjects to ease developing + applications for mobile phones. + . + This package contains the development files and documentation. + +Package: gir1.2-handy-1 +Architecture: any +Multi-Arch: same +Section: introspection +Depends: + ${gir:Depends}, + ${misc:Depends}, +Description: GObject introspection files for libhandy + libhandy provides GTK widgets and GObjects to ease developing + applications for mobile phones. + . + This package contains the GObject-introspection data in binary typelib format. + +Package: handy-1-examples +Section: x11 +Architecture: any +Depends: ${misc:Depends}, + ${shlibs:Depends}, +Description: Example programs for libhandy + libhandy provides GTK widgets and GObjects to ease developing + applications for mobile phones. + . + This package contains example files and the demonstration program for + libhandy. diff --git a/subprojects/libhandy/debian/copyright b/subprojects/libhandy/debian/copyright new file mode 100644 index 0000000..357e8cd --- /dev/null +++ b/subprojects/libhandy/debian/copyright @@ -0,0 +1,22 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: libhandy +Source: https://gitlab.gnome.org/GNOME/libhandy + +Files: * +Copyright: 2018 Purism SPC +License: LGPL-2.1+ + This package is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/> + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/subprojects/libhandy/debian/docs b/subprojects/libhandy/debian/docs new file mode 100644 index 0000000..edc0071 --- /dev/null +++ b/subprojects/libhandy/debian/docs @@ -0,0 +1 @@ +NEWS diff --git a/subprojects/libhandy/debian/gir1.2-handy-1.install b/subprojects/libhandy/debian/gir1.2-handy-1.install new file mode 100644 index 0000000..ccdda39 --- /dev/null +++ b/subprojects/libhandy/debian/gir1.2-handy-1.install @@ -0,0 +1 @@ +usr/lib/*/girepository-1.0/* diff --git a/subprojects/libhandy/debian/handy-0.0-examples.examples b/subprojects/libhandy/debian/handy-0.0-examples.examples new file mode 100644 index 0000000..2c8fad4 --- /dev/null +++ b/subprojects/libhandy/debian/handy-0.0-examples.examples @@ -0,0 +1 @@ +examples/*.py diff --git a/subprojects/libhandy/debian/handy-0.0-examples.install b/subprojects/libhandy/debian/handy-0.0-examples.install new file mode 100644 index 0000000..bbaef53 --- /dev/null +++ b/subprojects/libhandy/debian/handy-0.0-examples.install @@ -0,0 +1 @@ +usr/bin/handy-0.0-demo diff --git a/subprojects/libhandy/debian/handy-1-examples.install b/subprojects/libhandy/debian/handy-1-examples.install new file mode 100644 index 0000000..e772481 --- /dev/null +++ b/subprojects/libhandy/debian/handy-1-examples.install @@ -0,0 +1 @@ +usr/bin diff --git a/subprojects/libhandy/debian/libhandy-1-0.install b/subprojects/libhandy/debian/libhandy-1-0.install new file mode 100644 index 0000000..5afd80a --- /dev/null +++ b/subprojects/libhandy/debian/libhandy-1-0.install @@ -0,0 +1,2 @@ +usr/lib/*/libhandy-?.so.* +usr/share/locale diff --git a/subprojects/libhandy/debian/libhandy-1-0.symbols b/subprojects/libhandy/debian/libhandy-1-0.symbols new file mode 100644 index 0000000..77c9884 --- /dev/null +++ b/subprojects/libhandy/debian/libhandy-1-0.symbols @@ -0,0 +1,328 @@ +libhandy-1.so.0 libhandy-1-0 #MINVER# + LIBHANDY_1_0@LIBHANDY_1_0 0.0~git20180429 + hdy_action_row_activate@LIBHANDY_1_0 0.0.6 + hdy_action_row_add_prefix@LIBHANDY_1_0 0.0.6 + hdy_action_row_get_activatable_widget@LIBHANDY_1_0 0.0.7 + hdy_action_row_get_icon_name@LIBHANDY_1_0 0.0.6 + hdy_action_row_get_subtitle@LIBHANDY_1_0 0.0.6 + hdy_action_row_get_type@LIBHANDY_1_0 0.0.6 + hdy_action_row_get_use_underline@LIBHANDY_1_0 0.0.6 + hdy_action_row_new@LIBHANDY_1_0 0.0.6 + hdy_action_row_set_activatable_widget@LIBHANDY_1_0 0.0.7 + hdy_action_row_set_icon_name@LIBHANDY_1_0 0.0.6 + hdy_action_row_set_subtitle@LIBHANDY_1_0 0.0.6 + hdy_action_row_set_use_underline@LIBHANDY_1_0 0.0.6 + hdy_application_window_get_type@LIBHANDY_1_0 0.80.0 + hdy_application_window_new@LIBHANDY_1_0 0.80.0 + hdy_avatar_get_icon_name@LIBHANDY_1_0 0.85.0 + hdy_avatar_get_show_initials@LIBHANDY_1_0 0.80.0 + hdy_avatar_get_size@LIBHANDY_1_0 0.80.0 + hdy_avatar_get_text@LIBHANDY_1_0 0.80.0 + hdy_avatar_get_type@LIBHANDY_1_0 0.80.0 + hdy_avatar_new@LIBHANDY_1_0 0.80.0 + hdy_avatar_set_icon_name@LIBHANDY_1_0 0.85.0 + hdy_avatar_set_image_load_func@LIBHANDY_1_0 0.80.0 + hdy_avatar_set_show_initials@LIBHANDY_1_0 0.80.0 + hdy_avatar_set_size@LIBHANDY_1_0 0.80.0 + hdy_avatar_set_text@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_allow_mouse_drag@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_animation_duration@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_interactive@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_n_pages@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_position@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_reveal_duration@LIBHANDY_1_0 0.81.0 + hdy_carousel_get_spacing@LIBHANDY_1_0 0.80.0 + hdy_carousel_get_type@LIBHANDY_1_0 0.80.0 + hdy_carousel_indicator_dots_get_type@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_dots_get_carousel@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_dots_new@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_dots_set_carousel@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_lines_get_type@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_lines_get_carousel@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_lines_new@LIBHANDY_1_0 0.90.0 + hdy_carousel_indicator_lines_set_carousel@LIBHANDY_1_0 0.90.0 + hdy_carousel_insert@LIBHANDY_1_0 0.80.0 + hdy_carousel_new@LIBHANDY_1_0 0.80.0 + hdy_carousel_prepend@LIBHANDY_1_0 0.80.0 + hdy_carousel_reorder@LIBHANDY_1_0 0.80.0 + hdy_carousel_scroll_to@LIBHANDY_1_0 0.80.0 + hdy_carousel_scroll_to_full@LIBHANDY_1_0 0.80.0 + hdy_carousel_set_allow_mouse_drag@LIBHANDY_1_0 0.80.0 + hdy_carousel_set_animation_duration@LIBHANDY_1_0 0.80.0 + hdy_carousel_set_interactive@LIBHANDY_1_0 0.80.0 + hdy_carousel_set_reveal_duration@LIBHANDY_1_0 0.81.0 + hdy_carousel_set_spacing@LIBHANDY_1_0 0.80.0 + hdy_centering_policy_get_type@LIBHANDY_1_0 0.0.10 + hdy_clamp_get_maximum_size@LIBHANDY_1_0 0.82.0 + hdy_clamp_get_tightening_threshold@LIBHANDY_1_0 0.82.0 + hdy_clamp_get_type@LIBHANDY_1_0 0.82.0 + hdy_clamp_new@LIBHANDY_1_0 0.82.0 + hdy_clamp_set_maximum_size@LIBHANDY_1_0 0.82.0 + hdy_clamp_set_tightening_threshold@LIBHANDY_1_0 0.82.0 + hdy_combo_row_bind_model@LIBHANDY_1_0 0.0.6 + hdy_combo_row_bind_name_model@LIBHANDY_1_0 0.0.6 + hdy_combo_row_get_model@LIBHANDY_1_0 0.0.6 + hdy_combo_row_get_selected_index@LIBHANDY_1_0 0.0.7 + hdy_combo_row_get_type@LIBHANDY_1_0 0.0.6 + hdy_combo_row_get_use_subtitle@LIBHANDY_1_0 0.0.10 + hdy_combo_row_new@LIBHANDY_1_0 0.0.6 + hdy_combo_row_set_for_enum@LIBHANDY_1_0 0.0.6 + hdy_combo_row_set_get_name_func@LIBHANDY_1_0 0.0.10 + hdy_combo_row_set_selected_index@LIBHANDY_1_0 0.0.7 + hdy_combo_row_set_use_subtitle@LIBHANDY_1_0 0.0.10 + hdy_deck_get_adjacent_child@LIBHANDY_1_0 0.81.0 + hdy_deck_get_can_swipe_back@LIBHANDY_1_0 0.80.0 + hdy_deck_get_can_swipe_forward@LIBHANDY_1_0 0.80.0 + hdy_deck_get_child_by_name@LIBHANDY_1_0 0.85.0 + hdy_deck_get_homogeneous@LIBHANDY_1_0 0.80.0 + hdy_deck_get_interpolate_size@LIBHANDY_1_0 0.80.0 + hdy_deck_get_transition_duration@LIBHANDY_1_0 0.80.0 + hdy_deck_get_transition_running@LIBHANDY_1_0 0.80.0 + hdy_deck_get_transition_type@LIBHANDY_1_0 0.80.0 + hdy_deck_get_type@LIBHANDY_1_0 0.80.0 + hdy_deck_get_visible_child@LIBHANDY_1_0 0.80.0 + hdy_deck_get_visible_child_name@LIBHANDY_1_0 0.80.0 + hdy_deck_navigate@LIBHANDY_1_0 0.80.0 + hdy_deck_new@LIBHANDY_1_0 0.80.0 + hdy_deck_set_can_swipe_back@LIBHANDY_1_0 0.80.0 + hdy_deck_set_can_swipe_forward@LIBHANDY_1_0 0.80.0 + hdy_deck_set_homogeneous@LIBHANDY_1_0 0.80.0 + hdy_deck_set_interpolate_size@LIBHANDY_1_0 0.80.0 + hdy_deck_set_transition_duration@LIBHANDY_1_0 0.80.0 + hdy_deck_set_transition_type@LIBHANDY_1_0 0.80.0 + hdy_deck_set_visible_child@LIBHANDY_1_0 0.80.0 + hdy_deck_set_visible_child_name@LIBHANDY_1_0 0.80.0 + hdy_deck_transition_type_get_type@LIBHANDY_1_0 0.80.0 + hdy_ease_out_cubic@LIBHANDY_1_0 0.0.11 + hdy_enum_value_object_get_name@LIBHANDY_1_0 0.0.6 + hdy_enum_value_object_get_nick@LIBHANDY_1_0 0.0.6 + hdy_enum_value_object_get_type@LIBHANDY_1_0 0.0.6 + hdy_enum_value_object_get_value@LIBHANDY_1_0 0.0.6 + hdy_enum_value_object_new@LIBHANDY_1_0 0.0.6 + hdy_enum_value_row_name@LIBHANDY_1_0 0.0.6 + hdy_expander_row_add_action@LIBHANDY_1_0 0.81.0 + hdy_expander_row_add_prefix@LIBHANDY_1_0 0.82.0 + hdy_expander_row_get_enable_expansion@LIBHANDY_1_0 0.0.6 + hdy_expander_row_get_expanded@LIBHANDY_1_0 0.0.9 + hdy_expander_row_get_icon_name@LIBHANDY_1_0 0.80.0 + hdy_expander_row_get_show_enable_switch@LIBHANDY_1_0 0.0.6 + hdy_expander_row_get_subtitle@LIBHANDY_1_0 0.80.0 + hdy_expander_row_get_type@LIBHANDY_1_0 0.0.6 + hdy_expander_row_get_use_underline@LIBHANDY_1_0 0.80.0 + hdy_expander_row_new@LIBHANDY_1_0 0.0.6 + hdy_expander_row_set_enable_expansion@LIBHANDY_1_0 0.0.6 + hdy_expander_row_set_expanded@LIBHANDY_1_0 0.0.9 + hdy_expander_row_set_icon_name@LIBHANDY_1_0 0.80.0 + hdy_expander_row_set_show_enable_switch@LIBHANDY_1_0 0.0.6 + hdy_expander_row_set_subtitle@LIBHANDY_1_0 0.80.0 + hdy_expander_row_set_use_underline@LIBHANDY_1_0 0.80.0 + hdy_get_enable_animations@LIBHANDY_1_0 0.0.11 + hdy_header_bar_get_centering_policy@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_custom_title@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_decoration_layout@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_has_subtitle@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_interpolate_size@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_show_close_button@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_subtitle@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_title@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_transition_duration@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_transition_running@LIBHANDY_1_0 0.0.10 + hdy_header_bar_get_type@LIBHANDY_1_0 0.0.10 + hdy_header_bar_new@LIBHANDY_1_0 0.0.10 + hdy_header_bar_pack_end@LIBHANDY_1_0 0.0.10 + hdy_header_bar_pack_start@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_centering_policy@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_custom_title@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_decoration_layout@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_has_subtitle@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_interpolate_size@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_show_close_button@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_subtitle@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_title@LIBHANDY_1_0 0.0.10 + hdy_header_bar_set_transition_duration@LIBHANDY_1_0 0.0.10 + hdy_header_group_add_gtk_header_bar@LIBHANDY_1_0 0.83.0 + hdy_header_group_add_header_bar@LIBHANDY_1_0 0.83.0 + hdy_header_group_add_header_group@LIBHANDY_1_0 0.83.0 + hdy_header_group_child_get_child_type@LIBHANDY_1_0 0.83.0 + hdy_header_group_child_get_gtk_header_bar@LIBHANDY_1_0 0.83.0 + hdy_header_group_child_get_header_bar@LIBHANDY_1_0 0.83.0 + hdy_header_group_child_get_header_group@LIBHANDY_1_0 0.83.0 + hdy_header_group_child_get_type@LIBHANDY_1_0 0.83.0 + hdy_header_group_child_type_get_type@LIBHANDY_1_0 0.83.0 + hdy_header_group_get_children@LIBHANDY_1_0 0.83.0 + hdy_header_group_get_decorate_all@LIBHANDY_1_0 0.83.0 + hdy_header_group_get_type@LIBHANDY_1_0 0.0.3 + hdy_header_group_new@LIBHANDY_1_0 0.0.3 + hdy_header_group_remove_child@LIBHANDY_1_0 0.83.0 + hdy_header_group_remove_gtk_header_bar@LIBHANDY_1_0 0.83.0 + hdy_header_group_remove_header_bar@LIBHANDY_1_0 0.83.0 + hdy_header_group_remove_header_group@LIBHANDY_1_0 0.83.0 + hdy_header_group_set_decorate_all@LIBHANDY_1_0 0.83.0 + hdy_init@LIBHANDY_1_0 0.82.0 + hdy_keypad_get_column_spacing@LIBHANDY_1_0 0.81.0 + hdy_keypad_get_end_action@LIBHANDY_1_0 0.85.0 + hdy_keypad_get_entry@LIBHANDY_1_0 0.0.12 + hdy_keypad_get_letters_visible@LIBHANDY_1_0 0.85.0 + hdy_keypad_get_row_spacing@LIBHANDY_1_0 0.81.0 + hdy_keypad_get_start_action@LIBHANDY_1_0 0.85.0 + hdy_keypad_get_symbols_visible@LIBHANDY_1_0 0.85.0 + hdy_keypad_get_type@LIBHANDY_1_0 0.0.12 + hdy_keypad_new@LIBHANDY_1_0 0.0.12 + hdy_keypad_set_column_spacing@LIBHANDY_1_0 0.81.0 + hdy_keypad_set_end_action@LIBHANDY_1_0 0.85.0 + hdy_keypad_set_entry@LIBHANDY_1_0 0.0.12 + hdy_keypad_set_letters_visible@LIBHANDY_1_0 0.85.0 + hdy_keypad_set_row_spacing@LIBHANDY_1_0 0.81.0 + hdy_keypad_set_start_action@LIBHANDY_1_0 0.85.0 + hdy_keypad_set_symbols_visible@LIBHANDY_1_0 0.85.0 + hdy_leaflet_get_adjacent_child@LIBHANDY_1_0 0.81.0 + hdy_leaflet_get_can_swipe_back@LIBHANDY_1_0 0.0.12 + hdy_leaflet_get_can_swipe_forward@LIBHANDY_1_0 0.0.12 + hdy_leaflet_get_child_by_name@LIBHANDY_1_0 0.85.0 + hdy_leaflet_get_child_transition_duration@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_child_transition_running@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_folded@LIBHANDY_1_0 0.80.0 + hdy_leaflet_get_homogeneous@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_interpolate_size@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_mode_transition_duration@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_transition_type@LIBHANDY_1_0 0.0.12 + hdy_leaflet_get_type@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_visible_child@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_get_visible_child_name@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_navigate@LIBHANDY_1_0 0.80.0 + hdy_leaflet_new@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_set_can_swipe_back@LIBHANDY_1_0 0.0.12 + hdy_leaflet_set_can_swipe_forward@LIBHANDY_1_0 0.0.12 + hdy_leaflet_set_child_transition_duration@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_set_homogeneous@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_set_interpolate_size@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_set_mode_transition_duration@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_set_transition_type@LIBHANDY_1_0 0.0.12 + hdy_leaflet_set_visible_child@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_set_visible_child_name@LIBHANDY_1_0 0.0~git20180429 + hdy_leaflet_transition_type_get_type@LIBHANDY_1_0 0.0.12 + hdy_navigation_direction_get_type@LIBHANDY_1_0 0.9.9 + hdy_preferences_group_get_description@LIBHANDY_1_0 0.0.10 + hdy_preferences_group_get_title@LIBHANDY_1_0 0.0.10 + hdy_preferences_group_get_type@LIBHANDY_1_0 0.0.10 + hdy_preferences_group_new@LIBHANDY_1_0 0.0.10 + hdy_preferences_group_set_description@LIBHANDY_1_0 0.0.10 + hdy_preferences_group_set_title@LIBHANDY_1_0 0.0.10 + hdy_preferences_page_get_icon_name@LIBHANDY_1_0 0.0.10 + hdy_preferences_page_get_title@LIBHANDY_1_0 0.0.10 + hdy_preferences_page_get_type@LIBHANDY_1_0 0.0.10 + hdy_preferences_page_new@LIBHANDY_1_0 0.0.10 + hdy_preferences_page_set_icon_name@LIBHANDY_1_0 0.0.10 + hdy_preferences_page_set_title@LIBHANDY_1_0 0.0.10 + hdy_preferences_row_get_title@LIBHANDY_1_0 0.0.10 + hdy_preferences_row_get_type@LIBHANDY_1_0 0.0.10 + hdy_preferences_row_get_use_underline@LIBHANDY_1_0 0.0.10 + hdy_preferences_row_new@LIBHANDY_1_0 0.0.10 + hdy_preferences_row_set_title@LIBHANDY_1_0 0.0.10 + hdy_preferences_row_set_use_underline@LIBHANDY_1_0 0.0.10 + hdy_preferences_window_close_subpage@LIBHANDY_1_0 0.85.0 + hdy_preferences_window_get_can_swipe_back@LIBHANDY_1_0 0.85.0 + hdy_preferences_window_get_search_enabled@LIBHANDY_1_0 0.80.0 + hdy_preferences_window_get_type@LIBHANDY_1_0 0.0.10 + hdy_preferences_window_new@LIBHANDY_1_0 0.0.10 + hdy_preferences_window_present_subpage@LIBHANDY_1_0 0.85.0 + hdy_preferences_window_set_can_swipe_back@LIBHANDY_1_0 0.85.0 + hdy_preferences_window_set_search_enabled@LIBHANDY_1_0 0.80.0 + hdy_search_bar_connect_entry@LIBHANDY_1_0 0.0.6 + hdy_search_bar_get_search_mode@LIBHANDY_1_0 0.0.6 + hdy_search_bar_get_show_close_button@LIBHANDY_1_0 0.0.6 + hdy_search_bar_get_type@LIBHANDY_1_0 0.0.6 + hdy_search_bar_handle_event@LIBHANDY_1_0 0.0.6 + hdy_search_bar_new@LIBHANDY_1_0 0.0.6 + hdy_search_bar_set_search_mode@LIBHANDY_1_0 0.0.6 + hdy_search_bar_set_show_close_button@LIBHANDY_1_0 0.0.6 + hdy_squeezer_get_child_enabled@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_homogeneous@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_interpolate_size@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_transition_duration@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_transition_running@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_transition_type@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_type@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_visible_child@LIBHANDY_1_0 0.0.10 + hdy_squeezer_get_xalign@LIBHANDY_1_0 0.85.0 + hdy_squeezer_get_yalign@LIBHANDY_1_0 0.85.0 + hdy_squeezer_new@LIBHANDY_1_0 0.0.10 + hdy_squeezer_set_child_enabled@LIBHANDY_1_0 0.0.10 + hdy_squeezer_set_homogeneous@LIBHANDY_1_0 0.0.10 + hdy_squeezer_set_interpolate_size@LIBHANDY_1_0 0.0.10 + hdy_squeezer_set_transition_duration@LIBHANDY_1_0 0.0.10 + hdy_squeezer_set_transition_type@LIBHANDY_1_0 0.0.10 + hdy_squeezer_set_xalign@LIBHANDY_1_0 0.85.0 + hdy_squeezer_set_yalign@LIBHANDY_1_0 0.85.0 + hdy_squeezer_transition_type_get_type@LIBHANDY_1_0 0.0.10 + hdy_swipe_group_add_swipeable@LIBHANDY_1_0 0.0.12 + hdy_swipe_group_get_swipeables@LIBHANDY_1_0 0.0.12 + hdy_swipe_group_get_type@LIBHANDY_1_0 0.0.12 + hdy_swipe_group_new@LIBHANDY_1_0 0.0.12 + hdy_swipe_group_remove_swipeable@LIBHANDY_1_0 0.0.12 + hdy_swipe_tracker_get_allow_mouse_drag@LIBHANDY_1_0 0.0.12 + hdy_swipe_tracker_get_enabled@LIBHANDY_1_0 0.0.11 + hdy_swipe_tracker_get_reversed@LIBHANDY_1_0 0.0.11 + hdy_swipe_tracker_get_swipeable@LIBHANDY_1_0 0.82.0 + hdy_swipe_tracker_get_type@LIBHANDY_1_0 0.0.11 + hdy_swipe_tracker_new@LIBHANDY_1_0 0.0.11 + hdy_swipe_tracker_set_allow_mouse_drag@LIBHANDY_1_0 0.0.12 + hdy_swipe_tracker_set_enabled@LIBHANDY_1_0 0.0.11 + hdy_swipe_tracker_set_reversed@LIBHANDY_1_0 0.0.11 + hdy_swipe_tracker_shift_position@LIBHANDY_1_0 0.81.0 + hdy_swipeable_emit_child_switched@LIBHANDY_1_0 0.82.0 + hdy_swipeable_get_cancel_progress@LIBHANDY_1_0 0.81.0 + hdy_swipeable_get_distance@LIBHANDY_1_0 0.81.0 + hdy_swipeable_get_progress@LIBHANDY_1_0 0.81.0 + hdy_swipeable_get_snap_points@LIBHANDY_1_0 0.81.0 + hdy_swipeable_get_swipe_area@LIBHANDY_1_0 0.82.0 + hdy_swipeable_get_swipe_tracker@LIBHANDY_1_0 0.82.0 + hdy_swipeable_get_type@LIBHANDY_1_0 0.0.12 + hdy_swipeable_switch_child@LIBHANDY_1_0 0.0.12 + hdy_title_bar_get_selection_mode@LIBHANDY_1_0 0.0.3 + hdy_title_bar_get_type@LIBHANDY_1_0 0.0.3 + hdy_title_bar_new@LIBHANDY_1_0 0.0.3 + hdy_title_bar_set_selection_mode@LIBHANDY_1_0 0.0.3 + hdy_value_object_copy_value@LIBHANDY_1_0 0.0.8 + hdy_value_object_dup_string@LIBHANDY_1_0 0.0.8 + hdy_value_object_get_string@LIBHANDY_1_0 0.0.8 + hdy_value_object_get_type@LIBHANDY_1_0 0.0.8 + hdy_value_object_get_value@LIBHANDY_1_0 0.0.8 + hdy_value_object_new@LIBHANDY_1_0 0.0.8 + hdy_value_object_new_collect@LIBHANDY_1_0 0.0.8 + hdy_value_object_new_string@LIBHANDY_1_0 0.0.8 + hdy_value_object_new_take_string@LIBHANDY_1_0 0.0.8 + hdy_view_switcher_bar_get_policy@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_get_reveal@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_get_stack@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_get_type@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_new@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_set_policy@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_set_reveal@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_bar_set_stack@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_get_narrow_ellipsize@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_get_policy@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_get_stack@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_get_type@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_new@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_policy_get_type@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_set_narrow_ellipsize@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_set_policy@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_set_stack@LIBHANDY_1_0 0.0.10 + hdy_view_switcher_title_get_policy@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_get_stack@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_get_subtitle@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_get_title@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_get_title_visible@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_get_type@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_get_view_switcher_enabled@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_new@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_set_policy@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_set_stack@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_set_subtitle@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_set_title@LIBHANDY_1_0 0.80.0 + hdy_view_switcher_title_set_view_switcher_enabled@LIBHANDY_1_0 0.80.0 + hdy_window_get_type@LIBHANDY_1_0 0.80.0 + hdy_window_handle_get_type@LIBHANDY_1_0 0.80.0 + hdy_window_handle_new@LIBHANDY_1_0 0.80.0 + hdy_window_new@LIBHANDY_1_0 0.80.0 diff --git a/subprojects/libhandy/debian/libhandy-1-dev.install b/subprojects/libhandy/debian/libhandy-1-dev.install new file mode 100644 index 0000000..9cccd33 --- /dev/null +++ b/subprojects/libhandy/debian/libhandy-1-dev.install @@ -0,0 +1,8 @@ +usr/include/* +usr/lib/*/libhandy-?.so +usr/lib/*/glade/modules/libglade-handy-?.so +usr/lib/*/pkgconfig/* +usr/share/gir-1.0/* +usr/share/glade/catalogs/ +usr/share/gtk-doc/ +usr/share/vala/vapi/ diff --git a/subprojects/libhandy/debian/rules b/subprojects/libhandy/debian/rules new file mode 100755 index 0000000..e1f6d22 --- /dev/null +++ b/subprojects/libhandy/debian/rules @@ -0,0 +1,16 @@ +#!/usr/bin/make -f + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +%: + dh $@ + +override_dh_auto_configure: + dh_auto_configure -- -Dgtk_doc=true + +override_dh_auto_test: + xvfb-run -s -noreset dh_auto_test + +override_dh_makeshlibs: + dh_makeshlibs --package=libhandy-1-0 -- -c2 + diff --git a/subprojects/libhandy/debian/source/format b/subprojects/libhandy/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/subprojects/libhandy/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/subprojects/libhandy/debian/tests/build-test b/subprojects/libhandy/debian/tests/build-test new file mode 100755 index 0000000..434a846 --- /dev/null +++ b/subprojects/libhandy/debian/tests/build-test @@ -0,0 +1,30 @@ +#!/bin/sh +set -eu + +if [ -n "${DEB_HOST_GNU_TYPE:-}" ]; then + CROSS_COMPILE="$DEB_HOST_GNU_TYPE-" +else + CROSS_COMPILE= +fi + +cd "$AUTOPKGTEST_TMP" + +cat <<EOF > handytest.c +#include <gtk/gtk.h> +#include <handy.h> + +int +main (int argc, + char **argv) +{ + gtk_init(&argc, &argv); + hdy_init(); + hdy_keypad_new(FALSE, TRUE); +} +EOF + +${CROSS_COMPILE}gcc -o handytest handytest.c $(${CROSS_COMPILE}pkg-config --cflags --libs libhandy-1) +echo "build ok" +[ -x handytest ] +xvfb-run -a -s "-screen 0 1024x768x24" ./handytest +echo "starts ok" diff --git a/subprojects/libhandy/debian/tests/control b/subprojects/libhandy/debian/tests/control new file mode 100644 index 0000000..595abee --- /dev/null +++ b/subprojects/libhandy/debian/tests/control @@ -0,0 +1,8 @@ +Tests: build-test +Depends: libhandy-1-dev, build-essential, pkg-config, xauth, xvfb +Restrictions: allow-stderr + +Tests: python-gi-test +Depends: gir1.2-handy-1, python3-gi, python3 +Restrictions: allow-stderr + diff --git a/subprojects/libhandy/debian/tests/python-gi-test b/subprojects/libhandy/debian/tests/python-gi-test new file mode 100755 index 0000000..dfb1bf3 --- /dev/null +++ b/subprojects/libhandy/debian/tests/python-gi-test @@ -0,0 +1,13 @@ +#!/usr/bin/python3 +# +# Make sure gobject introspection works + +import gi + +gi.require_version('Handy', '1') +from gi.repository import Handy + +group = Handy.SwipeGroup() + +assert type(group).__name__ == 'SwipeGroup' +assert group.get_swipeables() == [] diff --git a/subprojects/libhandy/doc/build-howto.xml b/subprojects/libhandy/doc/build-howto.xml new file mode 100644 index 0000000..a6f731f --- /dev/null +++ b/subprojects/libhandy/doc/build-howto.xml @@ -0,0 +1,159 @@ +<?xml version="1.0"?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN" + "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [ + <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'"> + <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent"> + %gtkdocentities; +]> + +<refentry id="build-howto"> + <refmeta> + <refentrytitle>Compiling with &package_string;</refentrytitle> + <manvolnum>3</manvolnum> + </refmeta> + + <refnamediv> + <refname>Compiling with &package_string;</refname><refpurpose>Notes on compiling.</refpurpose> + </refnamediv> + + <refsect2> + <title>Building</title> + + <para> + If you need to build <application>&package_string;</application>, get the + source from <ulink type="http" url="&package_url;">here</ulink> and see + the <literal>README.md</literal> file. + </para> + </refsect2> + + <refsect2> + <title>Using pkg-config</title> + + <para> Like other GNOME libraries, + <application>&package_string;</application> uses + <application>pkg-config</application> to provide compiler options. The + package name is "<literal>&package_ver_str;</literal>". + </para> + + <para> + If you use Automake/Autoconf, in your <literal>configure.ac</literal> + script, you might specify something like: + </para> + + <informalexample><programlisting> + PKG_CHECK_MODULES(LIBHANDY, [&package_ver_str;]) + AC_SUBST(LIBHANDY_CFLAGS) + AC_SUBST(LIBHANDY_LIBS) + </programlisting></informalexample> + + <para> + Or when using the Meson build system you can declare a dependency like: + </para> + + <informalexample><programlisting> + dependency('&package_ver_str;') + </programlisting></informalexample> + + <para> + The "<literal>&package_api_version;</literal>" in the package name is the + "API version" (indicating "the version of the <application> + &package_string;</application> API that first appeared in version + &package_api_version;") and is essentially just part of the package name. + </para> + </refsect2> + + <refsect2> + <title>Bundling the library</title> + + <para> + As <application>&package_string;</application> uses the Meson build + system, bundling it as a subproject when it is not installed is easy. + Add this to your <literal>meson.build</literal>: + </para> + + <informalexample><programlisting> + &package_string;_dep = dependency('&package_ver_str;', version: '>= &package_version;', required: false) + if not &package_string;_dep.found() + &package_string; = subproject( + '&package_string;', + install: false, + default_options: [ + 'examples=false', + 'package_subdir=my-project-name', + 'tests=false', + ] + ) + &package_string;_dep = &package_string;.get_variable('&package_string;_dep') + endif + </programlisting></informalexample> + + <para> + Then add &package_string; as a git submodule: + </para> + + <informalexample><programlisting> + git submodule add &package_url;.git subprojects/&package_string; + </programlisting></informalexample> + + <para> + To bundle the library with your Flatpak application, add the following + module to your manifest: + </para> + + <informalexample><programlisting> + { + "name" : "&package_string;", + "buildsystem" : "meson", + "builddir" : true, + "config-opts": [ + "-Dexamples=false", + "-Dtests=false" + ], + "sources" : [ + { + "type" : "git", + "url" : "&package_url;.git" + } + ] + } + </programlisting></informalexample> + </refsect2> + + <refsect2> + <title>Building on macOS</title> + + <para> + To build on macOS you need to install the build-dependencies first. This can e.g. be done via <ulink url="https://brew.sh"><literal>brew</literal></ulink>: + </para> + + <informalexample> + <programlisting> + brew install pkg-config gtk+3 adwaita-icon-theme meson glade gobject-introspection vala + </programlisting> + </informalexample> + + <para> + After running the command above, one may now build the library: + </para> + + <informalexample> + <programlisting> + git clone https://gitlab.gnome.org/GNOME/libhandy.git + cd libhandy + meson . _build + ninja -C _build test + ninja -C _build install + </programlisting> + </informalexample> + + <para> + Working with the library on macOS is pretty much the same as on Linux. To link it, use <literal>pkg-config</literal>: + </para> + + <informalexample> + <programlisting> + gcc $(pkg-config --cflags --libs gtk+-3.0) $(pkg-config --cflags --libs libhandy-1) main.c -o main + </programlisting> + </informalexample> + </refsect2> +</refentry> diff --git a/subprojects/libhandy/doc/handy-docs.xml b/subprojects/libhandy/doc/handy-docs.xml new file mode 100644 index 0000000..0bf7eab --- /dev/null +++ b/subprojects/libhandy/doc/handy-docs.xml @@ -0,0 +1,140 @@ +<?xml version="1.0"?> +<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN" + "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" +[ + <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'"> + <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent"> + %gtkdocentities; +]> +<book id="index"> + <bookinfo> + <title>&package_name; Reference Manual</title> + <releaseinfo> + <para>This document is the API reference for &package_name; &package_version;.</para> + <para> + <ulink type="http" url="&package_url;">Handy</ulink> is a library to help you write apps for GTK/GNOME based mobile phones. + </para> + <para> + If you find any issues in this API reference, please report it using + <ulink type="http" url="&package_bugreport;">the bugtracker</ulink>. + </para> + </releaseinfo> + + <copyright> + <year>2017-2020</year> + <holder>Purism SPC</holder> + </copyright> + </bookinfo> + + <chapter id="intro"> + <title>Introduction</title> + + <xi:include href="build-howto.xml"/> + <xi:include href="visual-index.xml"/> + </chapter> + + <chapter id="core-api"> + <title>Widgets and Objects</title> + <xi:include href="xml/hdy-action-row.xml"/> + <xi:include href="xml/hdy-animation.xml"/> + <xi:include href="xml/hdy-application-window.xml"/> + <xi:include href="xml/hdy-avatar.xml"/> + <xi:include href="xml/hdy-carousel.xml"/> + <xi:include href="xml/hdy-carousel-indicator-dots.xml"/> + <xi:include href="xml/hdy-carousel-indicator-lines.xml"/> + <xi:include href="xml/hdy-clamp.xml"/> + <xi:include href="xml/hdy-combo-row.xml"/> + <xi:include href="xml/hdy-deck.xml"/> + <xi:include href="xml/hdy-enum-value-object.xml"/> + <xi:include href="xml/hdy-expander-row.xml"/> + <xi:include href="xml/hdy-header-bar.xml"/> + <xi:include href="xml/hdy-header-group.xml"/> + <xi:include href="xml/hdy-keypad.xml"/> + <xi:include href="xml/hdy-leaflet.xml"/> + <xi:include href="xml/hdy-navigation-direction.xml"/> + <xi:include href="xml/hdy-preferences-group.xml"/> + <xi:include href="xml/hdy-preferences-page.xml"/> + <xi:include href="xml/hdy-preferences-row.xml"/> + <xi:include href="xml/hdy-preferences-window.xml"/> + <xi:include href="xml/hdy-search-bar.xml"/> + <xi:include href="xml/hdy-squeezer.xml"/> + <xi:include href="xml/hdy-swipeable.xml"/> + <xi:include href="xml/hdy-swipe-group.xml"/> + <xi:include href="xml/hdy-swipe-tracker.xml"/> + <xi:include href="xml/hdy-title-bar.xml"/> + <xi:include href="xml/hdy-value-object.xml"/> + <xi:include href="xml/hdy-view-switcher.xml"/> + <xi:include href="xml/hdy-view-switcher-bar.xml"/> + <xi:include href="xml/hdy-view-switcher-title.xml"/> + <xi:include href="xml/hdy-window.xml"/> + <xi:include href="xml/hdy-window-handle.xml"/> + </chapter> + + <chapter id="helpers"> + <title>Helpers</title> + <xi:include href="xml/hdy-version.xml"/> + <xi:include href="xml/hdy-main.xml"/> + </chapter> + + <chapter id="migrating"> + <title>Migrating from Previous Versions of Handy</title> + + <xi:include href="hdy-migrating-0-0-to-1.xml"/> + </chapter> + + <chapter id="object-tree"> + <title>Object Hierarchy</title> + <xi:include href="xml/tree_index.sgml"/> + </chapter> + + <index id="api-index-full"> + <title>API Index</title> + <xi:include href="xml/api-index-full.xml"><xi:fallback /></xi:include> + </index> + + <index id="deprecated-api-index" role="deprecated"> + <title>Index of deprecated API</title> + <xi:include href="xml/api-index-deprecated.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-0-0-6" role="0.0.6"> + <title>Index of new symbols in 0.0.6</title> + <xi:include href="xml/api-index-0.0.6.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-0-0-7" role="0.0.7"> + <title>Index of new symbols in 0.0.7</title> + <xi:include href="xml/api-index-0.0.7.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-0-0-8" role="0.0.8"> + <title>Index of new symbols in 0.0.8</title> + <xi:include href="xml/api-index-0.0.8.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-0-0-10" role="0.0.10"> + <title>Index of new symbols in 0.0.10</title> + <xi:include href="xml/api-index-0.0.10.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-0-0-11" role="0.0.11"> + <title>Index of new symbols in 0.0.11</title> + <xi:include href="xml/api-index-0.0.11.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-0-0-12" role="0.0.12"> + <title>Index of new symbols in 0.0.12</title> + <xi:include href="xml/api-index-0.0.12.xml"><xi:fallback /></xi:include> + </index> + + <index id="api-index-1-0" role="1.0"> + <title>Index of new symbols in 1.0</title> + <xi:include href="xml/api-index-1.0.xml"><xi:fallback /></xi:include> + </index> + + <index id="annotations-glossary"> + <title>Annotations glossary</title> + <xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include> + </index> + +</book> diff --git a/subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml b/subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml new file mode 100644 index 0000000..70b5e15 --- /dev/null +++ b/subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml @@ -0,0 +1,356 @@ +<?xml version="1.0"?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN" + "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [ + <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'"> + <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent"> + %gtkdocentities; +]> + +<refentry id="hdy-migrating-0-0-to-1"> + <refmeta> + <refentrytitle>Migrating from Handy 0.0.x to Handy 1</refentrytitle> + <manvolnum>3</manvolnum> + </refmeta> + + <refnamediv> + <refname>Migrating from Handy 0.0.x to Handy 1</refname><refpurpose>Notes on migration to Handy 1.</refpurpose> + </refnamediv> + + <para> + Handy 1 is a major new version of Handy that breaks both API and ABI + compared to Handy 0.0.x. Thankfully, most of the changes are not hard to + adapt to and there are a number of steps that you can take to prepare your + Handy 0.0.x application for the switch to Handy 1. After that, there's a + number of adjustments that you may have to do when you actually switch your + application to build against Handy 1. + </para> + + <refsect2> + <title>Preparation in Handy 0.0.x</title> + + <para> + The steps outlined in the following sections assume that your application + is working with Handy 0.0.13, which is the final stable release of Handy + 0.0.x. It includes all the necessary APIs and tools to help you port your + application to Handy 1. If you are using an older version of Handy 0.0.x, + you should first get your application to build and work with Handy 0.0.13. + </para> + + <refsect3> + <title>Do not use the static build option</title> + <para> + Static linking support has been removed, and so did the static build + option. + You must adapt you program to link to the library dynamically, using + the package_subdir build option if needed. + </para> + </refsect3> + + <refsect3> + <title>Do not use deprecated symbols</title> + <para> + Over the years, a number of functions, and in some cases, entire widgets + have been deprecated. These deprecations are clearly spelled out in the + API reference, with hints about the recommended replacements. + The API reference for GTK 3 also includes an + <ulink url="https://developer.puri.sm/projects/libhandy/unstable/deprecated-api-index.html">index</ulink> + of all deprecated symbols. + </para> + </refsect3> + + </refsect2> + + <refsect2> + <title>Changes that need to be done at the time of the switch</title> + + <para> + This section outlines porting tasks that you need to tackle when you get + to the point that you actually build your application against Handy 1. + Making it possible to prepare for these in Handy 0.0 would have been + either impossible or impractical. + </para> + + <refsect3> + <title>hdy_init takes no parameters</title> + <para> + hdy_init() has been modified to take no parameters. + It must be called just after initializing GTK, if you are using + #GtkApplication it means it must be called when the + #GApplication::startup signal is emitted. + </para> + <para> + It initializes the localization, the types, the themes, and the icons. + </para> + </refsect3> + + <refsect3> + <title>Adapt to widget constructor changes</title> + <para> + All widget constructors now return the #GtkWidget type rather than the + constructed widget's type, following the same convention as GTK 3. + </para> + <para> + Affected widgets: + #HdyActionRow, #HdyComboRow, #HdyExpanderRow, + #HdyPreferencesGroup, #HdyPreferencesPage, #HdyPreferencesRow, + #HdyPreferencesWindow, #HdySqueezer, #HdyTitleBar, #HdyViewSwitcherBar, + #HdyViewSwitcher + </para> + </refsect3> + + <refsect3> + <title>Adapt to derivability changes</title> + <para> + Some widgets are now final, if your code is deriving from them, use + composition instead. + </para> + <para> + Affected widgets: + #HdySqueezer, #HdyViewSwitcher, #HdyViewSwitcherBar + </para> + </refsect3> + + <refsect3> + <title>HdyFold has been removed</title> + <para> + #HdyFold has been removed. This affects the API of #HdyLeaflet, see the + “Adapt to HdyLeaflet API changes” section to know how. + </para> + </refsect3> + + <refsect3> + <title>Replace HdyColumn by HdyClamp</title> + <para> + HdyColumn has been renamed #HdyClamp as it now implements + #GtkOrientable, so you should replace the former by the later. + Its “maximum-width” and “linear-growth-width” properties have been + renamed #HdyClamp:maximum-size and #HdyClamp:tightening-threshold + respectively to better reflect their role. + It won't set the .narrow, .medium and .wide style classes depending on + its size, but the .small, .medium and .large ones instead. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyPaginator API changes</title> + <para> + HdyPaginator has been renamed HdyCarousel, so you should replace the + former by the later. + </para> + <para> + The “indicator-style”, “indicator-spacing” and “center-content” + properties have been removed, instead use #HdyCarouselIndicatorDots + or #HdyCarouselIndicatorLines widgets. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyHeaderGroup API changes</title> + <para> + The #HdyHeaderGroup object has been largely redesigned, most of its + methods changed, see its documentation to know more. + </para> + <para> + The child type is now #HdyHeaderGroupChild, which can represent either + a #GtkHeaderBar, a #HdyHeaderBar, or a #HdyHeaderGroup. + </para> + <para> + The “focus” property has been replaced by #HdyHeaderGroup:decorate-all, + which works quite differently. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyLeaflet API changes</title> + <para> + The #HdyFold type has been removed in favor of using a boolean, and + #HdyLeaflet adjusted to that as the #HdyLeaflet:fold property has been + removed in favor of #HdyLeaflet:folded. + Also, the hdy_leaflet_get_homogeneous() and + hdy_leaflet_set_homogeneous() accessors take a boolean parameter instead + of a #HdyFold. + </para> + <para> + On touchscreens, swiping forward with the “over” transition and swiping + back with the “under” transition can now only be done from the edge + where the upper child is. + </para> + <para> + 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. + </para> + <para> + The “allow-visible” child property has been renamed “navigatable”. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyLeaflet API changes</title> + <para> + The “none” transition type has been removed. The default value for the + #HdyLeaflet:transition-type property has been changed to “over”. + “over” is the recommended transition for typical #HdyLeaflet use-cases, + if this isn't what you want to use, be sure to adapt your code. If + transitions are undesired, set #HdyLeaflet:mode-transition-duration and + #HdyLeaflet:child-transition-duration properties to 0. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyViewSwitcher API changes</title> + <para> + #HdyViewSwitcher doesn't subclass #GtkBox anymore. Instead, it + subclasses #GtkBin and contains a box. + </para> + <para> + The “icon-size” property has been dropped without replacement, you must + stop using it. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyViewSwitcherBar API changes</title> + <para> + #HdyViewSwitcherBar won't be revealed if the #HdyViewSwitcherBar:stack + property is %NULL or if it has less than two pages, even if you set + #HdyViewSwitcherBar:reveal to %TRUE. + </para> + <para> + The “icon-size” property has been dropped without replacement, you must + stop using it. + </para> + </refsect3> + + <refsect3> + <title>Adapt to CSS node name changes</title> + <para> + Widgets with a customn CSS node name got their name changed to be the + class' name in lowercase, with no separation between words, and with no + namespace prefix. E.g. the CSS node name of HdyViewSwitcher is + viewswitcher. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyActionRow API changes</title> + <para> + Action items were packed from the end toward the start of the row. It is + now reversed, and widgets have to be packed from the start to the end. + </para> + <para> + It isn't possible to add children at the bottom of a #HdyActionRow + anymore, instead use other widgets like #HdyExpanderRow. + Widgets added to a #HdyActionRow will now be added at the end of the + row, and the hdy_action_row_add_action() method and the action child + type have been removed. + </para> + <para> + The main horizontal box of #HdyActionRow had the row-header CSS style + class, it now has the header CSS style class and can hence be accessed + as box.header subnode. + </para> + <para> + #HdyActionRow is now unactivatable by default, giving it an activatable + widget will automatically make it activatable. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyComboRow API changes</title> + <para> + #HdyComboRow is now unactivatable by default, binding and unbinding a + model will toggle its activatability. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyExpanderRow API changes</title> + <para> + #HdyExpanderRow doesn't descend from #HdyActionRow anymore but from + #HdyPreferencesRow. + It reimplement some features from #HdyActionRow, like the + #HdyExpanderRow:title, #HdyExpanderRow:subtitle, + #HdyExpanderRow:use-underline and #HdyExpanderRow:icon-name, but it + doesn't offer the “activate” signal nor the ability to add widgets in + its header row. + </para> + <para> + Widgets you add to it will be added to its inner #GtkListBox. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyPreferencesPage API changes</title> + <para> + #HdyPreferencesPage doesn't subclass #GtkScrolledWindow anymore. + Instead, it subclasses #GtkBin and contains a scrolled window. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyPreferencesGroup API changes</title> + <para> + #HdyPreferencesGroup doesn't subclass #GtkBox anymore. + Instead, it subclasses #GtkBin and contains a box. + </para> + </refsect3> + + <refsect3> + <title>Adapt to HdyKeypad API changes</title> + <para> + #HdyKeypad doesn't subclass #Gtkgrid anymore. Instead, it subclasses + #GtkBin and contains a grid. + </para> + <para> + The “show-symbols” property has been replaced by + #HdyHeaderGroup:letters-visible. + </para> + <para> + The “only-digits” property has been replaced by + #HdyHeaderGroup:symbols-visible, which has a inverse boolean meaning. + This also affects the corresponding parameter of the constructor. + </para> + <para> + The “left-action” property has been replaced by + #HdyHeaderGroup:start-action, and the “right-action” property has been + replaced by #HdyHeaderGroup:end-action. + </para> + <para> + The “entry” property isn't a #GtkWidget anymore but a #GtkEntry. + </para> + </refsect3> + + <refsect3> + <title>Stop using hdy_list_box_separator_header()</title> + <para> + Instead, either use CSS styling (the list.content style class may + fit your need), or implement it yourself as it is trivial. + </para> + </refsect3> + + <refsect3> + <title>Stop acknowledging the Instability</title> + <para> + When the library was young and changing a lot, we required you to + acknowledge that your are using an unstable API. To do so, you had to + define <literal>HANDY_USE_UNSTABLE_API</literal> for compilation to + succeed. + </para> + <para> + The API remained stable since many versions, despite this acknowlegment + still being required. To reflect that proven stability, the + acknowlegment isn't necessary and you can stop defining + <literal>HANDY_USE_UNSTABLE_API</literal>, either before including the + &package_string; header in C-compatible languages, or with the + definition option of your compiler. + </para> + </refsect3> + + </refsect2> + +</refentry> + diff --git a/subprojects/libhandy/doc/images/avatar.png b/subprojects/libhandy/doc/images/avatar.png Binary files differnew file mode 100644 index 0000000..07a156f --- /dev/null +++ b/subprojects/libhandy/doc/images/avatar.png diff --git a/subprojects/libhandy/doc/images/header-bar.png b/subprojects/libhandy/doc/images/header-bar.png Binary files differnew file mode 100644 index 0000000..c15b1f6 --- /dev/null +++ b/subprojects/libhandy/doc/images/header-bar.png diff --git a/subprojects/libhandy/doc/images/keypad.png b/subprojects/libhandy/doc/images/keypad.png Binary files differnew file mode 100644 index 0000000..6bc18d5 --- /dev/null +++ b/subprojects/libhandy/doc/images/keypad.png diff --git a/subprojects/libhandy/doc/images/list.png b/subprojects/libhandy/doc/images/list.png Binary files differnew file mode 100644 index 0000000..e02833c --- /dev/null +++ b/subprojects/libhandy/doc/images/list.png diff --git a/subprojects/libhandy/doc/images/preferences-window.png b/subprojects/libhandy/doc/images/preferences-window.png Binary files differnew file mode 100644 index 0000000..bd696a7 --- /dev/null +++ b/subprojects/libhandy/doc/images/preferences-window.png diff --git a/subprojects/libhandy/doc/images/search.png b/subprojects/libhandy/doc/images/search.png Binary files differnew file mode 100644 index 0000000..38d14ef --- /dev/null +++ b/subprojects/libhandy/doc/images/search.png diff --git a/subprojects/libhandy/doc/images/view-switcher-bar.png b/subprojects/libhandy/doc/images/view-switcher-bar.png Binary files differnew file mode 100644 index 0000000..3a0836d --- /dev/null +++ b/subprojects/libhandy/doc/images/view-switcher-bar.png diff --git a/subprojects/libhandy/doc/images/view-switcher.png b/subprojects/libhandy/doc/images/view-switcher.png Binary files differnew file mode 100644 index 0000000..b8727df --- /dev/null +++ b/subprojects/libhandy/doc/images/view-switcher.png diff --git a/subprojects/libhandy/doc/meson.build b/subprojects/libhandy/doc/meson.build new file mode 100644 index 0000000..eeab57a --- /dev/null +++ b/subprojects/libhandy/doc/meson.build @@ -0,0 +1,75 @@ +if get_option('gtk_doc') + +subdir('xml') + +private_headers = [ + 'config.h', + 'gtkprogresstrackerprivate.h', + 'gtk-window-private.h', + 'hdy-animation-private.h', + 'hdy-carousel-box-private.h', + 'hdy-css-private.h', + 'hdy-enums.h', + 'hdy-enums-private.h', + 'hdy-main-private.h', + 'hdy-nothing-private.h', + 'hdy-keypad-button-private.h', + 'hdy-preferences-group-private.h', + 'hdy-preferences-page-private.h', + 'hdy-shadow-helper-private.h', + 'hdy-stackable-box-private.h', + 'hdy-swipe-tracker-private.h', + 'hdy-types.h', + 'hdy-view-switcher-button-private.h', + 'hdy-window-handle-controller-private.h', + 'hdy-window-mixin-private.h', +] + +images = [ + 'images/avatar.png', + 'images/header-bar.png', + 'images/keypad.png', + 'images/list.png', + 'images/preferences-window.png', + 'images/search.png', + 'images/view-switcher.png', + 'images/view-switcher-bar.png', +] + +content_files = [ + 'build-howto.xml', + 'hdy-migrating-0-0-to-1.xml', + 'visual-index.xml', +] + +glib_prefix = dependency('glib-2.0').get_pkgconfig_variable('prefix') +glib_docpath = glib_prefix / 'share' / 'gtk-doc' / 'html' +docpath = get_option('datadir') / 'gtk-doc' / 'html' + +gnome.gtkdoc('libhandy', + main_xml: 'handy-docs.xml', + src_dir: [ + meson.source_root() / 'src', + meson.build_root() / 'src', + ], + dependencies: libhandy_dep, + gobject_typesfile: 'libhandy.types', + scan_args: [ + '--rebuild-types', + '--ignore-headers=' + ' '.join(private_headers), + ], + fixxref_args: [ + '--html-dir=@0@'.format(docpath), + '--extra-dir=@0@'.format(glib_docpath / 'glib'), + '--extra-dir=@0@'.format(glib_docpath / 'gobject'), + '--extra-dir=@0@'.format(glib_docpath / 'gio'), + '--extra-dir=@0@'.format(glib_docpath / 'gi'), + '--extra-dir=@0@'.format(glib_docpath / 'gtk3'), + '--extra-dir=@0@'.format(glib_docpath / 'gdk-pixbuf'), + ], + install_dir: 'libhandy-' + apiversion, + content_files: content_files, + html_assets: images, + install: true) + +endif diff --git a/subprojects/libhandy/doc/visual-index.xml b/subprojects/libhandy/doc/visual-index.xml new file mode 100644 index 0000000..1648608 --- /dev/null +++ b/subprojects/libhandy/doc/visual-index.xml @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN" + "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [ + <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'"> + <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent"> + %gtkdocentities; +]> + +<refentry id="visual-index"> + <refmeta> + <refentrytitle>Visual index</refentrytitle> + <manvolnum>3</manvolnum> + </refmeta> + <refnamediv> + <refname>Widgets in &package_string;</refname><refpurpose>Widget overview.</refpurpose> + </refnamediv> + + <refsect2> + <title>Widgets</title> + <para role="gallery"> + <link linkend="HdyAvatar"> + <inlinegraphic fileref="avatar.png" format="PNG"></inlinegraphic> + </link> + </para> + <para role="gallery"> + <link linkend="HdyKeypad"> + <inlinegraphic fileref="keypad.png" format="PNG"></inlinegraphic> + </link> + </para> + <para role="gallery"> + <link> + <inlinegraphic fileref="list.png" format="PNG" scale="60"></inlinegraphic> + </link> + <link linkend="HdySearchBar"> + <inlinegraphic fileref="search.png" format="PNG"></inlinegraphic> + </link> + </para> + <para role="gallery"> + <link linkend="HdyHeaderBar"> + <inlinegraphic fileref="header-bar.png" format="PNG"></inlinegraphic> + </link> + <link linkend="HdyPreferencesWindow"> + <inlinegraphic fileref="preferences-window.png" format="PNG"></inlinegraphic> + </link> + </para> + </refsect2> + <refsect2> + <title>HdyViewSwitcher</title> + <para role="gallery"> + <link linkend="HdyViewSwitcher"> + <inlinegraphic fileref="view-switcher.png" format="PNG"></inlinegraphic> + </link> + <link linkend="HdyViewSwitcherBar"> + <inlinegraphic fileref="view-switcher-bar.png" format="PNG"></inlinegraphic> + </link> + </para> + </refsect2> +</refentry> diff --git a/subprojects/libhandy/doc/xml/gtkdocentities.ent.in b/subprojects/libhandy/doc/xml/gtkdocentities.ent.in new file mode 100644 index 0000000..45d322c --- /dev/null +++ b/subprojects/libhandy/doc/xml/gtkdocentities.ent.in @@ -0,0 +1,9 @@ +<!ENTITY package "@PACKAGE@"> +<!ENTITY package_bugreport "@PACKAGE_BUGREPORT@"> +<!ENTITY package_name "@PACKAGE_NAME@"> +<!ENTITY package_string "@PACKAGE_STRING@"> +<!ENTITY package_tarname "@PACKAGE_TARNAME@"> +<!ENTITY package_url "@PACKAGE_URL@"> +<!ENTITY package_version "@PACKAGE_VERSION@"> +<!ENTITY package_api_version "@PACKAGE_API_VERSION@"> +<!ENTITY package_ver_str "@PACKAGE_API_NAME@"> diff --git a/subprojects/libhandy/doc/xml/meson.build b/subprojects/libhandy/doc/xml/meson.build new file mode 100644 index 0000000..5f73097 --- /dev/null +++ b/subprojects/libhandy/doc/xml/meson.build @@ -0,0 +1,12 @@ +ent_conf = configuration_data() +ent_conf.set('PACKAGE', 'Handy') +ent_conf.set('PACKAGE_BUGREPORT', 'https://gitlab.gnome.org/GNOME/libhandy/issues') +ent_conf.set('PACKAGE_NAME', 'Handy') +ent_conf.set('PACKAGE_STRING', 'libhandy') +ent_conf.set('PACKAGE_TARNAME', 'libhandy-' + meson.project_version()) +ent_conf.set('PACKAGE_URL', 'https://gitlab.gnome.org/GNOME/libhandy') +ent_conf.set('PACKAGE_VERSION', meson.project_version()) +ent_conf.set('PACKAGE_API_VERSION', apiversion) +ent_conf.set('PACKAGE_API_NAME', package_api_name) +configure_file(input: 'gtkdocentities.ent.in', output: 'gtkdocentities.ent', configuration: ent_conf) + diff --git a/subprojects/libhandy/examples/example.py b/subprojects/libhandy/examples/example.py new file mode 100755 index 0000000..2a6f27a --- /dev/null +++ b/subprojects/libhandy/examples/example.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +import gi + +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +gi.require_version('Handy', '1') +from gi.repository import Handy +import sys + + +window = Gtk.Window(title = "Keypad Example with Python") +vbox = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) +entry = Gtk.Entry() +keypad = Handy.Keypad() + +vbox.add(entry) # widget to show dialed number +vbox.add(keypad) +vbox.set_halign(Gtk.Align.CENTER) +vbox.set_valign(Gtk.Align.CENTER) + +vbox.props.margin = 18 +vbox.props.spacing = 18 +keypad.set_row_spacing(6) +keypad.set_column_spacing(6) + +keypad.set_entry(entry) # attach the entry widget + +window.connect("destroy", Gtk.main_quit) +window.add(vbox) +window.show_all() +Gtk.main() diff --git a/subprojects/libhandy/examples/handy-demo.c b/subprojects/libhandy/examples/handy-demo.c new file mode 100644 index 0000000..052ce4c --- /dev/null +++ b/subprojects/libhandy/examples/handy-demo.c @@ -0,0 +1,65 @@ +#include <gtk/gtk.h> +#include <handy.h> + +#include "hdy-demo-preferences-window.h" +#include "hdy-demo-window.h" + +static void +show_preferences (GSimpleAction *action, + GVariant *state, + gpointer user_data) +{ + GtkApplication *app = GTK_APPLICATION (user_data); + GtkWindow *window = gtk_application_get_active_window (app); + HdyDemoPreferencesWindow *preferences = hdy_demo_preferences_window_new (); + + gtk_window_set_transient_for (GTK_WINDOW (preferences), window); + gtk_widget_show (GTK_WIDGET (preferences)); +} + +static void +startup (GtkApplication *app) +{ + GtkCssProvider *css_provider = gtk_css_provider_new (); + + hdy_init (); + + gtk_css_provider_load_from_resource (css_provider, "/sm/puri/Handy/Demo/ui/style.css"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + g_object_unref (css_provider); +} + +static void +show_window (GtkApplication *app) +{ + HdyDemoWindow *window; + + window = hdy_demo_window_new (app); + + gtk_widget_show (GTK_WIDGET (window)); +} + +int +main (int argc, + char **argv) +{ + GtkApplication *app; + int status; + static GActionEntry app_entries[] = { + { "preferences", show_preferences, NULL, NULL, NULL }, + }; + + app = gtk_application_new ("sm.puri.Handy.Demo", G_APPLICATION_FLAGS_NONE); + g_action_map_add_action_entries (G_ACTION_MAP (app), + app_entries, G_N_ELEMENTS (app_entries), + app); + g_signal_connect (app, "startup", G_CALLBACK (startup), NULL); + g_signal_connect (app, "activate", G_CALLBACK (show_window), NULL); + status = g_application_run (G_APPLICATION (app), argc, argv); + g_object_unref (app); + + return status; +} diff --git a/subprojects/libhandy/examples/handy-demo.gresources.xml b/subprojects/libhandy/examples/handy-demo.gresources.xml new file mode 100644 index 0000000..e0825c1 --- /dev/null +++ b/subprojects/libhandy/examples/handy-demo.gresources.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/sm/puri/Handy/Demo"> + <file preprocess="xml-stripblanks">icons/dark-mode-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg</file> + <file preprocess="xml-stripblanks">icons/gesture-touchscreen-swipe-back-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/gesture-touchpad-swipe-back-symbolic-rtl.svg</file> + <file preprocess="xml-stripblanks">icons/gesture-touchpad-swipe-back-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/gnome-smartphone-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/light-mode-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-carousel-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-clamp-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-deck-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-keypad-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-leaflet-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-list-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-search-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-view-switcher-symbolic.svg</file> + <file preprocess="xml-stripblanks">icons/widget-window-symbolic.svg</file> + </gresource> + <gresource prefix="/sm/puri/Handy/Demo/ui"> + <file preprocess="xml-stripblanks">hdy-demo-preferences-window.ui</file> + <file preprocess="xml-stripblanks">hdy-demo-window.ui</file> + <file preprocess="xml-stripblanks">hdy-view-switcher-demo-window.ui</file> + <file compressed="true">style.css</file> + </gresource> +</gresources> diff --git a/subprojects/libhandy/examples/hdy-demo-preferences-window.c b/subprojects/libhandy/examples/hdy-demo-preferences-window.c new file mode 100644 index 0000000..4481664 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-demo-preferences-window.c @@ -0,0 +1,56 @@ +#include "hdy-demo-preferences-window.h" + +struct _HdyDemoPreferencesWindow +{ + HdyPreferencesWindow parent_instance; + + GtkWidget *subpage1; + GtkWidget *subpage2; +}; + +G_DEFINE_TYPE (HdyDemoPreferencesWindow, hdy_demo_preferences_window, HDY_TYPE_PREFERENCES_WINDOW) + +HdyDemoPreferencesWindow * +hdy_demo_preferences_window_new (void) +{ + return g_object_new (HDY_TYPE_DEMO_PREFERENCES_WINDOW, NULL); +} + +static void +return_to_preferences_cb (HdyDemoPreferencesWindow *self) +{ + hdy_preferences_window_close_subpage (HDY_PREFERENCES_WINDOW (self)); +} + +static void +subpage1_activated_cb (HdyDemoPreferencesWindow *self) +{ + hdy_preferences_window_present_subpage (HDY_PREFERENCES_WINDOW (self), self->subpage1); +} + +static void +subpage2_activated_cb (HdyDemoPreferencesWindow *self) +{ + hdy_preferences_window_present_subpage (HDY_PREFERENCES_WINDOW (self), self->subpage2); +} + +static void +hdy_demo_preferences_window_class_init (HdyDemoPreferencesWindowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-demo-preferences-window.ui"); + + gtk_widget_class_bind_template_child (widget_class, HdyDemoPreferencesWindow, subpage1); + gtk_widget_class_bind_template_child (widget_class, HdyDemoPreferencesWindow, subpage2); + + gtk_widget_class_bind_template_callback (widget_class, return_to_preferences_cb); + gtk_widget_class_bind_template_callback (widget_class, subpage1_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, subpage2_activated_cb); +} + +static void +hdy_demo_preferences_window_init (HdyDemoPreferencesWindow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} diff --git a/subprojects/libhandy/examples/hdy-demo-preferences-window.h b/subprojects/libhandy/examples/hdy-demo-preferences-window.h new file mode 100644 index 0000000..accb7b5 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-demo-preferences-window.h @@ -0,0 +1,13 @@ +#pragma once + +#include <handy.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_DEMO_PREFERENCES_WINDOW (hdy_demo_preferences_window_get_type()) + +G_DECLARE_FINAL_TYPE (HdyDemoPreferencesWindow, hdy_demo_preferences_window, HDY, DEMO_PREFERENCES_WINDOW, HdyPreferencesWindow) + +HdyDemoPreferencesWindow *hdy_demo_preferences_window_new (void); + +G_END_DECLS diff --git a/subprojects/libhandy/examples/hdy-demo-preferences-window.ui b/subprojects/libhandy/examples/hdy-demo-preferences-window.ui new file mode 100644 index 0000000..83c83a9 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-demo-preferences-window.ui @@ -0,0 +1,256 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.0"/> + <template class="HdyDemoPreferencesWindow" parent="HdyPreferencesWindow"> + <property name="can-swipe-back">True</property> + <child> + <object class="HdyPreferencesPage"> + <property name="icon_name">edit-select-all-symbolic</property> + <property name="title">Layout</property> + <property name="visible">True</property> + <child> + <object class="HdyPreferencesGroup"> + <property name="visible">True</property> + <child> + <object class="HdyPreferencesRow"> + <property name="title" bind-source="welcome_label" bind-property="label" bind-flags="sync-create"/> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="welcome_label"> + <property name="ellipsize">end</property> + <property name="label" translatable="yes">This is a preferences window</property> + <property name="margin">12</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyPreferencesGroup"> + <property name="description" translatable="yes">Preferences are organized in pages, this example has the following pages:</property> + <property name="title" translatable="yes">Pages</property> + <property name="visible">True</property> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Layout</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Search</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyPreferencesGroup"> + <property name="description" translatable="yes">Preferences are grouped together, a group can have a tile and a description. Descriptions will be wrapped if they are too long. This page has the following groups:</property> + <property name="title" translatable="yes">Groups</property> + <property name="visible">True</property> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">An untitled group</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Pages</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Groups</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Preferences</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyPreferencesGroup"> + <property name="title" translatable="yes">Preferences</property> + <property name="visible">True</property> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Preferences rows are appended to the list box</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <style> + <class name="inline-toolbar"/> + </style> + <child> + <object class="GtkLabel"> + <property name="ellipsize">end</property> + <property name="label" translatable="yes">Other widgets are appended after the list box</property> + <property name="margin">12</property> + <property name="visible">True</property> + <property name="xalign">0</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyPreferencesGroup"> + <property name="description" translatable="yes">Preferences windows can have subpages.</property> + <property name="title" translatable="yes">Subpages</property> + <property name="visible">True</property> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Go to a subpage</property> + <property name="visible">True</property> + <property name="activatable">True</property> + <signal name="activated" handler="subpage1_activated_cb" swapped="yes"/> + <child> + <object class="GtkImage"> + <property name="icon_name">go-next-symbolic</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Go to another subpage</property> + <property name="visible">True</property> + <property name="activatable">True</property> + <signal name="activated" handler="subpage2_activated_cb" swapped="yes"/> + <child> + <object class="GtkImage"> + <property name="icon_name">go-next-symbolic</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyPreferencesPage"> + <property name="icon_name">edit-find-symbolic</property> + <property name="title">Search</property> + <property name="visible">True</property> + <child> + <object class="HdyPreferencesGroup"> + <property name="description">Preferences can be searched, do so using one of the following ways:</property> + <property name="title">Searching</property> + <property name="visible">True</property> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Activate the search button</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyPreferencesRow"> + <property name="title" translatable="yes">Ctrl + F</property> + <property name="visible">True</property> + <child> + <object class="GtkShortcutLabel"> + <property name="accelerator"><ctrl>f</property> + <property name="margin">12</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Directly type your search</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkBox" id="subpage1"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + <property name="spacing">24</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">This is a subpage</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="opacity">0.5</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Return to the preferences</property> + <signal name="clicked" handler="return_to_preferences_cb" swapped="yes"/> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + <object class="GtkBox" id="subpage2"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + <property name="spacing">24</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">This is another subpage</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="opacity">0.5</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Return to the preferences</property> + <signal name="clicked" handler="return_to_preferences_cb" swapped="yes"/> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> +</interface> diff --git a/subprojects/libhandy/examples/hdy-demo-window.c b/subprojects/libhandy/examples/hdy-demo-window.c new file mode 100644 index 0000000..55c872f --- /dev/null +++ b/subprojects/libhandy/examples/hdy-demo-window.c @@ -0,0 +1,573 @@ +#include "hdy-demo-window.h" + +#include <glib/gi18n.h> +#include "hdy-view-switcher-demo-window.h" + +struct _HdyDemoWindow +{ + HdyApplicationWindow parent_instance; + + HdyLeaflet *content_box; + GtkStack *header_revealer; + GtkStack *header_stack; + GtkImage *theme_variant_image; + GtkStackSidebar *sidebar; + GtkStack *stack; + HdyComboRow *leaflet_transition_row; + HdyDeck *content_deck; + HdyComboRow *deck_transition_row; + GtkWidget *box_keypad; + GtkListBox *keypad_listbox; + HdyKeypad *keypad; + HdySearchBar *search_bar; + GtkEntry *search_entry; + GtkListBox *lists_listbox; + HdyComboRow *combo_row; + HdyComboRow *enum_combo_row; + HdyCarousel *carousel; + GtkBox *carousel_box; + GtkListBox *carousel_listbox; + GtkStack *carousel_indicators_stack; + HdyComboRow *carousel_orientation_row; + HdyComboRow *carousel_indicators_row; + GListStore *carousel_indicators_model; + HdyAvatar *avatar; + GtkEntry *avatar_text; + GtkFileChooserButton *avatar_filechooser; + GtkListBox *avatar_contacts; +}; + +G_DEFINE_TYPE (HdyDemoWindow, hdy_demo_window, HDY_TYPE_APPLICATION_WINDOW) + +static void +theme_variant_button_clicked_cb (HdyDemoWindow *self) +{ + GtkSettings *settings = gtk_settings_get_default (); + gboolean prefer_dark_theme; + + g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_theme, NULL); + g_object_set (settings, "gtk-application-prefer-dark-theme", !prefer_dark_theme, NULL); +} + +static gboolean +prefer_dark_theme_to_icon_name_cb (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + g_value_set_string (to_value, + g_value_get_boolean (from_value) ? "light-mode-symbolic" : + "dark-mode-symbolic"); + + return TRUE; +} + +static gboolean +hdy_demo_window_key_pressed_cb (GtkWidget *sender, + GdkEvent *event, + HdyDemoWindow *self) +{ + GdkModifierType default_modifiers = gtk_accelerator_get_default_mod_mask (); + guint keyval; + GdkModifierType state; + + gdk_event_get_keyval (event, &keyval); + gdk_event_get_state (event, &state); + + if ((keyval == GDK_KEY_q || keyval == GDK_KEY_Q) && + (state & default_modifiers) == GDK_CONTROL_MASK) { + gtk_widget_destroy (GTK_WIDGET (self)); + + return TRUE; + } + + return FALSE; +} + +static void +update (HdyDemoWindow *self) +{ + const gchar *header_bar_name = "default"; + + if (g_strcmp0 (gtk_stack_get_visible_child_name (self->stack), "deck") == 0) + header_bar_name = "deck"; + else if (g_strcmp0 (gtk_stack_get_visible_child_name (self->stack), "search-bar") == 0) + header_bar_name = "search-bar"; + + gtk_stack_set_visible_child_name (self->header_stack, header_bar_name); +} + +static void +hdy_demo_window_notify_deck_visible_child_cb (HdyDemoWindow *self) +{ + update (self); +} + +static void +hdy_demo_window_notify_visible_child_cb (GObject *sender, + GParamSpec *pspec, + HdyDemoWindow *self) +{ + update (self); + + hdy_leaflet_navigate (self->content_box, HDY_NAVIGATION_DIRECTION_FORWARD); +} + +static void +hdy_demo_window_back_clicked_cb (GtkWidget *sender, + HdyDemoWindow *self) +{ + hdy_leaflet_navigate (self->content_box, HDY_NAVIGATION_DIRECTION_BACK); +} + +static void +hdy_demo_window_deck_back_clicked_cb (GtkWidget *sender, + HdyDemoWindow *self) +{ + hdy_deck_navigate (self->content_deck, HDY_NAVIGATION_DIRECTION_BACK); +} + +static gchar * +leaflet_transition_name (HdyEnumValueObject *value, + gpointer user_data) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL); + + switch (hdy_enum_value_object_get_value (value)) { + case HDY_LEAFLET_TRANSITION_TYPE_OVER: + return g_strdup (_("Over")); + case HDY_LEAFLET_TRANSITION_TYPE_UNDER: + return g_strdup (_("Under")); + case HDY_LEAFLET_TRANSITION_TYPE_SLIDE: + return g_strdup (_("Slide")); + default: + return NULL; + } +} + +static void +notify_leaflet_transition_cb (GObject *sender, + GParamSpec *pspec, + HdyDemoWindow *self) +{ + HdyComboRow *row = HDY_COMBO_ROW (sender); + + g_assert (HDY_IS_COMBO_ROW (row)); + g_assert (HDY_IS_DEMO_WINDOW (self)); + + hdy_leaflet_set_transition_type (HDY_LEAFLET (self->content_box), hdy_combo_row_get_selected_index (row)); +} + +static gchar * +deck_transition_name (HdyEnumValueObject *value, + gpointer user_data) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL); + + switch (hdy_enum_value_object_get_value (value)) { + case HDY_DECK_TRANSITION_TYPE_OVER: + return g_strdup (_("Over")); + case HDY_DECK_TRANSITION_TYPE_UNDER: + return g_strdup (_("Under")); + case HDY_DECK_TRANSITION_TYPE_SLIDE: + return g_strdup (_("Slide")); + default: + return NULL; + } +} + +static void +notify_deck_transition_cb (GObject *sender, + GParamSpec *pspec, + HdyDemoWindow *self) +{ + HdyComboRow *row = HDY_COMBO_ROW (sender); + + g_assert (HDY_IS_COMBO_ROW (row)); + g_assert (HDY_IS_DEMO_WINDOW (self)); + + hdy_deck_set_transition_type (HDY_DECK (self->content_deck), hdy_combo_row_get_selected_index (row)); +} + +static void +deck_go_next_row_activated_cb (HdyDemoWindow *self) +{ + g_assert (HDY_IS_DEMO_WINDOW (self)); + + hdy_deck_navigate (self->content_deck, HDY_NAVIGATION_DIRECTION_FORWARD); +} + +static void +view_switcher_demo_clicked_cb (GtkButton *btn, + HdyDemoWindow *self) +{ + HdyViewSwitcherDemoWindow *window = hdy_view_switcher_demo_window_new (); + + gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (self)); + gtk_widget_show (GTK_WIDGET (window)); +} + +static gchar * +carousel_orientation_name (HdyEnumValueObject *value, + gpointer user_data) +{ + g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL); + + switch (hdy_enum_value_object_get_value (value)) { + case GTK_ORIENTATION_HORIZONTAL: + return g_strdup (_("Horizontal")); + case GTK_ORIENTATION_VERTICAL: + return g_strdup (_("Vertical")); + default: + return NULL; + } +} + +static void +notify_carousel_orientation_cb (GObject *sender, + GParamSpec *pspec, + HdyDemoWindow *self) +{ + HdyComboRow *row = HDY_COMBO_ROW (sender); + + g_assert (HDY_IS_COMBO_ROW (row)); + g_assert (HDY_IS_DEMO_WINDOW (self)); + + gtk_orientable_set_orientation (GTK_ORIENTABLE (self->carousel_box), + 1 - hdy_combo_row_get_selected_index (row)); + gtk_orientable_set_orientation (GTK_ORIENTABLE (self->carousel), + hdy_combo_row_get_selected_index (row)); +} + +static gchar * +carousel_indicators_name (HdyValueObject *value) +{ + const gchar *style; + + g_assert (HDY_IS_VALUE_OBJECT (value)); + + style = hdy_value_object_get_string (value); + + if (!g_strcmp0 (style, "dots")) + return g_strdup (_("Dots")); + + if (!g_strcmp0 (style, "lines")) + return g_strdup (_("Lines")); + + return NULL; +} + +static void +notify_carousel_indicators_cb (GObject *sender, + GParamSpec *pspec, + HdyDemoWindow *self) +{ + HdyComboRow *row = HDY_COMBO_ROW (sender); + HdyValueObject *obj; + + g_assert (HDY_IS_COMBO_ROW (row)); + g_assert (HDY_IS_DEMO_WINDOW (self)); + + obj = g_list_model_get_item (G_LIST_MODEL (self->carousel_indicators_model), + hdy_combo_row_get_selected_index (row)); + + gtk_stack_set_visible_child_name (self->carousel_indicators_stack, + hdy_value_object_get_string (obj)); +} + +static void +carousel_return_clicked_cb (GtkButton *btn, + HdyDemoWindow *self) +{ + g_autoptr (GList) children = + gtk_container_get_children (GTK_CONTAINER (self->carousel)); + + hdy_carousel_scroll_to (self->carousel, GTK_WIDGET (children->data)); +} + +HdyDemoWindow * +hdy_demo_window_new (GtkApplication *application) +{ + return g_object_new (HDY_TYPE_DEMO_WINDOW, "application", application, NULL); +} + +static void +avatar_file_remove_cb (HdyDemoWindow *self) +{ + g_autofree gchar *file = NULL; + + g_assert (HDY_IS_DEMO_WINDOW (self)); + + g_signal_handlers_disconnect_by_data (self->avatar, self); + file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->avatar_filechooser)); + if (file) + gtk_file_chooser_unselect_filename (GTK_FILE_CHOOSER (self->avatar_filechooser), file); + hdy_avatar_set_image_load_func (self->avatar, NULL, NULL, NULL); +} + +static GdkPixbuf * +avatar_load_file (gint size, HdyDemoWindow *self) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autofree gchar *file = NULL; + gint width, height; + + g_assert (HDY_IS_DEMO_WINDOW (self)); + + file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->avatar_filechooser)); + + 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 g_steal_pointer (&pixbuf); +} + +static void +avatar_file_set_cb (HdyDemoWindow *self) +{ + g_assert (HDY_IS_DEMO_WINDOW (self)); + + hdy_avatar_set_image_load_func (self->avatar, (HdyAvatarImageLoadFunc) avatar_load_file, self, NULL); +} + +static gchar * +avatar_new_random_name (void) +{ + static const char *first_names[] = { + "Adam", + "Adrian", + "Anna", + "Charlotte", + "Frédérique", + "Ilaria", + "Jakub", + "Jennyfer", + "Julia", + "Justin", + "Mario", + "Miriam", + "Mohamed", + "Nourimane", + "Owen", + "Peter", + "Petra", + "Rachid", + "Rebecca", + "Sarah", + "Thibault", + "Wolfgang", + }; + static const char *last_names[] = { + "Bailey", + "Berat", + "Chen", + "Farquharson", + "Ferber", + "Franco", + "Galinier", + "Han", + "Lawrence", + "Lepied", + "Lopez", + "Mariotti", + "Rossi", + "Urasawa", + "Zwickelman", + }; + + return g_strdup_printf ("%s %s", + first_names[g_random_int_range (0, G_N_ELEMENTS (first_names))], + last_names[g_random_int_range (0, G_N_ELEMENTS (last_names))]); +} + +static void +avatar_update_contacts (HdyDemoWindow *self) +{ + g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->avatar_contacts)); + + for (GList *child = children; child; child = child->next) + gtk_container_remove (GTK_CONTAINER (self->avatar_contacts), child->data); + + for (int i = 0; i < 30; i++) { + g_autofree gchar *name = avatar_new_random_name (); + GtkWidget *contact = hdy_action_row_new (); + GtkWidget *avatar = hdy_avatar_new (40, name, TRUE); + + gtk_widget_show (contact); + gtk_widget_show (avatar); + + gtk_widget_set_margin_top (avatar, 12); + gtk_widget_set_margin_bottom (avatar, 12); + + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (contact), name); + hdy_action_row_add_prefix (HDY_ACTION_ROW (contact), avatar); + gtk_container_add (GTK_CONTAINER (self->avatar_contacts), contact); + } +} + +static void +hdy_demo_window_constructed (GObject *object) +{ + HdyDemoWindow *self = HDY_DEMO_WINDOW (object); + + G_OBJECT_CLASS (hdy_demo_window_parent_class)->constructed (object); + + hdy_search_bar_connect_entry (self->search_bar, self->search_entry); +} + + +static void +hdy_demo_window_class_init (HdyDemoWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = hdy_demo_window_constructed; + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-demo-window.ui"); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, content_box); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, header_revealer); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, header_stack); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, theme_variant_image); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, sidebar); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, stack); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, leaflet_transition_row); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, content_deck); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, deck_transition_row); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, box_keypad); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, keypad_listbox); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, keypad); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, search_bar); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, search_entry); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, lists_listbox); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, combo_row); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, enum_combo_row); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_box); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_listbox); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_indicators_stack); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_orientation_row); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_indicators_row); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar_text); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar_filechooser); + gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar_contacts); + gtk_widget_class_bind_template_callback_full (widget_class, "key_pressed_cb", G_CALLBACK(hdy_demo_window_key_pressed_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "notify_visible_child_cb", G_CALLBACK(hdy_demo_window_notify_visible_child_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "notify_deck_visible_child_cb", G_CALLBACK(hdy_demo_window_notify_deck_visible_child_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "back_clicked_cb", G_CALLBACK(hdy_demo_window_back_clicked_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "deck_back_clicked_cb", G_CALLBACK(hdy_demo_window_deck_back_clicked_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "notify_leaflet_transition_cb", G_CALLBACK(notify_leaflet_transition_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "notify_deck_transition_cb", G_CALLBACK(notify_deck_transition_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "deck_go_next_row_activated_cb", G_CALLBACK(deck_go_next_row_activated_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "theme_variant_button_clicked_cb", G_CALLBACK(theme_variant_button_clicked_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "view_switcher_demo_clicked_cb", G_CALLBACK(view_switcher_demo_clicked_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "notify_carousel_orientation_cb", G_CALLBACK(notify_carousel_orientation_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "notify_carousel_indicators_cb", G_CALLBACK(notify_carousel_indicators_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "carousel_return_clicked_cb", G_CALLBACK(carousel_return_clicked_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "avatar_file_remove_cb", G_CALLBACK(avatar_file_remove_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, "avatar_file_set_cb", G_CALLBACK(avatar_file_set_cb)); +} + +static void +lists_page_init (HdyDemoWindow *self) +{ + GListStore *list_store; + HdyValueObject *obj; + + list_store = g_list_store_new (HDY_TYPE_VALUE_OBJECT); + + obj = hdy_value_object_new_string ("Foo"); + g_list_store_insert (list_store, 0, obj); + g_clear_object (&obj); + + obj = hdy_value_object_new_string ("Bar"); + g_list_store_insert (list_store, 1, obj); + g_clear_object (&obj); + + obj = hdy_value_object_new_string ("Baz"); + g_list_store_insert (list_store, 2, obj); + g_clear_object (&obj); + + hdy_combo_row_bind_name_model (self->combo_row, G_LIST_MODEL (list_store), (HdyComboRowGetNameFunc) hdy_value_object_dup_string, NULL, NULL); + + hdy_combo_row_set_for_enum (self->enum_combo_row, GTK_TYPE_LICENSE, hdy_enum_value_row_name, NULL, NULL); + update (self); +} + +static void +carousel_page_init (HdyDemoWindow *self) +{ + HdyValueObject *obj; + + hdy_combo_row_set_for_enum (self->carousel_orientation_row, + GTK_TYPE_ORIENTATION, + carousel_orientation_name, + NULL, + NULL); + + self->carousel_indicators_model = g_list_store_new (HDY_TYPE_VALUE_OBJECT); + + obj = hdy_value_object_new_string ("dots"); + g_list_store_insert (self->carousel_indicators_model, 0, obj); + g_clear_object (&obj); + + obj = hdy_value_object_new_string ("lines"); + g_list_store_insert (self->carousel_indicators_model, 1, obj); + g_clear_object (&obj); + + hdy_combo_row_bind_name_model (self->carousel_indicators_row, + G_LIST_MODEL (self->carousel_indicators_model), + (HdyComboRowGetNameFunc) carousel_indicators_name, + NULL, + NULL); +} + +static void +avatar_page_init (HdyDemoWindow *self) +{ + g_autofree gchar *name = avatar_new_random_name (); + + gtk_entry_set_text (self->avatar_text, name); + + avatar_update_contacts (self); +} + +static void +hdy_demo_window_init (HdyDemoWindow *self) +{ + GtkSettings *settings = gtk_settings_get_default (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_bind_property_full (settings, "gtk-application-prefer-dark-theme", + self->theme_variant_image, "icon-name", + G_BINDING_SYNC_CREATE, + prefer_dark_theme_to_icon_name_cb, + NULL, + NULL, + NULL); + + hdy_combo_row_set_for_enum (self->leaflet_transition_row, HDY_TYPE_LEAFLET_TRANSITION_TYPE, leaflet_transition_name, NULL, NULL); + hdy_combo_row_set_selected_index (self->leaflet_transition_row, HDY_LEAFLET_TRANSITION_TYPE_OVER); + + hdy_combo_row_set_for_enum (self->deck_transition_row, HDY_TYPE_DECK_TRANSITION_TYPE, deck_transition_name, NULL, NULL); + hdy_combo_row_set_selected_index (self->deck_transition_row, HDY_DECK_TRANSITION_TYPE_OVER); + + lists_page_init (self); + carousel_page_init (self); + avatar_page_init (self); + + hdy_leaflet_set_visible_child_name (self->content_box, "content"); +} diff --git a/subprojects/libhandy/examples/hdy-demo-window.h b/subprojects/libhandy/examples/hdy-demo-window.h new file mode 100644 index 0000000..d263e13 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-demo-window.h @@ -0,0 +1,13 @@ +#pragma once + +#include <handy.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_DEMO_WINDOW (hdy_demo_window_get_type()) + +G_DECLARE_FINAL_TYPE (HdyDemoWindow, hdy_demo_window, HDY, DEMO_WINDOW, HdyApplicationWindow) + +HdyDemoWindow *hdy_demo_window_new (GtkApplication *application); + +G_END_DECLS diff --git a/subprojects/libhandy/examples/hdy-demo-window.ui b/subprojects/libhandy/examples/hdy-demo-window.ui new file mode 100644 index 0000000..9d00588 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-demo-window.ui @@ -0,0 +1,2352 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.16"/> + <requires lib="libhandy" version="1.0"/> + <menu id="primary_menu"> + <section> + <item> + <attribute name="label" translatable="yes">Preferences</attribute> + <attribute name="action">app.preferences</attribute> + </item> + </section> + </menu> + <template class="HdyDemoWindow" parent="HdyApplicationWindow"> + <property name="can_focus">False</property> + <property name="title">Handy Demo</property> + <property name="default_width">800</property> + <property name="default_height">576</property> + <signal name="key-press-event" handler="key_pressed_cb" after="yes" swapped="no"/> + <child> + <object class="HdyLeaflet" id="content_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="can-swipe-back">True</property> + <property name="width-request">360</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRevealer" id="header_revealer"> + <property name="visible">True</property> + <property name="transition-type">slide-down</property> + <property name="reveal-child" bind-source="window_header_revealer_switch" bind-property="state" bind-flags="sync-create | bidirectional"/> + <child> + <object class="HdyHeaderBar" id="header_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title">Handy Demo</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <signal name="clicked" handler="theme_variant_button_clicked_cb" swapped="yes"/> + <child> + <object class="GtkImage" id="theme_variant_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuButton" id="menu_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="focus_on_click">False</property> + <property name="menu_model">primary_menu</property> + <property name="use_popover">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">open-menu-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStackSidebar" id="sidebar"> + <property name="width_request">270</property> + <property name="visible">True</property> + <property name="vexpand">True</property> + <property name="can_focus">False</property> + <property name="stack">stack</property> + </object> + </child> + </object> + <packing> + <property name="name">sidebar</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRevealer"> + <property name="visible">True</property> + <property name="transition-type" bind-source="header_revealer" bind-property="transition-type" bind-flags="bidirectional|sync-create"/> + <property name="reveal-child" bind-source="header_revealer" bind-property="reveal-child" bind-flags="bidirectional|sync-create"/> + <child> + <object class="HdyWindowHandle" id="header_separator"> + <property name="visible">True</property> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <style> + <class name="sidebar"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="vexpand">True</property> + <style> + <class name="sidebar"/> + </style> + </object> + </child> + </object> + <packing> + <property name="navigatable">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="right_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRevealer"> + <property name="visible">True</property> + <property name="transition-type" bind-source="header_revealer" bind-property="transition-type" bind-flags="bidirectional|sync-create"/> + <property name="reveal-child" bind-source="header_revealer" bind-property="reveal-child" bind-flags="bidirectional|sync-create"/> + <child> + <object class="GtkStack" id="header_stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vexpand">False</property> + <property name="transition-type" bind-source="stack" bind-property="transition-type" bind-flags="sync-create"/> + <child> + <object class="HdyHeaderBar" id="default_header_bar"> + <property name="visible">True</property> + <property name="expand">True</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton" id="back"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="visible" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/> + <signal name="clicked" handler="back_clicked_cb"/> + <style> + <class name="image-button"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-back"> + <property name="accessible-name" translatable="yes">Back</property> + </object> + </child> + <child> + <object class="GtkImage" id="back_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">default</property> + </packing> + </child> + <child> + <object class="HdyDeck" id="header_deck"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vexpand">True</property> + <property name="can-swipe-back">True</property> + <property name="transition-type" bind-source="content_deck" bind-property="transition-type" bind-flags="sync-create"/> + <child> + <object class="HdyHeaderBar" id="deck_header_bar"> + <property name="visible">True</property> + <property name="expand">True</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton" id="deck-back"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="visible" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/> + <signal name="clicked" handler="back_clicked_cb"/> + <style> + <class name="image-button"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-deck-back"> + <property name="accessible-name" translatable="yes">Back</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyHeaderBar" id="deck_sub_header_bar"> + <property name="visible">True</property> + <property name="expand">True</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton" id="deck-sub-back"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="visible">True</property> + <signal name="clicked" handler="deck_back_clicked_cb"/> + <style> + <class name="image-button"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject" id="a11y-deck-sub-back"> + <property name="accessible-name" translatable="yes">Back</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">deck</property> + </packing> + </child> + <child> + <object class="HdyHeaderBar" id="search_bar_header_bar"> + <property name="visible">True</property> + <property name="expand">True</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="visible" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/> + <signal name="clicked" handler="back_clicked_cb"/> + <style> + <class name="image-button"/> + </style> + <child internal-child="accessible"> + <object class="AtkObject"> + <property name="accessible-name" translatable="yes">Back</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">go-previous-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkToggleButton"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="active" bind-source="search_bar" bind-property="search-mode-enabled" bind-flags="sync-create|bidirectional"/> + <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" id="search_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">edit-find-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + <packing> + <property name="name">search-bar</property> + </packing> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolled_window"> + <property name="visible">True</property> + <property name="hscrollbar_policy">never</property> + <property name="vexpand">True</property> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vhomogeneous">False</property> + <signal name="notify::visible-child" handler="notify_visible_child_cb" after="yes" swapped="no"/> + <child> + <object class="GtkBox" id="welcome"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkImage" id="icon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="margin_bottom">18</property> + <property name="pixel_size">128</property> + <property name="icon_name">gnome-smartphone-symbolic</property> + <property name="icon_size">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <child> + <object class="GtkLabel" id="label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="halign">center</property> + <property name="margin_bottom">12</property> + <property name="label" translatable="yes">Welcome to Handy Demo</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="empty-state-label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="label" translatable="yes">This is a tour of the features the library has to offer.</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">welcome</property> + <property name="title">Welcome</property> + </packing> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <property name="expand">True</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-leaflet-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Leaflet</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A widget showing either all its children or only one, depending on the available space. This window is using a leaflet, you can control it with the settings below.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="label" translatable="yes">Leaflet</property> + <property name="justify">left</property> + <property name="halign">start</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyComboRow" id="leaflet_transition_row"> + <property name="subtitle" translatable="yes">The type of transition to use when the leaflet adapts its size or when changing the visible child</property> + <property name="title" translatable="yes">Transition type</property> + <property name="visible">True</property> + <signal name="notify::selected-index" handler="notify_leaflet_transition_cb" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">leaflet</property> + <property name="title">Leaflet</property> + </packing> + </child> + <child> + <object class="HdyDeck" id="content_deck"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vexpand">True</property> + <property name="transition-type">over</property> + <property name="can-swipe-back">True</property> + <signal name="notify::visible-child" handler="notify_deck_visible_child_cb" after="yes" swapped="yes"/> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <property name="expand">True</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-deck-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Deck</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A widget showing only one of its children at a time. This page is using decks, you can control them with the settings below.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="label" translatable="yes">Deck</property> + <property name="justify">left</property> + <property name="halign">start</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyComboRow" id="deck_transition_row"> + <property name="subtitle" translatable="yes">The type of transition to use when the decks adapt their size or when changing the visible child</property> + <property name="title" translatable="yes">Transition type</property> + <property name="visible">True</property> + <signal name="notify::selected-index" handler="notify_deck_transition_cb" swapped="no"/> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Go to the next page of the deck</property> + <property name="use_underline">True</property> + <property name="visible">True</property> + <property name="activatable">True</property> + <signal name="activated" handler="deck_go_next_row_activated_cb" swapped="yes"/> + <child> + <object class="GtkImage"> + <property name="icon_name">go-next-symbolic</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <property name="visible">True</property> + <property name="margin">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="halign">center</property> + <property name="label" translatable="yes">Go back</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">gesture-touchscreen-swipe-back-symbolic</property> + <property name="pixel-size">128</property> + <property name="visible">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">gesture-touchpad-swipe-back-symbolic</property> + <property name="pixel-size">128</property> + <property name="visible">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + <packing> + <property name="name">sub</property> + </packing> + </child> + </object> + <packing> + <property name="name">deck</property> + <property name="title">Deck</property> + </packing> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <property name="expand">True</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-keypad-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Keypad</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A number keypad.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="maximum-size">300</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox" id="box_keypad"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkListBox" id="keypad_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Show letters</property> + <property name="activatable_widget">keypad_letters_visible</property> + <child> + <object class="GtkSwitch" id="keypad_letters_visible"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="state">True</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Show symbols</property> + <property name="activatable_widget">keypad_symbols_visible</property> + <child> + <object class="GtkSwitch" id="keypad_symbols_visible"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="state">True</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkEntry" id="entry_keypad"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + <child> + <object class="HdyKeypad" id="keypad"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="entry">entry_keypad</property> + <property name="symbols-visible" bind-source="keypad_symbols_visible" bind-property="state" bind-flags="sync-create | bidirectional"/> + <property name="letters-visible" bind-source="keypad_letters_visible" bind-property="state" bind-flags="sync-create | bidirectional"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">keypad</property> + <property name="title">Keypad</property> + </packing> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <property name="expand">True</property> + <property name="maximum-size" bind-source="clamp_maximum_size_adjustment" bind-property="value" bind-flags="sync-create"/> + <property name="tightening-threshold" bind-source="clamp_tightening_threshold_adjustment" bind-property="value" bind-flags="sync-create"/> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-clamp-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Clamp</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">This page is clamped to smoothly grow up to a maximum width.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="label" translatable="yes">Clamp</property> + <property name="justify">left</property> + <property name="halign">start</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkListBox" id="clamp_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyActionRow"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Maximum width</property> + <property name="visible">True</property> + <child> + <object class="GtkSpinButton"> + <property name="adjustment">clamp_maximum_size_adjustment</property> + <property name="valign">center</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Tightening threshold</property> + <property name="visible">True</property> + <child> + <object class="GtkSpinButton"> + <property name="adjustment">clamp_tightening_threshold_adjustment</property> + <property name="valign">center</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">clamp</property> + <property name="title">Clamp</property> + </packing> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <property name="expand">True</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-list-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Lists</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Rows and helpers for <i>GtkListBox</i>.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="label" translatable="yes">Lists</property> + <property name="justify">left</property> + <property name="halign">start</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkListBox" id="lists_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyActionRow"> + <property name="icon_name" translatable="yes">preferences-other-symbolic</property> + <property name="subtitle" translatable="yes">They also have a subtitle and an icon</property> + <property name="title" translatable="yes">Rows have a title</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="activatable_widget">frobnicate</property> + <property name="title" translatable="yes">Rows can have action widgets</property> + <property name="visible">True</property> + <child> + <object class="GtkButton" id="frobnicate"> + <property name="can_focus">True</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Frobnicate</property> + <property name="valign">center</property> + <property name="visible">True</property> + <style> + <class name="list-button"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="activatable_widget">radio_button_1</property> + <property name="title" translatable="yes">Rows can have prefix widgets</property> + <property name="visible">True</property> + <child type="prefix"> + <object class="GtkRadioButton" id="radio_button_1"> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="activatable_widget">radio_button_2</property> + <property name="title" translatable="yes">Rows can have prefix widgets</property> + <property name="visible">True</property> + <child type="prefix"> + <object class="GtkRadioButton" id="radio_button_2"> + <property name="can_focus">False</property> + <property name="group">radio_button_1</property> + <property name="valign">center</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyComboRow" id="combo_row"> + <property name="title" translatable="yes">Combo row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyComboRow" id="enum_combo_row"> + <property name="subtitle" translatable="yes">This combo row was created from an enumeration</property> + <property name="title" translatable="yes">Enumeration combo row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyExpanderRow" id="expander_row"> + <property name="title" translatable="yes">Expander row</property> + <property name="visible">True</property> + <child> + <object class="GtkListBoxRow"> + <property name="activatable">False</property> + <property name="visible">True</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Hello, world!</property> + <property name="margin">12</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyExpanderRow" id="action_expander_row"> + <property name="title" translatable="yes">Expander row with an action</property> + <property name="visible">True</property> + <child type="action"> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="valign">center</property> + <style> + <class name="list-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">edit-copy-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">A nested row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Another nested row</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyExpanderRow"> + <property name="title" translatable="yes">Expander row with a prefix</property> + <property name="visible">True</property> + <child type="prefix"> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="valign">center</property> + <style> + <class name="list-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">system-shutdown-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">A nested row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Another nested row</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyExpanderRow"> + <property name="title" translatable="yes">Expander row with a prefix and icon</property> + <property name="visible">True</property> + <property name="icon-name">action-unavailable-symbolic</property> + <child type="prefix"> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="valign">center</property> + <style> + <class name="list-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">system-shutdown-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">A nested row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Another nested row</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyExpanderRow" id="enable_expander_row"> + <property name="show_enable_switch">True</property> + <property name="title" translatable="yes">Toggleable expander row</property> + <property name="visible">True</property> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">A nested row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Another nested row</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyExpanderRow" id="action_switch_expander_row"> + <property name="show_enable_switch">True</property> + <property name="title" translatable="yes">Toggleable expander row with an action</property> + <property name="visible">True</property> + <child type="action"> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="valign">center</property> + <style> + <class name="list-button"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">edit-copy-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">A nested row</property> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="title" translatable="yes">Another nested row</property> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">lists</property> + <property name="title">Lists</property> + </packing> + </child> + <child> + <object class="GtkOverlay"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child type="overlay"> + <object class="HdySearchBar" id="search_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">start</property> + <property name="hexpand">True</property> + <property name="show-close-button">True</property> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="visible">True</property> + <property name="hexpand">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="margin_bottom">18</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-search-symbolic</property> + <property name="icon_size">0</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="halign">center</property> + <property name="margin_bottom">12</property> + <property name="label" translatable="yes">Search bar</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="margin_bottom">6</property> + <property name="label" translatable="yes">A search bar that gives your search entry all the space it needs.</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="opacity">0.5</property> + <property name="label" translatable="yes">Try using it with an horizontaly expanded clamp to make your search entry adaptive.</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="name">search-bar</property> + <property name="title">Search bar</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-view-switcher-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">View Switcher</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Widgets to switch the window's view.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="spacing">12</property> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="label" translatable="yes">Run the demo</property> + <signal name="clicked" handler="view_switcher_demo_clicked_cb" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="name">view-switcher</property> + <property name="title">View Switcher</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkBox" id="carousel_box"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="carousel_empty_box"> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="HdyCarousel" id="carousel"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-carousel-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + <class name="carousel-icon"/> + </style> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Carousel</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A widget for paginated scrolling.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="expand">True</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkListBox" id="carousel_listbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyComboRow" id="carousel_orientation_row"> + <property name="title" translatable="yes">Orientation</property> + <property name="visible">True</property> + <signal name="notify::selected-index" handler="notify_carousel_orientation_cb" swapped="no"/> + </object> + </child> + <child> + <object class="HdyComboRow" id="carousel_indicators_row"> + <property name="title" translatable="yes">Page Indicators</property> + <property name="visible">True</property> + <signal name="notify::selected-index" handler="notify_carousel_indicators_cb" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + <property name="spacing">24</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Another page</property> + <property name="justify">center</property> + <property name="wrap">True</property> + <property name="opacity">0.5</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Return to the first page</property> + <property name="use-underline">True</property> + <signal name="clicked" handler="carousel_return_clicked_cb" swapped="no"/> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="carousel_indicators_stack"> + <property name="visible">True</property> + <property name="homogeneous">False</property> + <property name="margin">6</property> + <child> + <object class="HdyCarouselIndicatorDots"> + <property name="visible">True</property> + <property name="carousel">carousel</property> + <property name="orientation" bind-source="carousel" bind-property="orientation" bind-flags="sync-create"/> + </object> + <packing> + <property name="name">dots</property> + </packing> + </child> + <child> + <object class="HdyCarouselIndicatorLines"> + <property name="visible">True</property> + <property name="carousel">carousel</property> + <property name="orientation" bind-source="carousel" bind-property="orientation" bind-flags="sync-create"/> + </object> + <packing> + <property name="name">lines</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="name">carousel</property> + <property name="title">Carousel</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="HdyAvatar" id="avatar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="halign">center</property> + <property name="size" bind-source="avatar_size" bind-property="value" bind-flags="sync-create"></property> + <property name="margin-bottom">18</property> + <property name="show-initials" bind-source="avatar_show_initials" bind-property="state" bind-flags="sync-create"/> + <property name="text" bind-source="avatar_text" bind-property="text" bind-flags="sync-create"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Avatar</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A user avatar with generated fallback.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Text</property> + <child> + <object class="GtkEntry" id="avatar_text"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Show initials</property> + <property name="activatable_widget">avatar_show_initials</property> + <child> + <object class="GtkSwitch" id="avatar_show_initials"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="valign">center</property> + <property name="state">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">File</property> + <child> + <object class="GtkFileChooserButton" id="avatar_filechooser"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="title" translatable="yes"/> + <signal name="file-set" swapped="yes" handler="avatar_file_set_cb"/> + <property name="filter">avatar_file_filter</property> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="valign">center</property> + <signal name="clicked" swapped="yes" handler="avatar_file_remove_cb"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">user-trash-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Size</property> + <child> + <object class="GtkSpinButton" id="avatar_size"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="valign">center</property> + <property name="numeric">True</property> + <property name="adjustment">avatar_adjustment</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkListBox" id="avatar_contacts"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">avatar</property> + <property name="title">Avatar</property> + </packing> + </child> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="margin-bottom">32</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">32</property> + <property name="expand">True</property> + <property name="maximum-size">400</property> + <property name="tightening-threshold">300</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="valign">start</property> + <property name="expand">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="margin-bottom">32</property> + <property name="expand">True</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="pixel_size">128</property> + <property name="icon_name">widget-window-symbolic</property> + <property name="icon-size">0</property> + <property name="margin-bottom">18</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Window</property> + <property name="halign">center</property> + <property name="xalign">0</property> + <property name="margin-bottom">6</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="2"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A freeform window.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + <property name="margin-bottom">6</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="opacity">0.5</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">It allows to have headerbar in content area, incl. above content, and round corners on the bottom. This window is an example, try hiding the titlebar.</property> + <property name="justify">center</property> + <property name="use_markup">true</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="label" translatable="yes">Window</property> + <property name="justify">left</property> + <property name="halign">start</property> + <property name="margin-bottom">12</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Show titlebar</property> + <property name="activatable_widget">window_header_revealer_switch</property> + <child> + <object class="GtkSwitch" id="window_header_revealer_switch"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="state">True</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="HdyWindowHandle"> + <property name="visible">True</property> + <property name="margin-top">12</property> + <child> + <object class="GtkListBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <property name="selection-mode">none</property> + <style> + <class name="content"/> + </style> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">This row acts as a titlebar</property> + <property name="subtitle" translatable="yes">Try dragging or right clicking it.</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">window</property> + <property name="title">Window</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">content</property> + </packing> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup"> + <property name="mode">vertical</property> + <widgets> + <widget name="header_bar"/> + <widget name="header_separator"/> + <widget name="header_stack"/> + </widgets> + </object> + <object class="HdyHeaderGroup" id="header_group"> + <property name="decorate-all" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/> + <headerbars> + <headerbar name="header_bar"/> + <headerbar name="default_header_bar"/> + <headerbar name="search_bar_header_bar"/> + <headerbar name="deck_header_bar"/> + <headerbar name="deck_sub_header_bar"/> + </headerbars> + </object> + <object class="HdySwipeGroup" id="deck_swipe_group"> + <swipeables> + <swipeable name="header_deck"/> + <swipeable name="content_deck"/> + </swipeables> + </object> + <object class="GtkAdjustment" id="clamp_maximum_size_adjustment"> + <property name="lower">0</property> + <property name="upper">10000</property> + <property name="value">600</property> + <property name="page-increment">100</property> + <property name="step-increment">10</property> + </object> + <object class="GtkAdjustment" id="clamp_tightening_threshold_adjustment"> + <property name="lower">0</property> + <property name="upper">10000</property> + <property name="value">500</property> + <property name="page-increment">100</property> + <property name="step-increment">10</property> + </object> + <object class="GtkSizeGroup"> + <property name="mode">both</property> + <widgets> + <widget name="carousel_empty_box"/> + <widget name="carousel_indicators_stack"/> + </widgets> + </object> + <object class="GtkAdjustment" id="avatar_adjustment"> + <property name="lower">24</property> + <property name="upper">320</property> + <property name="value">128</property> + <property name="page-increment">8</property> + <property name="step-increment">8</property> + </object> + <object class="GtkFileFilter" id="avatar_file_filter"> + <mime-types> + <mime-type>image/png</mime-type> + <mime-type>image/jpeg</mime-type> + <mime-type>image/jpg</mime-type> + <mime-type>image/gif</mime-type> + </mime-types> + </object> +</interface> diff --git a/subprojects/libhandy/examples/hdy-view-switcher-demo-window.c b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.c new file mode 100644 index 0000000..2251658 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.c @@ -0,0 +1,30 @@ +#include "hdy-view-switcher-demo-window.h" + +#include <glib/gi18n.h> + +struct _HdyViewSwitcherDemoWindow +{ + HdyWindow parent_instance; +}; + +G_DEFINE_TYPE (HdyViewSwitcherDemoWindow, hdy_view_switcher_demo_window, HDY_TYPE_WINDOW) + +static void +hdy_view_switcher_demo_window_class_init (HdyViewSwitcherDemoWindowClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-view-switcher-demo-window.ui"); +} + +static void +hdy_view_switcher_demo_window_init (HdyViewSwitcherDemoWindow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +HdyViewSwitcherDemoWindow * +hdy_view_switcher_demo_window_new (void) +{ + return g_object_new (HDY_TYPE_VIEW_SWITCHER_DEMO_WINDOW, NULL); +} diff --git a/subprojects/libhandy/examples/hdy-view-switcher-demo-window.h b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.h new file mode 100644 index 0000000..76dcdd7 --- /dev/null +++ b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.h @@ -0,0 +1,13 @@ +#pragma once + +#include <handy.h> + +G_BEGIN_DECLS + +#define HDY_TYPE_VIEW_SWITCHER_DEMO_WINDOW (hdy_view_switcher_demo_window_get_type()) + +G_DECLARE_FINAL_TYPE (HdyViewSwitcherDemoWindow, hdy_view_switcher_demo_window, HDY, VIEW_SWITCHER_DEMO_WINDOW, HdyWindow) + +HdyViewSwitcherDemoWindow *hdy_view_switcher_demo_window_new (void); + +G_END_DECLS diff --git a/subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui new file mode 100644 index 0000000..d6cc82e --- /dev/null +++ b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <requires lib="libhandy" version="0.0"/> + <template class="HdyViewSwitcherDemoWindow" parent="HdyWindow"> + <property name="can_focus">False</property> + <property name="modal">True</property> + <property name="window_position">center-on-parent</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="HdyHeaderBar"> + <property name="visible">True</property> + <property name="centering_policy">strict</property> + <property name="can_focus">False</property> + <property name="show_close_button">True</property> + <property name="title">HdyViewSwitcher Demo</property> + <child type="title"> + <object class="HdyViewSwitcherTitle" id="switcher_title"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stack">stack</property> + <property name="title" translatable="yes">View Switcher Example</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin">24</property> + <property name="label" translatable="yes">World</property> + </object> + <packing> + <property name="name">page1</property> + <property name="title" translatable="yes">World</property> + <property name="icon_name">help-about-symbolic</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin">24</property> + <property name="label" translatable="yes">Alarm</property> + </object> + <packing> + <property name="name">page2</property> + <property name="title" translatable="yes">Alarm</property> + <property name="icon_name">alarm-symbolic</property> + <property name="position">1</property> + <property name="needs_attention">True</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin">24</property> + <property name="label" translatable="yes">Stopwatch</property> + </object> + <packing> + <property name="name">page3</property> + <property name="title" translatable="yes">Stopwatch</property> + <property name="icon_name">document-print-symbolic</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin">24</property> + <property name="label" translatable="yes">Timer</property> + </object> + <packing> + <property name="name">page0</property> + <property name="title" translatable="yes">Timer</property> + <property name="icon_name">weather-clear-symbolic</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="HdyViewSwitcherBar" id="switcher_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="stack">stack</property> + <property name="reveal" bind-source="switcher_title" bind-property="title-visible" bind-flags="sync-create"/> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/subprojects/libhandy/examples/icons/dark-mode-symbolic.svg b/subprojects/libhandy/examples/icons/dark-mode-symbolic.svg new file mode 100644 index 0000000..256ca41 --- /dev/null +++ b/subprojects/libhandy/examples/icons/dark-mode-symbolic.svg @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + inkscape:version="1.0beta2 (2b71d25d45, 2019-12-03)" + sodipodi:docname="dark-mode-symbolic.svg" + id="svg8" + version="1.1" + height="16" + width="16"> + <metadata + id="metadata14"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs12" /> + <sodipodi:namedview + inkscape:current-layer="g6" + inkscape:window-maximized="1" + inkscape:window-y="0" + inkscape:window-x="0" + inkscape:cy="9.636866" + inkscape:cx="14.061833" + inkscape:zoom="35.858323" + showgrid="true" + id="namedview10" + inkscape:window-height="1016" + inkscape:window-width="1920" + inkscape:pageshadow="2" + inkscape:pageopacity="0" + guidetolerance="10" + gridtolerance="10" + objecttolerance="10" + borderopacity="1" + inkscape:document-rotation="0" + bordercolor="#666666" + pagecolor="#ffffff"> + <inkscape:grid + id="grid841" + type="xygrid" /> + </sodipodi:namedview> + <g + id="g6" + fill="#2e3436"> + <path + d="M 8 4.0058594 C 5.805 4.0058594 4 5.8108594 4 8.0058594 C 4 10.200859 5.805 12.005859 8 12.005859 C 10.195 12.005859 12 10.200859 12 8.0058594 C 12 5.8108594 10.195 4.0058594 8 4.0058594 z M 8 6 C 9.1007925 6 10.005859 6.9050669 10.005859 8.0058594 C 10.005859 9.1066519 9.1007925 10.011719 8 10.011719 C 6.8992075 10.011719 5.9941406 9.1066519 5.9941406 8.0058594 C 5.9941406 6.9050669 6.8992075 6 8 6 z " + style="line-height:normal;-inkscape-font-specification:Sans;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;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none" + id="path2" /> + <path + style="fill:#2e3436;fill-opacity:1" + d="m 12.596194,2.6966997 0.707107,0.7071067 c 0.195869,0.1958686 0.195869,0.5112382 0,0.7071068 L 12.596194,4.81802 c -0.195868,0.1958686 -0.511238,0.1958686 -0.707106,0 L 11.181981,4.1109132 c -0.195817,-0.1950323 -0.195817,-0.5120745 0,-0.7071068 l 0.707107,-0.7071067 c 0.195868,-0.1958686 0.511238,-0.1958686 0.707106,0 z m -8.4852811,8.4852813 0.7071068,0.707107 c 0.1958686,0.195868 0.1958686,0.511238 0,0.707107 l -0.7071068,0.707106 c -0.1958686,0.195869 -0.5112382,0.195869 -0.7071068,0 L 2.6966994,12.596195 c -0.1958161,-0.195033 -0.1958166,-0.512075 0,-0.707107 l 0.7071067,-0.707107 c 0.1958686,-0.195869 0.5112382,-0.195869 0.7071068,0 z M 7.5000757,1.0005015 h 0.999849 C 8.7762374,0.99998906 9.0003616,1.2241133 8.9998492,1.500426 V 2.500124 C 9.0003616,2.7764366 8.7762374,3.0005609 8.4999247,3.0000485 H 7.5000757 C 7.2237631,3.0005609 6.9996388,2.7764366 7.0001512,2.500124 V 1.500426 C 6.9996388,1.2241133 7.2237631,0.99998906 7.5000757,1.0005015 Z M 7.4993686,13.00066 8.4999247,12.999953 c 0.2763126,-5.13e-4 0.5004372,0.223611 0.4999245,0.499924 l 7.071e-4,1.000405 c 5.127e-4,0.276313 -0.2236119,0.500437 -0.4999245,0.499925 L 7.5000757,14.9995 C 7.2237631,15.000012 6.9996386,14.775888 7.0001512,14.499575 V 13.499877 C 6.9996386,13.223564 7.2237631,12.99944 7.5000757,12.999953 Z M 1.0005012,8.499925 V 7.500076 C 0.99998861,7.2237635 1.2241132,6.9996389 1.5004257,7.0001515 h 0.999698 C 2.7764364,6.999639 3.0005607,7.2237633 3.0000482,7.500076 V 8.499925 C 3.0005607,8.7762375 2.7764362,9.0003621 2.5001237,8.9998495 H 1.5004257 C 1.224113,9.000362 0.99998869,8.7762377 1.0005012,8.499925 Z M 13.000659,8.5006321 12.999952,7.500076 C 12.99944,7.2237633 13.223564,6.999639 13.499877,7.0001515 h 0.999698 c 0.276312,-5.126e-4 0.500437,0.223612 0.499924,0.4999245 l 7.07e-4,1.0005561 c 5.13e-4,0.2763127 -0.223611,0.500437 -0.499924,0.4999245 L 13.499877,8.9998495 C 13.223564,9.0003621 12.99944,8.7762375 12.999952,8.499925 Z m 0.302642,4.0955629 -0.707107,0.707106 c -0.195868,0.195869 -0.511238,0.195869 -0.707106,0 l -0.707107,-0.707106 c -0.195817,-0.195033 -0.195816,-0.512075 0,-0.707107 l 0.707107,-0.707107 c 0.195868,-0.195869 0.511238,-0.195869 0.707106,0 l 0.707107,0.707107 c 0.195869,0.195868 0.195869,0.511238 0,0.707107 z M 4.8180197,4.1109132 4.1109129,4.81802 c -0.1958686,0.1958686 -0.5112382,0.1958686 -0.7071068,0 L 2.6966994,4.1109132 c -0.1958164,-0.1950323 -0.1958164,-0.5120745 0,-0.7071068 L 3.4038061,2.6966997 c 0.1958686,-0.1958686 0.5112382,-0.1958686 0.7071068,0 l 0.7071068,0.7071067 c 0.1958686,0.1958686 0.1958686,0.5112382 0,0.7071068 z" + id="path844" + inkscape:connector-curvature="0" + sodipodi:nodetypes="sssssccsssssssccssccccccccccccccccccccccccccccccccccccccsssccsssssssccssss" /> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg new file mode 100644 index 0000000..e25947c --- /dev/null +++ b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="64" + height="64" + viewBox="0 0 64 64.000001" + id="svg6535" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="two-finger-swipe-left.svg"> + <defs + id="defs6537" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1" + inkscape:cx="0.8190337" + inkscape:cy="13.44962" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:showpageshadow="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="2560" + inkscape:window-height="1376" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1"> + <inkscape:grid + type="xygrid" + id="grid7931" /> + </sodipodi:namedview> + <metadata + id="metadata6540"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(180,-470.14793)"> + <path + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="M 38.271484 6.0039062 C 37.981892 5.9956378 37.692703 6.0019094 37.40625 6.0214844 C 34.246377 6.2374271 31.332975 8.1249345 29.902344 11.082031 C 29.173793 10.990041 28.447857 10.978702 27.738281 11.0625 C 24.36672 11.460668 21.373919 13.762084 20.242188 17.148438 L 12 17.148438 L 12 10.148438 L 2 19.648438 L 12 29.148438 L 12 22.148438 L 20.048828 22.148438 C 20.694441 24.742195 22.473591 26.994506 25 28.162109 L 25 47 L 24 47 L 24 39.185547 C 24 36.866788 22.215997 35 20 35 C 17.784003 35 16 36.866788 16 39.185547 L 16 47 L 16 55.814453 L 16 60 L 20 60 L 45 60 L 56 60 C 58.215997 60 60 58.133212 60 55.814453 L 60 43.185547 L 60 34.185547 C 60 31.866788 58.215997 30 56 30 C 53.784003 30 52 31.866788 52 34.185547 L 52 39 L 51 39 L 51 28.185547 C 51 25.866788 49.215997 24 47 24 C 44.784003 24 43 25.866788 43 28.185547 L 43 39 L 42 39 L 42 23.042969 C 44.136586 21.978813 45.853243 20.084457 46.603516 17.640625 C 48.028796 12.998095 45.481769 8.0270562 40.880859 6.4726562 C 40.018187 6.1812063 39.140263 6.0287116 38.271484 6.0039062 z M 38.171875 8.9980469 C 38.752124 9.0164906 39.339992 9.1204781 39.919922 9.3164062 C 43.012882 10.361346 44.694478 13.640769 43.736328 16.761719 C 43.402083 17.85044 42.787396 18.769252 42 19.474609 L 42 15.185547 C 42 12.866788 40.215997 11 38 11 C 35.784003 11 34 12.866788 34 15.185547 L 34 22.945312 C 33.72503 23.436938 33.386634 23.876948 33 24.261719 L 33 20.185547 C 33 17.866788 31.215997 16 29 16 C 26.784003 16 25 17.866788 25 20.185547 L 25 24.666016 C 23.150282 23.179661 22.297135 20.683826 23.027344 18.265625 C 23.981604 15.105485 27.261604 13.320305 30.433594 14.234375 A 1.50015 1.50015 0 0 0 32.361328 12.945312 A 1.50015 1.50015 0 0 0 32.371094 12.921875 C 33.289974 10.433529 35.657461 8.9181241 38.171875 8.9980469 z M 34 27.302734 L 34 39 L 33 39 L 33 27.925781 C 33.346618 27.7409 33.679606 27.531269 34 27.302734 z " + transform="translate(-180,470.14793)" + id="rect7308" /> + <g + style="display:inline" + transform="translate(80,114.14791)" + id="g7391"> + <g + id="g7393"> + <rect + rx="3.999995" + ry="4.1854858" + y="391" + x="-324" + height="24.999994" + width="7.99999" + id="rect7395" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7397" + width="7.99999" + height="43.999989" + x="-315" + y="372.00003" + ry="4.1854858" + rx="3.999995" /> + <rect + rx="3.999995" + ry="4.1854858" + y="367" + x="-306" + height="49.000015" + width="7.99999" + id="rect7399" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7401" + width="7.99999" + height="36.000011" + x="-297" + y="380" + ry="4.1854858" + rx="3.999995" /> + <rect + rx="3.999995" + ry="4.1854858" + y="386" + x="-288" + height="30.000006" + width="7.99999" + id="rect7403" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7405" + width="28.999994" + height="12.99999" + x="-324" + y="403" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7407" + width="34.999985" + height="21" + x="-315" + y="395" + ry="4.1854858" + rx="3.999995" /> + <path + sodipodi:open="true" + d="m -309.03562,368.40192 a 7.4999938,7.5000024 0 0 1 9.43617,-4.50737 7.4999938,7.5000024 0 0 1 4.76917,9.30661 7.4999938,7.5000024 0 0 1 -9.16976,5.02725" + sodipodi:end="1.8407347" + sodipodi:start="3.4953343" + sodipodi:ry="7.5000024" + sodipodi:rx="7.4999938" + sodipodi:cy="371" + sodipodi:cx="-302" + sodipodi:type="arc" + id="path7409" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /> + <path + sodipodi:open="true" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" + id="path7411" + sodipodi:type="arc" + sodipodi:cx="-311.22797" + sodipodi:cy="375.99963" + sodipodi:rx="7.4999938" + sodipodi:ry="7.5000024" + sodipodi:start="0.31864739" + sodipodi:end="4.9929531" + d="m -304.10552,378.34925 a 7.4999938,7.5000024 0 0 1 -9.38146,4.80209 7.4999938,7.5000024 0 0 1 -4.92078,-9.31976 7.4999938,7.5000024 0 0 1 9.25653,-5.03869" /> + </g> + </g> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + d="m -248,499.29695 -10,-9.5 10,-9.5 z" + id="path7413" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccc" /> + <rect + transform="scale(-1,-1)" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7415" + width="13.061281" + height="4.9999938" + x="238.62494" + y="-492.29697" /> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg new file mode 100644 index 0000000..e27b4b1 --- /dev/null +++ b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg @@ -0,0 +1,179 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="64" + height="64" + viewBox="0 0 64 64.000001" + id="svg6535" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="two-finger-swipe-right.svg"> + <defs + id="defs6537" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1" + inkscape:cx="15.562966" + inkscape:cy="0.15090121" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:showpageshadow="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="2560" + inkscape:window-height="1376" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1"> + <inkscape:grid + type="xygrid" + id="grid7931" /> + </sodipodi:namedview> + <metadata + id="metadata6540"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(180,-470.14793)"> + <path + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m -129,475.29637 0,7 -14.48047,0 c -0.8685,-2.58098 -2.88029,-4.74387 -5.63867,-5.67578 -1.15023,-0.3886 -2.3288,-0.52948 -3.47461,-0.45118 -3.16004,0.21596 -6.07335,2.10345 -7.50391,5.06055 -0.72859,-0.0926 -1.45444,-0.10529 -2.16406,-0.0215 -3.45482,0.40801 -6.51775,2.81143 -7.58203,6.33594 -1.30748,4.32992 0.83762,8.91584 4.84375,10.76757 l 0,18.83594 -1,0 0,-7.81445 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,7.81445 0,8.81445 0,4.18555 4,0 25,0 11,0 c 2.216,0 4,-1.86679 4,-4.18555 l 0,-12.6289 0,-9 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,4.81445 -1,0 0,-10.81445 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,10.81445 -1,0 0,-15.95703 c 2.13659,-1.06416 3.85324,-2.95852 4.60352,-5.40235 0.0502,-0.16362 0.0846,-0.32791 0.125,-0.49218 l 14.27148,0 0,7 10,-9.5 -10,-9.5 z m -22.82812,3.84961 c 0.58024,0.0183 1.16811,0.12047 1.74804,0.3164 2.47175,0.83508 4.03799,3.09857 4.08008,5.56641 l 0,0.23242 c -0.012,0.54544 -0.0952,1.09784 -0.26367,1.64649 -0.33419,1.08853 -0.94913,2.00757 -1.73633,2.71289 l 0,-4.28711 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,7.75976 c -0.27497,0.49163 -0.61337,0.93164 -1,1.31641 l 0,-4.07617 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,4.48242 c -1.84972,-1.48636 -2.70286,-3.98414 -1.97266,-6.40235 0.95426,-3.16014 4.23426,-4.94531 7.40625,-4.03125 a 1.50015,1.50015 0 0 0 1.92774,-1.28906 1.5004025,1.5004025 0 0 0 0.01,-0.0234 c 0.91888,-2.48834 3.28636,-4.00326 5.80078,-3.92383 z M -156,497.45066 l 0,11.69727 -1,0 0,-11.07227 c 0.34667,-0.1849 0.67956,-0.39643 1,-0.625 z" + id="rect6513" + inkscape:connector-curvature="0" /> + <g + id="g7304" + transform="translate(18,114.14791)" + style="display:inline"> + <g + id="g7306"> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7308" + width="7.99999" + height="24.999994" + x="-324" + y="391" + ry="4.1854858" + rx="3.999995" /> + <rect + rx="3.999995" + ry="4.1854858" + y="372.00003" + x="-315" + height="43.999989" + width="7.99999" + id="rect7310" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7312" + width="7.99999" + height="49.000015" + x="-306" + y="367" + ry="4.1854858" + rx="3.999995" /> + <rect + rx="3.999995" + ry="4.1854858" + y="380" + x="-297" + height="36.000011" + width="7.99999" + id="rect7314" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect7316" + width="7.99999" + height="30.000006" + x="-288" + y="386" + ry="4.1854858" + rx="3.999995" /> + <rect + y="403" + x="-324" + height="12.99999" + width="28.999994" + id="rect7318" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + rx="3.999995" + ry="4.1854858" + y="395" + x="-315" + height="21" + width="34.999985" + id="rect7320" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" + id="path7322" + sodipodi:type="arc" + sodipodi:cx="-302" + sodipodi:cy="371" + sodipodi:rx="7.4999938" + sodipodi:ry="7.5000024" + sodipodi:start="3.4953343" + sodipodi:end="1.8407347" + d="m -309.03562,368.40192 a 7.4999938,7.5000024 0 0 1 9.43617,-4.50737 7.4999938,7.5000024 0 0 1 4.76917,9.30661 7.4999938,7.5000024 0 0 1 -9.16976,5.02725" + sodipodi:open="true" /> + <path + d="m -304.10552,378.34925 a 7.4999938,7.5000024 0 0 1 -9.38146,4.80209 7.4999938,7.5000024 0 0 1 -4.92078,-9.31976 7.4999938,7.5000024 0 0 1 9.25653,-5.03869" + sodipodi:end="4.9929531" + sodipodi:start="0.31864739" + sodipodi:ry="7.5000024" + sodipodi:rx="7.4999938" + sodipodi:cy="375.99963" + sodipodi:cx="-311.22797" + sodipodi:type="arc" + id="path7324" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" + sodipodi:open="true" /> + </g> + </g> + <path + sodipodi:nodetypes="cccc" + inkscape:connector-curvature="0" + id="path7326" + d="m -260.99992,494.29695 10,-9.5 -10,-9.5 z" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" /> + <rect + y="-487.29697" + x="-278" + height="4.9999919" + width="20.686279" + id="rect7328" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + transform="scale(1,-1)" /> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg new file mode 100644 index 0000000..a395bf5 --- /dev/null +++ b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="64" + height="64" + viewBox="0 0 64 64.000001" + id="svg6535" + version="1.1" + inkscape:version="0.92.4 5da689c313, 2019-01-14" + sodipodi:docname="gesture-palm-swipe-right-rtl-symbolic.svg"> + <defs + id="defs6537" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="6.7670154" + inkscape:cx="32.723254" + inkscape:cy="14.119354" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:showpageshadow="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1280" + inkscape:window-height="1376" + inkscape:window-x="1280" + inkscape:window-y="27" + inkscape:window-maximized="0" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:object-nodes="true" + inkscape:snap-nodes="true" + inkscape:snap-bbox="false" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true"> + <inkscape:grid + type="xygrid" + id="grid4193" /> + </sodipodi:namedview> + <metadata + id="metadata6540"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(180,-470.14793)"> + <path + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="M 9.6542969 6.9921875 C 8.6081142 6.96848 7.5705827 7.347386 6.7871094 8.1308594 C 5.2201627 9.697806 5.2783584 12.278358 6.9179688 13.917969 L 10 17 L 10 7.0332031 C 9.8847052 7.0208034 9.7697279 6.9948033 9.6542969 6.9921875 z M 6.1171875 16.185547 C 5.0710043 16.16184 4.0354273 16.538791 3.2519531 17.322266 C 1.6850047 18.889214 1.7432022 21.471718 3.3828125 23.111328 L 10 29.728516 L 10 18.414062 L 9.0390625 17.453125 C 8.2192573 16.63332 7.1633707 16.209254 6.1171875 16.185547 z M 6.8242188 29.621094 C 5.778036 29.597386 4.7424577 29.974339 3.9589844 30.757812 C 2.3920377 32.324759 2.4502333 34.905312 4.0898438 36.544922 L 10 42.455078 L 10 31.142578 L 9.7460938 30.888672 C 8.9262885 30.068867 7.8704014 29.644801 6.8242188 29.621094 z " + transform="translate(-180,470.14793)" + id="rect7306" /> + <path + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 54 10 L 54 49 L 62 49 L 62 34 L 54 10 z " + transform="translate(-180,470.14793)" + id="path838" /> + <path + style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + d="M 10 7 L 10 57 L 12 57 L 12 7 L 10 7 z M 52 7 L 52 57 L 54 57 L 54 7 L 52 7 z " + transform="translate(-180,470.14793)" + id="rect869" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;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:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m -148,493.14844 c -4.95279,0 -9,4.0472 -9,9 0,4.95279 4.04721,9 9,9 4.95279,0 9,-4.04721 9,-9 0,-4.9528 -4.04721,-9 -9,-9 z m 0,3 c 3.33147,0 6,2.66852 6,6 0,3.33148 -2.66853,6 -6,6 -3.33147,0 -6,-2.66852 -6,-6 0,-3.33148 2.66853,-6 6,-6 z" + id="ellipse7314" + inkscape:connector-curvature="0" /> + <path + style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z" + id="path831" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccsscccccc" /> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + d="m -159,494.64792 -8,7.5 8,7.5 v -5 h 3.5 v -5 h -3.5 z" + id="path7413" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccccc" /> + <g + id="g1107" + transform="translate(-64)"> + <g + id="g1080" + transform="rotate(-45,-191.43501,474.81355)" + style="fill:#000000;fill-opacity:1"> + <rect + rx="3.999995" + ry="4.1854858" + y="490.14792" + x="-185" + height="34" + width="7.99999" + id="rect1074" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect1076" + width="7.999999" + height="30" + x="-194" + y="494.14792" + ry="4.1854858" + rx="3.9999995" /> + <rect + rx="3.999995" + ry="4.1854858" + y="504.14792" + x="-203" + height="20" + width="7.99999" + id="rect1078" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + </g> + <path + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m -134,480.14793 v 39 h 16.00021 v -15 l -8.00013,-24 z" + id="path1082" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccc" /> + <rect + style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + id="rect1084" + width="44" + height="50" + x="-170" + y="477.14792" /> + <rect + y="477.14792" + x="-168" + height="50" + width="40" + id="rect1086" + style="opacity:1;fill:#26a269;fill-opacity:1;stroke:none;stroke-width:1.86338997;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" /> + <ellipse + cx="-148" + cy="502.14792" + rx="7.4999938" + ry="7.5000024" + id="ellipse1088" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /> + <path + style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z" + id="path1090" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccsscccccc" /> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + d="m -159,494.64792 -8,7.5 8,7.5 v -5 h 3.5 v -5 h -3.5 z" + id="path1092" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccccc" /> + <path + inkscape:connector-curvature="0" + id="path1094" + d="m -137,494.64792 8,7.5 -8,7.5 v -5 h -3.5 v -5 h 3.5 z" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + sodipodi:nodetypes="cccccccc" /> + </g> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg new file mode 100644 index 0000000..74b3e39 --- /dev/null +++ b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="64" + height="64" + viewBox="0 0 64 64.000001" + id="svg6535" + version="1.1" + inkscape:version="0.92.4 5da689c313, 2019-01-14" + sodipodi:docname="gesture-palm-swipe-right-symbolic.svg"> + <defs + id="defs6537" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="6.7670154" + inkscape:cx="32.723254" + inkscape:cy="14.119354" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:showpageshadow="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1280" + inkscape:window-height="1376" + inkscape:window-x="1280" + inkscape:window-y="27" + inkscape:window-maximized="0" + inkscape:object-paths="true" + inkscape:snap-intersection-paths="true" + inkscape:object-nodes="true" + inkscape:snap-nodes="true" + inkscape:snap-bbox="false" + inkscape:bbox-paths="true" + inkscape:bbox-nodes="true"> + <inkscape:grid + type="xygrid" + id="grid4193" /> + </sodipodi:namedview> + <metadata + id="metadata6540"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(180,-470.14793)"> + <path + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="M 9.6542969 6.9921875 C 8.6081142 6.96848 7.5705827 7.347386 6.7871094 8.1308594 C 5.2201627 9.697806 5.2783584 12.278358 6.9179688 13.917969 L 10 17 L 10 7.0332031 C 9.8847052 7.0208034 9.7697279 6.9948033 9.6542969 6.9921875 z M 6.1171875 16.185547 C 5.0710043 16.16184 4.0354273 16.538791 3.2519531 17.322266 C 1.6850047 18.889214 1.7432022 21.471718 3.3828125 23.111328 L 10 29.728516 L 10 18.414062 L 9.0390625 17.453125 C 8.2192573 16.63332 7.1633707 16.209254 6.1171875 16.185547 z M 6.8242188 29.621094 C 5.778036 29.597386 4.7424577 29.974339 3.9589844 30.757812 C 2.3920377 32.324759 2.4502333 34.905312 4.0898438 36.544922 L 10 42.455078 L 10 31.142578 L 9.7460938 30.888672 C 8.9262885 30.068867 7.8704014 29.644801 6.8242188 29.621094 z " + transform="translate(-180,470.14793)" + id="rect7306" /> + <path + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 54 10 L 54 49 L 62 49 L 62 34 L 54 10 z " + transform="translate(-180,470.14793)" + id="path838" /> + <path + style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + d="M 10 7 L 10 57 L 12 57 L 12 7 L 10 7 z M 52 7 L 52 57 L 54 57 L 54 7 L 52 7 z " + transform="translate(-180,470.14793)" + id="rect869" /> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;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:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m -148,493.14844 c -4.95279,0 -9,4.0472 -9,9 0,4.95279 4.04721,9 9,9 4.95279,0 9,-4.04721 9,-9 0,-4.9528 -4.04721,-9 -9,-9 z m 0,3 c 3.33147,0 6,2.66852 6,6 0,3.33148 -2.66853,6 -6,6 -3.33147,0 -6,-2.66852 -6,-6 0,-3.33148 2.66853,-6 6,-6 z" + id="ellipse7314" + inkscape:connector-curvature="0" /> + <path + style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z" + id="path831" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccsscccccc" /> + <path + inkscape:connector-curvature="0" + id="path906" + d="m -137,494.64792 8,7.5 -8,7.5 v -5 h -3.5 v -5 h 3.5 z" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + sodipodi:nodetypes="cccccccc" /> + <g + id="g1107" + transform="translate(-64)"> + <g + id="g1080" + transform="rotate(-45,-191.43501,474.81355)" + style="fill:#000000;fill-opacity:1"> + <rect + rx="3.999995" + ry="4.1854858" + y="490.14792" + x="-185" + height="34" + width="7.99999" + id="rect1074" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <rect + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + id="rect1076" + width="7.999999" + height="30" + x="-194" + y="494.14792" + ry="4.1854858" + rx="3.9999995" /> + <rect + rx="3.999995" + ry="4.1854858" + y="504.14792" + x="-203" + height="20" + width="7.99999" + id="rect1078" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + </g> + <path + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m -134,480.14793 v 39 h 16.00021 v -15 l -8.00013,-24 z" + id="path1082" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccc" /> + <rect + style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + id="rect1084" + width="44" + height="50" + x="-170" + y="477.14792" /> + <rect + y="477.14792" + x="-168" + height="50" + width="40" + id="rect1086" + style="opacity:1;fill:#26a269;fill-opacity:1;stroke:none;stroke-width:1.86338997;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" /> + <ellipse + cx="-148" + cy="502.14792" + rx="7.4999938" + ry="7.5000024" + id="ellipse1088" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /> + <path + style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" + d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z" + id="path1090" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccsscccccc" /> + <path + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + d="m -159,494.64792 -8,7.5 8,7.5 v -5 h 3.5 v -5 h -3.5 z" + id="path1092" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccccccc" /> + <path + inkscape:connector-curvature="0" + id="path1094" + d="m -137,494.64792 8,7.5 -8,7.5 v -5 h -3.5 v -5 h 3.5 z" + style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" + sodipodi:nodetypes="cccccccc" /> + </g> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg b/subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg new file mode 100644 index 0000000..39cc8cc --- /dev/null +++ b/subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="16" + height="16" + version="1.1" + id="svg4" + sodipodi:docname="gnome-smartphone-symbolic.svg" + inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> + <metadata + id="metadata10"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs8" /> + <sodipodi:namedview + inkscape:document-rotation="0" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="2560" + inkscape:window-height="1376" + id="namedview6" + showgrid="true" + inkscape:zoom="90.509668" + inkscape:cx="7.1287622" + inkscape:cy="10.843601" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg4"> + <inkscape:grid + type="xygrid" + id="grid855" /> + </sodipodi:namedview> + <path + d="M 4.1972656 0 C 2.9868777 0.01088842 2.0086867 0.9907661 2 2.2011719 L 2 13.800781 C 2 14.999781 2.9952656 16 4.1972656 16 L 11.804688 16 C 13.015135 15.98964 13.992393 15.009296 14 13.798828 L 14 2.1992188 C 14 1.0002185 13.006687 -2.9605947e-16 11.804688 0 L 4.1972656 0 z M 4.1992188 2 L 11.800781 2 C 11.911581 2 12 2.0884187 12 2.1992188 L 12 11.800781 C 12 11.911581 11.911581 12 11.800781 12 L 4.1992188 12 C 4.0884187 12 4 11.911581 4 11.800781 L 4 2.1992188 C 4 2.0884187 4.0884187 2 4.1992188 2 z M 8 12.529297 L 9.9023438 14.431641 C 9.9625935 14.491891 10 14.573683 10 14.666016 L 10 15 L 9.6660156 15 C 9.5736826 15 9.4918906 14.962594 9.4316406 14.902344 L 8 13.470703 L 6.5683594 14.902344 C 6.5081094 14.962594 6.4263177 15 6.3339844 15 L 6 15 L 6 14.666016 C 6 14.573686 6.0374063 14.491891 6.0976562 14.431641 L 8 12.529297 z " + 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:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000000;solid-opacity:1;marker:none" + id="path2" /> +</svg> diff --git a/subprojects/libhandy/examples/icons/light-mode-symbolic.svg b/subprojects/libhandy/examples/icons/light-mode-symbolic.svg new file mode 100644 index 0000000..c2e769b --- /dev/null +++ b/subprojects/libhandy/examples/icons/light-mode-symbolic.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <g fill="#2e3436"> + <path d="M8 4.006c-2.195 0-4 1.805-4 4 0 2.195 1.805 4 4 4 2.195 0 4-1.805 4-4 0-2.195-1.805-4-4-4z" style="line-height:normal;-inkscape-font-specification:Sans;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;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none" color="#bebebe" font-weight="400" font-family="Sans" overflow="visible"/> + <path d="M7.5 0h1c.277 0 .5.223.5.5v2c0 .277-.223.5-.5.5h-1a.499.499 0 0 1-.5-.5v-2c0-.277.223-.5.5-.5zM7.5 13h1c.277 0 .5.223.5.5v2c0 .277-.223.5-.5.5h-1a.499.499 0 0 1-.5-.5v-2c0-.277.223-.5.5-.5zM1.99 2.697l.707-.707a.499.499 0 0 1 .707 0l1.414 1.414a.499.499 0 0 1 0 .707l-.707.707a.499.499 0 0 1-.707 0L1.99 3.404a.499.499 0 0 1 0-.707zM11.182 11.89l.707-.708a.499.499 0 0 1 .707 0l1.415 1.414a.499.499 0 0 1 0 .707l-.708.707a.499.499 0 0 1-.707 0l-1.414-1.414a.499.499 0 0 1 0-.707zM2.697 14.01l-.707-.707a.499.499 0 0 1 0-.707l1.414-1.414a.499.499 0 0 1 .707 0l.707.707a.499.499 0 0 1 0 .707L3.404 14.01a.499.499 0 0 1-.707 0zM11.89 4.818l-.708-.707a.499.499 0 0 1 0-.707l1.414-1.414a.499.499 0 0 1 .707 0l.708.707a.499.499 0 0 1 0 .707l-1.415 1.414a.499.499 0 0 1-.707 0zM16 7.5v1c0 .277-.223.5-.5.5h-2a.499.499 0 0 1-.5-.5v-1c0-.277.223-.5.5-.5h2c.277 0 .5.223.5.5zM3 7.5v1c0 .277-.223.5-.5.5h-2a.499.499 0 0 1-.5-.5v-1c0-.277.223-.5.5-.5h2c.277 0 .5.223.5.5z"/> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg b/subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg new file mode 100644 index 0000000..2764cc2 --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + inkscape:version="1.0 (4035a4fb49, 2020-05-01)" + sodipodi:docname="view-continuous-symbolic.svg" + id="svg8" + version="1.1" + height="16" + width="16"> + <metadata + id="metadata14"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs12" /> + <sodipodi:namedview + inkscape:current-layer="svg8" + inkscape:window-maximized="0" + inkscape:window-y="23" + inkscape:window-x="26" + inkscape:cy="8" + inkscape:cx="8" + inkscape:zoom="32.480545" + showgrid="false" + id="namedview10" + inkscape:window-height="742" + inkscape:window-width="1230" + inkscape:pageshadow="2" + inkscape:pageopacity="0" + guidetolerance="10" + gridtolerance="10" + objecttolerance="10" + borderopacity="1" + bordercolor="#666666" + pagecolor="#ffffff" /> + <g + transform="rotate(-90,8,8)" + id="g6" + fill="#474747" + color="#bebebe"> + <path + id="path2" + opacity="0.35" + overflow="visible" + style="marker:none" + d="M 9.625,11 H 6.375 A 0.374,0.374 0 0 0 6,11.375 v 1.25 C 6,12.833 6.167,13 6.375,13 h 3.25 A 0.374,0.374 0 0 0 10,12.625 v -1.25 A 0.374,0.374 0 0 0 9.625,11 Z m 0,-11 H 6.375 A 0.374,0.374 0 0 0 6,0.375 v 1.25 C 6,1.833 6.167,2 6.375,2 h 3.25 A 0.374,0.374 0 0 0 10,1.625 V 0.375 A 0.374,0.374 0 0 0 9.625,0 Z m 0,14 H 6.375 A 0.374,0.374 0 0 0 6,14.375 v 1.25 C 6,15.833 6.167,16 6.375,16 h 3.25 A 0.374,0.374 0 0 0 10,15.625 v -1.25 A 0.374,0.374 0 0 0 9.625,14 Z m 0,-11 H 6.375 A 0.374,0.374 0 0 0 6,3.375 v 1.25 C 6,4.833 6.167,5 6.375,5 h 3.25 A 0.374,0.374 0 0 0 10,4.625 V 3.375 A 0.374,0.374 0 0 0 9.625,3 Z" /> + <path + id="path4" + overflow="visible" + style="marker:none" + d="M 14,7 H 2 v 2 h 12 z" /> + </g> +</svg> diff --git a/subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg b/subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg new file mode 100644 index 0000000..9269c40 --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" fill="#474747"><path d="M12.293 5.293L9.586 8l2.707 2.707 1.414-1.414L12.414 8l1.293-1.293z" 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" overflow="visible"/><path d="M13 10h1v1h-1zm0-5h1v1h-1z" style="marker:none" overflow="visible"/><path d="M13 5c.554 0 1 .446 1 1s-.446 1-1 1-1-.446-1-1 .446-1 1-1zm0 4c.554 0 1 .446 1 1s-.446 1-1 1-1-.446-1-1 .446-1 1-1z" style="marker:none" overflow="visible"/><path d="M3.707 5.293L2.293 6.707 3.586 8 2.293 9.293l1.414 1.414L6.414 8z" 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" overflow="visible"/><path d="M3 10H2v1h1zm0-5H2v1h1z" style="marker:none" overflow="visible"/><path d="M3 5c-.554 0-1 .446-1 1s.446 1 1 1 1-.446 1-1-.446-1-1-1zm0 4c-.554 0-1 .446-1 1s.446 1 1 1 1-.446 1-1-.446-1-1-1z" style="marker:none" overflow="visible"/></g></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-deck-symbolic.svg b/subprojects/libhandy/examples/icons/widget-deck-symbolic.svg new file mode 100644 index 0000000..3fe8afa --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-deck-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#bebebe" fill="#474747"><path d="M1 0v13h12V0zm2 2h8v9H3z" 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" overflow="visible"/><path d="M14 3v11H4v2h12V3z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" font-weight="400" font-family="Sans" overflow="visible"/><path d="M8.625 4h-3.25A.374.374 0 005 4.375v1.25c0 .208.167.375.375.375h3.25A.374.374 0 009 5.625v-1.25A.374.374 0 008.625 4zm0 3h-3.25A.374.374 0 005 7.375v1.25c0 .208.167.375.375.375h3.25A.374.374 0 009 8.625v-1.25A.374.374 0 008.625 7z" style="marker:none" overflow="visible" opacity=".35"/></g></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg b/subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg new file mode 100644 index 0000000..f390fd6 --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M2.5 1c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm-8 4c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm-8 4c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm-4 4c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5z" fill="#2e3436"/></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg b/subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg new file mode 100644 index 0000000..5763db7 --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#bebebe" fill="#474747"><path d="M0 1v13h6c.176 0 .535.14.822.332.288.192.467.371.467.371l.719.727.711-.735S9.615 14 10 14h6V1h-6c-.901 0-1.572.353-2.043.701-.025-.017-.018-.018-.045-.035C7.452 1.362 6.828 1 6 1zm2 2h4c.138 0 .515.138.813.334.297.196.492.385.492.385l.717.693.695-.715S9.619 3 10 3h4v9h-4c-.89 0-1.562.348-2.033.693-.018-.012-.013-.013-.031-.025C7.476 12.36 6.836 12 6 12H2z" 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" overflow="visible"/><path d="M5.625 5h-2.25A.374.374 0 003 5.375v1.25c0 .207.167.375.375.375h2.25A.374.374 0 006 6.625v-1.25A.374.374 0 005.625 5zm0 3h-2.25A.374.374 0 003 8.375v1.25c0 .208.167.375.375.375h2.25A.374.374 0 006 9.625v-1.25A.374.374 0 005.625 8zm7-3h-2.25a.374.374 0 00-.375.375v1.25c0 .208.167.375.375.375h2.25A.374.374 0 0013 6.625v-1.25A.374.374 0 0012.625 5zm0 3h-2.25a.374.374 0 00-.375.375v1.25c0 .208.167.375.375.375h2.25A.374.374 0 0013 9.625v-1.25A.374.374 0 0012.625 8z" style="marker:none" overflow="visible" opacity=".35"/></g></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-list-symbolic.svg b/subprojects/libhandy/examples/icons/widget-list-symbolic.svg new file mode 100644 index 0000000..c9fec6a --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-list-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M3 3h10v2H3zm0 4h10v2H3zm0 4h10v2H3z" style="marker:none" overflow="visible" color="#bebebe" fill="#474747"/></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-search-symbolic.svg b/subprojects/libhandy/examples/icons/widget-search-symbolic.svg new file mode 100644 index 0000000..1a6200e --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-search-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" font-weight="400" font-family="sans-serif" fill="#474747"><path d="M6.508 1C3.48 1 1.002 3.474 1.002 6.5S3.48 12 6.508 12s5.505-2.474 5.505-5.5S9.536 1 6.508 1zm0 2a3.488 3.488 0 013.505 3.5c0 1.944-1.557 3.5-3.505 3.5a3.488 3.488 0 01-3.506-3.5c0-1.944 1.557-3.5 3.506-3.5z" 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" overflow="visible"/><path d="M10 8.99a1 1 0 00-.696 1.717l4.004 4a1 1 0 101.414-1.414l-4.003-4a1 1 0 00-.72-.303z" 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" overflow="visible"/></g></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg b/subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg new file mode 100644 index 0000000..255754c --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M2 6.006a2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2 2 2 0 012-2zm6 0a2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2 2 2 0 012-2zm6 0a2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2 2 2 0 012-2z" fill="#2e3436"/></svg>
\ No newline at end of file diff --git a/subprojects/libhandy/examples/icons/widget-window-symbolic.svg b/subprojects/libhandy/examples/icons/widget-window-symbolic.svg new file mode 100644 index 0000000..993fd89 --- /dev/null +++ b/subprojects/libhandy/examples/icons/widget-window-symbolic.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <path d="M3.012 1.027c-1.215 0-1.994.779-1.995 1.95V14a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2.955c0-1.238-.8-1.928-1.972-1.928zm7.005 1.963h1v1h1v-1h1v1h-1v1h1v1h-1v-1h-1v1h-1v-1h1v-1h-1zm-7 4.076h10v5.935h-10z" 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;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#2e3436"/> +</svg> diff --git a/subprojects/libhandy/examples/meson.build b/subprojects/libhandy/examples/meson.build new file mode 100644 index 0000000..883607e --- /dev/null +++ b/subprojects/libhandy/examples/meson.build @@ -0,0 +1,26 @@ +if get_option('examples') + +handy_demo_resources = gnome.compile_resources( + 'handy-demo-resources', + 'handy-demo.gresources.xml', + + c_name: 'hdy', +) + +handy_demo_sources = [ + handy_demo_resources, + 'handy-demo.c', + 'hdy-demo-preferences-window.c', + 'hdy-demo-window.c', + 'hdy-view-switcher-demo-window.c', + libhandy_generated_headers, +] + +handy_demo = executable('handy-@0@-demo'.format(apiversion), + handy_demo_sources, + dependencies: libhandy_dep, + gui_app: true, + install: true, +) + +endif diff --git a/subprojects/libhandy/examples/sm.puri.Handy.Demo.json b/subprojects/libhandy/examples/sm.puri.Handy.Demo.json new file mode 100644 index 0000000..f59ea8f --- /dev/null +++ b/subprojects/libhandy/examples/sm.puri.Handy.Demo.json @@ -0,0 +1,29 @@ +{ + "app-id": "sm.puri.Handy.Demo", + "runtime": "org.gnome.Platform", + "runtime-version": "master", + "sdk": "org.gnome.Sdk", + "command": "handy-1-demo", + "finish-args": [ + "--device=all", + "--share=ipc", + "--socket=wayland", + "--socket=x11" + ], + "modules": [ + { + "name": "libhandy", + "buildsystem": "meson", + "builddir": true, + "config-opts": [ + "-Dglade_catalog=disabled" + ], + "sources": [ + { + "type": "git", + "url": "https://gitlab.gnome.org/GNOME/libhandy.git" + } + ] + } + ] +} diff --git a/subprojects/libhandy/examples/style.css b/subprojects/libhandy/examples/style.css new file mode 100644 index 0000000..7182936 --- /dev/null +++ b/subprojects/libhandy/examples/style.css @@ -0,0 +1,8 @@ +stacksidebar list { + border-left-width: 0px; + border-right-width: 0px; +} + +carousel.vertical .carousel-icon { + -gtk-icon-transform: rotate(90deg); +} diff --git a/subprojects/libhandy/glade/glade-catalog.dtd b/subprojects/libhandy/glade/glade-catalog.dtd new file mode 100644 index 0000000..c0d073e --- /dev/null +++ b/subprojects/libhandy/glade/glade-catalog.dtd @@ -0,0 +1,196 @@ +<!ELEMENT glade-catalog (init-function?, + glade-widget-classes?, + glade-widget-group*)> + +<!ATTLIST glade-catalog name CDATA #REQUIRED + version CDATA #IMPLIED + targetable CDATA #IMPLIED + library CDATA #IMPLIED + depends CDATA #IMPLIED + domain CDATA #IMPLIED + book CDATA #IMPLIED + icon-prefix CDATA #IMPLIED + requires CDATA #IMPLIED> + +<!ELEMENT glade-widget-classes (glade-widget-class+)> + +<!ELEMENT glade-widget-class (post-create-function?, + add-child-verify-function?, + add-child-function?, + remove-child-function?, + replace-child-function?, + get-children-function?, + get-internal-child-function?, + child-property-applies-function?, + child-action-activate-function?, + read-widget-function?, + write-widget-function?, + get-property-function?, + set-property-function?, + child-set-property-function?, + child-get-property-function?, + action-activate-function?, + verify-function?, + special-child-type?, + packing-properties?, + packing-actions?, + properties?, + children?, + packing-defaults?, + actions?)> + +<!ATTLIST glade-widget-class toplevel CDATA #IMPLIED + since CDATA #IMPLIED + deprecated CDATA #IMPLIED + use-placeholders CDATA #IMPLIED + default-width CDATA #IMPLIED + default-height CDATA #IMPLIED + name CDATA #REQUIRED + generic-name CDATA #IMPLIED + icon-name CDATA #IMPLIED + title CDATA #REQUIRED + parent CDATA #IMPLIED + get-type-function CDATA #IMPLIED + adaptor CDATA #IMPLIED> + +<!ELEMENT properties (property+)> + +<!ELEMENT property (spec?, + type?, + parameter-spec?, + tooltip?, + parameters?, + set-function?, + get-function?, + verify-function?, + displayable-values?)> + +<!ATTLIST property id CDATA #REQUIRED + since CDATA #IMPLIED + deprecated CDATA #IMPLIED + create-type CDATA #IMPLIED + name CDATA #IMPLIED + tooltip CDATA #IMPLIED + themed-icon CDATA #IMPLIED + stock CDATA #IMPLIED + stock-icon CDATA #IMPLIED + weight CDATA #IMPLIED + transfer-on-paste CDATA #IMPLIED + save-always CDATA #IMPLIED + parentless-widget CDATA #IMPLIED + atk-property CDATA #IMPLIED + default CDATA #IMPLIED + query CDATA #IMPLIED + save CDATA #IMPLIED + common CDATA #IMPLIED + disabled CDATA #IMPLIED + visible CDATA #IMPLIED + custom-layout CDATA #IMPLIED + multiline CDATA #IMPLIED + optional CDATA #IMPLIED + optional-default CDATA #IMPLIED + ignore CDATA #IMPLIED + needs-sync CDATA #IMPLIED + construct-only CDATA #IMPLIED + translatable CDATA #IMPLIED> + +<!ELEMENT parameter-spec (type?, + value-type?, + min?)> +<!ELEMENT value-type (#PCDATA)> +<!ELEMENT min (#PCDATA)> +<!ELEMENT set-function (#PCDATA)> +<!ELEMENT get-function (#PCDATA)> +<!ELEMENT spec (#PCDATA)> +<!ELEMENT tooltip (#PCDATA)> +<!ELEMENT verify-function (#PCDATA)> + +<!ELEMENT displayable-values (value+)> + +<!ELEMENT value EMPTY> + +<!ATTLIST value id CDATA #REQUIRED + name CDATA #REQUIRED> + +<!ELEMENT parameters (parameter+)> + +<!ELEMENT parameter EMPTY> + +<!ATTLIST parameter key CDATA #REQUIRED + value CDATA #REQUIRED> + +<!ELEMENT children (child+)> + +<!ELEMENT child (type, + add-child-function?, + remove-child-function?, + get-children-function?, + get-all-children-function?, + set-property-function?, + get-property-function?, + replace-child-function?, + fill-empty-function?, + properties?)> + +<!ELEMENT type (#PCDATA)> +<!ELEMENT add-child-verify-function (#PCDATA)> +<!ELEMENT add-child-function (#PCDATA)> +<!ELEMENT remove-child-function (#PCDATA)> +<!ELEMENT get-children-function (#PCDATA)> +<!ELEMENT get-all-children-function (#PCDATA)> +<!ELEMENT set-prop-function (#PCDATA)> +<!ELEMENT get-prop-function (#PCDATA)> +<!ELEMENT fill-empty-function (#PCDATA)> +<!ELEMENT replace-child-function (#PCDATA)> +<!ELEMENT child-set-property-function (#PCDATA)> +<!ELEMENT child-get-property-function (#PCDATA)> +<!ELEMENT action-activate-function (#PCDATA)> + +<!ELEMENT post-create-function (#PCDATA)> +<!ELEMENT get-internal-child-function (#PCDATA)> +<!ELEMENT child-property-applies-function (#PCDATA)> + +<!ELEMENT child-action-activate-function (#PCDATA)> + +<!ELEMENT read-widget-function (#PCDATA)> +<!ELEMENT write-widget-function (#PCDATA)> +<!ELEMENT get-property-function (#PCDATA)> +<!ELEMENT set-property-function (#PCDATA)> + +<!ELEMENT glade-widget-group (default-palette-state?, + glade-widget-class-ref+)> + +<!ATTLIST glade-widget-group name CDATA #REQUIRED + title CDATA #REQUIRED> + +<!ELEMENT default-palette-state EMPTY> +<!ATTLIST default-palette-state expanded CDATA #IMPLIED> + +<!ELEMENT glade-widget-class-ref EMPTY> +<!ATTLIST glade-widget-class-ref name CDATA #REQUIRED> + +<!ELEMENT packing-defaults (parent-class+)> + +<!ELEMENT parent-class (child-property+)> +<!ATTLIST parent-class name CDATA #REQUIRED> + +<!ELEMENT child-property EMPTY> +<!ATTLIST child-property id CDATA #REQUIRED + default CDATA #REQUIRED> + +<!ELEMENT special-child-type (#PCDATA)> + +<!ELEMENT packing-properties (property+)> + +<!ELEMENT packing-actions (action+)> + +<!ELEMENT actions (action+)> + +<!ELEMENT action EMPTY> + +<!ATTLIST action id CDATA #REQUIRED + name CDATA #REQUIRED + stock CDATA #IMPLIED + important CDATA #IMPLIED> + +<!ELEMENT init-function (#PCDATA)> diff --git a/subprojects/libhandy/glade/glade-hdy-carousel.c b/subprojects/libhandy/glade/glade-hdy-carousel.c new file mode 100644 index 0000000..c06a4e4 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-carousel.c @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-carousel.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +#include <math.h> + +static gint +hdy_carousel_get_page (HdyCarousel *carousel) +{ + return round (hdy_carousel_get_position (carousel)); +} + +static gboolean +hdy_carousel_is_transient (HdyCarousel *carousel) +{ + return fmod (hdy_carousel_get_position (carousel), 1.0) > 0.00001; +} + +static gint +get_n_pages_excluding_placeholders (GtkContainer *container) +{ + GList *children, *l; + gint n_pages; + + children = gtk_container_get_children (container); + + n_pages = 0; + for (l = children; l; l = l->next) + if (!GLADE_IS_PLACEHOLDER (l->data)) + n_pages++; + + g_list_free (children); + + + return n_pages; +} + +static void +selection_changed_cb (GladeProject *project, + GladeWidget *gwidget) +{ + GList *list; + GtkWidget *page, *sel_widget; + GtkContainer *container; + GList *children, *l; + gint index; + + list = glade_project_selection_get (project); + if (!list || g_list_length (list) != 1) + return; + + sel_widget = list->data; + + container = GTK_CONTAINER (glade_widget_get_object (gwidget)); + + if (!GTK_IS_WIDGET (sel_widget) || + !gtk_widget_is_ancestor (sel_widget, GTK_WIDGET (container))) + return; + + children = gtk_container_get_children (container); + for (l = children; l; l = l->next) { + page = l->data; + if (sel_widget == page || gtk_widget_is_ancestor (sel_widget, page)) { + hdy_carousel_scroll_to (HDY_CAROUSEL (container), page); + index = glade_hdy_get_child_index (container, page); + glade_widget_property_set (gwidget, "page", index); + break; + } + } + g_list_free (children); +} + +static void +project_changed_cb (GladeWidget *gwidget, + GParamSpec *pspec, + gpointer user_data) +{ + GladeProject *project, *old_project; + + project = glade_widget_get_project (gwidget); + old_project = g_object_get_data (G_OBJECT (gwidget), "carousel-project-ptr"); + + if (old_project) + g_signal_handlers_disconnect_by_func (G_OBJECT (old_project), + G_CALLBACK (selection_changed_cb), + gwidget); + + if (project) + g_signal_connect (G_OBJECT (project), "selection-changed", + G_CALLBACK (selection_changed_cb), gwidget); + + g_object_set_data (G_OBJECT (gwidget), "carousel-project-ptr", project); +} + +static void +position_changed_cb (HdyCarousel *carousel, + GParamSpec *pspec, + GladeWidget *gwidget) +{ + gint old_page, new_page; + + glade_widget_property_get (gwidget, "page", &old_page); + new_page = hdy_carousel_get_page (carousel); + + if (old_page == new_page || hdy_carousel_is_transient (carousel)) + return; + + glade_widget_property_set (gwidget, "page", new_page); +} + +void +glade_hdy_carousel_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (container); + + if (reason == GLADE_CREATE_USER) + gtk_container_add (GTK_CONTAINER (container), glade_placeholder_new ()); + + g_signal_connect (G_OBJECT (gwidget), "notify::project", + G_CALLBACK (project_changed_cb), NULL); + + project_changed_cb (gwidget, NULL, NULL); + + g_signal_connect (G_OBJECT (container), "notify::position", + G_CALLBACK (position_changed_cb), gwidget); +} + +void +glade_hdy_carousel_child_action_activate (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *object, + const gchar *action_path) +{ + if (!strcmp (action_path, "insert_page_after") || + !strcmp (action_path, "insert_page_before")) { + GladeWidget *parent; + GladeProperty *property; + GtkWidget *placeholder; + gint pages, index; + + parent = glade_widget_get_from_gobject (container); + glade_widget_property_get (parent, "pages", &pages); + + glade_command_push_group (_("Insert placeholder to %s"), + glade_widget_get_name (parent)); + + index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (object)); + if (!strcmp (action_path, "insert_page_after")) + index++; + + placeholder = glade_placeholder_new (); + + hdy_carousel_insert (HDY_CAROUSEL (container), placeholder, index); + hdy_carousel_scroll_to (HDY_CAROUSEL (container), placeholder); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + property = glade_widget_get_property (parent, "pages"); + glade_command_set_property (property, pages + 1); + + property = glade_widget_get_property (parent, "page"); + glade_command_set_property (property, index); + + glade_command_pop_group (); + } else if (strcmp (action_path, "remove_page") == 0) { + GladeWidget *parent; + GladeProperty *property; + gint pages, position; + + parent = glade_widget_get_from_gobject (container); + glade_widget_property_get (parent, "pages", &pages); + + glade_command_push_group (_("Remove placeholder from %s"), + glade_widget_get_name (parent)); + + g_assert (GLADE_IS_PLACEHOLDER (object)); + gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (object)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + property = glade_widget_get_property (parent, "pages"); + glade_command_set_property (property, pages - 1); + + glade_widget_property_get (parent, "page", &position); + property = glade_widget_get_property (parent, "page"); + glade_command_set_property (property, position); + + glade_command_pop_group (); + } else + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor, + container, + object, + action_path); +} + +static void +set_n_pages (GObject *container, + const GValue *value) +{ + GladeWidget *gbox; + GtkWidget *child; + gint old_size, new_size, i, page; + + new_size = g_value_get_int (value); + old_size = hdy_carousel_get_n_pages (HDY_CAROUSEL (container)); + + if (old_size == new_size) + return; + + for (i = old_size; i < new_size; i++) + gtk_container_add (GTK_CONTAINER (container), glade_placeholder_new ()); + + for (i = old_size; i > 0; i--) { + if (old_size <= new_size) + break; + child = glade_hdy_get_nth_child (GTK_CONTAINER (container), i - 1); + if (GLADE_IS_PLACEHOLDER (child)) { + gtk_container_remove (GTK_CONTAINER (container), child); + old_size--; + } + } + + gbox = glade_widget_get_from_gobject (container); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +static void +set_page (GObject *object, + const GValue *value) +{ + gint new_page; + GtkWidget *child; + + new_page = g_value_get_int (value); + child = glade_hdy_get_nth_child (GTK_CONTAINER (object), new_page); + + if (child) + hdy_carousel_scroll_to (HDY_CAROUSEL (object), child); +} + +void +glade_hdy_carousel_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value) +{ + if (!strcmp (id, "pages")) + set_n_pages (object, value); + else if (!strcmp (id, "page")) + set_page (object, value); + else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object, id, value); + } +} + +void +glade_hdy_carousel_get_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + GValue *value) +{ + if (!strcmp (id, "pages")) { + g_value_reset (value); + g_value_set_int (value, hdy_carousel_get_n_pages (HDY_CAROUSEL (object))); + } else if (!strcmp (id, "page")) { + g_value_reset (value); + g_value_set_int (value, hdy_carousel_get_page (HDY_CAROUSEL (object))); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_property (adaptor, object, id, value); + } +} + +static gboolean +glade_hdy_carousel_verify_n_pages (GObject *object, + const GValue *value) +{ + gint new_size, old_size; + + new_size = g_value_get_int (value); + old_size = get_n_pages_excluding_placeholders (GTK_CONTAINER (object)); + + return old_size <= new_size; +} + +static gboolean +glade_hdy_carousel_verify_page (GObject *object, + const GValue *value) +{ + gint page, pages; + + page = g_value_get_int (value); + pages = hdy_carousel_get_n_pages (HDY_CAROUSEL (object)); + + return 0 <= page && page < pages; +} + +gboolean +glade_hdy_carousel_verify_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value) +{ + if (!strcmp (id, "pages")) + return glade_hdy_carousel_verify_n_pages (object, value); + else if (!strcmp (id, "page")) + return glade_hdy_carousel_verify_page (object, value); + else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property) + return GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, object, + id, value); + + return TRUE; +} + +void +glade_hdy_carousel_add_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child) +{ + GladeWidget *gbox, *gchild; + gint pages, page; + + if (!glade_widget_superuser () && !GLADE_IS_PLACEHOLDER (child)) { + GList *l, *children; + + children = gtk_container_get_children (GTK_CONTAINER (container)); + + for (l = g_list_last (children); l; l = l->prev) { + GtkWidget *widget = l->data; + + if (GLADE_IS_PLACEHOLDER (widget)) { + gtk_container_remove (GTK_CONTAINER (container), widget); + break; + } + } + + g_list_free (children); + } + + gtk_container_add (GTK_CONTAINER (container), GTK_WIDGET (child)); + + gchild = glade_widget_get_from_gobject (child); + if (gchild) + glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + gbox = glade_widget_get_from_gobject (container); + glade_widget_property_get (gbox, "pages", &pages); + glade_widget_property_set (gbox, "pages", pages); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +void +glade_hdy_carousel_remove_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child) +{ + GladeWidget *gbox; + gint pages, page; + + gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (child)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + gbox = glade_widget_get_from_gobject (container); + glade_widget_property_get (gbox, "pages", &pages); + glade_widget_property_set (gbox, "pages", pages); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +void +glade_hdy_carousel_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget) +{ + GladeWidget *gbox, *gchild; + gint pages, page, index; + + index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (current)); + gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (current)); + hdy_carousel_insert (HDY_CAROUSEL (container), GTK_WIDGET (new_widget), + index); + hdy_carousel_scroll_to_full (HDY_CAROUSEL (container), + GTK_WIDGET (new_widget), 0); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + gchild = glade_widget_get_from_gobject (new_widget); + if (gchild) + glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE); + + /* NOTE: make sure to sync this at the end because new_widget could be + * a placeholder and syncing these properties could destroy it. + */ + gbox = glade_widget_get_from_gobject (container); + glade_widget_property_get (gbox, "pages", &pages); + glade_widget_property_set (gbox, "pages", pages); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +void +glade_hdy_carousel_get_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (strcmp (property_name, "position") == 0) + g_value_set_int (value, glade_hdy_get_child_index (GTK_CONTAINER (container), + GTK_WIDGET (child))); + else + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor, + container, + child, + property_name, + value); +} + +void +glade_hdy_carousel_set_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (strcmp (property_name, "position") == 0) { + glade_hdy_reorder_child (GTK_CONTAINER (container), + GTK_WIDGET (child), + g_value_get_int (value)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor, + container, + child, + property_name, + value); + } +} diff --git a/subprojects/libhandy/glade/glade-hdy-carousel.h b/subprojects/libhandy/glade/glade-hdy-carousel.h new file mode 100644 index 0000000..211c0a6 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-carousel.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + + +void glade_hdy_carousel_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason); + +void glade_hdy_carousel_child_action_activate (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *object, + const gchar *action_path); + +void glade_hdy_carousel_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value); +void glade_hdy_carousel_get_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + GValue *value); +gboolean glade_hdy_carousel_verify_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value); + +void glade_hdy_carousel_add_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_carousel_remove_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_carousel_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget); + +void glade_hdy_carousel_get_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); +void glade_hdy_carousel_set_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); diff --git a/subprojects/libhandy/glade/glade-hdy-expander-row.c b/subprojects/libhandy/glade/glade-hdy-expander-row.c new file mode 100644 index 0000000..d1a2ccc --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-expander-row.c @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-list-box.c - GladeWidgetAdaptor for GtkListBox + * Copyright (C) 2013 Kalev Lember + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-expander-row.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +void +glade_hdy_expander_row_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason) +{ + g_object_set (container, "expanded", TRUE, NULL); +} + +void +glade_hdy_expander_row_get_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (strcmp (property_name, "position") == 0) + g_value_set_int (value, glade_hdy_get_child_index (GTK_CONTAINER (container), + GTK_WIDGET (child))); + else + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor, + container, + child, + property_name, + value); +} + +void +glade_hdy_expander_row_set_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (strcmp (property_name, "position") == 0) + glade_hdy_reorder_child (GTK_CONTAINER (container), + GTK_WIDGET (child), + g_value_get_int (value)); + else + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor, + container, + child, + property_name, + value); +} + +void +glade_hdy_expander_row_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (object)); +} + +void +glade_hdy_expander_row_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (object)); +} + +gboolean +glade_hdy_expander_row_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback) +{ + if (GTK_IS_LIST_BOX_ROW (child)) + return TRUE; + + if (user_feedback) { + GladeWidgetAdaptor *row_adaptor = + glade_widget_adaptor_get_by_type (GTK_TYPE_LIST_BOX_ROW); + + glade_util_ui_message (glade_app_get_window (), + GLADE_UI_INFO, NULL, + ONLY_THIS_GOES_IN_THAT_MSG, + glade_widget_adaptor_get_title (row_adaptor), + glade_widget_adaptor_get_title (adaptor)); + } + + return FALSE; +} diff --git a/subprojects/libhandy/glade/glade-hdy-expander-row.h b/subprojects/libhandy/glade/glade-hdy-expander-row.h new file mode 100644 index 0000000..221f65e --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-expander-row.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + +void glade_hdy_expander_row_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason); + +void glade_hdy_expander_row_get_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); +void glade_hdy_expander_row_set_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); + +void glade_hdy_expander_row_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); + +void glade_hdy_expander_row_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); + +gboolean glade_hdy_expander_row_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback); diff --git a/subprojects/libhandy/glade/glade-hdy-header-bar.c b/subprojects/libhandy/glade/glade-hdy-header-bar.c new file mode 100644 index 0000000..12b7afe --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-header-bar.c @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-header-bar.c - GladeWidgetAdaptor for GtkHeaderBar + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-header-bar.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +#define TITLE_DISABLED_MESSAGE _("This property does not apply when a custom title is set") + +typedef struct { + GtkContainer *parent; + GtkWidget *custom_title; + gboolean include_placeholders; + gint count; +} ChildrenData; + +static void +count_children (GtkWidget *widget, gpointer data) +{ + ChildrenData *cdata = data; + + if (widget == cdata->custom_title) + return; + + if ((GLADE_IS_PLACEHOLDER (widget) && cdata->include_placeholders) || + glade_widget_get_from_gobject (widget) != NULL) + cdata->count++; +} + +static gboolean +verify_size (GObject *object, + const GValue *value) +{ + gint new_size; + ChildrenData data; + + new_size = g_value_get_int (value); + + data.parent = GTK_CONTAINER (object); + data.custom_title = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)); + data.include_placeholders = FALSE; + data.count = 0; + + gtk_container_foreach (data.parent, count_children, &data); + + return data.count <= new_size; +} + +static gint +get_n_children (GObject *object) +{ + ChildrenData data; + + data.parent = GTK_CONTAINER (object); + data.custom_title = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)); + data.include_placeholders = TRUE; + data.count = 0; + + gtk_container_foreach (data.parent, count_children, &data); + + return data.count; +} + +static void +parse_finished_cb (GladeProject *project, + GObject *object) +{ + GladeWidget *gbox; + + gbox = glade_widget_get_from_gobject (object); + glade_widget_property_set (gbox, "size", get_n_children (object)); + glade_widget_property_set (gbox, "use-custom-title", hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)) != NULL); +} + +void +glade_hdy_header_bar_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason) +{ + GladeWidget *parent = glade_widget_get_from_gobject (container); + GladeProject *project = glade_widget_get_project (parent); + + if (reason == GLADE_CREATE_LOAD) { + g_signal_connect (project, "parse-finished", + G_CALLBACK (parse_finished_cb), + container); + + return; + } + + if (reason == GLADE_CREATE_USER) + hdy_header_bar_pack_start (HDY_HEADER_BAR (container), + glade_placeholder_new ()); +} + +void +glade_hdy_header_bar_action_activate (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *action_path) +{ + if (!strcmp (action_path, "add_slot")) { + GladeWidget *parent; + GladeProperty *property; + gint size; + + parent = glade_widget_get_from_gobject (object); + + glade_command_push_group (_("Insert placeholder to %s"), + glade_widget_get_name (parent)); + + property = glade_widget_get_property (parent, "size"); + glade_property_get (property, &size); + glade_command_set_property (property, size + 1); + + glade_command_pop_group (); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->action_activate (adaptor, + object, + action_path); + } +} + +void +glade_hdy_header_bar_child_action_activate (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *object, + const gchar *action_path) +{ + if (strcmp (action_path, "remove_slot") == 0) { + GladeWidget *parent; + GladeProperty *property; + + parent = glade_widget_get_from_gobject (container); + glade_command_push_group (_("Remove placeholder from %s"), + glade_widget_get_name (parent)); + + if (g_object_get_data (object, "special-child-type")) { + property = glade_widget_get_property (parent, "use-custom-title"); + glade_command_set_property (property, FALSE); + } else { + gint size; + + gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (object)); + + property = glade_widget_get_property (parent, "size"); + glade_property_get (property, &size); + glade_command_set_property (property, size - 1); + } + + glade_command_pop_group (); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor, + container, + object, + action_path); + } +} + +void +glade_hdy_header_bar_get_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + GValue *value) +{ + if (!strcmp (id, "use-custom-title")) { + g_value_reset (value); + g_value_set_boolean (value, hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)) != NULL); + } else if (!strcmp (id, "size")) { + g_value_reset (value); + g_value_set_int (value, get_n_children (object)); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_property (adaptor, object, id, value); + } +} + +static void +set_size (GObject *object, + const GValue *value) +{ + GList *l, *next; + g_autoptr (GList) children = NULL; + GtkWidget *child; + guint new_size, old_size, i; + + if (glade_util_object_is_loading (object)) + return; + + children = gtk_container_get_children (GTK_CONTAINER (object)); + l = children; + + while (l) { + next = l->next; + if (l->data == hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)) || + (!glade_widget_get_from_gobject (l->data) && !GLADE_IS_PLACEHOLDER (l->data))) + children = g_list_delete_link (children, l); + l = next; + } + + old_size = g_list_length (children); + new_size = g_value_get_int (value); + + if (old_size == new_size) + return; + + for (i = old_size; i < new_size; i++) { + GtkWidget *placeholder = glade_placeholder_new (); + hdy_header_bar_pack_start (HDY_HEADER_BAR (object), placeholder); + } + + for (l = g_list_last (children); l && old_size > new_size; l = l->prev) { + child = l->data; + if (glade_widget_get_from_gobject (child) || !GLADE_IS_PLACEHOLDER (child)) + continue; + + gtk_container_remove (GTK_CONTAINER (object), child); + old_size--; + } +} + +static void +set_use_custom_title (GObject *object, + gboolean use_custom_title) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (object); + GtkWidget *child; + + if (use_custom_title) { + child = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)); + if (!child) { + child = glade_placeholder_new (); + g_object_set_data (G_OBJECT (child), "special-child-type", "title"); + } + } else { + child = NULL; + } + + hdy_header_bar_set_custom_title (HDY_HEADER_BAR (object), child); + + if (GLADE_IS_PLACEHOLDER (child)) { + GList *list, *l; + + list = glade_placeholder_packing_actions (GLADE_PLACEHOLDER (child)); + for (l = list; l; l = l->next) { + GladeWidgetAction *gwa = l->data; + if (!strcmp (glade_widget_action_get_def (gwa)->id, "remove_slot")) + glade_widget_action_set_visible (gwa, FALSE); + } + } + + if (use_custom_title) { + glade_widget_property_set_sensitive (gwidget, "title", FALSE, TITLE_DISABLED_MESSAGE); + glade_widget_property_set_sensitive (gwidget, "subtitle", FALSE, TITLE_DISABLED_MESSAGE); + glade_widget_property_set_sensitive (gwidget, "has-subtitle", FALSE, TITLE_DISABLED_MESSAGE); + } else { + glade_widget_property_set_sensitive (gwidget, "title", TRUE, NULL); + glade_widget_property_set_sensitive (gwidget, "subtitle", TRUE, NULL); + glade_widget_property_set_sensitive (gwidget, "has-subtitle", TRUE, NULL); + } +} + +void +glade_hdy_header_bar_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value) +{ + if (!strcmp (id, "use-custom-title")) { + set_use_custom_title (object, g_value_get_boolean (value)); + } else if (!strcmp (id, "show-close-button")) { + GladeWidget *gwidget = glade_widget_get_from_gobject (object); + + /* We don't set the property to 'ignore' so that we catch this in the adaptor, + * but we also do not apply the property to the runtime object here, thus + * avoiding showing the close button which would in turn close glade itself + * when clicked. + */ + glade_widget_property_set_sensitive (gwidget, "decoration-layout", + g_value_get_boolean (value), + _("The decoration layout does not apply to header bars " + "which do no show window controls")); + } else if (!strcmp (id, "size")) { + set_size (object, value); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object, id, value); + } +} + +void +glade_hdy_header_bar_add_child (GladeWidgetAdaptor *adaptor, + GObject *parent, + GObject *child) +{ + GladeWidget *gbox, *gchild; + gint size; + gchar *special_child_type; + + gchild = glade_widget_get_from_gobject (child); + if (gchild) + glade_widget_set_pack_action_visible (gchild, "remove_slot", FALSE); + + special_child_type = g_object_get_data (child, "special-child-type"); + + if (special_child_type && !strcmp (special_child_type, "title")) { + hdy_header_bar_set_custom_title (HDY_HEADER_BAR (parent), GTK_WIDGET (child)); + + return; + } + + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->add (adaptor, parent, child); + + gbox = glade_widget_get_from_gobject (parent); + if (!glade_widget_superuser ()) { + glade_widget_property_get (gbox, "size", &size); + glade_widget_property_set (gbox, "size", size); + } +} + +void +glade_hdy_header_bar_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + GladeWidget *gbox; + gint size; + gchar *special_child_type; + + special_child_type = g_object_get_data (child, "special-child-type"); + + if (special_child_type && !strcmp (special_child_type, "title")) { + GtkWidget *replacement = glade_placeholder_new (); + + g_object_set_data (G_OBJECT (replacement), "special-child-type", "title"); + hdy_header_bar_set_custom_title (HDY_HEADER_BAR (object), replacement); + + return; + } + + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child)); + + /* Synchronize number of placeholders, this should trigger the set_property method with the + * correct value (not the arbitrary number of children currently in the headerbar) + */ + gbox = glade_widget_get_from_gobject (object); + if (!glade_widget_superuser ()) { + glade_widget_property_get (gbox, "size", &size); + glade_widget_property_set (gbox, "size", size); + } +} + +void +glade_hdy_header_bar_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget) +{ + GladeWidget *gbox; + gchar *special_child_type; + gint size; + + special_child_type = + g_object_get_data (G_OBJECT (current), "special-child-type"); + + if (special_child_type && !strcmp (special_child_type, "title")) { + g_object_set_data (G_OBJECT (new_widget), "special-child-type", "title"); + hdy_header_bar_set_custom_title (HDY_HEADER_BAR (container), + GTK_WIDGET (new_widget)); + + return; + } + + g_object_set_data (G_OBJECT (new_widget), "special-child-type", NULL); + + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->replace_child (adaptor, + container, + current, + new_widget); + + gbox = glade_widget_get_from_gobject (container); + if (!glade_widget_superuser ()) { + glade_widget_property_get (gbox, "size", &size); + glade_widget_property_set (gbox, "size", size); + } +} + +gboolean +glade_hdy_header_bar_verify_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value) +{ + if (!strcmp (id, "size")) + return verify_size (object, value); + else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property) + return GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, + object, + id, + value); + + return TRUE; +} + +static gint +sort_children (GtkWidget *widget_a, GtkWidget *widget_b, GtkWidget *bar) +{ + GladeWidget *gwidget_a, *gwidget_b; + gint position_a, position_b; + GtkWidget *title; + + /* title goes first */ + title = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (bar)); + if (title == widget_a) + return -1; + if (title == widget_b) + return 1; + + if ((gwidget_a = glade_widget_get_from_gobject (widget_a)) && + (gwidget_b = glade_widget_get_from_gobject (widget_b))) { + glade_widget_pack_property_get (gwidget_a, "position", &position_a); + glade_widget_pack_property_get (gwidget_b, "position", &position_b); + + /* If position is the same, try to give an stable order */ + if (position_a == position_b) + return g_strcmp0 (glade_widget_get_name (gwidget_a), + glade_widget_get_name (gwidget_b)); + } else { + gtk_container_child_get (GTK_CONTAINER (bar), widget_a, + "position", &position_a, NULL); + gtk_container_child_get (GTK_CONTAINER (bar), widget_b, + "position", &position_b, NULL); + } + + return position_a - position_b; +} + +GList * +glade_hdy_header_bar_get_children (GladeWidgetAdaptor *adaptor, + GObject *container) +{ + GList *children = GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_children (adaptor, container); + + return g_list_sort_with_data (children, (GCompareDataFunc) sort_children, container); +} + + +void +glade_hdy_header_bar_child_set_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + const GValue *value) +{ + GladeWidget *gbox, *gchild, *gchild_iter; + GList *children, *list; + gboolean is_position; + gint old_position, iter_position, new_position; + static gboolean recursion = FALSE; + + g_return_if_fail (HDY_IS_HEADER_BAR (container)); + g_return_if_fail (GTK_IS_WIDGET (child)); + g_return_if_fail (property_name != NULL || value != NULL); + + gbox = glade_widget_get_from_gobject (container); + gchild = glade_widget_get_from_gobject (child); + + g_return_if_fail (GLADE_IS_WIDGET (gbox)); + + /* Get old position */ + if ((is_position = (strcmp (property_name, "position") == 0)) != FALSE) { + gtk_container_child_get (GTK_CONTAINER (container), + GTK_WIDGET (child), + "position", &old_position, + NULL); + + + /* Get the real value */ + new_position = g_value_get_int (value); + } + + if (is_position && recursion == FALSE) { + children = glade_widget_get_children (gbox); + + for (list = children; list; list = list->next) { + gchild_iter = glade_widget_get_from_gobject (list->data); + + if (gchild_iter == gchild) { + gtk_container_child_set (GTK_CONTAINER (container), + GTK_WIDGET (child), + "position", new_position, + NULL); + + continue; + } + + /* Get the old value from glade */ + glade_widget_pack_property_get (gchild_iter, "position", &iter_position); + + /* Search for the child at the old position and update it */ + if (iter_position == new_position && + glade_property_superuser () == FALSE) { + /* Update glade with the real value */ + recursion = TRUE; + glade_widget_pack_property_set (gchild_iter, "position", old_position); + recursion = FALSE; + + continue; + } else { + gtk_container_child_set (GTK_CONTAINER (container), + GTK_WIDGET (list->data), + "position", iter_position, + NULL); + } + } + + for (list = children; list; list = list->next) { + gchild_iter = glade_widget_get_from_gobject (list->data); + + /* Refresh values yet again */ + glade_widget_pack_property_get (gchild_iter, "position", &iter_position); + + gtk_container_child_set (GTK_CONTAINER (container), + GTK_WIDGET (list->data), + "position", iter_position, + NULL); + } + + if (children) + g_list_free (children); + } + + /* Chain Up */ + if (!is_position) + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor, + container, + child, + property_name, + value); +} diff --git a/subprojects/libhandy/glade/glade-hdy-header-bar.h b/subprojects/libhandy/glade/glade-hdy-header-bar.h new file mode 100644 index 0000000..8f6c84a --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-header-bar.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + + +void glade_hdy_header_bar_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason); + +void glade_hdy_header_bar_action_activate (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *action_path); + +void glade_hdy_header_bar_child_action_activate (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *object, + const gchar *action_path); + +void glade_hdy_header_bar_get_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + GValue *value); +void glade_hdy_header_bar_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value); + +void glade_hdy_header_bar_add_child (GladeWidgetAdaptor *adaptor, + GObject *parent, + GObject *child); +void glade_hdy_header_bar_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); +void glade_hdy_header_bar_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget); + +gboolean glade_hdy_header_bar_verify_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value); + +GList *glade_hdy_header_bar_get_children (GladeWidgetAdaptor *adaptor, + GObject *container); + +void glade_hdy_header_bar_child_set_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + const GValue *value); diff --git a/subprojects/libhandy/glade/glade-hdy-header-group.c b/subprojects/libhandy/glade/glade-hdy-header-group.c new file mode 100644 index 0000000..3ef633e --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-header-group.c @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-size-group.c - GladeWidgetAdaptor for GtkSizeGroup + * Copyright (C) 2013 Tristan Van Berkom + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-header-group.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +#define GLADE_TAG_HEADERGROUP_WIDGETS "headerbars" +#define GLADE_TAG_HEADERGROUP_WIDGET "headerbar" + + +static void +glade_hdy_header_group_read_widgets (GladeWidget *widget, GladeXmlNode *node) +{ + GladeXmlNode *widgets_node; + GladeProperty *property; + gchar *string = NULL; + + if ((widgets_node = + glade_xml_search_child (node, GLADE_TAG_HEADERGROUP_WIDGETS)) != NULL) { + GladeXmlNode *n; + + for (n = glade_xml_node_get_children (widgets_node); + n; n = glade_xml_node_next (n)) { + gchar *widget_name, *tmp; + + if (!glade_xml_node_verify (n, GLADE_TAG_HEADERGROUP_WIDGET)) + continue; + + widget_name = glade_xml_get_property_string_required + (n, GLADE_TAG_NAME, NULL); + + if (string == NULL) { + string = widget_name; + } else if (widget_name != NULL) { + tmp = + g_strdup_printf ("%s%s%s", string, GLADE_PROPERTY_DEF_OBJECT_DELIMITER, + widget_name); + string = (g_free (string), tmp); + g_free (widget_name); + } + } + } + + if (string) { + property = glade_widget_get_property (widget, "headerbars"); + g_assert (property); + + /* we must synchronize this directly after loading this project + * (i.e. lookup the actual objects after they've been parsed and + * are present). + */ + g_object_set_data_full (G_OBJECT (property), + "glade-loaded-object", string, g_free); + } +} + +void +glade_hdy_header_group_read_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlNode *node) +{ + if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) || + glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE))) + return; + + /* First chain up and read in all the normal properties.. */ + GWA_GET_CLASS (G_TYPE_OBJECT)->read_widget (adaptor, widget, node); + + glade_hdy_header_group_read_widgets (widget, node); +} + + +static void +glade_hdy_header_group_write_widgets (GladeWidget *widget, + GladeXmlContext *context, + GladeXmlNode *node) +{ + GladeXmlNode *widgets_node, *widget_node; + GList *widgets = NULL, *list; + GladeWidget *awidget; + + widgets_node = glade_xml_node_new (context, GLADE_TAG_HEADERGROUP_WIDGETS); + + if (glade_widget_property_get (widget, "headerbars", &widgets)) { + for (list = widgets; list; list = list->next) { + awidget = glade_widget_get_from_gobject (list->data); + widget_node = + glade_xml_node_new (context, GLADE_TAG_HEADERGROUP_WIDGET); + glade_xml_node_append_child (widgets_node, widget_node); + glade_xml_node_set_property_string (widget_node, GLADE_TAG_NAME, + glade_widget_get_name (awidget)); + } + } + + if (!glade_xml_node_get_children (widgets_node)) + glade_xml_node_delete (widgets_node); + else + glade_xml_node_append_child (node, widgets_node); +} + + +void +glade_hdy_header_group_write_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlContext *context, + GladeXmlNode *node) +{ + if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) || + glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE))) + return; + + /* First chain up and read in all the normal properties.. */ + GWA_GET_CLASS (G_TYPE_OBJECT)->write_widget (adaptor, widget, context, node); + + glade_hdy_header_group_write_widgets (widget, context, node); +} + + +void +glade_hdy_header_group_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *property_name, + const GValue *value) +{ + if (!strcmp (property_name, "headerbars")) { + GSList *sg_widgets, *slist; + GList *widgets, *list; + + /* remove old widgets */ + if ((sg_widgets = + hdy_header_group_get_children (HDY_HEADER_GROUP (object))) != NULL) { + /* copy since we are modifying an internal list */ + sg_widgets = g_slist_copy (sg_widgets); + for (slist = sg_widgets; slist; slist = slist->next) + hdy_header_group_remove_child (HDY_HEADER_GROUP (object), + HDY_HEADER_GROUP_CHILD (slist->data)); + g_slist_free (sg_widgets); + } + + /* add new widgets */ + if ((widgets = g_value_get_boxed (value)) != NULL) { + for (list = widgets; list; list = list->next) + hdy_header_group_add_header_bar (HDY_HEADER_GROUP (object), + HDY_HEADER_BAR (list->data)); + } + } else { + GWA_GET_CLASS (G_TYPE_OBJECT)->set_property (adaptor, object, + property_name, value); + } +} diff --git a/subprojects/libhandy/glade/glade-hdy-header-group.h b/subprojects/libhandy/glade/glade-hdy-header-group.h new file mode 100644 index 0000000..5333c0f --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-header-group.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + + +void glade_hdy_header_group_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *property_name, + const GValue *value); +void glade_hdy_header_group_write_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlContext *context, + GladeXmlNode *node); +void glade_hdy_header_group_read_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlNode *node); diff --git a/subprojects/libhandy/glade/glade-hdy-leaflet.c b/subprojects/libhandy/glade/glade-hdy-leaflet.c new file mode 100644 index 0000000..4032f9f --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-leaflet.c @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-leaflet.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +#define PAGE_DISABLED_MESSAGE _("This property only applies when the leaflet is folded") + +static void +selection_changed_cb (GladeProject *project, + GladeWidget *gwidget) +{ + GList *list; + GtkWidget *page, *sel_widget; + GtkContainer *container = GTK_CONTAINER (glade_widget_get_object (gwidget)); + gint index; + + if ((list = glade_project_selection_get (project)) != NULL && + g_list_length (list) == 1) { + sel_widget = list->data; + + if (GTK_IS_WIDGET (sel_widget) && + gtk_widget_is_ancestor (sel_widget, GTK_WIDGET (container))) { + g_autoptr (GList) children = gtk_container_get_children (container); + GList *l; + + index = 0; + for (l = children; l; l = l->next) { + page = l->data; + if (sel_widget == page || + gtk_widget_is_ancestor (sel_widget, page)) { + glade_widget_property_set (gwidget, "page", index); + + break; + } + + index++; + } + } + } +} + +static void +project_changed_cb (GladeWidget *gwidget, + GParamSpec *pspec, + gpointer userdata) +{ + GladeProject *project = glade_widget_get_project (gwidget); + GladeProject *old_project = g_object_get_data (G_OBJECT (gwidget), + "project-ptr"); + + if (old_project) + g_signal_handlers_disconnect_by_func (G_OBJECT (old_project), + G_CALLBACK (selection_changed_cb), + gwidget); + + if (project) + g_signal_connect (G_OBJECT (project), + "selection-changed", + G_CALLBACK (selection_changed_cb), + gwidget); + + g_object_set_data (G_OBJECT (gwidget), "project-ptr", project); +} + +static void +add_named (GtkContainer *container, + GtkWidget *child, + const gchar *name) +{ + gtk_container_add_with_properties (container, + child, + "name", name, + NULL); +} + +static void +folded_changed_cb (HdyLeaflet *leaflet, + GParamSpec *pspec, + gpointer userdata) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (leaflet); + gboolean folded = hdy_leaflet_get_folded (leaflet); + + glade_widget_property_set_sensitive (gwidget, + "page", + folded, + folded ? NULL : PAGE_DISABLED_MESSAGE); +} + +void +glade_hdy_leaflet_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (container); + + if (reason == GLADE_CREATE_USER) + add_named (GTK_CONTAINER (container), + glade_placeholder_new (), + "page0"); + + g_signal_connect (G_OBJECT (gwidget), + "notify::project", + G_CALLBACK (project_changed_cb), + NULL); + + project_changed_cb (gwidget, NULL, NULL); + + if (HDY_IS_LEAFLET (container)) { + g_signal_connect (container, + "notify::folded", + G_CALLBACK (folded_changed_cb), + NULL); + + folded_changed_cb (HDY_LEAFLET (container), NULL, NULL); + } +} + +static GtkWidget * +get_child_by_name (GtkContainer *container, + const gchar *name) +{ + g_autoptr (GList) children = gtk_container_get_children (container); + GList *l; + + for (l = children; l; l = l->next) { + const gchar *child_name; + + gtk_container_child_get (container, l->data, "name", &child_name, NULL); + + if (child_name && !strcmp (child_name, name)) + return l->data; + } + + return NULL; +} + +static gchar * +get_unused_name (GtkContainer *container) +{ + gint i = 0; + + while (TRUE) { + g_autofree gchar *name = g_strdup_printf ("page%d", i); + + if (get_child_by_name (container, name) == NULL) + return g_steal_pointer (&name); + + i++; + } + + return NULL; +} + +void +glade_hdy_leaflet_child_action_activate (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *object, + const gchar *action_path) +{ + if (!strcmp (action_path, "insert_page_after") || + !strcmp (action_path, "insert_page_before")) { + GladeWidget *parent = glade_widget_get_from_gobject (container); + GladeProperty *property; + g_autofree gchar *name = NULL; + GtkWidget *new_widget; + gint pages, index; + + glade_widget_property_get (parent, "pages", &pages); + + glade_command_push_group (_("Insert placeholder to %s"), + glade_widget_get_name (parent)); + + index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (object)); + + if (!strcmp (action_path, "insert_page_after")) + index++; + + name = get_unused_name (GTK_CONTAINER (container)); + new_widget = glade_placeholder_new (); + add_named (GTK_CONTAINER (container), new_widget, name); + glade_hdy_reorder_child (GTK_CONTAINER (container), new_widget, index); + g_object_set (container, "visible-child", new_widget, NULL); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + property = glade_widget_get_property (parent, "pages"); + glade_command_set_property (property, pages + 1); + + property = glade_widget_get_property (parent, "page"); + glade_command_set_property (property, index); + + glade_command_pop_group (); + } else if (strcmp (action_path, "remove_page") == 0) { + GladeWidget *parent = glade_widget_get_from_gobject (container); + GladeProperty *property; + gint pages, index; + + glade_widget_property_get (parent, "pages", &pages); + + glade_command_push_group (_("Remove placeholder from %s"), + glade_widget_get_name (parent)); + g_assert (GLADE_IS_PLACEHOLDER (object)); + gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (object)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + property = glade_widget_get_property (parent, "pages"); + glade_command_set_property (property, pages - 1); + + glade_widget_property_get (parent, "page", &index); + property = glade_widget_get_property (parent, "page"); + glade_command_set_property (property, index); + + glade_command_pop_group (); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor, + container, + object, + action_path); + } +} + +typedef struct { + gint size; + gboolean include_placeholders; +} ChildData; + +static void +count_child (GtkWidget *child, + gpointer data) +{ + ChildData *cdata = data; + + if (cdata->include_placeholders || !GLADE_IS_PLACEHOLDER (child)) + cdata->size++; +} + +static gint +get_n_pages (GtkContainer *container, + gboolean include_placeholders) +{ + ChildData data; + + data.size = 0; + data.include_placeholders = include_placeholders; + gtk_container_foreach (container, count_child, &data); + + return data.size; +} + +static void +set_n_pages (GObject *object, + const GValue *value) +{ + GladeWidget *gbox; + GtkContainer *container = GTK_CONTAINER (object); + GtkWidget *child; + gint new_size = g_value_get_int (value); + gint old_size = get_n_pages (container, TRUE); + gint i, page; + + if (old_size == new_size) + return; + + for (i = old_size; i < new_size; i++) { + g_autofree gchar *name = get_unused_name (container); + child = glade_placeholder_new (); + add_named (container, child, name); + } + + for (i = old_size; i > 0; i--) { + if (old_size <= new_size) + break; + + child = glade_hdy_get_nth_child (container, i - 1); + if (GLADE_IS_PLACEHOLDER (child)) { + gtk_container_remove (container, child); + old_size--; + } + } + + gbox = glade_widget_get_from_gobject (container); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +static void +set_page (GObject *object, + const GValue *value) +{ + gint new_page = g_value_get_int (value); + GtkWidget *child = glade_hdy_get_nth_child (GTK_CONTAINER (object), new_page); + + if (!child) + return; + + g_object_set (object, "visible-child", child, NULL); +} + +static gint +get_page (GtkContainer *container) +{ + GtkWidget *child; + + g_object_get (container, "visible-child", &child, NULL); + + return glade_hdy_get_child_index (container, child); +} + +void +glade_hdy_leaflet_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value) +{ + if (!strcmp (id, "pages")) + set_n_pages (object, value); + else if (!strcmp (id, "page")) + set_page (object, value); + else + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object, id, value); +} + +void +glade_hdy_leaflet_get_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + GValue *value) +{ + if (!strcmp (id, "pages")) { + g_value_reset (value); + g_value_set_int (value, get_n_pages (GTK_CONTAINER (object), TRUE)); + } else if (!strcmp (id, "page")) { + g_value_reset (value); + g_value_set_int (value, get_page (GTK_CONTAINER (object))); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_property (adaptor, object, id, value); + } +} + +static gboolean +verify_n_pages (GObject *object, + const GValue *value) +{ + gint new_size = g_value_get_int (value); + gint old_size = get_n_pages (GTK_CONTAINER (object), FALSE); + + return old_size <= new_size; +} + +static gboolean +verify_page (GObject *object, + const GValue *value) +{ + gint page = g_value_get_int (value); + gint pages = get_n_pages (GTK_CONTAINER (object), TRUE); + + if (page < 0 && page >= pages) + return FALSE; + + if (HDY_IS_LEAFLET (object)) { + GtkWidget *child = glade_hdy_get_nth_child (GTK_CONTAINER (object), page); + gboolean navigatable; + + gtk_container_child_get (GTK_CONTAINER (object), child, + "navigatable", &navigatable, + NULL); + + if (!navigatable) + return FALSE; + } + + return TRUE; +} + +gboolean +glade_hdy_leaflet_verify_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value) +{ + if (!strcmp (id, "pages")) + return verify_n_pages (object, value); + else if (!strcmp (id, "page")) + return verify_page (object, value); + else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property) + return GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, object, id, value); + + return TRUE; +} + + +void +glade_hdy_leaflet_get_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (strcmp (property_name, "position") == 0) + g_value_set_int (value, glade_hdy_get_child_index (GTK_CONTAINER (container), + GTK_WIDGET (child))); + else + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor, + container, + child, + property_name, + value); +} + +void +glade_hdy_leaflet_set_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (strcmp (property_name, "position") == 0) { + glade_hdy_reorder_child (GTK_CONTAINER (container), + GTK_WIDGET (child), + g_value_get_int (value)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor, + container, + child, + property_name, + value); + } +} + +void +glade_hdy_leaflet_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + GladeWidget *gbox, *gchild; + gint pages, page; + + if (!glade_widget_superuser () && !GLADE_IS_PLACEHOLDER (child)) { + g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (object)); + GList *l; + + for (l = g_list_last (children); l; l = l->prev) { + GtkWidget *widget = l->data; + if (GLADE_IS_PLACEHOLDER (widget)) { + gtk_container_remove (GTK_CONTAINER (object), widget); + + break; + } + } + } + + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (object)); + + gchild = glade_widget_get_from_gobject (child); + if (gchild != NULL) + glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE); + + gbox = glade_widget_get_from_gobject (object); + glade_widget_property_get (gbox, "pages", &pages); + glade_widget_property_set (gbox, "pages", pages); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +void +glade_hdy_leaflet_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + GladeWidget *gbox; + gint pages, page; + + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child)); + + glade_hdy_sync_child_positions (GTK_CONTAINER (object)); + + gbox = glade_widget_get_from_gobject (object); + glade_widget_property_get (gbox, "pages", &pages); + glade_widget_property_set (gbox, "pages", pages); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} + +void +glade_hdy_leaflet_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget) +{ + GladeWidget *gchild; + GladeWidget *gbox; + gint pages, page, index; + + index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (current)); + gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (current)); + gtk_container_add (GTK_CONTAINER (container), GTK_WIDGET (new_widget)); + glade_hdy_reorder_child (GTK_CONTAINER (container), GTK_WIDGET (new_widget), index); + + glade_hdy_sync_child_positions (GTK_CONTAINER (container)); + + gbox = glade_widget_get_from_gobject (container); + + gchild = glade_widget_get_from_gobject (new_widget); + if (gchild != NULL) + glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE); + + /* NOTE: make sure to sync this at the end because new_widget could be + * a placeholder and syncing these properties could destroy it. + */ + glade_widget_property_get (gbox, "pages", &pages); + glade_widget_property_set (gbox, "pages", pages); + glade_widget_property_get (gbox, "page", &page); + glade_widget_property_set (gbox, "page", page); +} diff --git a/subprojects/libhandy/glade/glade-hdy-leaflet.h b/subprojects/libhandy/glade/glade-hdy-leaflet.h new file mode 100644 index 0000000..9f274b0 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-leaflet.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + + +void glade_hdy_leaflet_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason); + +void glade_hdy_leaflet_child_action_activate (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *object, + const gchar *action_path); + +void glade_hdy_leaflet_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value); +void glade_hdy_leaflet_get_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + GValue *value); +gboolean glade_hdy_leaflet_verify_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *id, + const GValue *value); + +void glade_hdy_leaflet_get_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); +void glade_hdy_leaflet_set_child_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); + +void glade_hdy_leaflet_add_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_leaflet_remove_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_leaflet_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget); diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-page.c b/subprojects/libhandy/glade/glade-hdy-preferences-page.c new file mode 100644 index 0000000..f1a62c7 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-preferences-page.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-preferences-page.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +static GtkWidget * +get_child_by_title (GtkContainer *container, + const gchar *title) +{ + g_autoptr (GList) children = gtk_container_get_children (container); + GList *l; + + for (l = children; l; l = l->next) { + const gchar *child_title; + + g_assert (HDY_IS_PREFERENCES_GROUP (l->data)); + + child_title = hdy_preferences_group_get_title (HDY_PREFERENCES_GROUP (l->data)); + + if (child_title && !strcmp (child_title, title)) + return l->data; + } + + return NULL; +} + +static gchar * +get_unused_title (GtkContainer *container) +{ + gint i = 1; + + while (TRUE) { + g_autofree gchar *title = g_strdup_printf ("Group %d", i); + + if (get_child_by_title (container, title) == NULL) + return g_steal_pointer (&title); + + i++; + } + + return NULL; +} + +static void +add_group (GladeWidgetAdaptor *adaptor, + GObject *container) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (container); + GladeWidget *gpage; + GladeWidgetAdaptor *page_adaptor; + g_autofree gchar *title = get_unused_title (GTK_CONTAINER (container)); + + page_adaptor = glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_GROUP); + + gpage = glade_widget_adaptor_create_widget (page_adaptor, FALSE, + "parent", gwidget, + "project", glade_widget_get_project (gwidget), + NULL); + + glade_widget_property_set (gpage, "title", title); + + glade_widget_add_child (gwidget, gpage, FALSE); +} + +void +glade_hdy_preferences_page_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason) +{ + if (reason == GLADE_CREATE_USER) { + add_group (adaptor, container); + add_group (adaptor, container); + add_group (adaptor, container); + } +} + +gboolean +glade_hdy_preferences_page_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback) +{ + if (!HDY_IS_PREFERENCES_GROUP (child)) { + if (user_feedback) { + GladeWidgetAdaptor *page_adaptor = + glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_GROUP); + + glade_util_ui_message (glade_app_get_window (), + GLADE_UI_INFO, NULL, + ONLY_THIS_GOES_IN_THAT_MSG, + glade_widget_adaptor_get_title (page_adaptor), + glade_widget_adaptor_get_title (adaptor)); + } + + return FALSE; + } + + return TRUE; +} + +void +glade_hdy_preferences_page_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child)); +} + +void +glade_hdy_preferences_page_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child)); +} + +void +glade_hdy_preferences_page_replace_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *current, + GObject *new_widget) +{ + gint index = glade_hdy_get_child_index (GTK_CONTAINER (object), GTK_WIDGET (current)); + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (current)); + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (new_widget)); + glade_hdy_reorder_child (GTK_CONTAINER (object), GTK_WIDGET (new_widget), index); +} + +GList * +glade_hdy_preferences_page_get_children (GladeWidgetAdaptor *adaptor, + GObject *object) +{ + return gtk_container_get_children (GTK_CONTAINER (object)); +} + +void +glade_hdy_preferences_page_action_activate (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *action_path) +{ + GladeWidget *parent = glade_widget_get_from_gobject (object); + + if (!g_strcmp0 (action_path, "add_group")) { + g_autofree gchar *title = get_unused_title (GTK_CONTAINER (object)); + GladeWidget *gchild; + + glade_command_push_group (_("Add group to %s"), + glade_widget_get_name (parent)); + + gchild = glade_command_create (glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_GROUP), + parent, + NULL, + glade_widget_get_project (parent)); + + glade_widget_property_set (gchild, "title", title); + + glade_command_pop_group (); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->action_activate (adaptor, + object, + action_path); + } +} + +void +glade_hdy_preferences_page_child_set_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + const GValue *value) +{ + if (!g_strcmp0 (property_name, "position")) { + GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child)); + + gtk_container_child_set_property (GTK_CONTAINER (parent), + GTK_WIDGET (child), property_name, value); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor, + container, + child, + property_name, + value); + } +} + +void +glade_hdy_preferences_page_child_get_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (!g_strcmp0 (property_name, "position")) { + GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child)); + + gtk_container_child_get_property (GTK_CONTAINER (parent), + GTK_WIDGET (child), property_name, value); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor, + container, + child, + property_name, + value); + } +} diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-page.h b/subprojects/libhandy/glade/glade-hdy-preferences-page.h new file mode 100644 index 0000000..2fd3f88 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-preferences-page.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + +void glade_hdy_preferences_page_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason); + +gboolean glade_hdy_preferences_page_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback); + +void glade_hdy_preferences_page_add_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_preferences_page_remove_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_preferences_page_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget); + +GList *glade_hdy_preferences_page_get_children (GladeWidgetAdaptor *adaptor, + GObject *object); + +void glade_hdy_preferences_page_action_activate (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *action_path); + +void glade_hdy_preferences_page_child_set_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + const GValue *value); + +void glade_hdy_preferences_page_child_get_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-window.c b/subprojects/libhandy/glade/glade-hdy-preferences-window.c new file mode 100644 index 0000000..1add369 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-preferences-window.c @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-preferences-window.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +static void +selection_changed_cb (GladeProject *project, + GladeWidget *gwidget) +{ + GList *list; + GtkWidget *page, *sel_widget; + GtkContainer *container = GTK_CONTAINER (glade_widget_get_object (gwidget)); + gint index; + + if ((list = glade_project_selection_get (project)) != NULL && + g_list_length (list) == 1) { + sel_widget = list->data; + + if (GTK_IS_WIDGET (sel_widget) && + gtk_widget_is_ancestor (sel_widget, GTK_WIDGET (container))) { + g_autoptr (GList) children = gtk_container_get_children (container); + GList *l; + + index = 0; + for (l = children; l; l = l->next) { + page = l->data; + if (sel_widget == page || + gtk_widget_is_ancestor (sel_widget, page)) { + GtkWidget *parent = gtk_widget_get_parent (page); + + g_object_set (parent, "visible-child", page, NULL); + + break; + } + + index++; + } + } + } +} + +static void +project_changed_cb (GladeWidget *gwidget, + GParamSpec *pspec, + gpointer userdata) +{ + GladeProject *project = glade_widget_get_project (gwidget); + GladeProject *old_project = g_object_get_data (G_OBJECT (gwidget), + "project-ptr"); + + if (old_project) + g_signal_handlers_disconnect_by_func (G_OBJECT (old_project), + G_CALLBACK (selection_changed_cb), + gwidget); + + if (project) + g_signal_connect (G_OBJECT (project), + "selection-changed", + G_CALLBACK (selection_changed_cb), + gwidget); + + g_object_set_data (G_OBJECT (gwidget), "project-ptr", project); +} + +static GtkWidget * +get_child_by_title (GtkContainer *container, + const gchar *title) +{ + g_autoptr (GList) children = gtk_container_get_children (container); + GList *l; + + for (l = children; l; l = l->next) { + const gchar *child_title; + + g_assert (HDY_IS_PREFERENCES_PAGE (l->data)); + + child_title = hdy_preferences_page_get_title (HDY_PREFERENCES_PAGE (l->data)); + + if (child_title && !strcmp (child_title, title)) + return l->data; + } + + return NULL; +} + +static gchar * +get_unused_title (GtkContainer *container) +{ + gint i = 1; + + while (TRUE) { + g_autofree gchar *title = g_strdup_printf ("Page %d", i); + + if (get_child_by_title (container, title) == NULL) + return g_steal_pointer (&title); + + i++; + } + + return NULL; +} + +static void +add_page (GladeWidgetAdaptor *adaptor, + GObject *container) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (container); + GladeWidget *gpage; + GladeWidgetAdaptor *page_adaptor; + g_autofree gchar *title = get_unused_title (GTK_CONTAINER (container)); + + page_adaptor = glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_PAGE); + + gpage = glade_widget_adaptor_create_widget (page_adaptor, FALSE, + "parent", gwidget, + "project", glade_widget_get_project (gwidget), + NULL); + + glade_widget_property_set (gpage, "title", title); + + glade_widget_add_child (gwidget, gpage, FALSE); +} + +void +glade_hdy_preferences_window_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason) +{ + GladeWidget *gwidget = glade_widget_get_from_gobject (container); + + if (reason == GLADE_CREATE_USER) { + add_page (adaptor, container); + add_page (adaptor, container); + add_page (adaptor, container); + } + + g_signal_connect (G_OBJECT (gwidget), + "notify::project", + G_CALLBACK (project_changed_cb), + NULL); + + project_changed_cb (gwidget, NULL, NULL); +} + +gboolean +glade_hdy_preferences_window_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback) +{ + if (!HDY_IS_PREFERENCES_PAGE (child)) { + if (user_feedback) { + GladeWidgetAdaptor *page_adaptor = + glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_PAGE); + + glade_util_ui_message (glade_app_get_window (), + GLADE_UI_INFO, NULL, + ONLY_THIS_GOES_IN_THAT_MSG, + glade_widget_adaptor_get_title (page_adaptor), + glade_widget_adaptor_get_title (adaptor)); + } + + return FALSE; + } + + return TRUE; +} + +void +glade_hdy_preferences_window_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child)); +} + +void +glade_hdy_preferences_window_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child)); +} + +void +glade_hdy_preferences_window_replace_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *current, + GObject *new_widget) +{ + gint index = glade_hdy_get_child_index (GTK_CONTAINER (object), GTK_WIDGET (current)); + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (current)); + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (new_widget)); + glade_hdy_reorder_child (GTK_CONTAINER (object), GTK_WIDGET (new_widget), index); +} + +GList * +glade_hdy_preferences_window_get_children (GladeWidgetAdaptor *adaptor, + GObject *object) +{ + return gtk_container_get_children (GTK_CONTAINER (object)); +} + +void +glade_hdy_preferences_window_action_activate (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *action_path) +{ + GladeWidget *parent = glade_widget_get_from_gobject (object); + + if (!g_strcmp0 (action_path, "add_page")) { + g_autofree gchar *title = get_unused_title (GTK_CONTAINER (object)); + GladeWidget *gchild; + + glade_command_push_group (_("Add page to %s"), + glade_widget_get_name (parent)); + + gchild = glade_command_create (glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_PAGE), + parent, + NULL, + glade_widget_get_project (parent)); + + glade_widget_property_set (gchild, "title", title); + + glade_command_pop_group (); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->action_activate (adaptor, + object, + action_path); + } +} + +void +glade_hdy_preferences_window_child_set_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + const GValue *value) +{ + if (!g_strcmp0 (property_name, "position")) { + GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child)); + + gtk_container_child_set_property (GTK_CONTAINER (parent), + GTK_WIDGET (child), property_name, value); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor, + container, + child, + property_name, + value); + } +} + +void +glade_hdy_preferences_window_child_get_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value) +{ + if (!g_strcmp0 (property_name, "position")) { + GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child)); + + gtk_container_child_get_property (GTK_CONTAINER (parent), + GTK_WIDGET (child), property_name, value); + } else { + GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor, + container, + child, + property_name, + value); + } +} diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-window.h b/subprojects/libhandy/glade/glade-hdy-preferences-window.h new file mode 100644 index 0000000..e4f1f37 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-preferences-window.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + +void glade_hdy_preferences_window_post_create (GladeWidgetAdaptor *adaptor, + GObject *container, + GladeCreateReason reason); + +gboolean glade_hdy_preferences_window_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback); + +void glade_hdy_preferences_window_add_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_preferences_window_remove_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child); +void glade_hdy_preferences_window_replace_child (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *current, + GObject *new_widget); + +GList *glade_hdy_preferences_window_get_children (GladeWidgetAdaptor *adaptor, + GObject *object); + +void glade_hdy_preferences_window_action_activate (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *action_path); + +void glade_hdy_preferences_window_child_set_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + const GValue *value); + +void glade_hdy_preferences_window_child_get_property (GladeWidgetAdaptor *adaptor, + GObject *container, + GObject *child, + const gchar *property_name, + GValue *value); diff --git a/subprojects/libhandy/glade/glade-hdy-search-bar.c b/subprojects/libhandy/glade/glade-hdy-search-bar.c new file mode 100644 index 0000000..c13f1eb --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-search-bar.c @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-searchbar.c - GladeWidgetAdaptor for GtkSearchBar + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-search-bar.h" + +#include <gladeui/glade.h> + +void +glade_hdy_search_bar_post_create (GladeWidgetAdaptor *adaptor, + GObject *widget, + GladeCreateReason reason) +{ + if (reason == GLADE_CREATE_USER) { + GtkWidget *child = glade_placeholder_new (); + gtk_container_add (GTK_CONTAINER (widget), child); + g_object_set_data (G_OBJECT (widget), "child", child); + } + + hdy_search_bar_set_search_mode (HDY_SEARCH_BAR (widget), TRUE); + hdy_search_bar_set_show_close_button (HDY_SEARCH_BAR (widget), FALSE); +} + +void +glade_hdy_search_bar_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + GObject *current = g_object_get_data (G_OBJECT (object), "child"); + + if (current) + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (current))), + GTK_WIDGET (current)); + + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child)); + g_object_set_data (G_OBJECT (object), "child", child); +} + +void +glade_hdy_search_bar_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + GObject *current = g_object_get_data (G_OBJECT (object), "child"); + GtkWidget *new_child; + + if (current != child) + return; + + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (child))), GTK_WIDGET (child)); + new_child = glade_placeholder_new (); + gtk_container_add (GTK_CONTAINER (object), new_child); + g_object_set_data (G_OBJECT (object), "child", new_child); +} + +void +glade_hdy_search_bar_replace_child (GladeWidgetAdaptor *adaptor, + GtkWidget *container, + GtkWidget *current, + GtkWidget *new_widget) +{ + if (current != (GtkWidget *) g_object_get_data (G_OBJECT (container), "child")) + return; + + gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (current))), + GTK_WIDGET (current)); + gtk_container_add (GTK_CONTAINER (container), new_widget); + g_object_set_data (G_OBJECT (container), "child", new_widget); +} + +GList * +glade_hdy_search_bar_get_children (GladeWidgetAdaptor *adaptor, + GObject *widget) +{ + GObject *current = g_object_get_data (G_OBJECT (widget), "child"); + + return g_list_append (NULL, current); +} + +gboolean +glade_hdy_search_bar_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *container, + GtkWidget *child, + gboolean user_feedback) +{ + GObject *current = g_object_get_data (G_OBJECT (container), "child"); + + if (!GLADE_IS_PLACEHOLDER (current)) { + if (user_feedback) + glade_util_ui_message (glade_app_get_window (), + GLADE_UI_INFO, NULL, + _("Search bar is already full")); + + return FALSE; + } + + return TRUE; +} diff --git a/subprojects/libhandy/glade/glade-hdy-search-bar.h b/subprojects/libhandy/glade/glade-hdy-search-bar.h new file mode 100644 index 0000000..05306b8 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-search-bar.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + +void glade_hdy_search_bar_post_create (GladeWidgetAdaptor *adaptor, + GObject *widget, + GladeCreateReason reason); + +void glade_hdy_search_bar_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); +void glade_hdy_search_bar_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); +void glade_hdy_search_bar_replace_child (GladeWidgetAdaptor *adaptor, + GtkWidget *container, + GtkWidget *current, + GtkWidget *new_widget); + +GList *glade_hdy_search_bar_get_children (GladeWidgetAdaptor *adaptor, + GObject *widget); + +gboolean glade_hdy_search_bar_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *container, + GtkWidget *child, + gboolean user_feedback); diff --git a/subprojects/libhandy/glade/glade-hdy-swipe-group.c b/subprojects/libhandy/glade/glade-hdy-swipe-group.c new file mode 100644 index 0000000..059138d --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-swipe-group.c @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * hdy-header-group.c - GladeWidgetAdaptor for HdyHeaderGroup + * Copyright (C) 2018 Purism SPC + * Copyright (C) 2013 Tristan Van Berkom + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-swipe-group.h" + +#include <gladeui/glade.h> +#include "glade-hdy-utils.h" + +#define PROP_SWIPEABLES "swipeables" +#define GLADE_TAG_SWIPEGROUP_SWIPEABLES "swipeables" +#define GLADE_TAG_SWIPEGROUP_SWIPEABLE "swipeable" + +static void +glade_hdy_swipe_group_read_widgets (GladeWidget *widget, + GladeXmlNode *node) +{ + GladeXmlNode *widgets_node; + GladeProperty *property; + gchar *string = NULL; + + if ((widgets_node = + glade_xml_search_child (node, GLADE_TAG_SWIPEGROUP_SWIPEABLES)) != NULL) { + GladeXmlNode *n; + + for (n = glade_xml_node_get_children (widgets_node); + n; n = glade_xml_node_next (n)) { + gchar *widget_name, *tmp; + + if (!glade_xml_node_verify (n, GLADE_TAG_SWIPEGROUP_SWIPEABLE)) + continue; + + widget_name = glade_xml_get_property_string_required + (n, GLADE_TAG_NAME, NULL); + + if (string == NULL) { + string = widget_name; + } else if (widget_name != NULL) { + tmp = + g_strdup_printf ("%s%s%s", string, GLADE_PROPERTY_DEF_OBJECT_DELIMITER, + widget_name); + string = (g_free (string), tmp); + g_free (widget_name); + } + } + } + + if (string) { + property = glade_widget_get_property (widget, PROP_SWIPEABLES); + g_assert (property); + + /* we must synchronize this directly after loading this project + * (i.e. lookup the actual objects after they've been parsed and + * are present). + */ + g_object_set_data_full (G_OBJECT (property), + "glade-loaded-object", string, g_free); + } +} + +void +glade_hdy_swipe_group_read_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlNode *node) +{ + if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) || + glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE))) + return; + + /* First chain up and read in all the normal properties.. */ + GWA_GET_CLASS (G_TYPE_OBJECT)->read_widget (adaptor, widget, node); + + glade_hdy_swipe_group_read_widgets (widget, node); +} + +static void +glade_hdy_swipe_group_write_widgets (GladeWidget *widget, + GladeXmlContext *context, + GladeXmlNode *node) +{ + GladeXmlNode *widgets_node, *widget_node; + GList *widgets = NULL, *list; + GladeWidget *awidget; + + widgets_node = glade_xml_node_new (context, GLADE_TAG_SWIPEGROUP_SWIPEABLES); + + if (glade_widget_property_get (widget, PROP_SWIPEABLES, &widgets)) { + for (list = widgets; list; list = list->next) { + awidget = glade_widget_get_from_gobject (list->data); + widget_node = + glade_xml_node_new (context, GLADE_TAG_SWIPEGROUP_SWIPEABLE); + glade_xml_node_append_child (widgets_node, widget_node); + glade_xml_node_set_property_string (widget_node, GLADE_TAG_NAME, + glade_widget_get_name (awidget)); + } + } + + if (!glade_xml_node_get_children (widgets_node)) + glade_xml_node_delete (widgets_node); + else + glade_xml_node_append_child (node, widgets_node); +} + +void +glade_hdy_swipe_group_write_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlContext *context, + GladeXmlNode *node) +{ + if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) || + glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE))) + return; + + /* First chain up and read in all the normal properties.. */ + GWA_GET_CLASS (G_TYPE_OBJECT)->write_widget (adaptor, widget, context, node); + + glade_hdy_swipe_group_write_widgets (widget, context, node); +} + +void +glade_hdy_swipe_group_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *property_name, + const GValue *value) +{ + if (!strcmp (property_name, PROP_SWIPEABLES)) { + GSList *sg_widgets, *slist; + GList *widgets, *list; + + /* remove old widgets */ + if ((sg_widgets = + hdy_swipe_group_get_swipeables (HDY_SWIPE_GROUP (object))) != NULL) { + /* copy since we are modifying an internal list */ + sg_widgets = g_slist_copy (sg_widgets); + for (slist = sg_widgets; slist; slist = slist->next) + hdy_swipe_group_remove_swipeable (HDY_SWIPE_GROUP (object), + HDY_SWIPEABLE (slist->data)); + g_slist_free (sg_widgets); + } + + /* add new widgets */ + if ((widgets = g_value_get_boxed (value)) != NULL) { + for (list = widgets; list; list = list->next) + hdy_swipe_group_add_swipeable (HDY_SWIPE_GROUP (object), + HDY_SWIPEABLE (list->data)); + } + } else { + GWA_GET_CLASS (G_TYPE_OBJECT)->set_property (adaptor, object, + property_name, value); + } +} diff --git a/subprojects/libhandy/glade/glade-hdy-swipe-group.h b/subprojects/libhandy/glade/glade-hdy-swipe-group.h new file mode 100644 index 0000000..5f593e5 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-swipe-group.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * hdy-header-group.h - GladeWidgetAdaptor for HdyHeaderGroup + * Copyright (C) 2018 Purism SPC + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + + +void glade_hdy_swipe_group_set_property (GladeWidgetAdaptor *adaptor, + GObject *object, + const gchar *property_name, + const GValue *value); +void glade_hdy_swipe_group_write_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlContext *context, + GladeXmlNode *node); +void glade_hdy_swipe_group_read_widget (GladeWidgetAdaptor *adaptor, + GladeWidget *widget, + GladeXmlNode *node); diff --git a/subprojects/libhandy/glade/glade-hdy-utils.c b/subprojects/libhandy/glade/glade-hdy-utils.c new file mode 100644 index 0000000..81b8ae5 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-utils.c @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-utils.h" + +#include <gladeui/glade.h> + +void +glade_hdy_init (const gchar *name) +{ + g_assert (strcmp (name, "libhandy") == 0); + + gtk_init (NULL, NULL); + hdy_init (); +} + +/* This function has been copied and modified from: + * glade-gtk-list-box.c - GladeWidgetAdaptor for GtkListBox widget + * + * Copyright (C) 2013 Kalev Lember + * + * Authors: + * Kalev Lember <kalevlember@gmail.com> + */ +void +glade_hdy_sync_child_positions (GtkContainer *container) +{ + g_autoptr (GList) children = NULL; + GList *l; + gint position; + static gboolean recursion = FALSE; + + /* Avoid feedback loop */ + if (recursion) + return; + + children = gtk_container_get_children (container); + + position = 0; + for (l = children; l; l = l->next) { + gint old_position; + + glade_widget_pack_property_get (glade_widget_get_from_gobject (l->data), + "position", &old_position); + if (position != old_position) { + /* Update glade with the new value */ + recursion = TRUE; + glade_widget_pack_property_set (glade_widget_get_from_gobject (l->data), + "position", position); + recursion = FALSE; + } + + position++; + } +} + +gint +glade_hdy_get_child_index (GtkContainer *container, + GtkWidget *child) +{ + g_autoptr (GList) children = gtk_container_get_children (container); + + return g_list_index (children, child); +} + +void +glade_hdy_reorder_child (GtkContainer *container, + GtkWidget *child, + gint index) +{ + gint old_index = glade_hdy_get_child_index (container, child); + gint i = 0, n; + g_autoptr (GList) children = NULL; + g_autoptr (GList) removed_children = NULL; + GList *l; + + if (old_index == index) + return; + + gtk_container_remove (container, g_object_ref (child)); + + children = gtk_container_get_children (container); + n = g_list_length (children); + + children = g_list_reverse (children); + l = children; + + if (index > old_index) + n--; + + for (i = n - 1; i >= index; i--) { + GtkWidget *last_child = l->data; + + gtk_container_remove (container, g_object_ref (last_child)); + l = l->next; + + removed_children = g_list_prepend (removed_children, last_child); + } + + removed_children = g_list_prepend (removed_children, child); + + for (l = removed_children; l; l = l->next) { + gtk_container_add (container, l->data); + g_object_unref (l->data); + } +} + +GtkWidget * +glade_hdy_get_nth_child (GtkContainer *container, + gint n) +{ + g_autoptr (GList) children = gtk_container_get_children (container); + + return g_list_nth_data (children, n); +} diff --git a/subprojects/libhandy/glade/glade-hdy-utils.h b/subprojects/libhandy/glade/glade-hdy-utils.h new file mode 100644 index 0000000..11870d1 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-utils.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + +#define ONLY_THIS_GOES_IN_THAT_MSG _("Only objects of type %s can be added to objects of type %s.") + +/* Guess whether we are using a Glade version older than 3.36. + * + * If yes, redefine some symbols which got renamed. + */ +#ifndef GLADE_PROPERTY_DEF_OBJECT_DELIMITER +#define GLADE_PROPERTY_DEF_OBJECT_DELIMITER GPC_OBJECT_DELIMITER +#define glade_widget_action_get_def glade_widget_action_get_class +#endif + +void glade_hdy_init (const gchar *name); + +void glade_hdy_sync_child_positions (GtkContainer *container); + +gint glade_hdy_get_child_index (GtkContainer *container, + GtkWidget *child); + +void glade_hdy_reorder_child (GtkContainer *container, + GtkWidget *child, + gint index); + +GtkWidget *glade_hdy_get_nth_child (GtkContainer *container, + gint n); diff --git a/subprojects/libhandy/glade/glade-hdy-window.c b/subprojects/libhandy/glade/glade-hdy-window.c new file mode 100644 index 0000000..7832f91 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-window.c @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + * + * Based on + * glade-gtk-searchbar.c - GladeWidgetAdaptor for GtkSearchBar + * Copyright (C) 2014 Red Hat, Inc. + */ + +#include <config.h> +#include <glib/gi18n-lib.h> + +#include "glade-hdy-window.h" + +#include <gladeui/glade.h> + +#define ALREADY_HAS_A_CHILD_MSG _("%s cannot have more than one child.") + +static GtkWidget * +get_child (GtkContainer *window) +{ + g_autoptr (GList) children = gtk_container_get_children (window); + + if (!children) + return NULL; + + return children->data; +} + +void +glade_hdy_window_post_create (GladeWidgetAdaptor *adaptor, + GObject *object, + GladeCreateReason reason) +{ + if (reason != GLADE_CREATE_USER) + return; + + gtk_container_add (GTK_CONTAINER (object), glade_placeholder_new ()); +} + +void +glade_hdy_window_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + GtkWidget *window_child = get_child (GTK_CONTAINER (object)); + + if (window_child) { + if (GLADE_IS_PLACEHOLDER (window_child)) { + gtk_container_remove (GTK_CONTAINER (object), window_child); + } else { + g_critical ("Can't add more than one widget to a HdyWindow"); + + return; + } + } + + gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child)); +} + +void +glade_hdy_window_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child) +{ + gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child)); + gtk_container_add (GTK_CONTAINER (object), glade_placeholder_new ()); +} + +void +glade_hdy_window_replace_child (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *current, + GtkWidget *new_widget) +{ + gtk_container_remove (GTK_CONTAINER (object), current); + gtk_container_add (GTK_CONTAINER (object), new_widget); +} + +GList * +glade_hdy_window_get_children (GladeWidgetAdaptor *adaptor, + GObject *object) +{ + return gtk_container_get_children (GTK_CONTAINER (object)); +} + +gboolean +glade_hdy_window_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback) +{ + GtkWidget *window_child = get_child (GTK_CONTAINER (object)); + + if (window_child && !GLADE_IS_PLACEHOLDER (window_child)) { + if (user_feedback) + glade_util_ui_message (glade_app_get_window (), + GLADE_UI_INFO, NULL, + ALREADY_HAS_A_CHILD_MSG, + glade_widget_adaptor_get_title (adaptor)); + + return FALSE; + } + + return TRUE; +} + diff --git a/subprojects/libhandy/glade/glade-hdy-window.h b/subprojects/libhandy/glade/glade-hdy-window.h new file mode 100644 index 0000000..75f15e4 --- /dev/null +++ b/subprojects/libhandy/glade/glade-hdy-window.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <gladeui/glade.h> + +#include <handy.h> + +void glade_hdy_window_post_create (GladeWidgetAdaptor *adaptor, + GObject *object, + GladeCreateReason reason); + +void glade_hdy_window_add_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); +void glade_hdy_window_remove_child (GladeWidgetAdaptor *adaptor, + GObject *object, + GObject *child); +void glade_hdy_window_replace_child (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *current, + GtkWidget *new_widget); + +GList *glade_hdy_window_get_children (GladeWidgetAdaptor *adaptor, + GObject *object); + +gboolean glade_hdy_window_add_verify (GladeWidgetAdaptor *adaptor, + GtkWidget *object, + GtkWidget *child, + gboolean user_feedback); diff --git a/subprojects/libhandy/glade/libhandy.xml b/subprojects/libhandy/glade/libhandy.xml new file mode 100644 index 0000000..7f60118 --- /dev/null +++ b/subprojects/libhandy/glade/libhandy.xml @@ -0,0 +1,420 @@ +<?xml version="1.0" encoding="UTF-8"?> +<glade-catalog name="libhandy" library="glade-handy-1" depends="gtk+" book="libhandy"> + <init-function>glade_hdy_init</init-function> + <glade-widget-classes> + <glade-widget-class name="HdyActionRow" generic-name="actionrow" title="Action Row" since="0.0.6"> + <properties> + <property id="icon-name" themed-icon="True" /> + <property id="title" translatable="True" /> + <property id="subtitle" translatable="True" /> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyAvatar" generic-name="avatar" title="Avatar" since="1.0"/> + <glade-widget-class name="HdyApplicationWindow" generic-name="applicationwindow" title="Application Window" since="1.0" use-placeholders="False"> + <post-create-function>glade_hdy_window_post_create</post-create-function> + <add-child-verify-function>glade_hdy_window_add_verify</add-child-verify-function> + <add-child-function>glade_hdy_window_add_child</add-child-function> + <remove-child-function>glade_hdy_window_remove_child</remove-child-function> + <replace-child-function>glade_hdy_window_replace_child</replace-child-function> + <get-children-function>glade_hdy_window_get_children</get-children-function> + <properties> + <property id="show-menubar" disabled="True" /> + <property id="use-csd" disabled="True" /> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyCarousel" generic-name="carousel" title="Carousel" since="1.0"> + <post-create-function>glade_hdy_carousel_post_create</post-create-function> + <add-child-function>glade_hdy_carousel_add_child</add-child-function> + <remove-child-function>glade_hdy_carousel_remove_child</remove-child-function> + <replace-child-function>glade_hdy_carousel_replace_child</replace-child-function> + <child-action-activate-function>glade_hdy_carousel_child_action_activate</child-action-activate-function> + <get-property-function>glade_hdy_carousel_get_property</get-property-function> + <set-property-function>glade_hdy_carousel_set_property</set-property-function> + <child-set-property-function>glade_hdy_carousel_set_child_property</child-set-property-function> + <child-get-property-function>glade_hdy_carousel_get_child_property</child-get-property-function> + <verify-function>glade_hdy_carousel_verify_property</verify-function> + <packing-properties> + <property id="position" name="Position" default="0" save="False"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>The position of the page in the carousel</tooltip> + </property> + </packing-properties> + <packing-actions> + <action id="insert_page_before" name="Insert Page Before" stock="list-add"/> + <action id="insert_page_after" name="Insert Page After" stock="list-add"/> + <action id="remove_page" name="Remove Page" stock="list-remove"/> + </packing-actions> + <properties> + <property id="pages" name="Number of pages" save="False" default="1"> + <parameter-spec> + <type>GParamInt</type> + <min>1</min> + </parameter-spec> + <tooltip>The number of pages in the stack</tooltip> + </property> + <property id="page" name="Edit page" save="False" default="0"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>Set the currently active page to edit, this property will not be saved</tooltip> + </property> + <property id="above-child" disabled="True" /> + <property id="visible-window" disabled="True" /> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyCarouselIndicatorDots" generic-name="carouselindicatordots" title="Carousel Indicator Dots" since="1.0"/> + <glade-widget-class name="HdyCarouselIndicatorLines" generic-name="carouselindicatorlines" title="Carousel Indicator Lines" since="1.0"/> + <glade-widget-class name="HdyClamp" generic-name="clamp" title="Clamp" since="1.0"/> + <glade-widget-class name="HdyComboRow" generic-name="comborow" title="Combo Row" since="0.0.6"/> + <glade-widget-class name="HdyDeck" generic-name="deck" title="Deck" since="1.0"> + <post-create-function>glade_hdy_leaflet_post_create</post-create-function> + <add-child-function>glade_hdy_leaflet_add_child</add-child-function> + <remove-child-function>glade_hdy_leaflet_remove_child</remove-child-function> + <replace-child-function>glade_hdy_leaflet_replace_child</replace-child-function> + <child-action-activate-function>glade_hdy_leaflet_child_action_activate</child-action-activate-function> + <get-property-function>glade_hdy_leaflet_get_property</get-property-function> + <set-property-function>glade_hdy_leaflet_set_property</set-property-function> + <child-set-property-function>glade_hdy_leaflet_set_child_property</child-set-property-function> + <child-get-property-function>glade_hdy_leaflet_get_child_property</child-get-property-function> + <verify-function>glade_hdy_leaflet_verify_property</verify-function> + <packing-properties> + <property id="position" name="Position" default="0" save="False"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>The position of the page in the deck</tooltip> + </property> + </packing-properties> + <packing-actions> + <action id="insert_page_before" name="Insert Page Before" stock="list-add"/> + <action id="insert_page_after" name="Insert Page After" stock="list-add"/> + <action id="remove_page" name="Remove Page" stock="list-remove"/> + </packing-actions> + <properties> + <property id="pages" name="Number of pages" save="False" default="1"> + <parameter-spec> + <type>GParamInt</type> + <min>1</min> + </parameter-spec> + <tooltip>The number of pages in the deck</tooltip> + </property> + <property id="page" name="Edit page" save="False" default="0"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>Set the currently active page to edit, this property will not be saved</tooltip> + </property> + <property id="transition-type"> + <displayable-values> + <!-- HdyDeckTransitionType enumeration value --> + <value id="HDY_DECK_TRANSITION_TYPE_OVER" name="Over"/> + <!-- HdyDeckTransitionType enumeration value --> + <value id="HDY_DECK_TRANSITION_TYPE_UNDER" name="Under"/> + <!-- HdyDeckTransitionType enumeration value --> + <value id="HDY_DECK_TRANSITION_TYPE_SLIDE" name="Slide"/> + </displayable-values> + </property> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyExpanderRow" generic-name="expanderrow" title="Expander Row" since="0.0.6" use-placeholders="False"> + <post-create-function>glade_hdy_expander_row_post_create</post-create-function> + <add-child-verify-function>glade_hdy_expander_row_add_verify</add-child-verify-function> + <add-child-function>glade_hdy_expander_row_add_child</add-child-function> + <remove-child-function>glade_hdy_expander_row_remove_child</remove-child-function> + <child-set-property-function>glade_hdy_expander_row_set_child_property</child-set-property-function> + <child-get-property-function>glade_hdy_expander_row_get_child_property</child-get-property-function> + <packing-properties> + <property id="position" name="Position" default="0" save="False"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>The position of the list row in the expander row</tooltip> + </property> + </packing-properties> + <properties> + <property id="expanded" save="True" ignore="True"/> + <property id="enable-expansion" save="True" ignore="True"/> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyHeaderBar" generic-name="headerbar" title="Header Bar" since="0.0.10"> + <post-create-function>glade_hdy_header_bar_post_create</post-create-function> + <add-child-function>glade_hdy_header_bar_add_child</add-child-function> + <remove-child-function>glade_hdy_header_bar_remove_child</remove-child-function> + <replace-child-function>glade_hdy_header_bar_replace_child</replace-child-function> + <get-children-function>glade_hdy_header_bar_get_children</get-children-function> + <child-action-activate-function>glade_hdy_header_bar_child_action_activate</child-action-activate-function> + <get-property-function>glade_hdy_header_bar_get_property</get-property-function> + <set-property-function>glade_hdy_header_bar_set_property</set-property-function> + <child-set-property-function>glade_hdy_header_bar_child_set_property</child-set-property-function> + <action-activate-function>glade_hdy_header_bar_action_activate</action-activate-function> + <verify-function>glade_hdy_header_bar_verify_property</verify-function> + <special-child-type>type</special-child-type> + <packing-properties> + <property id="pack-type" transfer-on-paste="True" /> + </packing-properties> + <packing-actions> + <action id="remove_slot" name="Remove Slot" stock="gtk-remove"/> + </packing-actions> + <properties> + <property id="title" translatable="True"/> + <property id="subtitle" translatable="True"/> + <property id="has-subtitle" name="Reserve space for subtitle"> + <tooltip>Keep the headerbar height the same as the subtitle changes dynamically.</tooltip> + </property> + <property id="show-close-button" name="Show window controls" needs-sync="True"/> + <property id="spacing"/> + <property id="decoration-layout"/> + <property id="decoration-layout-set" disabled="True"/> + <property id="custom-title" disabled="True"/> + <property id="use-custom-title" name="Custom Title" default="FALSE" visible="True" save="False"> + <parameter-spec> + <type>GParamBoolean</type> + </parameter-spec> + </property> + <property visible="True" save="False" id="size" default="1" name="Number of items"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>The number of items in the header bar</tooltip> + </property> + <property id="centering-policy"> + <displayable-values> + <!-- HdyCenteringPolicy enumeration value --> + <value id="HDY_CENTERING_POLICY_LOOSE" name="Loose"/> + <!-- HdyCenteringPolicy enumeration value --> + <value id="HDY_CENTERING_POLICY_STRICT" name="Strict"/> + </displayable-values> + </property> + </properties> + <actions> + <action id="add_slot" name="Add Slot" stock="list-add"/> + </actions> + </glade-widget-class> + <glade-widget-class name="HdyHeaderGroup" generic-name="headergroup" title="Header Group" toplevel="True"> + <read-widget-function>glade_hdy_header_group_read_widget</read-widget-function> + <write-widget-function>glade_hdy_header_group_write_widget</write-widget-function> + <set-property-function>glade_hdy_header_group_set_property</set-property-function> + <properties> + <property id="headerbars" name="Headerbars" save="False"> + <parameter-spec> + <type>GladeParamObjects</type> + <value-type>HdyHeaderBar</value-type> + </parameter-spec> + <tooltip>List of headerbars in this group</tooltip> + </property> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyKeypad" generic-name="keypad" title="Keypad"/> + <glade-widget-class name="HdyLeaflet" generic-name="leaflet" title="Leaflet"> + <post-create-function>glade_hdy_leaflet_post_create</post-create-function> + <add-child-function>glade_hdy_leaflet_add_child</add-child-function> + <remove-child-function>glade_hdy_leaflet_remove_child</remove-child-function> + <replace-child-function>glade_hdy_leaflet_replace_child</replace-child-function> + <child-action-activate-function>glade_hdy_leaflet_child_action_activate</child-action-activate-function> + <get-property-function>glade_hdy_leaflet_get_property</get-property-function> + <set-property-function>glade_hdy_leaflet_set_property</set-property-function> + <child-set-property-function>glade_hdy_leaflet_set_child_property</child-set-property-function> + <child-get-property-function>glade_hdy_leaflet_get_child_property</child-get-property-function> + <verify-function>glade_hdy_leaflet_verify_property</verify-function> + <packing-properties> + <property id="position" name="Position" default="0" save="False"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>The position of the page in the leaflet</tooltip> + </property> + </packing-properties> + <packing-actions> + <action id="insert_page_before" name="Insert Page Before" stock="list-add"/> + <action id="insert_page_after" name="Insert Page After" stock="list-add"/> + <action id="remove_page" name="Remove Page" stock="list-remove"/> + </packing-actions> + <properties> + <property id="pages" name="Number of pages" save="False" default="1"> + <parameter-spec> + <type>GParamInt</type> + <min>1</min> + </parameter-spec> + <tooltip>The number of pages in the leaflet</tooltip> + </property> + <property id="page" name="Edit page" save="False" default="0"> + <parameter-spec> + <type>GParamInt</type> + <min>0</min> + </parameter-spec> + <tooltip>Set the currently active page to edit, this property will not be saved</tooltip> + </property> + <property id="transition-type"> + <displayable-values> + <!-- HdyLeafletTransitionType enumeration value --> + <value id="HDY_LEAFLET_TRANSITION_TYPE_OVER" name="Over"/> + <!-- HdyLeafletTransitionType enumeration value --> + <value id="HDY_LEAFLET_TRANSITION_TYPE_UNDER" name="Under"/> + <!-- HdyLeafletTransitionType enumeration value --> + <value id="HDY_LEAFLET_TRANSITION_TYPE_SLIDE" name="Slide"/> + </displayable-values> + </property> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyPreferencesGroup" generic-name="preferencesgroup" title="Preferences Group" since="0.0.10" use-placeholders="False"> + <properties> + <property id="title" translatable="True" /> + <property id="description" translatable="True" /> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyPreferencesPage" generic-name="preferencespage" title="Preferences Page" since="0.0.10" use-placeholders="False"> + <post-create-function>glade_hdy_preferences_page_post_create</post-create-function> + <add-child-verify-function>glade_hdy_preferences_page_add_verify</add-child-verify-function> + <add-child-function>glade_hdy_preferences_page_add_child</add-child-function> + <remove-child-function>glade_hdy_preferences_page_remove_child</remove-child-function> + <replace-child-function>glade_hdy_preferences_page_replace_child</replace-child-function> + <get-children-function>glade_hdy_preferences_page_get_children</get-children-function> + <child-set-property-function>glade_hdy_preferences_page_child_set_property</child-set-property-function> + <child-get-property-function>glade_hdy_preferences_page_child_get_property</child-get-property-function> + <action-activate-function>glade_hdy_preferences_page_action_activate</action-activate-function> + <packing-properties> + <property id="position" name="Position" default="-1" save="False"> + <parameter-spec> + <type>GParamInt</type> + <min>-1</min> + </parameter-spec> + <tooltip>The position of the group in the preferences page</tooltip> + </property> + </packing-properties> + <properties> + <property id="icon-name" themed-icon="True" /> + <property id="title" translatable="True" /> + </properties> + <actions> + <action id="add_group" name="Add Group" stock="list-add" important="True"/> + </actions> + </glade-widget-class> + <glade-widget-class name="HdyPreferencesRow" generic-name="preferencesrow" title="Preferences Row" since="0.0.10"/> + <glade-widget-class name="HdyPreferencesWindow" generic-name="preferenceswindow" title="Preferences Window" since="0.0.10" use-placeholders="False"> + <post-create-function>glade_hdy_preferences_window_post_create</post-create-function> + <add-child-verify-function>glade_hdy_preferences_window_add_verify</add-child-verify-function> + <add-child-function>glade_hdy_preferences_window_add_child</add-child-function> + <remove-child-function>glade_hdy_preferences_window_remove_child</remove-child-function> + <replace-child-function>glade_hdy_preferences_window_replace_child</replace-child-function> + <get-children-function>glade_hdy_preferences_window_get_children</get-children-function> + <child-set-property-function>glade_hdy_preferences_window_child_set_property</child-set-property-function> + <child-get-property-function>glade_hdy_preferences_window_child_get_property</child-get-property-function> + <action-activate-function>glade_hdy_preferences_window_action_activate</action-activate-function> + <packing-properties> + <property id="position" name="Position" default="-1" save="False"> + <parameter-spec> + <type>GParamInt</type> + <min>-1</min> + </parameter-spec> + <tooltip>The position of the page in the preferences window</tooltip> + </property> + </packing-properties> + <properties> + <property id="use-csd" disabled="True" /> + </properties> + <actions> + <action id="add_page" name="Add Page" stock="list-add" important="True"/> + </actions> + </glade-widget-class> + <glade-widget-class name="HdySearchBar" generic-name="searchbar" title="Search Bar" since="0.0.6"> + <post-create-function>glade_hdy_search_bar_post_create</post-create-function> + <add-child-verify-function>glade_hdy_search_bar_add_verify</add-child-verify-function> + <add-child-function>glade_hdy_search_bar_add_child</add-child-function> + <remove-child-function>glade_hdy_search_bar_remove_child</remove-child-function> + <replace-child-function>glade_hdy_search_bar_replace_child</replace-child-function> + <get-children-function>glade_hdy_search_bar_get_children</get-children-function> + <properties> + <property id="search-mode-enabled" save="True" ignore="True"/> + <property id="show-close-button" save="True" ignore="True"/> + </properties> + </glade-widget-class> + <glade-widget-class name="HdySqueezer" generic-name="squeezer" title="Squeezer" since="0.0.10"/> + <glade-widget-class name="HdySwipeGroup" generic-name="swipegroup" title="Swipe Group" toplevel="True"> + <read-widget-function>glade_hdy_swipe_group_read_widget</read-widget-function> + <write-widget-function>glade_hdy_swipe_group_write_widget</write-widget-function> + <set-property-function>glade_hdy_swipe_group_set_property</set-property-function> + <properties> + <property id="swipeables" name="Widgets" save="False"> + <parameter-spec> + <type>GladeParamObjects</type> + <value-type>HdySwipeable</value-type> + </parameter-spec> + <tooltip>List of widgets in this group</tooltip> + </property> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyTitleBar" generic-name="titlebar" title="Title Bar"/> + <glade-widget-class name="HdyViewSwitcher" generic-name="viewswitcher" title="View Switcher" since="0.0.10"/> + <glade-widget-class name="HdyViewSwitcherBar" generic-name="viewswitcherbar" title="View Switcher Bar" since="0.0.10"/> + <glade-widget-class name="HdyViewSwitcherTitle" generic-name="viewswitchertitle" title="View Switcher Title" since="1.0"> + <properties> + <property id="policy"> + <displayable-values> + <!-- HdyViewSwitcherPolicy enumeration value --> + <value id="HDY_VIEW_SWITCHER_POLICY_AUTO" name="Auto"/> + <!-- HdyViewSwitcherPolicy enumeration value --> + <value id="HDY_VIEW_SWITCHER_POLICY_NARROW" name="Narrow"/> + <!-- HdyViewSwitcherPolicy enumeration value --> + <value id="HDY_VIEW_SWITCHER_POLICY_WIDE" name="Wide"/> + </displayable-values> + </property> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyWindow" generic-name="window" title="Window" since="1.0" use-placeholders="False"> + <post-create-function>glade_hdy_window_post_create</post-create-function> + <add-child-verify-function>glade_hdy_window_add_verify</add-child-verify-function> + <add-child-function>glade_hdy_window_add_child</add-child-function> + <remove-child-function>glade_hdy_window_remove_child</remove-child-function> + <replace-child-function>glade_hdy_window_replace_child</replace-child-function> + <get-children-function>glade_hdy_window_get_children</get-children-function> + <properties> + <property id="use-csd" disabled="True" /> + </properties> + </glade-widget-class> + <glade-widget-class name="HdyWindowHandle" generic-name="windowhandle" title="Window Handle" since="1.0"> + <properties> + <property id="above-child" disabled="True" /> + <property id="visible-window" disabled="True" /> + </properties> + </glade-widget-class> + </glade-widget-classes> + + <glade-widget-group name="handy" title="Libhandy Widgets"> + <glade-widget-class-ref name="HdyActionRow"/> + <glade-widget-class-ref name="HdyApplicationWindow"/> + <glade-widget-class-ref name="HdyAvatar"/> + <glade-widget-class-ref name="HdyCarousel"/> + <glade-widget-class-ref name="HdyCarouselIndicatorDots"/> + <glade-widget-class-ref name="HdyCarouselIndicatorLines"/> + <glade-widget-class-ref name="HdyClamp"/> + <glade-widget-class-ref name="HdyComboRow"/> + <glade-widget-class-ref name="HdyDeck"/> + <glade-widget-class-ref name="HdyExpanderRow"/> + <glade-widget-class-ref name="HdyHeaderBar"/> + <glade-widget-class-ref name="HdyHeaderGroup"/> + <glade-widget-class-ref name="HdyKeypad"/> + <glade-widget-class-ref name="HdyLeaflet"/> + <glade-widget-class-ref name="HdyPreferencesGroup"/> + <glade-widget-class-ref name="HdyPreferencesPage"/> + <glade-widget-class-ref name="HdyPreferencesRow"/> + <glade-widget-class-ref name="HdyPreferencesWindow"/> + <glade-widget-class-ref name="HdySearchBar"/> + <glade-widget-class-ref name="HdySqueezer"/> + <glade-widget-class-ref name="HdySwipeGroup"/> + <glade-widget-class-ref name="HdyTitleBar"/> + <glade-widget-class-ref name="HdyViewSwitcher"/> + <glade-widget-class-ref name="HdyViewSwitcherBar"/> + <glade-widget-class-ref name="HdyViewSwitcherTitle"/> + <glade-widget-class-ref name="HdyWindow"/> + <glade-widget-class-ref name="HdyWindowHandle"/> + </glade-widget-group> +</glade-catalog> diff --git a/subprojects/libhandy/glade/meson.build b/subprojects/libhandy/glade/meson.build new file mode 100644 index 0000000..cba9a36 --- /dev/null +++ b/subprojects/libhandy/glade/meson.build @@ -0,0 +1,59 @@ +if glade_catalog + +glade_xml = 'libhandy.xml' +module_dir = gladeui_dep.get_pkgconfig_variable('moduledir', + define_variable: ['libdir', get_option('libdir')]) +dtd = meson.current_source_dir() / 'glade-catalog.dtd' +glade_catalogdir = gladeui_dep.get_pkgconfig_variable('catalogdir', + define_variable: ['datadir', get_option('datadir')]) + +libglade_hdy_sources = [ + 'glade-hdy-carousel.c', + 'glade-hdy-expander-row.c', + 'glade-hdy-header-bar.c', + 'glade-hdy-header-group.c', + 'glade-hdy-leaflet.c', + 'glade-hdy-preferences-page.c', + 'glade-hdy-preferences-window.c', + 'glade-hdy-search-bar.c', + 'glade-hdy-swipe-group.c', + 'glade-hdy-window.c', + 'glade-hdy-utils.c', +] + +libglade_hdy_deps = [ + libhandy_dep, + gladeui_dep, +] + +libglade_hdy_args = [] +# Our custom glade module +libglade_hdy = shared_library( + 'glade-handy-' + apiversion, + libglade_hdy_sources, + c_args: libglade_hdy_args, + dependencies: libglade_hdy_deps, + include_directories: [ root_inc, src_inc ], + install: true, + install_dir: module_dir, +) + +# Validate glade catalog +xmllint = find_program('xmllint', required: true) +if xmllint.found() + custom_target( + 'xmllint', + build_by_default: true, + input: glade_xml, + output: 'doesnotexist', + command: [xmllint, '--dtdvalid', dtd, '--noout', '@INPUT@'], + ) +endif + +# Install glade catalog +install_data( + glade_xml, + rename: 'libhandy-@0@.xml'.format(apiversion), + install_dir: glade_catalogdir) + +endif diff --git a/subprojects/libhandy/glade/rename-id.patch b/subprojects/libhandy/glade/rename-id.patch new file mode 100644 index 0000000..0dc3ab1 --- /dev/null +++ b/subprojects/libhandy/glade/rename-id.patch @@ -0,0 +1,28 @@ +From f4711b392e26d36266a88df4371230418e24cfc9 Mon Sep 17 00:00:00 2001 +From: Adrien Plazas <kekun.plazas@laposte.net> +Date: Mon, 11 May 2020 10:45:17 +0200 +Subject: [PATCH] Rename the ID to sm.puri.Handy.Glade + +Make the GtkApplication ID match the Flatpak ID, which is required for +the app to start. + +--- + src/main.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/main.c b/src/main.c +index 8a81771f..7a505255 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -161,7 +161,7 @@ main (int argc, char *argv[]) + return -1; + } + +- app = gtk_application_new ("org.gnome.Glade", G_APPLICATION_HANDLES_OPEN); ++ app = gtk_application_new ("sm.puri.Handy.Glade", G_APPLICATION_HANDLES_OPEN); + + g_application_set_option_context_summary (G_APPLICATION (app), + N_("Create or edit user interface designs for GTK+ or GNOME applications.")); +-- +2.26.0 + diff --git a/subprojects/libhandy/glade/sm.puri.Handy.Glade.json b/subprojects/libhandy/glade/sm.puri.Handy.Glade.json new file mode 100644 index 0000000..ef73458 --- /dev/null +++ b/subprojects/libhandy/glade/sm.puri.Handy.Glade.json @@ -0,0 +1,83 @@ +{ + "app-id" : "sm.puri.Handy.Glade", + "runtime" : "org.gnome.Platform", + "runtime-version" : "master", + "sdk" : "org.gnome.Sdk", + "command" : "glade", + "rename-desktop-file" : "org.gnome.Glade.desktop", + "rename-appdata-file" : "org.gnome.Glade.appdata.xml", + "rename-icon" : "org.gnome.Glade", + "copy-icon" : true, + "desktop-file-name-suffix" : " (Handy Nightly)", + "finish-args" : [ + /* X11 + XShm access */ + "--share=ipc", "--socket=fallback-x11", + /* Wayland access */ + "--socket=wayland", + /* We want full fs access so we can read the files */ + "--filesystem=host", + /* Support GL widgets */ + "--device=dri" + ], + "cleanup" : ["/include", "/lib/pkgconfig", + "/share/pkgconfig", "/share/aclocal", + "/man", "/share/man", "/share/gtk-doc", + "/share/vala", + "*.la", "*.a"], + "modules" : [ + { + "name" : "gnome-common", + "sources" : [ + { + "type" : "git", + "url" : "https://gitlab.gnome.org/GNOME/gnome-common.git" + } + ] + }, + { + "name" : "intltool", + "sources" : [ + { + "type" : "git", + "url" : "https://gitlab.gnome.org/World/intltool.git" + } + ] + }, + { + "name" : "glade", + "config-opts": [ + "--disable-man-pages", + "--disable-introspection" + ], + "sources" : [ + { + "type" : "git", + "url" : "https://gitlab.gnome.org/GNOME/glade.git", + "tag" : "GLADE_3_36_0" + }, + { + "type" : "patch", + "path" : "rename-id.patch" + } + ] + }, + { + "name" : "libhandy", + "buildsystem" : "meson", + "builddir" : true, + "config-opts" : [ + "-Dexamples=false", + "-Dglade_catalog=enabled", + "-Dintrospection=disabled", + "-Dtests=false", + "-Dvapi=false" + ], + "sources" : [ + { + "type" : "git", + "url" : "https://gitlab.gnome.org/GNOME/libhandy.git" + } + ] + } + ] +} diff --git a/subprojects/libhandy/libhandy.doap b/subprojects/libhandy/libhandy.doap new file mode 100644 index 0000000..d5bb1e8 --- /dev/null +++ b/subprojects/libhandy/libhandy.doap @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" + xmlns:foaf="http://xmlns.com/foaf/0.1/" + xmlns:gnome="http://api.gnome.org/doap-extensions#" + xmlns="http://usefulinc.com/ns/doap#"> + + <name>libhandy</name> + <shortname>libhandy</shortname> + <shortdesc>Building blocks for modern adaptive GNOME apps</shortdesc> + <description> + libhandy is a collection of GTK widgets for adaptive applications targeting + form-factors from mobile to desktop. + It also offers innovative widgets following the GNOME design guidelines. + </description> + <homepage rdf:resource="https://gitlab.gnome.org/GNOME/libhandy" /> + <license rdf:resource="http://usefulinc.com/doap/licenses/lgpl" /> + + <programming-language>C</programming-language> + + <maintainer> + <foaf:Person> + <foaf:name>Guido Günther</foaf:name> + <foaf:mbox rdf:resource="mailto:agx@sigxcpu.org" /> + <gnome:userid>guidog</gnome:userid> + </foaf:Person> + </maintainer> + <maintainer> + <foaf:Person> + <foaf:name>Adrien Plazas</foaf:name> + <foaf:mbox rdf:resource="mailto:kekun.plazas@laposte.net" /> + <gnome:userid>aplazas</gnome:userid> + </foaf:Person> + </maintainer> + +</Project> + diff --git a/subprojects/libhandy/libhandy.syms b/subprojects/libhandy/libhandy.syms new file mode 100644 index 0000000..64dafcb --- /dev/null +++ b/subprojects/libhandy/libhandy.syms @@ -0,0 +1,6 @@ +LIBHANDY_1_0 { + global: + hdy_*; + local: + *; +}; diff --git a/subprojects/libhandy/lint/api-visibility.sh b/subprojects/libhandy/lint/api-visibility.sh new file mode 100755 index 0000000..ce8e7f5 --- /dev/null +++ b/subprojects/libhandy/lint/api-visibility.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Check that private headers aren't included in public ones. +if grep "include.*private.h" $(ls src/*.h | grep -v "private.h"); +then + echo "Private headers shouldn't be included in public ones." + exit 1 +fi + +# Check that handy.h contains all the public headers. +for header in $(ls src | grep "\.h$" | grep -v "private.h" | grep -v handy.h); +do + if ! grep -q "$(basename $header)" src/handy.h; + then + echo "The public header" $(basename $header) "should be included in handy.h." + exit 1 + fi +done diff --git a/subprojects/libhandy/meson.build b/subprojects/libhandy/meson.build new file mode 100644 index 0000000..aae0533 --- /dev/null +++ b/subprojects/libhandy/meson.build @@ -0,0 +1,156 @@ +project('libhandy', 'c', + version: '0.90.0', + license: 'LGPL-2.1+', + meson_version: '>= 0.49.0', + default_options: [ 'warning_level=1', 'buildtype=debugoptimized', 'c_std=gnu11' ], +) + +version_arr = meson.project_version().split('.') +handy_version_major = version_arr[0].to_int() +handy_version_minor = version_arr[1].to_int() +handy_version_micro = version_arr[2].to_int() + +# The major api version as encoded in the libraries name +apiversion = '1' +# The so major version of the library +soversion = 0 +package_api_name = '@0@-@1@'.format(meson.project_name(), apiversion) + +if handy_version_minor.is_odd() + handy_interface_age = 0 +else + handy_interface_age = handy_version_micro +endif + +# maintaining compatibility with libtool versioning +# current = minor * 100 + micro - interface +# revision = interface +current = handy_version_minor * 100 + handy_version_micro - handy_interface_age +revision = handy_interface_age +libversion = '@0@.@1@.@2@'.format(soversion, current, revision) + +add_project_arguments([ + '-DHAVE_CONFIG_H', + '-DHANDY_COMPILATION', + '-I' + meson.build_root(), +], language: 'c') + +root_inc = include_directories('.') +src_inc = include_directories('src') + +cc = meson.get_compiler('c') + +global_c_args = [] +test_c_args = [ + '-Wcast-align', + '-Wdate-time', + '-Wdeclaration-after-statement', + ['-Werror=format-security', '-Werror=format=2'], + '-Wendif-labels', + '-Werror=incompatible-pointer-types', + '-Werror=missing-declarations', + '-Werror=overflow', + '-Werror=return-type', + '-Werror=shift-count-overflow', + '-Werror=shift-overflow=2', + '-Werror=implicit-fallthrough=3', + '-Wformat-nonliteral', + '-Wformat-security', + '-Winit-self', + '-Wmaybe-uninitialized', + '-Wmissing-field-initializers', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wnested-externs', + '-Wno-missing-field-initializers', + '-Wno-sign-compare', + '-Wno-strict-aliasing', + '-Wno-unused-parameter', + '-Wold-style-definition', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wshadow', + '-Wstrict-prototypes', + '-Wswitch-default', + '-Wswitch-enum', + '-Wtype-limits', + '-Wundef', + '-Wunused-function', +] + +target_system = target_machine.system() + +if get_option('buildtype') != 'plain' + if target_system == 'windows' + test_c_args += '-fstack-protector' + else + test_c_args += '-fstack-protector-strong' + endif +endif +if get_option('profiling') + test_c_args += '-pg' +endif + +foreach arg: test_c_args + if cc.has_multi_arguments(arg) + global_c_args += arg + endif +endforeach +add_project_arguments( + global_c_args, + language: 'c' +) + +# Setup various paths that subdirectory meson.build files need +package_subdir = get_option('package_subdir') # When used as subproject +datadir = get_option('datadir') / package_subdir +libdir = get_option('libdir') / package_subdir +girdir = get_option('datadir') / package_subdir / 'gir-1.0' +typelibdir = get_option('libdir') / package_subdir / 'girepository-1.0' +if package_subdir != '' + vapidir = get_option('datadir') / package_subdir / 'vapi' +else + vapidir = get_option('datadir') / 'vala' / 'vapi' +endif + +glade_catalog_feature = get_option('glade_catalog') +gladeui_dep = dependency('gladeui-2.0', required : glade_catalog_feature) +glade_catalog = not glade_catalog_feature.disabled() and gladeui_dep.found() + +introspection_feature = get_option('introspection') +introspection = introspection_feature.enabled() or introspection_feature.auto() + +gnome = import('gnome') + +subdir('src') +subdir('po') +subdir('examples') +subdir('tests') +subdir('doc') +subdir('glade') + +run_data = configuration_data() +run_data.set('ABS_BUILDDIR', meson.current_build_dir()) +run_data.set('ABS_SRCDIR', meson.current_source_dir()) +configure_file( + input: 'run.in', + output: 'run', + configuration: run_data) + +summary = [ + '', + '------', + 'Handy @0@ (@1@)'.format(current, apiversion), + '', + ' Tests: @0@'.format(get_option('tests')), + ' Examples: @0@'.format(get_option('examples')), + ' Documentation: @0@'.format(get_option('gtk_doc')), + ' Introspection: @0@'.format(introspection), + ' Vapi: @0@'.format(get_option('vapi')), + ' Glade Catalog: @0@'.format(glade_catalog), + '------', + '' +] + +message('\n'.join(summary)) + diff --git a/subprojects/libhandy/meson_options.txt b/subprojects/libhandy/meson_options.txt new file mode 100644 index 0000000..fd0eea5 --- /dev/null +++ b/subprojects/libhandy/meson_options.txt @@ -0,0 +1,25 @@ +# Performance and debugging related options +option('profiling', type: 'boolean', value: false) + +option('introspection', type: 'feature', value: 'auto') +option('vapi', type: 'boolean', value: true) + +# Subproject +option('package_subdir', type: 'string', + description: 'Subdirectory to append to all installed files, for use as subproject' +) + +option('gtk_doc', + type: 'boolean', value: false, + description: 'Whether to generate the API reference for Handy') + +option('tests', + type: 'boolean', value: true, + description: 'Whether to compile unit tests') + +option('examples', + type: 'boolean', value: true, + description: 'Build and install the examples and demo applications') + +option('glade_catalog', type: 'feature', value: 'auto', + description: 'Install a glade catalog file') diff --git a/subprojects/libhandy/po/LINGUAS b/subprojects/libhandy/po/LINGUAS new file mode 100644 index 0000000..7534794 --- /dev/null +++ b/subprojects/libhandy/po/LINGUAS @@ -0,0 +1,6 @@ +en_GB +es +pl +pt_BR +ro +uk diff --git a/subprojects/libhandy/po/POTFILES.in b/subprojects/libhandy/po/POTFILES.in new file mode 100644 index 0000000..b8dd5d5 --- /dev/null +++ b/subprojects/libhandy/po/POTFILES.in @@ -0,0 +1,40 @@ +# List of source files containing translatable strings. +# Please keep this file sorted alphabetically. +glade/glade-hdy-carousel.c +glade/glade-hdy-header-bar.c +glade/glade-hdy-leaflet.c +glade/glade-hdy-preferences-page.c +glade/glade-hdy-preferences-window.c +glade/glade-hdy-search-bar.c +glade/glade-hdy-utils.h +src/hdy-action-row.c +src/hdy-carousel-box.c +src/hdy-carousel.c +src/hdy-carousel-indicator-dots.c +src/hdy-carousel-indicator-lines.c +src/hdy-clamp.c +src/hdy-combo-row.c +src/hdy-deck.c +src/hdy-expander-row.c +src/hdy-header-bar.c +src/hdy-header-group.c +src/hdy-keypad-button.c +src/hdy-keypad.c +src/hdy-leaflet.c +src/hdy-preferences-group.c +src/hdy-preferences-page.c +src/hdy-preferences-row.c +src/hdy-preferences-window.c +src/hdy-preferences-window.ui +src/hdy-search-bar.c +src/hdy-shadow-helper.c +src/hdy-squeezer.c +src/hdy-stackable-box.c +src/hdy-swipe-tracker.c +src/hdy-title-bar.c +src/hdy-value-object.c +src/hdy-view-switcher-bar.c +src/hdy-view-switcher-button.c +src/hdy-view-switcher.c +src/hdy-view-switcher-title.c +src/hdy-window-handle-controller.c diff --git a/subprojects/libhandy/po/POTFILES.skip b/subprojects/libhandy/po/POTFILES.skip new file mode 100644 index 0000000..0c7d9c5 --- /dev/null +++ b/subprojects/libhandy/po/POTFILES.skip @@ -0,0 +1,6 @@ +# List of source files that should *not* be translated. +# Please keep this file sorted alphabetically. +examples/hdy-demo-preferences-window.ui +examples/hdy-demo-window.c +examples/hdy-demo-window.ui +examples/hdy-view-switcher-demo-window.ui diff --git a/subprojects/libhandy/po/en_GB.po b/subprojects/libhandy/po/en_GB.po new file mode 100644 index 0000000..d4bd6ab --- /dev/null +++ b/subprojects/libhandy/po/en_GB.po @@ -0,0 +1,866 @@ +# British English translation for libhandy. +# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER +# This file is distributed under the same license as the libhandy package. +# Zander Brown <zbrown@gnome.org>, 2020. +# Bruce Cowan <bruce@bcowan.me.uk>, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: libhandy master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n" +"POT-Creation-Date: 2020-08-06 15:34+0000\n" +"PO-Revision-Date: 2020-08-06 19:58+0100\n" +"Last-Translator: Bruce Cowan <bruce@bcowan.me.uk>\n" +"Language-Team: English - United Kingdom <en@li.org>\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 3.36.0\n" + +#: glade/glade-hdy-carousel.c:160 glade/glade-hdy-header-bar.c:118 +#: glade/glade-hdy-leaflet.c:184 +#, c-format +msgid "Insert placeholder to %s" +msgstr "Insert placeholder to %s" + +#: glade/glade-hdy-carousel.c:189 glade/glade-hdy-header-bar.c:144 +#: glade/glade-hdy-leaflet.c:214 +#, c-format +msgid "Remove placeholder from %s" +msgstr "Remove placeholder from %s" + +#: glade/glade-hdy-header-bar.c:18 +msgid "This property does not apply when a custom title is set" +msgstr "This property does not apply when a custom title is set" + +#: glade/glade-hdy-header-bar.c:289 +msgid "" +"The decoration layout does not apply to header bars which do no show window " +"controls" +msgstr "" +"The decoration layout does not apply to header bars which do no show window " +"controls" + +#: glade/glade-hdy-leaflet.c:19 +msgid "This property only applies when the leaflet is folded" +msgstr "This property only applies when the leaflet is folded" + +#: glade/glade-hdy-preferences-page.c:160 +#, c-format +msgid "Add group to %s" +msgstr "Add group to %s" + +#: glade/glade-hdy-preferences-window.c:228 +#, c-format +msgid "Add page to %s" +msgstr "Add page to %s" + +#: glade/glade-hdy-search-bar.c:101 +msgid "Search bar is already full" +msgstr "Search bar is already full" + +#: glade/glade-hdy-utils.h:14 +#, c-format +msgid "Only objects of type %s can be added to objects of type %s." +msgstr "Only objects of type %s can be added to objects of type %s." + +#: src/hdy-action-row.c:354 src/hdy-action-row.c:355 src/hdy-expander-row.c:314 +#: src/hdy-expander-row.c:315 src/hdy-preferences-page.c:179 +#: src/hdy-preferences-page.c:180 +msgid "Icon name" +msgstr "Icon name" + +#: src/hdy-action-row.c:368 +msgid "Activatable widget" +msgstr "Activatable widget" + +#: src/hdy-action-row.c:369 +msgid "The widget to be activated when the row is activated" +msgstr "The widget to be activated when the row is activated" + +#: src/hdy-action-row.c:382 src/hdy-action-row.c:383 src/hdy-expander-row.c:285 +#: src/hdy-header-bar.c:2105 src/hdy-view-switcher-title.c:272 +msgid "Subtitle" +msgstr "Subtitle" + +#: src/hdy-action-row.c:397 src/hdy-expander-row.c:300 +#: src/hdy-preferences-row.c:130 +msgid "Use underline" +msgstr "Use underline" + +#: src/hdy-action-row.c:398 src/hdy-expander-row.c:301 +#: src/hdy-preferences-row.c:131 +msgid "" +"If set, an underline in the text indicates the next character should be used " +"for the mnemonic accelerator key" +msgstr "" +"If set, an underline in the text indicates the next character should be used " +"for the mnemonic accelerator key" + +#: src/hdy-carousel-box.c:1088 src/hdy-carousel-box.c:1089 +#: src/hdy-carousel.c:575 src/hdy-carousel.c:576 +msgid "Number of pages" +msgstr "Number of pages" + +#: src/hdy-carousel-box.c:1104 src/hdy-carousel.c:592 src/hdy-header-bar.c:2091 +msgid "Position" +msgstr "Position" + +#: src/hdy-carousel-box.c:1105 src/hdy-carousel.c:593 +msgid "Current scrolling position" +msgstr "Current scrolling position" + +#: src/hdy-carousel-box.c:1120 src/hdy-carousel.c:623 src/hdy-header-bar.c:2119 +msgid "Spacing" +msgstr "Spacing" + +#: src/hdy-carousel-box.c:1121 src/hdy-carousel.c:624 +msgid "Spacing between pages" +msgstr "Spacing between pages" + +#: src/hdy-carousel-box.c:1137 src/hdy-carousel.c:668 +msgid "Reveal duration" +msgstr "Reveal duration" + +#: src/hdy-carousel-box.c:1138 src/hdy-carousel.c:669 +msgid "Page reveal duration" +msgstr "Page reveal duration" + +#: src/hdy-carousel.c:609 +msgid "Interactive" +msgstr "Interactive" + +#: src/hdy-carousel.c:610 +msgid "Whether the widget can be swiped" +msgstr "Whether the widget can be swiped" + +#: src/hdy-carousel.c:639 +msgid "Animation duration" +msgstr "Animation duration" + +#: src/hdy-carousel.c:640 +msgid "Default animation duration" +msgstr "Default animation duration" + +#: src/hdy-carousel.c:654 src/hdy-swipe-tracker.c:803 +msgid "Allow mouse drag" +msgstr "Allow mouse drag" + +#: src/hdy-carousel.c:655 src/hdy-swipe-tracker.c:804 +msgid "Whether to allow dragging with mouse pointer" +msgstr "Whether to allow dragging with mouse pointer" + +#: src/hdy-carousel-indicator-dots.c:392 src/hdy-carousel-indicator-dots.c:393 +#: src/hdy-carousel-indicator-lines.c:391 +#: src/hdy-carousel-indicator-lines.c:392 +msgid "Carousel" +msgstr "Carousel" + +#: src/hdy-clamp.c:417 +msgid "Maximum size" +msgstr "Maximum size" + +#: src/hdy-clamp.c:418 +msgid "The maximum size allocated to the child" +msgstr "The maximum size allocated to the child" + +#: src/hdy-clamp.c:442 +msgid "Tightening threshold" +msgstr "Tightening threshold" + +#: src/hdy-clamp.c:443 +msgid "The size from which the clamp will tighten its grip on the child" +msgstr "The size from which the clamp will tighten its grip on the child" + +#: src/hdy-combo-row.c:411 +msgid "Selected index" +msgstr "Selected index" + +#: src/hdy-combo-row.c:412 +msgid "The index of the selected item" +msgstr "The index of the selected item" + +#: src/hdy-combo-row.c:430 +msgid "Use subtitle" +msgstr "Use subtitle" + +#: src/hdy-combo-row.c:431 +msgid "Set the current value as the subtitle" +msgstr "Set the current value as the subtitle" + +#: src/hdy-deck.c:888 +msgid "Horizontally homogeneous" +msgstr "Horizontally homogeneous" + +#: src/hdy-deck.c:889 +msgid "Horizontally homogeneous sizing" +msgstr "Horizontally homogeneous sizing" + +#: src/hdy-deck.c:902 +msgid "Vertically homogeneous" +msgstr "Vertically homogeneous" + +#: src/hdy-deck.c:903 +msgid "Vertically homogeneous sizing" +msgstr "Vertically homogeneous sizing" + +#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1098 +#: src/hdy-stackable-box.c:2993 +msgid "Visible child" +msgstr "Visible child" + +#: src/hdy-deck.c:917 +msgid "The widget currently visible" +msgstr "The widget currently visible" + +#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3000 +msgid "Name of visible child" +msgstr "Name of visible child" + +#: src/hdy-deck.c:931 +msgid "The name of the widget currently visible" +msgstr "The name of the widget currently visible" + +#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1112 +#: src/hdy-stackable-box.c:3019 +msgid "Transition type" +msgstr "Transition type" + +#: src/hdy-deck.c:950 +msgid "The type of animation used to transition between children" +msgstr "The type of animation used to transition between children" + +#: src/hdy-deck.c:963 src/hdy-header-bar.c:2201 src/hdy-squeezer.c:1105 +msgid "Transition duration" +msgstr "Transition duration" + +#: src/hdy-deck.c:964 +msgid "The transition animation duration, in milliseconds" +msgstr "The transition animation duration, in milliseconds" + +#: src/hdy-deck.c:977 src/hdy-header-bar.c:2208 src/hdy-squeezer.c:1120 +msgid "Transition running" +msgstr "Transition running" + +#: src/hdy-deck.c:978 src/hdy-header-bar.c:2209 src/hdy-squeezer.c:1121 +msgid "Whether or not the transition is currently running" +msgstr "Whether or not the transition is currently running" + +#: src/hdy-deck.c:992 src/hdy-header-bar.c:2215 src/hdy-leaflet.c:1072 +#: src/hdy-squeezer.c:1127 src/hdy-stackable-box.c:3047 +msgid "Interpolate size" +msgstr "Interpolate size" + +#: src/hdy-deck.c:993 src/hdy-header-bar.c:2216 src/hdy-leaflet.c:1073 +#: src/hdy-squeezer.c:1128 src/hdy-stackable-box.c:3048 +msgid "" +"Whether or not the size should smoothly change when changing between " +"differently sized children" +msgstr "" +"Whether or not the size should smoothly change when changing between " +"differently sized children" + +#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-preferences-window.c:497 +#: src/hdy-stackable-box.c:3062 +msgid "Can swipe back" +msgstr "Can swipe back" + +#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3063 +msgid "" +"Whether or not swipe gesture can be used to switch to the previous child" +msgstr "" +"Whether or not swipe gesture can be used to switch to the previous child" + +#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3077 +msgid "Can swipe forward" +msgstr "Can swipe forward" + +#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3078 +msgid "Whether or not swipe gesture can be used to switch to the next child" +msgstr "Whether or not swipe gesture can be used to switch to the next child" + +#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111 +msgid "Name" +msgstr "Name" + +#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112 +msgid "The name of the child page" +msgstr "The name of the child page" + +#: src/hdy-expander-row.c:286 +msgid "The subtitle for this row" +msgstr "The subtitle for this row" + +#: src/hdy-expander-row.c:326 +msgid "Expanded" +msgstr "Expanded" + +#: src/hdy-expander-row.c:327 +msgid "Whether the row is expanded" +msgstr "Whether the row is expanded" + +#: src/hdy-expander-row.c:338 +msgid "Enable expansion" +msgstr "Enable expansion" + +#: src/hdy-expander-row.c:339 +msgid "Whether the expansion is enabled" +msgstr "Whether the expansion is enabled" + +#: src/hdy-expander-row.c:350 +msgid "Show enable switch" +msgstr "Show enable switch" + +#: src/hdy-expander-row.c:351 +msgid "Whether the switch enabling the expansion is visible" +msgstr "Whether the switch enabling the expansion is visible" + +#: src/hdy-header-bar.c:485 +msgid "Application menu" +msgstr "Application menu" + +#: src/hdy-header-bar.c:507 src/hdy-window-handle-controller.c:275 +msgid "Minimize" +msgstr "Minimise" + +#: src/hdy-header-bar.c:529 src/hdy-window-handle-controller.c:241 +msgid "Restore" +msgstr "Restore" + +#: src/hdy-header-bar.c:529 src/hdy-window-handle-controller.c:284 +msgid "Maximize" +msgstr "Maximise" + +#: src/hdy-header-bar.c:547 src/hdy-window-handle-controller.c:311 +msgid "Close" +msgstr "Close" + +#: src/hdy-header-bar.c:563 +msgid "Back" +msgstr "Back" + +#: src/hdy-header-bar.c:2084 +msgid "Pack type" +msgstr "Pack type" + +#: src/hdy-header-bar.c:2085 +msgid "" +"A GtkPackType indicating whether the child is packed with reference to the " +"start or end of the parent" +msgstr "" +"A GtkPackType indicating whether the child is packed with reference to the " +"start or end of the parent" + +#: src/hdy-header-bar.c:2092 +msgid "The index of the child in the parent" +msgstr "The index of the child in the parent" + +#: src/hdy-header-bar.c:2098 src/hdy-preferences-group.c:265 +#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193 +#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115 +#: src/hdy-view-switcher-title.c:258 +msgid "Title" +msgstr "Title" + +#: src/hdy-header-bar.c:2099 src/hdy-view-switcher-title.c:259 +msgid "The title to display" +msgstr "The title to display" + +#: src/hdy-header-bar.c:2106 src/hdy-view-switcher-title.c:273 +msgid "The subtitle to display" +msgstr "The subtitle to display" + +#: src/hdy-header-bar.c:2112 +msgid "Custom Title" +msgstr "Custom Title" + +#: src/hdy-header-bar.c:2113 +msgid "Custom title widget to display" +msgstr "Custom title widget to display" + +#: src/hdy-header-bar.c:2120 +msgid "The amount of space between children" +msgstr "The amount of space between children" + +#: src/hdy-header-bar.c:2139 +msgid "Show decorations" +msgstr "Show decorations" + +#: src/hdy-header-bar.c:2140 +msgid "Whether to show window decorations" +msgstr "Whether to show window decorations" + +#: src/hdy-header-bar.c:2158 +msgid "Decoration Layout" +msgstr "Decoration Layout" + +#: src/hdy-header-bar.c:2159 +msgid "The layout for window decorations" +msgstr "The layout for window decorations" + +#: src/hdy-header-bar.c:2172 +msgid "Decoration Layout Set" +msgstr "Decoration Layout Set" + +#: src/hdy-header-bar.c:2173 +msgid "Whether the decoration-layout property has been set" +msgstr "Whether the decoration-layout property has been set" + +#: src/hdy-header-bar.c:2187 +msgid "Has Subtitle" +msgstr "Has Subtitle" + +#: src/hdy-header-bar.c:2188 +msgid "Whether to reserve space for a subtitle" +msgstr "Whether to reserve space for a subtitle" + +#: src/hdy-header-bar.c:2194 +msgid "Centering policy" +msgstr "Centring policy" + +#: src/hdy-header-bar.c:2195 +msgid "The policy to horizontally align the center widget" +msgstr "The policy to horizontally align the centre widget" + +#: src/hdy-header-bar.c:2202 src/hdy-squeezer.c:1106 +msgid "The animation duration, in milliseconds" +msgstr "The animation duration, in milliseconds" + +#: src/hdy-header-group.c:827 +msgid "Decorate all" +msgstr "Decorate all" + +#: src/hdy-header-group.c:828 +msgid "" +"Whether the elements of the group should all receive the full decoration" +msgstr "" +"Whether the elements of the group should all receive the full decoration" + +#: src/hdy-keypad-button.c:225 +msgid "Digit" +msgstr "Digit" + +#: src/hdy-keypad-button.c:226 +msgid "The keypad digit of the button" +msgstr "The keypad digit of the button" + +#: src/hdy-keypad-button.c:232 +msgid "Symbols" +msgstr "Symbols" + +#: src/hdy-keypad-button.c:233 +msgid "The keypad symbols of the button. The first symbol is used as the digit" +msgstr "" +"The keypad symbols of the button. The first symbol is used as the digit" + +#: src/hdy-keypad-button.c:239 +msgid "Show symbols" +msgstr "Show symbols" + +#: src/hdy-keypad-button.c:240 +msgid "Whether the second line of symbols should be shown or not" +msgstr "Whether the second line of symbols should be shown or not" + +#: src/hdy-keypad.c:247 +msgid "Row spacing" +msgstr "Row spacing" + +#: src/hdy-keypad.c:248 +msgid "The amount of space between two consecutive rows" +msgstr "The amount of space between two consecutive rows" + +#: src/hdy-keypad.c:261 +msgid "Column spacing" +msgstr "Column spacing" + +#: src/hdy-keypad.c:262 +msgid "The amount of space between two consecutive columns" +msgstr "The amount of space between two consecutive columns" + +#: src/hdy-keypad.c:276 +msgid "Letters visible" +msgstr "Letters visible" + +#: src/hdy-keypad.c:277 +msgid "Whether the letters below the digits should be visible" +msgstr "Whether the letters below the digits should be visible" + +#: src/hdy-keypad.c:291 +msgid "Symbols visible" +msgstr "Symbols visible" + +#: src/hdy-keypad.c:292 +msgid "Whether the hash, plus, and asterisk symbols should be visible" +msgstr "Whether the hash, plus, and asterisk symbols should be visible" + +#: src/hdy-keypad.c:306 +msgid "Entry" +msgstr "Entry" + +#: src/hdy-keypad.c:307 +msgid "The entry widget connected to the keypad" +msgstr "The entry widget connected to the keypad" + +#: src/hdy-keypad.c:320 +msgid "End action" +msgstr "End action" + +#: src/hdy-keypad.c:321 +msgid "The end action widget" +msgstr "The end action widget" + +#: src/hdy-keypad.c:334 +msgid "Start action" +msgstr "Start action" + +#: src/hdy-keypad.c:335 +msgid "The start action widget" +msgstr "The start action widget" + +#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2938 +msgid "Folded" +msgstr "Folded" + +#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2939 +msgid "Whether the widget is folded" +msgstr "Whether the widget is folded" + +#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2950 +msgid "Horizontally homogeneous folded" +msgstr "Horizontally homogeneous folded" + +#: src/hdy-leaflet.c:976 +msgid "Horizontally homogeneous sizing when the leaflet is folded" +msgstr "Horizontally homogeneous sizing when the leaflet is folded" + +#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2962 +msgid "Vertically homogeneous folded" +msgstr "Vertically homogeneous folded" + +#: src/hdy-leaflet.c:988 +msgid "Vertically homogeneous sizing when the leaflet is folded" +msgstr "Vertically homogeneous sizing when the leaflet is folded" + +#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2974 +msgid "Box horizontally homogeneous" +msgstr "Box horizontally homogeneous" + +#: src/hdy-leaflet.c:1000 +msgid "Horizontally homogeneous sizing when the leaflet is unfolded" +msgstr "Horizontally homogeneous sizing when the leaflet is unfolded" + +#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2986 +msgid "Box vertically homogeneous" +msgstr "Box vertically homogeneous" + +#: src/hdy-leaflet.c:1012 +msgid "Vertically homogeneous sizing when the leaflet is unfolded" +msgstr "Vertically homogeneous sizing when the leaflet is unfolded" + +#: src/hdy-leaflet.c:1019 +msgid "The widget currently visible when the leaflet is folded" +msgstr "The widget currently visible when the leaflet is folded" + +#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3001 +msgid "The name of the widget currently visible when the children are stacked" +msgstr "The name of the widget currently visible when the children are stacked" + +#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3020 +msgid "The type of animation used to transition between modes and children" +msgstr "The type of animation used to transition between modes and children" + +#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3026 +msgid "Mode transition duration" +msgstr "Mode transition duration" + +#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3027 +msgid "The mode transition animation duration, in milliseconds" +msgstr "The mode transition animation duration, in milliseconds" + +#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3033 +msgid "Child transition duration" +msgstr "Child transition duration" + +#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3034 +msgid "The child transition animation duration, in milliseconds" +msgstr "The child transition animation duration, in milliseconds" + +#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3040 +msgid "Child transition running" +msgstr "Child transition running" + +#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3041 +msgid "Whether or not the child transition is currently running" +msgstr "Whether or not the child transition is currently running" + +#: src/hdy-leaflet.c:1129 +msgid "Navigatable" +msgstr "Navigable" + +#: src/hdy-leaflet.c:1130 +msgid "Whether the child can be navigated to" +msgstr "Whether the child can be navigated to" + +#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252 +msgid "Description" +msgstr "Description" + +#: src/hdy-preferences-row.c:116 +msgid "The title of the preference" +msgstr "The title of the preference" + +#: src/hdy-preferences-window.c:141 +msgid "Untitled page" +msgstr "Untitled page" + +#: src/hdy-preferences-window.c:483 +msgid "Search enabled" +msgstr "Search enabled" + +#: src/hdy-preferences-window.c:484 +msgid "Whether search is enabled" +msgstr "Whether search is enabled" + +#: src/hdy-preferences-window.c:498 +msgid "" +"Whether or not swipe gesture can be used to switch from a subpage to the " +"preferences" +msgstr "" +"Whether or not swipe gesture can be used to switch from a sub-page to the " +"preferences" + +#: src/hdy-preferences-window.ui:9 +msgid "Preferences" +msgstr "Preferences" + +#: src/hdy-preferences-window.ui:78 +msgid "Search" +msgstr "Search" + +#: src/hdy-preferences-window.ui:201 +msgid "No Results Found" +msgstr "No Results Found" + +#: src/hdy-preferences-window.ui:216 +msgid "Try a different search" +msgstr "Try a different search" + +#: src/hdy-search-bar.c:451 +msgid "Search Mode Enabled" +msgstr "Search Mode Enabled" + +#: src/hdy-search-bar.c:452 +msgid "Whether the search mode is on and the search bar shown" +msgstr "Whether the search mode is on and the search bar shown" + +#: src/hdy-search-bar.c:463 +msgid "Show Close Button" +msgstr "Show Close Button" + +#: src/hdy-search-bar.c:464 +msgid "Whether to show the close button in the toolbar" +msgstr "Whether to show the close button in the toolbar" + +#: src/hdy-shadow-helper.c:254 +msgid "Widget" +msgstr "Widget" + +#: src/hdy-shadow-helper.c:255 +msgid "The widget the shadow will be drawn for" +msgstr "The widget the shadow will be drawn for" + +#: src/hdy-squeezer.c:1091 +msgid "Homogeneous" +msgstr "Homogeneous" + +#: src/hdy-squeezer.c:1092 +msgid "Homogeneous sizing" +msgstr "Homogeneous sizing" + +#: src/hdy-squeezer.c:1099 +msgid "The widget currently visible in the squeezer" +msgstr "The widget currently visible in the squeezer" + +#: src/hdy-squeezer.c:1113 +msgid "The type of animation used to transition" +msgstr "The type of animation used to transition" + +#: src/hdy-squeezer.c:1148 +msgid "X align" +msgstr "X align" + +#: src/hdy-squeezer.c:1149 +msgid "The horizontal alignment, from 0 (start) to 1 (end)" +msgstr "The horizontal alignment, from 0 (start) to 1 (end)" + +#: src/hdy-squeezer.c:1170 +msgid "Y align" +msgstr "Y align" + +#: src/hdy-squeezer.c:1171 +msgid "The vertical alignment, from 0 (top) to 1 (bottom)" +msgstr "The vertical alignment, from 0 (top) to 1 (bottom)" + +#: src/hdy-squeezer.c:1180 src/hdy-swipe-tracker.c:773 +msgid "Enabled" +msgstr "Enabled" + +#: src/hdy-squeezer.c:1181 +msgid "" +"Whether the child can be picked or should be ignored when looking for the " +"child fitting the available size best" +msgstr "" +"Whether the child can be picked or should be ignored when looking for the " +"child fitting the available size best" + +#: src/hdy-stackable-box.c:2951 +msgid "Horizontally homogeneous sizing when the widget is folded" +msgstr "Horizontally homogeneous sizing when the widget is folded" + +#: src/hdy-stackable-box.c:2963 +msgid "Vertically homogeneous sizing when the widget is folded" +msgstr "Vertically homogeneous sizing when the widget is folded" + +#: src/hdy-stackable-box.c:2975 +msgid "Horizontally homogeneous sizing when the widget is unfolded" +msgstr "Horizontally homogeneous sizing when the widget is unfolded" + +#: src/hdy-stackable-box.c:2987 +msgid "Vertically homogeneous sizing when the widget is unfolded" +msgstr "Vertically homogeneous sizing when the widget is unfolded" + +#: src/hdy-stackable-box.c:2994 +msgid "The widget currently visible when the widget is folded" +msgstr "The widget currently visible when the widget is folded" + +#: src/hdy-stackable-box.c:3084 src/hdy-stackable-box.c:3085 +msgid "Orientation" +msgstr "Orientation" + +#: src/hdy-swipe-tracker.c:758 +msgid "Swipeable" +msgstr "Swipe-able" + +#: src/hdy-swipe-tracker.c:759 +msgid "The swipeable the swipe tracker is attached to" +msgstr "The swipe-able the swipe tracker is attached to" + +#: src/hdy-swipe-tracker.c:774 +msgid "Whether the swipe tracker processes events" +msgstr "Whether the swipe tracker processes events" + +#: src/hdy-swipe-tracker.c:788 +msgid "Reversed" +msgstr "Reversed" + +#: src/hdy-swipe-tracker.c:789 +msgid "Whether swipe direction is reversed" +msgstr "Whether swipe direction is reversed" + +#: src/hdy-title-bar.c:308 +msgid "Selection mode" +msgstr "Selection mode" + +#: src/hdy-title-bar.c:309 +msgid "Whether or not the title bar is in selection mode" +msgstr "Whether or not the title bar is in selection mode" + +#: src/hdy-value-object.c:191 +msgctxt "HdyValueObjectClass" +msgid "Value" +msgstr "Value" + +#: src/hdy-value-object.c:192 +msgctxt "HdyValueObjectClass" +msgid "The contained value" +msgstr "The contained value" + +#: src/hdy-view-switcher-bar.c:178 src/hdy-view-switcher.c:509 +#: src/hdy-view-switcher-title.c:230 +msgid "Policy" +msgstr "Policy" + +#: src/hdy-view-switcher-bar.c:179 src/hdy-view-switcher.c:510 +#: src/hdy-view-switcher-title.c:231 +msgid "The policy to determine the mode to use" +msgstr "The policy to determine the mode to use" + +#: src/hdy-view-switcher-bar.c:192 src/hdy-view-switcher-bar.c:193 +#: src/hdy-view-switcher.c:544 src/hdy-view-switcher.c:545 +#: src/hdy-view-switcher-title.c:244 src/hdy-view-switcher-title.c:245 +msgid "Stack" +msgstr "Stack" + +#: src/hdy-view-switcher-bar.c:206 +msgid "Reveal" +msgstr "Reveal" + +#: src/hdy-view-switcher-bar.c:207 +msgid "Whether the view switcher is revealed" +msgstr "Whether the view switcher is revealed" + +#: src/hdy-view-switcher-button.c:203 +msgid "Icon Name" +msgstr "Icon Name" + +#: src/hdy-view-switcher-button.c:204 +msgid "Icon name for image" +msgstr "Icon name for image" + +#: src/hdy-view-switcher-button.c:217 +msgid "Icon Size" +msgstr "Icon Size" + +#: src/hdy-view-switcher-button.c:218 +msgid "Symbolic size to use for named icon" +msgstr "Symbolic size to use for named icon" + +#: src/hdy-view-switcher-button.c:234 +msgid "Needs attention" +msgstr "Needs attention" + +#: src/hdy-view-switcher-button.c:235 +msgid "Hint the view needs attention" +msgstr "Hint the view needs attention" + +#: src/hdy-view-switcher.c:529 +msgid "Narrow ellipsize" +msgstr "Narrow ellipsise" + +#: src/hdy-view-switcher.c:530 +msgid "" +"The preferred place to ellipsize the string, if the narrow mode label does " +"not have enough room to display the entire string" +msgstr "" +"The preferred place to ellipsise the string, if the narrow mode label does " +"not have enough room to display the entire string" + +#: src/hdy-view-switcher-title.c:286 +msgid "View switcher enabled" +msgstr "View switcher enabled" + +#: src/hdy-view-switcher-title.c:287 +msgid "Whether the view switcher is enabled" +msgstr "Whether the view switcher is enabled" + +#: src/hdy-view-switcher-title.c:300 +msgid "Title visible" +msgstr "Title visible" + +#: src/hdy-view-switcher-title.c:301 +msgid "Whether the title label is visible" +msgstr "Whether the title label is visible" + +#: src/hdy-window-handle-controller.c:259 +msgid "Move" +msgstr "Move" + +#: src/hdy-window-handle-controller.c:267 +msgid "Resize" +msgstr "Resize" + +#: src/hdy-window-handle-controller.c:298 +msgid "Always on Top" +msgstr "Always on Top" diff --git a/subprojects/libhandy/po/es.po b/subprojects/libhandy/po/es.po new file mode 100644 index 0000000..a55ee22 --- /dev/null +++ b/subprojects/libhandy/po/es.po @@ -0,0 +1,850 @@ +# Spanish translation for libhandy. +# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER +# This file is distributed under the same license as the libhandy package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# FULL NAME <EMAIL@ADDRESS>, 2020. +# Daniel Mustieles <daniel.mustieles@gmail.com>, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: libhandy master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n" +"POT-Creation-Date: 2020-06-12 07:20+0000\n" +"PO-Revision-Date: 2020-07-01 09:37+0200\n" +"Last-Translator: Daniel Mustieles <daniel.mustieles@gmail.com>\n" +"Language-Team: Spanish - Spain <gnome-es-list@gnome.org>\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 3.36.0\n" + +#: glade/glade-hdy-carousel.c:20 +msgid "This property does not apply unless Show Indicators is set." +msgstr "" + +#: glade/glade-hdy-carousel.c:166 glade/glade-hdy-header-bar.c:117 +#: glade/glade-hdy-leaflet.c:183 +#, c-format +msgid "Insert placeholder to %s" +msgstr "Insertar marcador a %s" + +#: glade/glade-hdy-carousel.c:195 glade/glade-hdy-header-bar.c:143 +#: glade/glade-hdy-leaflet.c:213 +#, c-format +msgid "Remove placeholder from %s" +msgstr "Quitar marcador de %s" + +#: glade/glade-hdy-header-bar.c:17 +msgid "This property does not apply when a custom title is set" +msgstr "" + +#: glade/glade-hdy-header-bar.c:288 +msgid "" +"The decoration layout does not apply to header bars which do no show window " +"controls" +msgstr "" + +#: glade/glade-hdy-leaflet.c:18 +msgid "This property only applies when the leaflet is folded" +msgstr "" + +#: glade/glade-hdy-preferences-page.c:159 +#, c-format +msgid "Add group to %s" +msgstr "Añadir grupo a %s" + +#: glade/glade-hdy-preferences-window.c:227 +#, c-format +msgid "Add page to %s" +msgstr "Añadir página a %s" + +#: glade/glade-hdy-search-bar.c:100 +msgid "Search bar is already full" +msgstr "La barra de búsqueda ya está llena" + +#: glade/glade-hdy-utils.h:14 +#, c-format +msgid "Only objects of type %s can be added to objects of type %s." +msgstr "" + +#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:328 +#: src/hdy-expander-row.c:329 src/hdy-preferences-page.c:179 +#: src/hdy-preferences-page.c:180 +msgid "Icon name" +msgstr "Nombre del icono" + +#: src/hdy-action-row.c:375 +msgid "Activatable widget" +msgstr "Widget activable" + +#: src/hdy-action-row.c:376 +msgid "The widget to be activated when the row is activated" +msgstr "" + +#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:299 +#: src/hdy-header-bar.c:2149 src/hdy-view-switcher-title.c:295 +msgid "Subtitle" +msgstr "Subtítulo" + +#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:285 +#: src/hdy-header-bar.c:2142 src/hdy-preferences-group.c:265 +#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193 +#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115 +#: src/hdy-view-switcher-title.c:281 +msgid "Title" +msgstr "Título" + +#: src/hdy-action-row.c:418 src/hdy-expander-row.c:314 +#: src/hdy-preferences-row.c:130 +msgid "Use underline" +msgstr "Usar subrayado" + +#: src/hdy-action-row.c:419 src/hdy-expander-row.c:315 +#: src/hdy-preferences-row.c:131 +msgid "" +"If set, an underline in the text indicates the next character should be used " +"for the mnemonic accelerator key" +msgstr "" + +#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096 +#: src/hdy-carousel.c:970 src/hdy-carousel.c:971 +msgid "Number of pages" +msgstr "Número de páginas" + +#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:987 src/hdy-header-bar.c:2135 +msgid "Position" +msgstr "Posición" + +#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:988 +msgid "Current scrolling position" +msgstr "Posición actual del desplazamiento" + +#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1069 +#: src/hdy-header-bar.c:2163 +msgid "Spacing" +msgstr "Espaciado" + +#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1070 +msgid "Spacing between pages" +msgstr "Espacio entre páginas" + +#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1114 +msgid "Reveal duration" +msgstr "Mostrar duración" + +#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1115 +msgid "Page reveal duration" +msgstr "Tiempo que se muestra la página" + +#: src/hdy-carousel.c:1004 +msgid "Interactive" +msgstr "Interactivo" + +#: src/hdy-carousel.c:1005 +msgid "Whether the widget can be swiped" +msgstr "" + +#: src/hdy-carousel.c:1020 +msgid "Indicator style" +msgstr "Indicador de estilo" + +#: src/hdy-carousel.c:1021 +msgid "Page indicator style" +msgstr "Indicador de estilo de la página" + +#: src/hdy-carousel.c:1036 +msgid "Indicator spacing" +msgstr "Indicador de espaciado" + +#: src/hdy-carousel.c:1037 +msgid "Spacing between content and indicators" +msgstr "Espaciado entre contenido e indicadores" + +#: src/hdy-carousel.c:1055 +msgid "Center content" +msgstr "Centrar contenido" + +#: src/hdy-carousel.c:1056 +msgid "Whether to center pages to compensate for indicators" +msgstr "Indica si se deben centrar las páginas para compensar los indicadores" + +#: src/hdy-carousel.c:1085 +msgid "Animation duration" +msgstr "Duración de la animación" + +#: src/hdy-carousel.c:1086 +msgid "Default animation duration" +msgstr "Duración predeterminada de la animación" + +#: src/hdy-carousel.c:1100 src/hdy-swipe-tracker.c:645 +msgid "Allow mouse drag" +msgstr "Permitir arrastre con el ratón" + +#: src/hdy-carousel.c:1101 src/hdy-swipe-tracker.c:646 +msgid "Whether to allow dragging with mouse pointer" +msgstr "" + +#: src/hdy-column.c:306 +msgid "Maximum width" +msgstr "Anchura máxima" + +#: src/hdy-column.c:307 +msgid "The maximum width allocated to the child" +msgstr "" + +#: src/hdy-column.c:318 +msgid "Linear growth width" +msgstr "" + +#: src/hdy-column.c:319 +msgid "The width up to which the child will be allocated all the width" +msgstr "" + +#: src/hdy-combo-row.c:411 +msgid "Selected index" +msgstr "Índice seleccionada" + +#: src/hdy-combo-row.c:412 +msgid "The index of the selected item" +msgstr "El índice del elemento seleccionado" + +#: src/hdy-combo-row.c:430 +msgid "Use subtitle" +msgstr "Usar subtítulo" + +#: src/hdy-combo-row.c:431 +msgid "Set the current value as the subtitle" +msgstr "" + +#: src/hdy-deck.c:881 +msgid "Horizontally homogeneous" +msgstr "Homogéneo horizontalmente" + +#: src/hdy-deck.c:882 +msgid "Horizontally homogeneous sizing" +msgstr "" + +#: src/hdy-deck.c:895 +msgid "Vertically homogeneous" +msgstr "Homogéneo verticalmente" + +#: src/hdy-deck.c:896 +msgid "Vertically homogeneous sizing" +msgstr "" + +#: src/hdy-deck.c:909 src/hdy-leaflet.c:1010 src/hdy-squeezer.c:1100 +#: src/hdy-stackable-box.c:3436 +msgid "Visible child" +msgstr "Hijo visible" + +#: src/hdy-deck.c:910 +msgid "The widget currently visible" +msgstr "El widget actualmente visible" + +#: src/hdy-deck.c:923 src/hdy-leaflet.c:1017 src/hdy-stackable-box.c:3443 +msgid "Name of visible child" +msgstr "Nombre del hijo visible" + +#: src/hdy-deck.c:924 +msgid "The name of the widget currently visible" +msgstr "" + +#: src/hdy-deck.c:942 src/hdy-leaflet.c:1036 src/hdy-squeezer.c:1114 +#: src/hdy-stackable-box.c:3462 +msgid "Transition type" +msgstr "Tipo de transición" + +#: src/hdy-deck.c:943 +msgid "The type of animation used to transition between children" +msgstr "El tipo de animación usada para cambiar entre hijos" + +#: src/hdy-deck.c:956 src/hdy-header-bar.c:2245 src/hdy-squeezer.c:1107 +msgid "Transition duration" +msgstr "Duración de la transición" + +#: src/hdy-deck.c:957 +msgid "The transition animation duration, in milliseconds" +msgstr "" + +#: src/hdy-deck.c:970 src/hdy-header-bar.c:2252 src/hdy-squeezer.c:1122 +msgid "Transition running" +msgstr "Transición en ejecución" + +#: src/hdy-deck.c:971 src/hdy-header-bar.c:2253 src/hdy-squeezer.c:1123 +msgid "Whether or not the transition is currently running" +msgstr "" + +#: src/hdy-deck.c:985 src/hdy-header-bar.c:2259 src/hdy-leaflet.c:1064 +#: src/hdy-squeezer.c:1129 src/hdy-stackable-box.c:3490 +msgid "Interpolate size" +msgstr "Interpolar tamaño" + +#: src/hdy-deck.c:986 src/hdy-header-bar.c:2260 src/hdy-leaflet.c:1065 +#: src/hdy-squeezer.c:1130 src/hdy-stackable-box.c:3491 +msgid "" +"Whether or not the size should smoothly change when changing between " +"differently sized children" +msgstr "" + +#: src/hdy-deck.c:1000 src/hdy-leaflet.c:1079 src/hdy-stackable-box.c:3505 +msgid "Can swipe back" +msgstr "" + +#: src/hdy-deck.c:1001 src/hdy-leaflet.c:1080 src/hdy-stackable-box.c:3506 +msgid "" +"Whether or not swipe gesture can be used to switch to the previous child" +msgstr "" + +#: src/hdy-deck.c:1014 src/hdy-leaflet.c:1094 src/hdy-stackable-box.c:3520 +msgid "Can swipe forward" +msgstr "" + +#: src/hdy-deck.c:1015 src/hdy-leaflet.c:1095 src/hdy-stackable-box.c:3521 +msgid "Whether or not swipe gesture can be used to switch to the next child" +msgstr "" + +#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3537 +msgid "Name" +msgstr "Nombre" + +#: src/hdy-deck.c:1024 src/hdy-leaflet.c:1104 src/hdy-stackable-box.c:3538 +msgid "The name of the child page" +msgstr "El nombre de la página hija" + +#: src/hdy-expander-row.c:286 +msgid "The title for this row" +msgstr "El título para esta fila" + +#: src/hdy-expander-row.c:300 +msgid "The subtitle for this row" +msgstr "El subtítulo para esta fila" + +#: src/hdy-expander-row.c:340 +msgid "Expanded" +msgstr "Expandido" + +#: src/hdy-expander-row.c:341 +msgid "Whether the row is expanded" +msgstr "Indica si la fila está expandida" + +#: src/hdy-expander-row.c:352 +msgid "Enable expansion" +msgstr "Activar expansión" + +#: src/hdy-expander-row.c:353 +msgid "Whether the expansion is enabled" +msgstr "Indica si la expansión está activada" + +#: src/hdy-expander-row.c:364 +msgid "Show enable switch" +msgstr "" + +#: src/hdy-expander-row.c:365 +msgid "Whether the switch enabling the expansion is visible" +msgstr "" + +#: src/hdy-header-bar.c:484 +msgid "Application menu" +msgstr "Menú de la aplicación" + +#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275 +msgid "Minimize" +msgstr "Minimizar" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241 +msgid "Restore" +msgstr "Restaurar" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284 +msgid "Maximize" +msgstr "Maximizar" + +#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311 +msgid "Close" +msgstr "Cerrar" + +#: src/hdy-header-bar.c:562 +msgid "Back" +msgstr "Atrás" + +#: src/hdy-header-bar.c:2128 +msgid "Pack type" +msgstr "Tipo de paquete" + +#: src/hdy-header-bar.c:2129 +msgid "" +"A GtkPackType indicating whether the child is packed with reference to the " +"start or end of the parent" +msgstr "" + +#: src/hdy-header-bar.c:2136 +msgid "The index of the child in the parent" +msgstr "El índice del hijo en el padre" + +#: src/hdy-header-bar.c:2143 src/hdy-view-switcher-title.c:282 +msgid "The title to display" +msgstr "El título que mostrar" + +#: src/hdy-header-bar.c:2150 src/hdy-view-switcher-title.c:296 +msgid "The subtitle to display" +msgstr "El subtítulo que mostrar" + +#: src/hdy-header-bar.c:2156 +msgid "Custom Title" +msgstr "Título personalizado" + +#: src/hdy-header-bar.c:2157 +msgid "Custom title widget to display" +msgstr "Wigdet de título personalizado que mostrar" + +#: src/hdy-header-bar.c:2164 +msgid "The amount of space between children" +msgstr "" + +#: src/hdy-header-bar.c:2183 +msgid "Show decorations" +msgstr "Mostrar decoración" + +#: src/hdy-header-bar.c:2184 +msgid "Whether to show window decorations" +msgstr "Indica si se debe mostrar la decoración de la ventana" + +#: src/hdy-header-bar.c:2202 +msgid "Decoration Layout" +msgstr "Distribución de la decoración" + +#: src/hdy-header-bar.c:2203 +msgid "The layout for window decorations" +msgstr "La distribución de la decoración de la ventana" + +#: src/hdy-header-bar.c:2216 +msgid "Decoration Layout Set" +msgstr "" + +#: src/hdy-header-bar.c:2217 +msgid "Whether the decoration-layout property has been set" +msgstr "" + +#: src/hdy-header-bar.c:2231 +msgid "Has Subtitle" +msgstr "Tiene subtítulo" + +#: src/hdy-header-bar.c:2232 +msgid "Whether to reserve space for a subtitle" +msgstr "Indica si se debe reservar espacio para un subtítulo" + +#: src/hdy-header-bar.c:2238 +msgid "Centering policy" +msgstr "Política de centrado" + +#: src/hdy-header-bar.c:2239 +msgid "The policy to horizontally align the center widget" +msgstr "La política para alinear horizontalmente el widget centrad" + +#: src/hdy-header-bar.c:2246 src/hdy-squeezer.c:1108 +msgid "The animation duration, in milliseconds" +msgstr "La duración de la animación en milisegundos" + +#: src/hdy-header-group.c:590 +msgid "Focus" +msgstr "Foco" + +#: src/hdy-header-group.c:591 +msgid "The header bar that should have the focus" +msgstr "La barra de cabecera que debe tener el foco" + +#: src/hdy-keypad-button.c:227 +msgid "Digit" +msgstr "Dígito" + +#: src/hdy-keypad-button.c:228 +msgid "The keypad digit of the button" +msgstr "" + +#: src/hdy-keypad-button.c:234 +msgid "Symbols" +msgstr "Símbolos" + +#: src/hdy-keypad-button.c:235 +msgid "The keypad symbols of the button. The first symbol is used as the digit" +msgstr "" + +#: src/hdy-keypad-button.c:241 src/hdy-keypad.c:278 +msgid "Show Symbols" +msgstr "" + +#: src/hdy-keypad-button.c:242 src/hdy-keypad.c:279 +msgid "Whether the second line of symbols should be shown or not" +msgstr "" + +#: src/hdy-keypad.c:264 +msgid "Row spacing" +msgstr "Espaciado de filas" + +#: src/hdy-keypad.c:265 +msgid "The amount of space between two consecutive rows" +msgstr "" + +#: src/hdy-keypad.c:271 +msgid "Column spacing" +msgstr "Espaciado de columnas" + +#: src/hdy-keypad.c:272 +msgid "The amount of space between two consecutive columns" +msgstr "" + +#: src/hdy-keypad.c:285 +msgid "Only Digits" +msgstr "Sólo dígitos" + +#: src/hdy-keypad.c:286 +msgid "" +"Whether the keypad should show only digits or also extra buttons for #, *" +msgstr "" + +#: src/hdy-keypad.c:292 +msgid "Entry widget" +msgstr "Widget de entrada" + +#: src/hdy-keypad.c:293 +msgid "The entry widget connected to the keypad" +msgstr "" + +#: src/hdy-keypad.c:299 +msgid "Right action widget" +msgstr "" + +#: src/hdy-keypad.c:300 +msgid "The right action widget" +msgstr "" + +#: src/hdy-keypad.c:306 +msgid "Left action widget" +msgstr "" + +#: src/hdy-keypad.c:307 +msgid "The left action widget" +msgstr "" + +#: src/hdy-leaflet.c:955 src/hdy-stackable-box.c:3381 +msgid "Folded" +msgstr "" + +#: src/hdy-leaflet.c:956 src/hdy-stackable-box.c:3382 +msgid "Whether the widget is folded" +msgstr "" + +#: src/hdy-leaflet.c:967 src/hdy-stackable-box.c:3393 +msgid "Horizontally homogeneous folded" +msgstr "" + +#: src/hdy-leaflet.c:968 +msgid "Horizontally homogeneous sizing when the leaflet is folded" +msgstr "" + +#: src/hdy-leaflet.c:979 src/hdy-stackable-box.c:3405 +msgid "Vertically homogeneous folded" +msgstr "" + +#: src/hdy-leaflet.c:980 +msgid "Vertically homogeneous sizing when the leaflet is folded" +msgstr "" + +#: src/hdy-leaflet.c:991 src/hdy-stackable-box.c:3417 +msgid "Box horizontally homogeneous" +msgstr "" + +#: src/hdy-leaflet.c:992 +msgid "Horizontally homogeneous sizing when the leaflet is unfolded" +msgstr "" + +#: src/hdy-leaflet.c:1003 src/hdy-stackable-box.c:3429 +msgid "Box vertically homogeneous" +msgstr "" + +#: src/hdy-leaflet.c:1004 +msgid "Vertically homogeneous sizing when the leaflet is unfolded" +msgstr "" + +#: src/hdy-leaflet.c:1011 +msgid "The widget currently visible when the leaflet is folded" +msgstr "" + +#: src/hdy-leaflet.c:1018 src/hdy-stackable-box.c:3444 +msgid "The name of the widget currently visible when the children are stacked" +msgstr "" + +#: src/hdy-leaflet.c:1037 src/hdy-stackable-box.c:3463 +msgid "The type of animation used to transition between modes and children" +msgstr "" + +#: src/hdy-leaflet.c:1043 src/hdy-stackable-box.c:3469 +msgid "Mode transition duration" +msgstr "" + +#: src/hdy-leaflet.c:1044 src/hdy-stackable-box.c:3470 +msgid "The mode transition animation duration, in milliseconds" +msgstr "" + +#: src/hdy-leaflet.c:1050 src/hdy-stackable-box.c:3476 +msgid "Child transition duration" +msgstr "" + +#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3477 +msgid "The child transition animation duration, in milliseconds" +msgstr "" + +#: src/hdy-leaflet.c:1057 src/hdy-stackable-box.c:3483 +msgid "Child transition running" +msgstr "" + +#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3484 +msgid "Whether or not the child transition is currently running" +msgstr "" + +#: src/hdy-leaflet.c:1120 +msgid "Allow visible" +msgstr "" + +#: src/hdy-leaflet.c:1121 +msgid "Whether the child can be visible in folded mode" +msgstr "" + +#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252 +msgid "Description" +msgstr "Descripción" + +#: src/hdy-preferences-row.c:116 +msgid "The title of the preference" +msgstr "El título de la preferencia" + +#: src/hdy-preferences-window.c:135 +msgid "Untitled page" +msgstr "Página sin título" + +#: src/hdy-preferences-window.c:438 +msgid "Search enabled" +msgstr "Búsqueda activada" + +#: src/hdy-preferences-window.c:439 +msgid "Whether search is enabled" +msgstr "Indica si la búsqueda está activada" + +#: src/hdy-preferences-window.ui:9 +msgid "Preferences" +msgstr "Preferencias" + +#: src/hdy-preferences-window.ui:72 +msgid "Search" +msgstr "Buscar" + +#: src/hdy-preferences-window.ui:197 +msgid "No Results Found" +msgstr "No se han encontrado resultados" + +#: src/hdy-preferences-window.ui:212 +msgid "Try a different search" +msgstr "Pruebe a hacer una búsqueda diferente" + +#: src/hdy-search-bar.c:451 +msgid "Search Mode Enabled" +msgstr "Modo de búsqueda activado" + +#: src/hdy-search-bar.c:452 +msgid "Whether the search mode is on and the search bar shown" +msgstr "" + +#: src/hdy-search-bar.c:463 +msgid "Show Close Button" +msgstr "Mostrar botón de cerrar" + +#: src/hdy-search-bar.c:464 +msgid "Whether to show the close button in the toolbar" +msgstr "" +"Indica si se debe mostrar el botón de cerrar en la barra de herramientas" + +#: src/hdy-shadow-helper.c:246 +msgid "Widget" +msgstr "Widget" + +#: src/hdy-shadow-helper.c:247 +msgid "The widget the shadow will be drawn for" +msgstr "" + +#: src/hdy-squeezer.c:1093 +msgid "Homogeneous" +msgstr "Homogéneo" + +#: src/hdy-squeezer.c:1094 +msgid "Homogeneous sizing" +msgstr "Tamaño homogéneo" + +#: src/hdy-squeezer.c:1101 +msgid "The widget currently visible in the squeezer" +msgstr "" + +#: src/hdy-squeezer.c:1115 +msgid "The type of animation used to transition" +msgstr "El tipo de animación usado para la transición" + +#: src/hdy-squeezer.c:1138 src/hdy-swipe-tracker.c:615 +msgid "Enabled" +msgstr "Activado" + +#: src/hdy-squeezer.c:1139 +msgid "" +"Whether the child can be picked or should be ignored when looking for the " +"child fitting the available size best" +msgstr "" + +#: src/hdy-stackable-box.c:3394 +msgid "Horizontally homogeneous sizing when the widget is folded" +msgstr "" + +#: src/hdy-stackable-box.c:3406 +msgid "Vertically homogeneous sizing when the widget is folded" +msgstr "" + +#: src/hdy-stackable-box.c:3418 +msgid "Horizontally homogeneous sizing when the widget is unfolded" +msgstr "" + +#: src/hdy-stackable-box.c:3430 +msgid "Vertically homogeneous sizing when the widget is unfolded" +msgstr "" + +#: src/hdy-stackable-box.c:3437 +msgid "The widget currently visible when the widget is folded" +msgstr "" + +#: src/hdy-stackable-box.c:3527 src/hdy-stackable-box.c:3528 +msgid "Orientation" +msgstr "Orientación" + +#: src/hdy-swipe-tracker.c:600 +msgid "Swipeable" +msgstr "" + +#: src/hdy-swipe-tracker.c:601 +msgid "The swipeable the swipe tracker is attached to" +msgstr "" + +#: src/hdy-swipe-tracker.c:616 +msgid "Whether the swipe tracker processes events" +msgstr "" + +#: src/hdy-swipe-tracker.c:630 +msgid "Reversed" +msgstr "" + +#: src/hdy-swipe-tracker.c:631 +msgid "Whether swipe direction is reversed" +msgstr "" + +#: src/hdy-title-bar.c:308 +msgid "Selection mode" +msgstr "Modo de selección" + +#: src/hdy-title-bar.c:309 +msgid "Whether or not the title bar is in selection mode" +msgstr "" + +#: src/hdy-value-object.c:191 +msgctxt "HdyValueObjectClass" +msgid "Value" +msgstr "Valor" + +#: src/hdy-value-object.c:192 +msgctxt "HdyValueObjectClass" +msgid "The contained value" +msgstr "El valor contenido" + +#: src/hdy-view-switcher-bar.c:185 src/hdy-view-switcher.c:530 +#: src/hdy-view-switcher-title.c:238 +msgid "Policy" +msgstr "Política" + +#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:531 +#: src/hdy-view-switcher-title.c:239 +msgid "The policy to determine the mode to use" +msgstr "La política para determinar el modo que usar" + +#: src/hdy-view-switcher-bar.c:200 src/hdy-view-switcher-button.c:228 +#: src/hdy-view-switcher.c:545 src/hdy-view-switcher-title.c:253 +msgid "Icon Size" +msgstr "Tamaño del icono" + +#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:229 +#: src/hdy-view-switcher.c:546 src/hdy-view-switcher-title.c:254 +msgid "Symbolic size to use for named icon" +msgstr "Tamaño simbólico que usar para el icono con nombre" + +#: src/hdy-view-switcher-bar.c:214 src/hdy-view-switcher-bar.c:215 +#: src/hdy-view-switcher.c:580 src/hdy-view-switcher.c:581 +#: src/hdy-view-switcher-title.c:267 src/hdy-view-switcher-title.c:268 +msgid "Stack" +msgstr "Pila" + +#: src/hdy-view-switcher-bar.c:228 +msgid "Reveal" +msgstr "Mostrar" + +#: src/hdy-view-switcher-bar.c:229 +msgid "Whether the view switcher is revealed" +msgstr "" + +#: src/hdy-view-switcher-button.c:214 +msgid "Icon Name" +msgstr "Nombre del icono" + +#: src/hdy-view-switcher-button.c:215 +msgid "Icon name for image" +msgstr "Nombre de icono para la imagen" + +#: src/hdy-view-switcher-button.c:245 +msgid "Needs attention" +msgstr "Requiere atención" + +#: src/hdy-view-switcher-button.c:246 +msgid "Hint the view needs attention" +msgstr "" + +#: src/hdy-view-switcher.c:565 +msgid "Narrow ellipsize" +msgstr "Estrechar elipse" + +#: src/hdy-view-switcher.c:566 +msgid "" +"The preferred place to ellipsize the string, if the narrow mode label does " +"not have enough room to display the entire string" +msgstr "" + +#: src/hdy-view-switcher-title.c:309 +msgid "View switcher enabled" +msgstr "" + +#: src/hdy-view-switcher-title.c:310 +msgid "Whether the view switcher is enabled" +msgstr "" + +#: src/hdy-view-switcher-title.c:323 +msgid "Title visible" +msgstr "Título visible" + +#: src/hdy-view-switcher-title.c:324 +msgid "Whether the title label is visible" +msgstr "Indica si la etiqueta de título es visible" + +#: src/hdy-window-handle-controller.c:259 +msgid "Move" +msgstr "Mover" + +#: src/hdy-window-handle-controller.c:267 +msgid "Resize" +msgstr "Redimensionar" + +#: src/hdy-window-handle-controller.c:298 +msgid "Always on Top" +msgstr "Siempre_encima" diff --git a/subprojects/libhandy/po/meson.build b/subprojects/libhandy/po/meson.build new file mode 100644 index 0000000..d1f4e16 --- /dev/null +++ b/subprojects/libhandy/po/meson.build @@ -0,0 +1,2 @@ +i18n = import('i18n') +i18n.gettext('libhandy', preset : 'glib') diff --git a/subprojects/libhandy/po/pl.po b/subprojects/libhandy/po/pl.po new file mode 100644 index 0000000..99f76a2 --- /dev/null +++ b/subprojects/libhandy/po/pl.po @@ -0,0 +1,76 @@ +# Polish translation for libhandy. +# Copyright © 2020 the libhandy authors. +# This file is distributed under the same license as the libhandy package. +# Piotr Drąg <piotrdrag@gmail.com>, 2020. +# Aviary.pl <community-poland@mozilla.org>, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: libhandy\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n" +"POT-Creation-Date: 2020-06-20 07:06+0000\n" +"PO-Revision-Date: 2020-06-21 11:20+0200\n" +"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n" +"Language-Team: Polish <community-poland@mozilla.org>\n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: src/hdy-header-bar.c:484 +msgid "Application menu" +msgstr "Menu programu" + +#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275 +msgid "Minimize" +msgstr "Zminimalizuj" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241 +msgid "Restore" +msgstr "Przywróć" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284 +msgid "Maximize" +msgstr "Zmaksymalizuj" + +#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311 +msgid "Close" +msgstr "Zamknij" + +#: src/hdy-header-bar.c:562 +msgid "Back" +msgstr "Wstecz" + +#: src/hdy-preferences-window.c:135 +msgid "Untitled page" +msgstr "Strona bez tytułu" + +#: src/hdy-preferences-window.ui:9 +msgid "Preferences" +msgstr "Preferencje" + +#: src/hdy-preferences-window.ui:72 +msgid "Search" +msgstr "Wyszukiwanie" + +#: src/hdy-preferences-window.ui:195 +msgid "No Results Found" +msgstr "Brak wyników" + +#: src/hdy-preferences-window.ui:210 +msgid "Try a different search" +msgstr "Proszę spróbować innych słów" + +#: src/hdy-window-handle-controller.c:259 +msgid "Move" +msgstr "Przenieś" + +#: src/hdy-window-handle-controller.c:267 +msgid "Resize" +msgstr "Zmień rozmiar" + +#: src/hdy-window-handle-controller.c:298 +msgid "Always on Top" +msgstr "Zawsze na wierzchu" diff --git a/subprojects/libhandy/po/pt_BR.po b/subprojects/libhandy/po/pt_BR.po new file mode 100644 index 0000000..f9facba --- /dev/null +++ b/subprojects/libhandy/po/pt_BR.po @@ -0,0 +1,955 @@ +# Brazilian Portuguese translation for libhandy. +# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER +# This file is distributed under the same license as the libhandy package. +# Rafael Fontenelle <rafaelff@gnome.org>, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: libhandy master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n" +"POT-Creation-Date: 2020-07-29 16:22+0000\n" +"PO-Revision-Date: 2020-07-30 17:40-0300\n" +"Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n" +"Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"X-Generator: Gtranslator 3.36.0\n" + +#: glade/glade-hdy-carousel.c:21 +msgid "This property does not apply unless Show Indicators is set." +msgstr "" +"Esta propriedade não se aplica a menos que “Mostrar indicadores” estiver " +"definido." + +#: glade/glade-hdy-carousel.c:167 glade/glade-hdy-header-bar.c:118 +#: glade/glade-hdy-leaflet.c:184 +#, c-format +msgid "Insert placeholder to %s" +msgstr "Inserir espaço reservado para %s" + +#: glade/glade-hdy-carousel.c:196 glade/glade-hdy-header-bar.c:144 +#: glade/glade-hdy-leaflet.c:214 +#, c-format +msgid "Remove placeholder from %s" +msgstr "Remover espaço reservado de %s" + +#: glade/glade-hdy-header-bar.c:18 +msgid "This property does not apply when a custom title is set" +msgstr "" +"Esta propriedade não se aplica quando um título personalizado está definido" + +#: glade/glade-hdy-header-bar.c:289 +msgid "" +"The decoration layout does not apply to header bars which do no show window " +"controls" +msgstr "" +"O layout de decoração não se aplica à barras de cabeçalho que não mostram os " +"controles de janela" + +#: glade/glade-hdy-leaflet.c:19 +msgid "This property only applies when the leaflet is folded" +msgstr "Esta propriedade somente se aplica quando o folheto está dobrado" + +#: glade/glade-hdy-preferences-page.c:160 +#, c-format +msgid "Add group to %s" +msgstr "Adicionar grupo a %s" + +#: glade/glade-hdy-preferences-window.c:228 +#, c-format +msgid "Add page to %s" +msgstr "Adicionar página a %s" + +#: glade/glade-hdy-search-bar.c:101 +msgid "Search bar is already full" +msgstr "A barra de pesquisa já está cheia" + +#: glade/glade-hdy-utils.h:14 +#, c-format +msgid "Only objects of type %s can be added to objects of type %s." +msgstr "Somente objetos do tipo %s podem ser adicionados a objetos do tipo %s." + +#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:335 +#: src/hdy-expander-row.c:336 src/hdy-preferences-page.c:179 +#: src/hdy-preferences-page.c:180 +msgid "Icon name" +msgstr "Nome do ícone" + +#: src/hdy-action-row.c:375 +msgid "Activatable widget" +msgstr "Widget ativável" + +#: src/hdy-action-row.c:376 +msgid "The widget to be activated when the row is activated" +msgstr "O widget para ser ativado quando a linha está ativada" + +#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:306 +#: src/hdy-header-bar.c:2147 src/hdy-view-switcher-title.c:294 +msgid "Subtitle" +msgstr "Subtítulo" + +#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:292 +#: src/hdy-header-bar.c:2140 src/hdy-preferences-group.c:265 +#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193 +#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115 +#: src/hdy-view-switcher-title.c:280 +msgid "Title" +msgstr "Título" + +#: src/hdy-action-row.c:418 src/hdy-expander-row.c:321 +#: src/hdy-preferences-row.c:130 +msgid "Use underline" +msgstr "Usar sublinhado" + +#: src/hdy-action-row.c:419 src/hdy-expander-row.c:322 +#: src/hdy-preferences-row.c:131 +msgid "" +"If set, an underline in the text indicates the next character should be used " +"for the mnemonic accelerator key" +msgstr "" +"Se definir, um sublinhado no texto indica a próprio caractere deve ser usado " +"para uma tecla aceleradora mnemônica" + +#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096 +#: src/hdy-carousel.c:947 src/hdy-carousel.c:948 +msgid "Number of pages" +msgstr "Número de páginas" + +#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:964 src/hdy-header-bar.c:2133 +msgid "Position" +msgstr "Posição" + +#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:965 +msgid "Current scrolling position" +msgstr "Posição de rolagem atual" + +#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1046 +#: src/hdy-header-bar.c:2161 +msgid "Spacing" +msgstr "Espaçamento" + +#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1047 +msgid "Spacing between pages" +msgstr "Espaçamento entre páginas" + +#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1091 +msgid "Reveal duration" +msgstr "Revelar duração" + +#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1092 +msgid "Page reveal duration" +msgstr "Duração de revelação de página" + +#: src/hdy-carousel.c:981 +msgid "Interactive" +msgstr "Interativa" + +#: src/hdy-carousel.c:982 +msgid "Whether the widget can be swiped" +msgstr "Se é possível deslizar pelo widget" + +#: src/hdy-carousel.c:997 +msgid "Indicator style" +msgstr "Estilo do indicador" + +#: src/hdy-carousel.c:998 +msgid "Page indicator style" +msgstr "Estilo do indicador de página" + +#: src/hdy-carousel.c:1013 +msgid "Indicator spacing" +msgstr "Espaçamento do indicador" + +#: src/hdy-carousel.c:1014 +msgid "Spacing between content and indicators" +msgstr "Espaçamento entro conteúdo e indicadores" + +#: src/hdy-carousel.c:1032 +msgid "Center content" +msgstr "Centralizar conteúdo" + +#: src/hdy-carousel.c:1033 +msgid "Whether to center pages to compensate for indicators" +msgstr "Se deve-se centralizar páginas para compensar os indicadores" + +#: src/hdy-carousel.c:1062 +msgid "Animation duration" +msgstr "Duração da animação" + +#: src/hdy-carousel.c:1063 +msgid "Default animation duration" +msgstr "Duração padrão da animação" + +#: src/hdy-carousel.c:1077 src/hdy-swipe-tracker.c:802 +msgid "Allow mouse drag" +msgstr "Permitir arrastar com mouse" + +#: src/hdy-carousel.c:1078 src/hdy-swipe-tracker.c:803 +msgid "Whether to allow dragging with mouse pointer" +msgstr "Se deve-se permitir arrastar com ponteiro do mouse" + +#: src/hdy-clamp.c:417 +#| msgid "Maximize" +msgid "Maximum size" +msgstr "Tamanho máximo" + +#: src/hdy-clamp.c:418 +#| msgid "The maximum width allocated to the child" +msgid "The maximum size allocated to the child" +msgstr "O tamanho máximo alocado para o filho" + +#: src/hdy-clamp.c:442 +msgid "Tightening threshold" +msgstr "Limiar de aperto" + +#: src/hdy-clamp.c:443 +msgid "The size from which the clamp will tighten its grip on the child" +msgstr "" +"O tamanho a partir do qual a operação de clamping vai apertar o tamanho do " +"filho" + +#: src/hdy-combo-row.c:411 +msgid "Selected index" +msgstr "Índice selecionado" + +#: src/hdy-combo-row.c:412 +msgid "The index of the selected item" +msgstr "O índice do item selecionado" + +#: src/hdy-combo-row.c:430 +msgid "Use subtitle" +msgstr "Usar subtítulo" + +#: src/hdy-combo-row.c:431 +msgid "Set the current value as the subtitle" +msgstr "Define o valor atual como o subtítulo" + +#: src/hdy-deck.c:888 +msgid "Horizontally homogeneous" +msgstr "Horizontalmente homogêneo" + +#: src/hdy-deck.c:889 +msgid "Horizontally homogeneous sizing" +msgstr "Dimensionamento horizontalmente homogêneo" + +#: src/hdy-deck.c:902 +msgid "Vertically homogeneous" +msgstr "Verticalmente homogêneo" + +#: src/hdy-deck.c:903 +msgid "Vertically homogeneous sizing" +msgstr "Dimensionamento verticalmente homogêneo" + +#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1093 +#: src/hdy-stackable-box.c:2993 +msgid "Visible child" +msgstr "Filho visível" + +#: src/hdy-deck.c:917 +msgid "The widget currently visible" +msgstr "O widget atualmente visível" + +#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3000 +msgid "Name of visible child" +msgstr "Nome do filho visível" + +#: src/hdy-deck.c:931 +msgid "The name of the widget currently visible" +msgstr "O nome do widget atualmente visível" + +#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1107 +#: src/hdy-stackable-box.c:3019 +msgid "Transition type" +msgstr "Tipo de transição" + +#: src/hdy-deck.c:950 +msgid "The type of animation used to transition between children" +msgstr "O tipo de animação usada para a transição entre filhos" + +#: src/hdy-deck.c:963 src/hdy-header-bar.c:2243 src/hdy-squeezer.c:1100 +msgid "Transition duration" +msgstr "Duração de transição" + +#: src/hdy-deck.c:964 +msgid "The transition animation duration, in milliseconds" +msgstr "A duração da animação de transição, em milissegundos" + +#: src/hdy-deck.c:977 src/hdy-header-bar.c:2250 src/hdy-squeezer.c:1115 +msgid "Transition running" +msgstr "Execução de transição" + +#: src/hdy-deck.c:978 src/hdy-header-bar.c:2251 src/hdy-squeezer.c:1116 +msgid "Whether or not the transition is currently running" +msgstr "Se a transição está atualmente em execução" + +#: src/hdy-deck.c:992 src/hdy-header-bar.c:2257 src/hdy-leaflet.c:1072 +#: src/hdy-squeezer.c:1122 src/hdy-stackable-box.c:3047 +msgid "Interpolate size" +msgstr "Tamanho da interpolação" + +#: src/hdy-deck.c:993 src/hdy-header-bar.c:2258 src/hdy-leaflet.c:1073 +#: src/hdy-squeezer.c:1123 src/hdy-stackable-box.c:3048 +msgid "" +"Whether or not the size should smoothly change when changing between " +"differently sized children" +msgstr "" +"Se o tamanho deve ou não ser suavemente alterado ao alterar entre filhos de " +"tamanhos diferentes" + +#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-preferences-window.c:497 +#: src/hdy-stackable-box.c:3062 +msgid "Can swipe back" +msgstr "Pode deslizar para trás" + +#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3063 +msgid "" +"Whether or not swipe gesture can be used to switch to the previous child" +msgstr "" +"Se o gesto de deslize pode ou não ser usado para alternar para o filho " +"anterior" + +#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3077 +msgid "Can swipe forward" +msgstr "Pode deslizar para frente" + +#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3078 +msgid "Whether or not swipe gesture can be used to switch to the next child" +msgstr "" +"Se o gesto de deslize pode ou não ser usado para alternar para o próximo " +"filho" + +#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111 +msgid "Name" +msgstr "Nome" + +#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112 +msgid "The name of the child page" +msgstr "O nome da página filha" + +#: src/hdy-expander-row.c:293 +msgid "The title for this row" +msgstr "O título para esta linha" + +#: src/hdy-expander-row.c:307 +msgid "The subtitle for this row" +msgstr "O subtítulo para esta linha" + +#: src/hdy-expander-row.c:347 +msgid "Expanded" +msgstr "Expandido" + +#: src/hdy-expander-row.c:348 +msgid "Whether the row is expanded" +msgstr "Se a linha é expandida" + +#: src/hdy-expander-row.c:359 +msgid "Enable expansion" +msgstr "Habilitar expansão" + +#: src/hdy-expander-row.c:360 +msgid "Whether the expansion is enabled" +msgstr "Se a expansão está habilitada" + +#: src/hdy-expander-row.c:371 +msgid "Show enable switch" +msgstr "Mostrar alternador de habilitação" + +#: src/hdy-expander-row.c:372 +msgid "Whether the switch enabling the expansion is visible" +msgstr "Se o alternador que habilita a expansão é visível" + +#: src/hdy-header-bar.c:484 +msgid "Application menu" +msgstr "Menu de aplicativo" + +#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275 +msgid "Minimize" +msgstr "Minimizar" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241 +msgid "Restore" +msgstr "Restaurar" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284 +msgid "Maximize" +msgstr "Maximizar" + +#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311 +msgid "Close" +msgstr "Fechar" + +#: src/hdy-header-bar.c:562 +msgid "Back" +msgstr "Retornar" + +#: src/hdy-header-bar.c:2126 +msgid "Pack type" +msgstr "Tipo de embalagem" + +#: src/hdy-header-bar.c:2127 +msgid "" +"A GtkPackType indicating whether the child is packed with reference to the " +"start or end of the parent" +msgstr "" +"Um GtkPackType indicando se o filho está empacotado com referência ao início " +"ou final do pai" + +#: src/hdy-header-bar.c:2134 +msgid "The index of the child in the parent" +msgstr "O índice do filho dentro do pai" + +#: src/hdy-header-bar.c:2141 src/hdy-view-switcher-title.c:281 +msgid "The title to display" +msgstr "O título para a ser exibido" + +#: src/hdy-header-bar.c:2148 src/hdy-view-switcher-title.c:295 +msgid "The subtitle to display" +msgstr "Subtítulo a ser exibido" + +#: src/hdy-header-bar.c:2154 +msgid "Custom Title" +msgstr "Título personalizado" + +#: src/hdy-header-bar.c:2155 +msgid "Custom title widget to display" +msgstr "Componente de título personalizado a ser exibido" + +#: src/hdy-header-bar.c:2162 +msgid "The amount of space between children" +msgstr "A quantidade de espaço entre filhos" + +#: src/hdy-header-bar.c:2181 +msgid "Show decorations" +msgstr "Mostrar decorações" + +#: src/hdy-header-bar.c:2182 +msgid "Whether to show window decorations" +msgstr "Se deve mostrar decorações da janela" + +#: src/hdy-header-bar.c:2200 +msgid "Decoration Layout" +msgstr "Disposição de decoração" + +#: src/hdy-header-bar.c:2201 +msgid "The layout for window decorations" +msgstr "A disposição de decorações da janela" + +#: src/hdy-header-bar.c:2214 +msgid "Decoration Layout Set" +msgstr "Disposição de decoração definido" + +#: src/hdy-header-bar.c:2215 +msgid "Whether the decoration-layout property has been set" +msgstr "Se a propriedade “decoration-layout” foi definida" + +#: src/hdy-header-bar.c:2229 +msgid "Has Subtitle" +msgstr "Possui subtítulo" + +#: src/hdy-header-bar.c:2230 +msgid "Whether to reserve space for a subtitle" +msgstr "Se deve reservar espaço para um subtítulo" + +#: src/hdy-header-bar.c:2236 +msgid "Centering policy" +msgstr "Política de centralização" + +#: src/hdy-header-bar.c:2237 +msgid "The policy to horizontally align the center widget" +msgstr "A política para alinhar horizontalmente o widget central" + +#: src/hdy-header-bar.c:2244 src/hdy-squeezer.c:1101 +msgid "The animation duration, in milliseconds" +msgstr "A duração da animação, em milissegundos" + +#: src/hdy-header-group.c:827 +#| msgid "Decoration Layout" +msgid "Decorate all" +msgstr "Decorar todos" + +#: src/hdy-header-group.c:828 +msgid "" +"Whether the elements of the group should all receive the full decoration" +msgstr "Se os elementos do grupo devem todos receber a decoração completa" + +#: src/hdy-keypad-button.c:225 +msgid "Digit" +msgstr "Dígito" + +#: src/hdy-keypad-button.c:226 +msgid "The keypad digit of the button" +msgstr "O dígito de teclado numérico do botão" + +#: src/hdy-keypad-button.c:232 +msgid "Symbols" +msgstr "Símbolos" + +#: src/hdy-keypad-button.c:233 +msgid "The keypad symbols of the button. The first symbol is used as the digit" +msgstr "" +"Os símbolos de teclado numérico do botão. O primeiro símbolo está suado como " +"o dígito" + +#: src/hdy-keypad-button.c:239 +#| msgid "Show Symbols" +msgid "Show symbols" +msgstr "Mostrar símbolos" + +#: src/hdy-keypad-button.c:240 +msgid "Whether the second line of symbols should be shown or not" +msgstr "Se a segunda linha de símbolos deve ser mostrada ou não" + +#: src/hdy-keypad.c:247 +msgid "Row spacing" +msgstr "Espaçamento de linha" + +#: src/hdy-keypad.c:248 +msgid "The amount of space between two consecutive rows" +msgstr "A quantidade de espaço entre duas linhas consecutivas" + +#: src/hdy-keypad.c:261 +msgid "Column spacing" +msgstr "Espaçamento de coluna" + +#: src/hdy-keypad.c:262 +msgid "The amount of space between two consecutive columns" +msgstr "A quantidade de espaço entre duas colunas consecutivas" + +#: src/hdy-keypad.c:276 +#| msgid "Title visible" +msgid "Letters visible" +msgstr "Letras visíveis" + +#: src/hdy-keypad.c:277 +#| msgid "Whether the title label is visible" +msgid "Whether the letters below the digits should be visible" +msgstr "Se as letras embaixo dos dígitos devem estar visíveis" + +#: src/hdy-keypad.c:291 +#| msgid "Allow visible" +msgid "Symbols visible" +msgstr "Símbolos visíveis" + +#: src/hdy-keypad.c:292 +#| msgid "Whether the second line of symbols should be shown or not" +msgid "Whether the hash, plus, and asterisk symbols should be visible" +msgstr "Se os símbolos de cerquilha, mais e asterisco devem estar visíveis" + +#: src/hdy-keypad.c:306 +msgid "Entry" +msgstr "Entrada" + +#: src/hdy-keypad.c:307 +msgid "The entry widget connected to the keypad" +msgstr "O widget de entrada conectado ao teclado" + +#: src/hdy-keypad.c:320 +msgid "End action" +msgstr "Ação de fim" + +#: src/hdy-keypad.c:321 +#| msgid "The left action widget" +msgid "The end action widget" +msgstr "O widget de ação de fim" + +#: src/hdy-keypad.c:334 +#| msgid "Orientation" +msgid "Start action" +msgstr "Ação de início" + +#: src/hdy-keypad.c:335 +#| msgid "The right action widget" +msgid "The start action widget" +msgstr "O widget de ação de início" + +#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2938 +msgid "Folded" +msgstr "Dobrado" + +#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2939 +msgid "Whether the widget is folded" +msgstr "Se o widget está dobrado" + +#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2950 +msgid "Horizontally homogeneous folded" +msgstr "Dobrado horizontalmente homogêneo" + +#: src/hdy-leaflet.c:976 +msgid "Horizontally homogeneous sizing when the leaflet is folded" +msgstr "" +"Dimensionamento horizontalmente homogêneo quando o folheto está dobrado" + +#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2962 +msgid "Vertically homogeneous folded" +msgstr "Dobrado verticalmente homogêneo" + +#: src/hdy-leaflet.c:988 +msgid "Vertically homogeneous sizing when the leaflet is folded" +msgstr "Dimensionamento verticalmente homogêneo quando o folheto está dobrado" + +#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2974 +msgid "Box horizontally homogeneous" +msgstr "Caixa horizontalmente homogênea" + +#: src/hdy-leaflet.c:1000 +msgid "Horizontally homogeneous sizing when the leaflet is unfolded" +msgstr "" +"Dimensionamento horizontalmente homogêneo quando o folheto está desdobrado" + +#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2986 +msgid "Box vertically homogeneous" +msgstr "Caixa verticalmente homogênea" + +#: src/hdy-leaflet.c:1012 +msgid "Vertically homogeneous sizing when the leaflet is unfolded" +msgstr "" +"Dimensionamento verticalmente homogêneo quando o folheto está desdobrado" + +#: src/hdy-leaflet.c:1019 +msgid "The widget currently visible when the leaflet is folded" +msgstr "O widget atualmente visível quando o folheto está dobrado" + +#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3001 +msgid "The name of the widget currently visible when the children are stacked" +msgstr "O nome do widget atualmente visível quando os filhos estão empilhados" + +#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3020 +msgid "The type of animation used to transition between modes and children" +msgstr "O tipo da animação usada para transicionar entre modos e filhos" + +#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3026 +msgid "Mode transition duration" +msgstr "Duração da transição de modo" + +#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3027 +msgid "The mode transition animation duration, in milliseconds" +msgstr "A duração da animação da transição de modo, em milissegundos" + +#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3033 +msgid "Child transition duration" +msgstr "Duração da transição de filho" + +#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3034 +msgid "The child transition animation duration, in milliseconds" +msgstr "A duração da animação de transição de filho, em milissegundos" + +#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3040 +msgid "Child transition running" +msgstr "Execução da transição de filho" + +#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3041 +msgid "Whether or not the child transition is currently running" +msgstr "Se a transição de filho está ou não atualmente em execução" + +#: src/hdy-leaflet.c:1129 +msgid "Navigatable" +msgstr "Navegável" + +#: src/hdy-leaflet.c:1130 +#| msgid "Whether the child can be visible in folded mode" +msgid "Whether the child can be navigated to" +msgstr "Se o filho pode ser navegado" + +#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252 +msgid "Description" +msgstr "Descrição" + +#: src/hdy-preferences-row.c:116 +msgid "The title of the preference" +msgstr "O título da preferência" + +#: src/hdy-preferences-window.c:141 +msgid "Untitled page" +msgstr "Página sem título" + +#: src/hdy-preferences-window.c:483 +msgid "Search enabled" +msgstr "Pesquisa habilitada" + +#: src/hdy-preferences-window.c:484 +msgid "Whether search is enabled" +msgstr "Se a pesquisa está habilitada" + +#: src/hdy-preferences-window.c:498 +#| msgid "" +#| "Whether or not swipe gesture can be used to switch to the previous child" +msgid "" +"Whether or not swipe gesture can be used to switch from a subpage to the " +"preferences" +msgstr "" +"Se o gesto de deslize pode ou não ser usado para alternar de uma subpágina " +"para as preferências" + +#: src/hdy-preferences-window.ui:9 +msgid "Preferences" +msgstr "Preferências" + +#: src/hdy-preferences-window.ui:78 +msgid "Search" +msgstr "Pesquisa" + +#: src/hdy-preferences-window.ui:201 +msgid "No Results Found" +msgstr "Nenhum resultado encontrado" + +#: src/hdy-preferences-window.ui:216 +msgid "Try a different search" +msgstr "Tente uma pesquisa diferente" + +#: src/hdy-search-bar.c:451 +msgid "Search Mode Enabled" +msgstr "Modo de pesquisa habilitado" + +#: src/hdy-search-bar.c:452 +msgid "Whether the search mode is on and the search bar shown" +msgstr "Se o modo de pesquisa está habilitado e a barra de pesquisa mostrada" + +#: src/hdy-search-bar.c:463 +msgid "Show Close Button" +msgstr "Mostrar o botão de fechar" + +#: src/hdy-search-bar.c:464 +msgid "Whether to show the close button in the toolbar" +msgstr "Se deve ser mostrado o botão de fechar na barra de ferramentas" + +#: src/hdy-shadow-helper.c:254 +msgid "Widget" +msgstr "Widget" + +#: src/hdy-shadow-helper.c:255 +msgid "The widget the shadow will be drawn for" +msgstr "O widget para a qual a sombra será desenhada" + +#: src/hdy-squeezer.c:1086 +msgid "Homogeneous" +msgstr "Homogêneo" + +#: src/hdy-squeezer.c:1087 +msgid "Homogeneous sizing" +msgstr "Dimensionamento homogêneo" + +#: src/hdy-squeezer.c:1094 +msgid "The widget currently visible in the squeezer" +msgstr "O widget atualmente visível no squeezer" + +#: src/hdy-squeezer.c:1108 +msgid "The type of animation used to transition" +msgstr "O tipo de animação usada para a transição" + +#: src/hdy-squeezer.c:1143 +msgid "X align" +msgstr "Alinh. X" + +#: src/hdy-squeezer.c:1144 +msgid "The horizontal alignment, from 0 (start) to 1 (end)" +msgstr "O alinhamento horizontal, de 0 (início) até 1 (fim)" + +#: src/hdy-squeezer.c:1165 +msgid "Y align" +msgstr "Alinh. Y" + +#: src/hdy-squeezer.c:1166 +msgid "The vertical alignment, from 0 (top) to 1 (bottom)" +msgstr "O alinhamento vertical, de 0 (topo) até 1 (base)" + +#: src/hdy-squeezer.c:1175 src/hdy-swipe-tracker.c:772 +msgid "Enabled" +msgstr "Habilitado" + +#: src/hdy-squeezer.c:1176 +msgid "" +"Whether the child can be picked or should be ignored when looking for the " +"child fitting the available size best" +msgstr "" +"Se o filho pode ser escolhido ou deve ser ignorado ao procurar pelo filho " +"que caiba no melhor tamanho disponível" + +#: src/hdy-stackable-box.c:2951 +msgid "Horizontally homogeneous sizing when the widget is folded" +msgstr "Dimensionamento horizontalmente homogêneo quando o widget está dobrado" + +#: src/hdy-stackable-box.c:2963 +msgid "Vertically homogeneous sizing when the widget is folded" +msgstr "Dimensionamento verticalmente homogêneo quando o widget está dobrado" + +#: src/hdy-stackable-box.c:2975 +msgid "Horizontally homogeneous sizing when the widget is unfolded" +msgstr "" +"Dimensionamento horizontalmente homogêneo quando o widget está desdobrado" + +#: src/hdy-stackable-box.c:2987 +msgid "Vertically homogeneous sizing when the widget is unfolded" +msgstr "" +"Dimensionamento verticalmente homogêneo quando o widget está desdobrado" + +#: src/hdy-stackable-box.c:2994 +msgid "The widget currently visible when the widget is folded" +msgstr "O widget atualmente visível quando o widget está desdobrado" + +#: src/hdy-stackable-box.c:3084 src/hdy-stackable-box.c:3085 +msgid "Orientation" +msgstr "Orientação" + +#: src/hdy-swipe-tracker.c:757 +msgid "Swipeable" +msgstr "Deslizável" + +#: src/hdy-swipe-tracker.c:758 +msgid "The swipeable the swipe tracker is attached to" +msgstr "O deslizável ao qual o rastreador de deslize está anexado" + +#: src/hdy-swipe-tracker.c:773 +msgid "Whether the swipe tracker processes events" +msgstr "Se o rastreador de deslize processa eventos" + +#: src/hdy-swipe-tracker.c:787 +msgid "Reversed" +msgstr "Invertida" + +#: src/hdy-swipe-tracker.c:788 +msgid "Whether swipe direction is reversed" +msgstr "Se a direção de deslize é revertida" + +#: src/hdy-title-bar.c:308 +msgid "Selection mode" +msgstr "Modo de seleção" + +#: src/hdy-title-bar.c:309 +msgid "Whether or not the title bar is in selection mode" +msgstr "Se a barra de título estão ou não no modo de seleção" + +#: src/hdy-value-object.c:191 +msgctxt "HdyValueObjectClass" +msgid "Value" +msgstr "Valor" + +#: src/hdy-value-object.c:192 +msgctxt "HdyValueObjectClass" +msgid "The contained value" +msgstr "O valor contido" + +#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:511 +#: src/hdy-view-switcher-title.c:237 +msgid "Policy" +msgstr "Política" + +#: src/hdy-view-switcher-bar.c:187 src/hdy-view-switcher.c:512 +#: src/hdy-view-switcher-title.c:238 +msgid "The policy to determine the mode to use" +msgstr "A política para determinar o modo para usar" + +#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:217 +#: src/hdy-view-switcher.c:526 src/hdy-view-switcher-title.c:252 +msgid "Icon Size" +msgstr "Tamanho do ícone" + +#: src/hdy-view-switcher-bar.c:202 src/hdy-view-switcher-button.c:218 +#: src/hdy-view-switcher.c:527 src/hdy-view-switcher-title.c:253 +msgid "Symbolic size to use for named icon" +msgstr "Tamanho simbólico a ser utilizado para ícone nomeado" + +#: src/hdy-view-switcher-bar.c:215 src/hdy-view-switcher-bar.c:216 +#: src/hdy-view-switcher.c:561 src/hdy-view-switcher.c:562 +#: src/hdy-view-switcher-title.c:266 src/hdy-view-switcher-title.c:267 +msgid "Stack" +msgstr "Pilha" + +#: src/hdy-view-switcher-bar.c:229 +msgid "Reveal" +msgstr "Revelar" + +#: src/hdy-view-switcher-bar.c:230 +msgid "Whether the view switcher is revealed" +msgstr "Se o alternador de visão é revelado" + +#: src/hdy-view-switcher-button.c:203 +msgid "Icon Name" +msgstr "Nome do ícone" + +#: src/hdy-view-switcher-button.c:204 +msgid "Icon name for image" +msgstr "Nome do ícone para a imagem" + +#: src/hdy-view-switcher-button.c:234 +msgid "Needs attention" +msgstr "Precisa de atenção" + +#: src/hdy-view-switcher-button.c:235 +msgid "Hint the view needs attention" +msgstr "Sugere que a visão precisa de atenção" + +#: src/hdy-view-switcher.c:546 +msgid "Narrow ellipsize" +msgstr "Reticência estreita" + +#: src/hdy-view-switcher.c:547 +msgid "" +"The preferred place to ellipsize the string, if the narrow mode label does " +"not have enough room to display the entire string" +msgstr "" +"O lugar preferido para colocar reticências na string, se o rótulo do modo " +"estreito não tiver espaço suficiente para exibir a string inteira" + +#: src/hdy-view-switcher-title.c:308 +msgid "View switcher enabled" +msgstr "Alternador de visão habilitado" + +#: src/hdy-view-switcher-title.c:309 +msgid "Whether the view switcher is enabled" +msgstr "Se o alternador de visão está habilitado" + +#: src/hdy-view-switcher-title.c:322 +msgid "Title visible" +msgstr "Título visível" + +#: src/hdy-view-switcher-title.c:323 +msgid "Whether the title label is visible" +msgstr "Se o rótulo do título está visível" + +#: src/hdy-window-handle-controller.c:259 +msgid "Move" +msgstr "Mover" + +#: src/hdy-window-handle-controller.c:267 +msgid "Resize" +msgstr "Redimensionar" + +#: src/hdy-window-handle-controller.c:298 +msgid "Always on Top" +msgstr "Sempre no topo" + +#~ msgid "Maximum width" +#~ msgstr "Largura máxima" + +#~ msgid "Linear growth width" +#~ msgstr "Largura de crescimento linear" + +#~ msgid "The width up to which the child will be allocated all the width" +#~ msgstr "A largura até a qual o filho terá a alocação de toda a largura" + +#~ msgid "Focus" +#~ msgstr "Foco" + +#~ msgid "The header bar that should have the focus" +#~ msgstr "A barra de cabeçalho que deve ter o foco" + +#~ msgid "Only Digits" +#~ msgstr "Apenas dígitos" + +#~ msgid "" +#~ "Whether the keypad should show only digits or also extra buttons for #, *" +#~ msgstr "" +#~ "Se o teclado numérico deve mostrar apenas dígitos ou também botões extras " +#~ "para #, *" + +#~ msgid "Entry widget" +#~ msgstr "Widget de entrada" + +#~ msgid "Right action widget" +#~ msgstr "Widget de ação direito" + +#~ msgid "Left action widget" +#~ msgstr "Widget de ação esquerdo" diff --git a/subprojects/libhandy/po/ro.po b/subprojects/libhandy/po/ro.po new file mode 100644 index 0000000..a7d2664 --- /dev/null +++ b/subprojects/libhandy/po/ro.po @@ -0,0 +1,871 @@ +# Romanian translation for libhandy. +# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER +# This file is distributed under the same license as the libhandy package. +# Florentina Mușat <florentina.musat.28@gmail.com>, 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: libhandy master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n" +"POT-Creation-Date: 2020-07-26 22:20+0000\n" +"PO-Revision-Date: 2020-07-27 15:48+0300\n" +"Language-Team: Romanian <gnomero-list@lists.sourceforge.net>\n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2);;\n" +"Last-Translator: Florentina Mușat <florentina.musat.28@gmail.com>\n" +"X-Generator: Poedit 2.4\n" + +#: glade/glade-hdy-carousel.c:21 +msgid "This property does not apply unless Show Indicators is set." +msgstr "" +"Această proprietate nu se aplică decât dacă Arată Indicatori este stabilită." + +#: glade/glade-hdy-carousel.c:167 glade/glade-hdy-header-bar.c:118 +#: glade/glade-hdy-leaflet.c:184 +#, c-format +msgid "Insert placeholder to %s" +msgstr "Inserează substituent pentru %s" + +#: glade/glade-hdy-carousel.c:196 glade/glade-hdy-header-bar.c:144 +#: glade/glade-hdy-leaflet.c:214 +#, c-format +msgid "Remove placeholder from %s" +msgstr "Elimină substituentul din %s" + +#: glade/glade-hdy-header-bar.c:18 +msgid "This property does not apply when a custom title is set" +msgstr "" +"Această proprietate nu se aplică când un titlu personalizat este stabilit" + +#: glade/glade-hdy-header-bar.c:289 +msgid "" +"The decoration layout does not apply to header bars which do no show window " +"controls" +msgstr "" +"Aspectul decorației nu se aplică barelor de antet care nu afișează controale " +"pentru fereastră" + +#: glade/glade-hdy-leaflet.c:19 +msgid "This property only applies when the leaflet is folded" +msgstr "Această proprietate se aplică doar când manifestul este pliat" + +#: glade/glade-hdy-preferences-page.c:160 +#, c-format +msgid "Add group to %s" +msgstr "Adaugă grupul la %s" + +#: glade/glade-hdy-preferences-window.c:228 +#, c-format +msgid "Add page to %s" +msgstr "Adaugă pagina la %s" + +#: glade/glade-hdy-search-bar.c:101 +msgid "Search bar is already full" +msgstr "Bara de căutare este plină deja" + +#: glade/glade-hdy-utils.h:14 +#, c-format +msgid "Only objects of type %s can be added to objects of type %s." +msgstr "Doar obiectele de tipul %s pot fi adăugate obiectelor de tip %s." + +#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:335 +#: src/hdy-expander-row.c:336 src/hdy-preferences-page.c:179 +#: src/hdy-preferences-page.c:180 +msgid "Icon name" +msgstr "Nume iconiță" + +#: src/hdy-action-row.c:375 +msgid "Activatable widget" +msgstr "Widget care se poate activa" + +#: src/hdy-action-row.c:376 +msgid "The widget to be activated when the row is activated" +msgstr "Widgetul de activat când rândul este activat" + +#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:306 +#: src/hdy-header-bar.c:2147 src/hdy-view-switcher-title.c:294 +msgid "Subtitle" +msgstr "Subitlu" + +#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:292 +#: src/hdy-header-bar.c:2140 src/hdy-preferences-group.c:265 +#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193 +#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115 +#: src/hdy-view-switcher-title.c:280 +msgid "Title" +msgstr "Titlu" + +#: src/hdy-action-row.c:418 src/hdy-expander-row.c:321 +#: src/hdy-preferences-row.c:130 +msgid "Use underline" +msgstr "Utilizează sublinieri" + +#: src/hdy-action-row.c:419 src/hdy-expander-row.c:322 +#: src/hdy-preferences-row.c:131 +msgid "" +"If set, an underline in the text indicates the next character should be used " +"for the mnemonic accelerator key" +msgstr "" +"Dacă este stabilit, o linie de subliniere în text indică faptul că următorul " +"caracter ar trebui să fie utilizat ca tasta de accelerare mnemonic" + +#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096 +#: src/hdy-carousel.c:949 src/hdy-carousel.c:950 +msgid "Number of pages" +msgstr "Număr de pagini" + +#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:966 src/hdy-header-bar.c:2133 +msgid "Position" +msgstr "Poziționează" + +#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:967 +msgid "Current scrolling position" +msgstr "Poziție de derulare curentă" + +#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1048 +#: src/hdy-header-bar.c:2161 +msgid "Spacing" +msgstr "Spațiere" + +#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1049 +msgid "Spacing between pages" +msgstr "Spațiere între pagini" + +#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1093 +msgid "Reveal duration" +msgstr "Durată de dezvăluire" + +#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1094 +msgid "Page reveal duration" +msgstr "Durată de dezvăluire a paginii" + +#: src/hdy-carousel.c:983 +msgid "Interactive" +msgstr "Interactiv" + +#: src/hdy-carousel.c:984 +msgid "Whether the widget can be swiped" +msgstr "Dacă widgetul poate fi glisat" + +#: src/hdy-carousel.c:999 +msgid "Indicator style" +msgstr "Stil de indicator" + +#: src/hdy-carousel.c:1000 +msgid "Page indicator style" +msgstr "Stil de indicator de pagină" + +#: src/hdy-carousel.c:1015 +msgid "Indicator spacing" +msgstr "Spațiere de indicator" + +#: src/hdy-carousel.c:1016 +msgid "Spacing between content and indicators" +msgstr "Spațiere între conținut și indicatori" + +#: src/hdy-carousel.c:1034 +msgid "Center content" +msgstr "Centrează conținutul" + +#: src/hdy-carousel.c:1035 +msgid "Whether to center pages to compensate for indicators" +msgstr "Dacă să se centreze paginile pentru a compensa pentru indicatori" + +#: src/hdy-carousel.c:1064 +msgid "Animation duration" +msgstr "Durata animației" + +#: src/hdy-carousel.c:1065 +msgid "Default animation duration" +msgstr "Durata de animație implicită" + +#: src/hdy-carousel.c:1079 src/hdy-swipe-tracker.c:802 +msgid "Allow mouse drag" +msgstr "Permite tragerea mausului" + +#: src/hdy-carousel.c:1080 src/hdy-swipe-tracker.c:803 +msgid "Whether to allow dragging with mouse pointer" +msgstr "Dacă să se permită tragerea cu indicatorul mausului" + +#: src/hdy-clamp.c:417 +msgid "Maximum size" +msgstr "Dimensiunea maximă" + +#: src/hdy-clamp.c:418 +msgid "The maximum size allocated to the child" +msgstr "Dimensiunea maximă alocată la copil" + +#: src/hdy-clamp.c:442 +msgid "Tightening threshold" +msgstr "Pragul de strângere" + +#: src/hdy-clamp.c:443 +msgid "The size from which the clamp will tighten its grip on the child" +msgstr "Dimensiunea de la care clema va strânge strânsoarea asupra copilului" + +#: src/hdy-combo-row.c:411 +msgid "Selected index" +msgstr "Indexul selectat" + +#: src/hdy-combo-row.c:412 +msgid "The index of the selected item" +msgstr "Indexul elementului selectat" + +#: src/hdy-combo-row.c:430 +msgid "Use subtitle" +msgstr "Utilizează subtitrare" + +#: src/hdy-combo-row.c:431 +msgid "Set the current value as the subtitle" +msgstr "Stabilește valoarea curentă ca subtitrare" + +#: src/hdy-deck.c:888 +msgid "Horizontally homogeneous" +msgstr "Omogenă orizontal" + +#: src/hdy-deck.c:889 +msgid "Horizontally homogeneous sizing" +msgstr "Dimensionare omogenă orizontală" + +#: src/hdy-deck.c:902 +msgid "Vertically homogeneous" +msgstr "Omogenă vertical" + +#: src/hdy-deck.c:903 +msgid "Vertically homogeneous sizing" +msgstr "Dimensionare verticală omogenă" + +#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1073 +#: src/hdy-stackable-box.c:3001 +msgid "Visible child" +msgstr "Copil vizibil" + +#: src/hdy-deck.c:917 +msgid "The widget currently visible" +msgstr "Widget-ul vizibil curent" + +#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3008 +msgid "Name of visible child" +msgstr "Numele copilului vizibil" + +#: src/hdy-deck.c:931 +msgid "The name of the widget currently visible" +msgstr "Numele widget-ului vizibil curent" + +#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1087 +#: src/hdy-stackable-box.c:3027 +msgid "Transition type" +msgstr "Tip de tranziție" + +#: src/hdy-deck.c:950 +msgid "The type of animation used to transition between children" +msgstr "Tipul de animație utilizat pentru tranziția dintre copii" + +#: src/hdy-deck.c:963 src/hdy-header-bar.c:2243 src/hdy-squeezer.c:1080 +msgid "Transition duration" +msgstr "Durata tranziției" + +#: src/hdy-deck.c:964 +msgid "The transition animation duration, in milliseconds" +msgstr "Durata animației de tranziție, în milisecunde" + +#: src/hdy-deck.c:977 src/hdy-header-bar.c:2250 src/hdy-squeezer.c:1095 +msgid "Transition running" +msgstr "Tranziția rulează" + +#: src/hdy-deck.c:978 src/hdy-header-bar.c:2251 src/hdy-squeezer.c:1096 +msgid "Whether or not the transition is currently running" +msgstr "Dacă rulează sau nu tranziția în mod curent" + +#: src/hdy-deck.c:992 src/hdy-header-bar.c:2257 src/hdy-leaflet.c:1072 +#: src/hdy-squeezer.c:1102 src/hdy-stackable-box.c:3055 +msgid "Interpolate size" +msgstr "Dimensiune interpolare" + +#: src/hdy-deck.c:993 src/hdy-header-bar.c:2258 src/hdy-leaflet.c:1073 +#: src/hdy-squeezer.c:1103 src/hdy-stackable-box.c:3056 +msgid "" +"Whether or not the size should smoothly change when changing between " +"differently sized children" +msgstr "" +"Dacă ar trebui sau nu să se schimbe neted dimensiunea când se comută între " +"copiii dimensionați diferit" + +#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-stackable-box.c:3070 +msgid "Can swipe back" +msgstr "Poate să gliseze înapoi" + +#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3071 +msgid "" +"Whether or not swipe gesture can be used to switch to the previous child" +msgstr "" +"Dacă se poate sau nu să se utilizeze un gest de glisare pentru a comuta la " +"copilul anterior" + +#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3085 +msgid "Can swipe forward" +msgstr "Poate să gliseze înainte" + +#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3086 +msgid "Whether or not swipe gesture can be used to switch to the next child" +msgstr "" +"Dacă se poate sau nu să se utilizeze un gest de glisare pentru a comuta la " +"copilul următor" + +#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111 src/hdy-stackable-box.c:3102 +msgid "Name" +msgstr "Nume" + +#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112 src/hdy-stackable-box.c:3103 +msgid "The name of the child page" +msgstr "Numele paginii copilului" + +#: src/hdy-expander-row.c:293 +msgid "The title for this row" +msgstr "Titlul pentru acest rând" + +#: src/hdy-expander-row.c:307 +msgid "The subtitle for this row" +msgstr "Subtitrarea pentru acest rând" + +#: src/hdy-expander-row.c:347 +msgid "Expanded" +msgstr "Extins" + +#: src/hdy-expander-row.c:348 +msgid "Whether the row is expanded" +msgstr "Dacă rândul este extins" + +#: src/hdy-expander-row.c:359 +msgid "Enable expansion" +msgstr "Activează extinderea" + +#: src/hdy-expander-row.c:360 +msgid "Whether the expansion is enabled" +msgstr "Dacă extinderea este activată" + +#: src/hdy-expander-row.c:371 +msgid "Show enable switch" +msgstr "Arată activează comutarea" + +#: src/hdy-expander-row.c:372 +msgid "Whether the switch enabling the expansion is visible" +msgstr "Dacă comutatorul care activează extinderea este vizibil" + +#: src/hdy-header-bar.c:484 +msgid "Application menu" +msgstr "Meniul aplicației" + +#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275 +msgid "Minimize" +msgstr "Minimizează" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241 +msgid "Restore" +msgstr "Restaurează" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284 +msgid "Maximize" +msgstr "Maximizează" + +#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311 +msgid "Close" +msgstr "Închide" + +#: src/hdy-header-bar.c:562 +msgid "Back" +msgstr "Înapoi" + +#: src/hdy-header-bar.c:2126 +msgid "Pack type" +msgstr "Tip de pachet" + +#: src/hdy-header-bar.c:2127 +msgid "" +"A GtkPackType indicating whether the child is packed with reference to the " +"start or end of the parent" +msgstr "" +"Un GtkPackType care indică dacă copilul este împachetat cu referința la " +"începutul sau sfârșitul părintelui" + +#: src/hdy-header-bar.c:2134 +msgid "The index of the child in the parent" +msgstr "Indexul copilului în părinte" + +#: src/hdy-header-bar.c:2141 src/hdy-view-switcher-title.c:281 +msgid "The title to display" +msgstr "Titlul de afișat" + +#: src/hdy-header-bar.c:2148 src/hdy-view-switcher-title.c:295 +msgid "The subtitle to display" +msgstr "Subtitrarea de afișat" + +#: src/hdy-header-bar.c:2154 +msgid "Custom Title" +msgstr "Titlu personalizat" + +#: src/hdy-header-bar.c:2155 +msgid "Custom title widget to display" +msgstr "Widget de titlu personalizat de afișat" + +#: src/hdy-header-bar.c:2162 +msgid "The amount of space between children" +msgstr "Cantitatea de spațiu între copii" + +#: src/hdy-header-bar.c:2181 +msgid "Show decorations" +msgstr "Arată decorațiile" + +#: src/hdy-header-bar.c:2182 +msgid "Whether to show window decorations" +msgstr "Dacă să se arate decorațiile ferestrei" + +#: src/hdy-header-bar.c:2200 +msgid "Decoration Layout" +msgstr "Aspect decorație" + +#: src/hdy-header-bar.c:2201 +msgid "The layout for window decorations" +msgstr "Aspectul decorațiilor ferestrei" + +#: src/hdy-header-bar.c:2214 +msgid "Decoration Layout Set" +msgstr "Aranjament de decorație stabilit" + +#: src/hdy-header-bar.c:2215 +msgid "Whether the decoration-layout property has been set" +msgstr "Dacă proprietatea de aranjament-decorație a fost stabilită" + +#: src/hdy-header-bar.c:2229 +msgid "Has Subtitle" +msgstr "Are subtitrare" + +#: src/hdy-header-bar.c:2230 +msgid "Whether to reserve space for a subtitle" +msgstr "Dacă să se rezerve spațiu pentru o subtitrare" + +#: src/hdy-header-bar.c:2236 +msgid "Centering policy" +msgstr "Politică de centrare" + +#: src/hdy-header-bar.c:2237 +msgid "The policy to horizontally align the center widget" +msgstr "Politic de aliniere orizontală a widgetului de centru" + +#: src/hdy-header-bar.c:2244 src/hdy-squeezer.c:1081 +msgid "The animation duration, in milliseconds" +msgstr "Durata animației, în milisecunde" + +#: src/hdy-header-group.c:827 +msgid "Decorate all" +msgstr "Decorează toate" + +#: src/hdy-header-group.c:828 +msgid "" +"Whether the elements of the group should all receive the full decoration" +msgstr "" +"Dacă elementele grupului ar trebui ca toate să primească decorația completă" + +#: src/hdy-keypad-button.c:225 +msgid "Digit" +msgstr "Cifră" + +#: src/hdy-keypad-button.c:226 +msgid "The keypad digit of the button" +msgstr "Cifra tastaturii butonului" + +#: src/hdy-keypad-button.c:232 +msgid "Symbols" +msgstr "Simboluri" + +#: src/hdy-keypad-button.c:233 +msgid "The keypad symbols of the button. The first symbol is used as the digit" +msgstr "Simbolurile tastaturii butonului. Primul simbol este utilizat ca cifra" + +#: src/hdy-keypad-button.c:239 src/hdy-keypad.c:278 +msgid "Show Symbols" +msgstr "Arată simbolurile" + +#: src/hdy-keypad-button.c:240 src/hdy-keypad.c:279 +msgid "Whether the second line of symbols should be shown or not" +msgstr "Dacă a doua linie de simboluri ar trebui să fie arătată sau nu" + +#: src/hdy-keypad.c:264 +msgid "Row spacing" +msgstr "Spațiere rânduri" + +#: src/hdy-keypad.c:265 +msgid "The amount of space between two consecutive rows" +msgstr "Cantitatea de spațiu între două rânduri consecutive" + +#: src/hdy-keypad.c:271 +msgid "Column spacing" +msgstr "Spațiere coloane" + +#: src/hdy-keypad.c:272 +msgid "The amount of space between two consecutive columns" +msgstr "Cantitatea de spațiu între două coloane consecutive" + +#: src/hdy-keypad.c:285 +msgid "Only Digits" +msgstr "Doar cifre" + +#: src/hdy-keypad.c:286 +msgid "" +"Whether the keypad should show only digits or also extra buttons for #, *" +msgstr "" +"Dacă tastatura ar trebui să arate doar cifre sau de asemenea butoane extra " +"pentru #, *" + +#: src/hdy-keypad.c:292 +msgid "Entry widget" +msgstr "Widget de intrare" + +#: src/hdy-keypad.c:293 +msgid "The entry widget connected to the keypad" +msgstr "Widget-ul de intrare conectat la tastatură" + +#: src/hdy-keypad.c:299 +msgid "Right action widget" +msgstr "Widget de acțiune dreapta" + +#: src/hdy-keypad.c:300 +msgid "The right action widget" +msgstr "Widget-ul de acțiune dreapta" + +#: src/hdy-keypad.c:306 +msgid "Left action widget" +msgstr "Widget de acțiune stânga" + +#: src/hdy-keypad.c:307 +msgid "The left action widget" +msgstr "Widget-ul de acțiune stânga" + +#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2946 +msgid "Folded" +msgstr "Pliat" + +#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2947 +msgid "Whether the widget is folded" +msgstr "Dacă widget-ul este pliat" + +#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2958 +msgid "Horizontally homogeneous folded" +msgstr "Pliat omogen orizontal" + +#: src/hdy-leaflet.c:976 +msgid "Horizontally homogeneous sizing when the leaflet is folded" +msgstr "Dimensionare omogenă orizontală când manifestul este pliat" + +#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2970 +msgid "Vertically homogeneous folded" +msgstr "Pliat omogen vertical" + +#: src/hdy-leaflet.c:988 +msgid "Vertically homogeneous sizing when the leaflet is folded" +msgstr "Dimensionare omogenă verticală când manifestul este pliat" + +#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2982 +msgid "Box horizontally homogeneous" +msgstr "Omogen orizontal cutie" + +#: src/hdy-leaflet.c:1000 +msgid "Horizontally homogeneous sizing when the leaflet is unfolded" +msgstr "Dimensionare omogenă orizontală când manifestul nu este pliat" + +#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2994 +msgid "Box vertically homogeneous" +msgstr "Omogen vertical cutie" + +#: src/hdy-leaflet.c:1012 +msgid "Vertically homogeneous sizing when the leaflet is unfolded" +msgstr "Dimensionare omogenă verticală când manifestul nu este pliat" + +#: src/hdy-leaflet.c:1019 +msgid "The widget currently visible when the leaflet is folded" +msgstr "Widget-ul vizibil curent când manifestul este pliat" + +#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3009 +msgid "The name of the widget currently visible when the children are stacked" +msgstr "Numele widget-ului vizibil curent când copiii sunt stivuiți" + +#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3028 +msgid "The type of animation used to transition between modes and children" +msgstr "" +"Tipul de animație utilizat pentru a face tranziție între moduri și copii" + +#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3034 +msgid "Mode transition duration" +msgstr "Durata tranziției modului" + +#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3035 +msgid "The mode transition animation duration, in milliseconds" +msgstr "Durata animației tranziției modului, în milisecunde" + +#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3041 +msgid "Child transition duration" +msgstr "Durata tranziției copilului" + +#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3042 +msgid "The child transition animation duration, in milliseconds" +msgstr "Durata animației tranziției copilului, în milisecunde" + +#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3048 +msgid "Child transition running" +msgstr "Rularea tranziției copilului" + +#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3049 +msgid "Whether or not the child transition is currently running" +msgstr "Dacă rulează sau nu în mod curent tranziția copilului" + +#: src/hdy-leaflet.c:1128 +msgid "Allow visible" +msgstr "Permite vizibil" + +#: src/hdy-leaflet.c:1129 +msgid "Whether the child can be visible in folded mode" +msgstr "Dacă copilul poate fi vizibil în modul pliat" + +#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252 +msgid "Description" +msgstr "Descriere" + +#: src/hdy-preferences-row.c:116 +msgid "The title of the preference" +msgstr "Titlul preferinței" + +#: src/hdy-preferences-window.c:135 +msgid "Untitled page" +msgstr "Pagină fără titlu" + +#: src/hdy-preferences-window.c:438 +msgid "Search enabled" +msgstr "Căutarea activată" + +#: src/hdy-preferences-window.c:439 +msgid "Whether search is enabled" +msgstr "Dacă căutarea este activată" + +#: src/hdy-preferences-window.ui:9 +msgid "Preferences" +msgstr "Preferințe" + +#: src/hdy-preferences-window.ui:72 +msgid "Search" +msgstr "Caută" + +#: src/hdy-preferences-window.ui:195 +msgid "No Results Found" +msgstr "Nu s-au găsit rezultate" + +#: src/hdy-preferences-window.ui:210 +msgid "Try a different search" +msgstr "Încercați o căutare diferită" + +#: src/hdy-search-bar.c:451 +msgid "Search Mode Enabled" +msgstr "Modul de căutare activat" + +#: src/hdy-search-bar.c:452 +msgid "Whether the search mode is on and the search bar shown" +msgstr "Dacă modul de căutare este activ și bara de căutare este arătată" + +#: src/hdy-search-bar.c:463 +msgid "Show Close Button" +msgstr "Arată butonul de închidere" + +#: src/hdy-search-bar.c:464 +msgid "Whether to show the close button in the toolbar" +msgstr "Dacă să se afișeze butonul de închidere în bara cu unelte" + +#: src/hdy-shadow-helper.c:254 +msgid "Widget" +msgstr "Widget" + +#: src/hdy-shadow-helper.c:255 +msgid "The widget the shadow will be drawn for" +msgstr "Widget-ul pentru care va fi desenată umbra" + +#: src/hdy-squeezer.c:1066 +msgid "Homogeneous" +msgstr "Omogen" + +#: src/hdy-squeezer.c:1067 +msgid "Homogeneous sizing" +msgstr "Dimensionare omogenă" + +#: src/hdy-squeezer.c:1074 +msgid "The widget currently visible in the squeezer" +msgstr "Widget-ul vizibil curent în storcător" + +#: src/hdy-squeezer.c:1088 +msgid "The type of animation used to transition" +msgstr "Tipul de animație folosit la tranziție" + +#: src/hdy-squeezer.c:1111 src/hdy-swipe-tracker.c:772 +msgid "Enabled" +msgstr "Activat" + +#: src/hdy-squeezer.c:1112 +msgid "" +"Whether the child can be picked or should be ignored when looking for the " +"child fitting the available size best" +msgstr "" +"Dacă copilul poate fi ales sau ar trebui să fie ignorat când se caută pentru " +"copilul care se potrivește cel mai bine cu dimensiunea disponibilă" + +#: src/hdy-stackable-box.c:2959 +msgid "Horizontally homogeneous sizing when the widget is folded" +msgstr "Dimensiune omogenă orizontală când widget-ul este pliat" + +#: src/hdy-stackable-box.c:2971 +msgid "Vertically homogeneous sizing when the widget is folded" +msgstr "Dimensiune omogenă verticală când widget-ul este pliat" + +#: src/hdy-stackable-box.c:2983 +msgid "Horizontally homogeneous sizing when the widget is unfolded" +msgstr "Dimensionare omogenă orizontală când widget-ul nu este pliat" + +#: src/hdy-stackable-box.c:2995 +msgid "Vertically homogeneous sizing when the widget is unfolded" +msgstr "Dimensiune omogenă verticală când widget-ul nu este pliat" + +#: src/hdy-stackable-box.c:3002 +msgid "The widget currently visible when the widget is folded" +msgstr "Widget-ul vizibil curent când widget-ul este pliat" + +#: src/hdy-stackable-box.c:3092 src/hdy-stackable-box.c:3093 +msgid "Orientation" +msgstr "Orientare" + +#: src/hdy-swipe-tracker.c:757 +msgid "Swipeable" +msgstr "Glisabil" + +#: src/hdy-swipe-tracker.c:758 +msgid "The swipeable the swipe tracker is attached to" +msgstr "Glisabilul la care este atașat urmăritorul de glisare" + +#: src/hdy-swipe-tracker.c:773 +msgid "Whether the swipe tracker processes events" +msgstr "Dacă urmăritorul de glisare procesează evenimente" + +#: src/hdy-swipe-tracker.c:787 +msgid "Reversed" +msgstr "Inversat" + +#: src/hdy-swipe-tracker.c:788 +msgid "Whether swipe direction is reversed" +msgstr "Dacă direcția de glisare este inversată" + +#: src/hdy-title-bar.c:308 +msgid "Selection mode" +msgstr "Mod de selecție" + +#: src/hdy-title-bar.c:309 +msgid "Whether or not the title bar is in selection mode" +msgstr "Dacă bara de titlu se află sau nu în modul de selecție" + +#: src/hdy-value-object.c:191 +msgctxt "HdyValueObjectClass" +msgid "Value" +msgstr "Valoare" + +#: src/hdy-value-object.c:192 +msgctxt "HdyValueObjectClass" +msgid "The contained value" +msgstr "Valoarea conținută" + +#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:511 +#: src/hdy-view-switcher-title.c:237 +msgid "Policy" +msgstr "Politică" + +#: src/hdy-view-switcher-bar.c:187 src/hdy-view-switcher.c:512 +#: src/hdy-view-switcher-title.c:238 +msgid "The policy to determine the mode to use" +msgstr "Politica de determinare a modului de utilizat" + +#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:217 +#: src/hdy-view-switcher.c:526 src/hdy-view-switcher-title.c:252 +msgid "Icon Size" +msgstr "Dimensiunea iconiței" + +#: src/hdy-view-switcher-bar.c:202 src/hdy-view-switcher-button.c:218 +#: src/hdy-view-switcher.c:527 src/hdy-view-switcher-title.c:253 +msgid "Symbolic size to use for named icon" +msgstr "Dimensiunea simbolică de utilizat pentru iconița numită" + +#: src/hdy-view-switcher-bar.c:215 src/hdy-view-switcher-bar.c:216 +#: src/hdy-view-switcher.c:561 src/hdy-view-switcher.c:562 +#: src/hdy-view-switcher-title.c:266 src/hdy-view-switcher-title.c:267 +msgid "Stack" +msgstr "Stivă" + +#: src/hdy-view-switcher-bar.c:229 +msgid "Reveal" +msgstr "Dezvăluie" + +#: src/hdy-view-switcher-bar.c:230 +msgid "Whether the view switcher is revealed" +msgstr "Dacă comutatorul de vizualizare este dezvăluit" + +#: src/hdy-view-switcher-button.c:203 +msgid "Icon Name" +msgstr "Nume iconiță" + +#: src/hdy-view-switcher-button.c:204 +msgid "Icon name for image" +msgstr "Numele iconiței pentru imagine" + +#: src/hdy-view-switcher-button.c:234 +msgid "Needs attention" +msgstr "Are nevoie de atenție" + +#: src/hdy-view-switcher-button.c:235 +msgid "Hint the view needs attention" +msgstr "Indiciu cum că vizualizarea are nevoie de atenție" + +#: src/hdy-view-switcher.c:546 +msgid "Narrow ellipsize" +msgstr "Transformare în elipsă îngustă" + +#: src/hdy-view-switcher.c:547 +msgid "" +"The preferred place to ellipsize the string, if the narrow mode label does " +"not have enough room to display the entire string" +msgstr "" +"Locul preferat pentru a transforma în elipsă șirul, dacă eticheta modului " +"îngust nu are destul spațiu pentru a afișa întregul șir" + +#: src/hdy-view-switcher-title.c:308 +msgid "View switcher enabled" +msgstr "Comutator de vizualizare activat" + +#: src/hdy-view-switcher-title.c:309 +msgid "Whether the view switcher is enabled" +msgstr "Dacă comutatorul de vizualizare este activat" + +#: src/hdy-view-switcher-title.c:322 +msgid "Title visible" +msgstr "Titlu vizibil" + +#: src/hdy-view-switcher-title.c:323 +msgid "Whether the title label is visible" +msgstr "Dacă eticheta titlului este vizibilă" + +#: src/hdy-window-handle-controller.c:259 +msgid "Move" +msgstr "Mută" + +#: src/hdy-window-handle-controller.c:267 +msgid "Resize" +msgstr "Redimensionează" + +#: src/hdy-window-handle-controller.c:298 +msgid "Always on Top" +msgstr "Întotdeauna deasupra" diff --git a/subprojects/libhandy/po/uk.po b/subprojects/libhandy/po/uk.po new file mode 100644 index 0000000..b92f214 --- /dev/null +++ b/subprojects/libhandy/po/uk.po @@ -0,0 +1,921 @@ +# Ukrainian translation for libhandy. +# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER +# This file is distributed under the same license as the libhandy package. +# +# Yuri Chornoivan <yurchor@ukr.net>, 2020. +msgid "" +msgstr "" +"Project-Id-Version: libhandy master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n" +"POT-Creation-Date: 2020-07-29 16:22+0000\n" +"PO-Revision-Date: 2020-07-30 13:02+0300\n" +"Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n" +"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n" +"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Lokalize 20.03.70\n" + +#: glade/glade-hdy-carousel.c:21 +msgid "This property does not apply unless Show Indicators is set." +msgstr "" +"Цю властивість не буде застосовано, доки не встановлено «Показувати " +"індикатори»." + +#: glade/glade-hdy-carousel.c:167 glade/glade-hdy-header-bar.c:118 +#: glade/glade-hdy-leaflet.c:184 +#, c-format +msgid "Insert placeholder to %s" +msgstr "Вставка заповнювача місця до %s" + +#: glade/glade-hdy-carousel.c:196 glade/glade-hdy-header-bar.c:144 +#: glade/glade-hdy-leaflet.c:214 +#, c-format +msgid "Remove placeholder from %s" +msgstr "Вилучення заповнювача з %s" + +#: glade/glade-hdy-header-bar.c:18 +msgid "This property does not apply when a custom title is set" +msgstr "Ця властивість не застосовується, якщо встановлено нетиповий заголовок" + +#: glade/glade-hdy-header-bar.c:289 +msgid "" +"The decoration layout does not apply to header bars which do no show window " +"controls" +msgstr "" +"Компонування декорацій не стосується смужок заголовків, на яких не буде " +"показано засоби керування вікном" + +#: glade/glade-hdy-leaflet.c:19 +msgid "This property only applies when the leaflet is folded" +msgstr "Ця властивість застосовується, лише якщо листівку згорнуто" + +#: glade/glade-hdy-preferences-page.c:160 +#, c-format +msgid "Add group to %s" +msgstr "Додати групу до %s" + +#: glade/glade-hdy-preferences-window.c:228 +#, c-format +msgid "Add page to %s" +msgstr "Додати сторінку до %s" + +#: glade/glade-hdy-search-bar.c:101 +msgid "Search bar is already full" +msgstr "Панель пошуку вже заповнено" + +#: glade/glade-hdy-utils.h:14 +#, c-format +msgid "Only objects of type %s can be added to objects of type %s." +msgstr "Лише об'єкти типу %s можна додавати до об'єктів типу %s." + +#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:335 +#: src/hdy-expander-row.c:336 src/hdy-preferences-page.c:179 +#: src/hdy-preferences-page.c:180 +msgid "Icon name" +msgstr "Назва піктограми" + +#: src/hdy-action-row.c:375 +msgid "Activatable widget" +msgstr "Активований віджет" + +#: src/hdy-action-row.c:376 +msgid "The widget to be activated when the row is activated" +msgstr "Віджет, який буде активовано, якщо активовано рядок" + +#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:306 +#: src/hdy-header-bar.c:2147 src/hdy-view-switcher-title.c:294 +msgid "Subtitle" +msgstr "Підзаголовок" + +#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:292 +#: src/hdy-header-bar.c:2140 src/hdy-preferences-group.c:265 +#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193 +#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115 +#: src/hdy-view-switcher-title.c:280 +msgid "Title" +msgstr "Заголовок" + +#: src/hdy-action-row.c:418 src/hdy-expander-row.c:321 +#: src/hdy-preferences-row.c:130 +msgid "Use underline" +msgstr "Використовувати підкреслення" + +#: src/hdy-action-row.c:419 src/hdy-expander-row.c:322 +#: src/hdy-preferences-row.c:131 +msgid "" +"If set, an underline in the text indicates the next character should be used " +"for the mnemonic accelerator key" +msgstr "" +"Якщо встановлено, то підкреслення в тексті означає, що наступний символ має " +"використовуватися в комбінації клавіш." + +#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096 +#: src/hdy-carousel.c:947 src/hdy-carousel.c:948 +msgid "Number of pages" +msgstr "Кількість сторінок" + +#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:964 src/hdy-header-bar.c:2133 +msgid "Position" +msgstr "Позиція" + +#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:965 +msgid "Current scrolling position" +msgstr "Поточна позиція гортання" + +#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1046 +#: src/hdy-header-bar.c:2161 +msgid "Spacing" +msgstr "Інтервал" + +#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1047 +msgid "Spacing between pages" +msgstr "Інтервал між сторінками" + +#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1091 +msgid "Reveal duration" +msgstr "Тривалість появи" + +#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1092 +msgid "Page reveal duration" +msgstr "Тривалість появи сторінки" + +#: src/hdy-carousel.c:981 +msgid "Interactive" +msgstr "Інтерактивний" + +#: src/hdy-carousel.c:982 +msgid "Whether the widget can be swiped" +msgstr "Визначає, чи можна змахнути віджет" + +#: src/hdy-carousel.c:997 +msgid "Indicator style" +msgstr "Стиль індикаторів" + +#: src/hdy-carousel.c:998 +msgid "Page indicator style" +msgstr "Стиль індикаторів сторінки" + +#: src/hdy-carousel.c:1013 +msgid "Indicator spacing" +msgstr "Інтервал індикатора" + +#: src/hdy-carousel.c:1014 +msgid "Spacing between content and indicators" +msgstr "Інтервал між вмістом та індикаторами" + +#: src/hdy-carousel.c:1032 +msgid "Center content" +msgstr "Центрувати вміст" + +#: src/hdy-carousel.c:1033 +msgid "Whether to center pages to compensate for indicators" +msgstr "Визначає, чи слід центрувати сторінки для компенсації індикаторів" + +#: src/hdy-carousel.c:1062 +msgid "Animation duration" +msgstr "Тривалість анімації" + +#: src/hdy-carousel.c:1063 +msgid "Default animation duration" +msgstr "Типова тривалість анімації" + +#: src/hdy-carousel.c:1077 src/hdy-swipe-tracker.c:802 +msgid "Allow mouse drag" +msgstr "Дозволити перетягування мишею" + +#: src/hdy-carousel.c:1078 src/hdy-swipe-tracker.c:803 +msgid "Whether to allow dragging with mouse pointer" +msgstr "Визначає, чи можна перетягувати за допомогою вказівника миші" + +#: src/hdy-clamp.c:417 +msgid "Maximum size" +msgstr "Максимальний розмір" + +#: src/hdy-clamp.c:418 +msgid "The maximum size allocated to the child" +msgstr "Максимальний розмір, який буде отримано для дочірнього об'єкта" + +#: src/hdy-clamp.c:442 +msgid "Tightening threshold" +msgstr "Поріг ущільнення" + +#: src/hdy-clamp.c:443 +msgid "The size from which the clamp will tighten its grip on the child" +msgstr "Розмір, починаючи з якого защіпка починає ущільнювати дочірній об'єкт" + +#: src/hdy-combo-row.c:411 +msgid "Selected index" +msgstr "Індекс позначеного" + +#: src/hdy-combo-row.c:412 +msgid "The index of the selected item" +msgstr "Індекс позначеного об'єкта" + +#: src/hdy-combo-row.c:430 +msgid "Use subtitle" +msgstr "Використовувати підзаголовок" + +#: src/hdy-combo-row.c:431 +msgid "Set the current value as the subtitle" +msgstr "Встановити поточне значення як підзаголовок" + +#: src/hdy-deck.c:888 +msgid "Horizontally homogeneous" +msgstr "Горизонтально однорідний" + +#: src/hdy-deck.c:889 +msgid "Horizontally homogeneous sizing" +msgstr "Однорідний за розміром горизонтально" + +#: src/hdy-deck.c:902 +msgid "Vertically homogeneous" +msgstr "Вертикально однорідний" + +#: src/hdy-deck.c:903 +msgid "Vertically homogeneous sizing" +msgstr "Однорідний за розміром вертикально" + +#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1093 +#: src/hdy-stackable-box.c:2993 +msgid "Visible child" +msgstr "Видимий дочірній елемент" + +#: src/hdy-deck.c:917 +msgid "The widget currently visible" +msgstr "Віджет є видимим" + +#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3000 +msgid "Name of visible child" +msgstr "Назва видимого дочірнього елемента" + +#: src/hdy-deck.c:931 +msgid "The name of the widget currently visible" +msgstr "Назва віджета є видимою" + +#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1107 +#: src/hdy-stackable-box.c:3019 +msgid "Transition type" +msgstr "Тип переходу" + +#: src/hdy-deck.c:950 +msgid "The type of animation used to transition between children" +msgstr "" +"Тип анімації, який буде використано для переходу між дочірніми об'єктами" + +#: src/hdy-deck.c:963 src/hdy-header-bar.c:2243 src/hdy-squeezer.c:1100 +msgid "Transition duration" +msgstr "Тривалість переходу" + +#: src/hdy-deck.c:964 +msgid "The transition animation duration, in milliseconds" +msgstr "Тривалість анімації переходу у мілісекундах" + +#: src/hdy-deck.c:977 src/hdy-header-bar.c:2250 src/hdy-squeezer.c:1115 +msgid "Transition running" +msgstr "Виконання переходу" + +#: src/hdy-deck.c:978 src/hdy-header-bar.c:2251 src/hdy-squeezer.c:1116 +msgid "Whether or not the transition is currently running" +msgstr "Чи виконується перехід цієї миті" + +#: src/hdy-deck.c:992 src/hdy-header-bar.c:2257 src/hdy-leaflet.c:1072 +#: src/hdy-squeezer.c:1122 src/hdy-stackable-box.c:3047 +msgid "Interpolate size" +msgstr "Інтерполювати розмір" + +#: src/hdy-deck.c:993 src/hdy-header-bar.c:2258 src/hdy-leaflet.c:1073 +#: src/hdy-squeezer.c:1123 src/hdy-stackable-box.c:3048 +msgid "" +"Whether or not the size should smoothly change when changing between " +"differently sized children" +msgstr "" +"Визначає, чи повинен розмір змінюватися плавно при перемиканні між дочірніми " +"елементами різних розмірів" + +#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-preferences-window.c:497 +#: src/hdy-stackable-box.c:3062 +msgid "Can swipe back" +msgstr "Можна змахнути назад" + +#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3063 +msgid "" +"Whether or not swipe gesture can be used to switch to the previous child" +msgstr "" +"Визначає, чи можна скористатися жестом змахування для перемикання до " +"попереднього дочірнього об'єкта" + +#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3077 +msgid "Can swipe forward" +msgstr "Можна змахувати вперед" + +#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3078 +msgid "Whether or not swipe gesture can be used to switch to the next child" +msgstr "" +"Визначає, чи можна скористатися жестом змахування для перемикання до " +"наступного дочірнього об'єкта" + +#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111 +msgid "Name" +msgstr "Назва" + +#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112 +msgid "The name of the child page" +msgstr "Назва дочірньої сторінки" + +#: src/hdy-expander-row.c:293 +msgid "The title for this row" +msgstr "Заголовок цього рядка" + +#: src/hdy-expander-row.c:307 +msgid "The subtitle for this row" +msgstr "Підзаголовок цього рядка" + +#: src/hdy-expander-row.c:347 +msgid "Expanded" +msgstr "Розгорнутий" + +#: src/hdy-expander-row.c:348 +msgid "Whether the row is expanded" +msgstr "Чи є рядок розгорнутим" + +#: src/hdy-expander-row.c:359 +msgid "Enable expansion" +msgstr "Увімкнути розгортання" + +#: src/hdy-expander-row.c:360 +msgid "Whether the expansion is enabled" +msgstr "Визначає, чи увімкнено розгортання" + +#: src/hdy-expander-row.c:371 +msgid "Show enable switch" +msgstr "Показувати перемикач вмикання" + +#: src/hdy-expander-row.c:372 +msgid "Whether the switch enabling the expansion is visible" +msgstr "Визначає, чи є видимим перемикач, який вмикає розгортання" + +#: src/hdy-header-bar.c:484 +msgid "Application menu" +msgstr "Меню програм" + +#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275 +msgid "Minimize" +msgstr "Мінімізувати" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241 +msgid "Restore" +msgstr "Відновити" + +#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284 +msgid "Maximize" +msgstr "Максимізувати" + +#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311 +msgid "Close" +msgstr "Закрити" + +#: src/hdy-header-bar.c:562 +msgid "Back" +msgstr "Назад" + +#: src/hdy-header-bar.c:2126 +msgid "Pack type" +msgstr "Тип упаковки" + +#: src/hdy-header-bar.c:2127 +msgid "" +"A GtkPackType indicating whether the child is packed with reference to the " +"start or end of the parent" +msgstr "" +"Об'єкт GtkPackType, що визначає відносно чого упаковується вкладений об'єкт " +"-- відносно початку, кінця, чи батьківського об'єкта" + +#: src/hdy-header-bar.c:2134 +msgid "The index of the child in the parent" +msgstr "Індекс вкладеного елемента у батьківському" + +#: src/hdy-header-bar.c:2141 src/hdy-view-switcher-title.c:281 +msgid "The title to display" +msgstr "Заголовок для показу" + +#: src/hdy-header-bar.c:2148 src/hdy-view-switcher-title.c:295 +msgid "The subtitle to display" +msgstr "Підзаголовок для показу" + +#: src/hdy-header-bar.c:2154 +msgid "Custom Title" +msgstr "Нетиповий заголовок" + +#: src/hdy-header-bar.c:2155 +msgid "Custom title widget to display" +msgstr "Віджет для показу нетипового заголовка" + +#: src/hdy-header-bar.c:2162 +msgid "The amount of space between children" +msgstr "Відстані між вкладеними елементами" + +#: src/hdy-header-bar.c:2181 +msgid "Show decorations" +msgstr "Показувати оформлення" + +#: src/hdy-header-bar.c:2182 +msgid "Whether to show window decorations" +msgstr "Чи показувати оформлення вікна" + +#: src/hdy-header-bar.c:2200 +msgid "Decoration Layout" +msgstr "Компонування оформлення" + +#: src/hdy-header-bar.c:2201 +msgid "The layout for window decorations" +msgstr "Компонування оформлення вікон" + +#: src/hdy-header-bar.c:2214 +msgid "Decoration Layout Set" +msgstr "Компонування оформлення встановлено" + +#: src/hdy-header-bar.c:2215 +msgid "Whether the decoration-layout property has been set" +msgstr "Чи було встановлено властивість decoration-layout" + +#: src/hdy-header-bar.c:2229 +msgid "Has Subtitle" +msgstr "Має підзаголовок" + +#: src/hdy-header-bar.c:2230 +msgid "Whether to reserve space for a subtitle" +msgstr "Чи резервувати місце для підзаголовка" + +#: src/hdy-header-bar.c:2236 +msgid "Centering policy" +msgstr "Правила центрування" + +#: src/hdy-header-bar.c:2237 +msgid "The policy to horizontally align the center widget" +msgstr "Правила для горизонтального вирівнювання центрального віджета" + +#: src/hdy-header-bar.c:2244 src/hdy-squeezer.c:1101 +msgid "The animation duration, in milliseconds" +msgstr "Тривалість анімації у мілісекундах" + +#: src/hdy-header-group.c:827 +msgid "Decorate all" +msgstr "Декорувати усе" + +#: src/hdy-header-group.c:828 +msgid "" +"Whether the elements of the group should all receive the full decoration" +msgstr "Чи мають елементи групи усі отримувати повну декорацію" + +#: src/hdy-keypad-button.c:225 +msgid "Digit" +msgstr "Цифра" + +#: src/hdy-keypad-button.c:226 +msgid "The keypad digit of the button" +msgstr "Цифра цифрової панелі для кнопки" + +#: src/hdy-keypad-button.c:232 +msgid "Symbols" +msgstr "Символи" + +#: src/hdy-keypad-button.c:233 +msgid "The keypad symbols of the button. The first symbol is used as the digit" +msgstr "" +"Символи цифрової панелі на кнопці. Перший символ буде використано як цифру" + +#: src/hdy-keypad-button.c:239 +msgid "Show symbols" +msgstr "Показувати символи" + +#: src/hdy-keypad-button.c:240 +msgid "Whether the second line of symbols should be shown or not" +msgstr "Визначає, чи має бути показано другий рядок символів" + +#: src/hdy-keypad.c:247 +msgid "Row spacing" +msgstr "Міжрядковий інтервал" + +#: src/hdy-keypad.c:248 +msgid "The amount of space between two consecutive rows" +msgstr "Інтервал між двома послідовними рядками" + +#: src/hdy-keypad.c:261 +msgid "Column spacing" +msgstr "Інтервал між стовпчиками" + +#: src/hdy-keypad.c:262 +msgid "The amount of space between two consecutive columns" +msgstr "Інтервал між двома послідовними стовпчиками" + +#: src/hdy-keypad.c:276 +msgid "Letters visible" +msgstr "Видимі літери" + +#: src/hdy-keypad.c:277 +msgid "Whether the letters below the digits should be visible" +msgstr "Визначає, чи будуть видимими літери під цифрами" + +#: src/hdy-keypad.c:291 +msgid "Symbols visible" +msgstr "Видимі символи" + +#: src/hdy-keypad.c:292 +msgid "Whether the hash, plus, and asterisk symbols should be visible" +msgstr "Визначає, чи має бути показано символи решітки, плюса та зірочки" + +#: src/hdy-keypad.c:306 +msgid "Entry" +msgstr "Введення" + +#: src/hdy-keypad.c:307 +msgid "The entry widget connected to the keypad" +msgstr "Віджет введення, з'єднаний із цифровою панеллю" + +#: src/hdy-keypad.c:320 +msgid "End action" +msgstr "Завершення дії" + +#: src/hdy-keypad.c:321 +msgid "The end action widget" +msgstr "Віджет завершення дії" + +#: src/hdy-keypad.c:334 +msgid "Start action" +msgstr "Початок дії" + +#: src/hdy-keypad.c:335 +msgid "The start action widget" +msgstr "Віджет початку дії" + +#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2938 +msgid "Folded" +msgstr "Згорнуто" + +#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2939 +msgid "Whether the widget is folded" +msgstr "Визначає, чи згорнуто віджет" + +#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2950 +msgid "Horizontally homogeneous folded" +msgstr "Горизонтально однорідний згорнутий" + +#: src/hdy-leaflet.c:976 +msgid "Horizontally homogeneous sizing when the leaflet is folded" +msgstr "Однорідний за розміром горизонтально, якщо листівку згорнуто" + +#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2962 +msgid "Vertically homogeneous folded" +msgstr "Вертикально однорідний згорнутий" + +#: src/hdy-leaflet.c:988 +msgid "Vertically homogeneous sizing when the leaflet is folded" +msgstr "Однорідний за розміром вертикально, якщо листівку згорнуто" + +#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2974 +msgid "Box horizontally homogeneous" +msgstr "Панель горизонтально однорідна" + +#: src/hdy-leaflet.c:1000 +msgid "Horizontally homogeneous sizing when the leaflet is unfolded" +msgstr "Однорідний за розміром горизонтально, якщо листівку розгорнуто" + +#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2986 +msgid "Box vertically homogeneous" +msgstr "Панель вертикально однорідна" + +#: src/hdy-leaflet.c:1012 +msgid "Vertically homogeneous sizing when the leaflet is unfolded" +msgstr "Однорідний за розміром вертикально, якщо листівку розгорнуто" + +#: src/hdy-leaflet.c:1019 +msgid "The widget currently visible when the leaflet is folded" +msgstr "Віджет є видимим, коли листівку згорнуто" + +#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3001 +msgid "The name of the widget currently visible when the children are stacked" +msgstr "Назва віджета є видимою, коли дочірні об'єкти розташовано у стосі" + +#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3020 +msgid "The type of animation used to transition between modes and children" +msgstr "" +"Тип анімації, який буде використано для переходу між режимами і дочірніми " +"об'єктами" + +#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3026 +msgid "Mode transition duration" +msgstr "Тривалість переходу між режимами" + +#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3027 +msgid "The mode transition animation duration, in milliseconds" +msgstr "Тривалість анімації переходу між режимами у мілісекундах" + +#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3033 +msgid "Child transition duration" +msgstr "Тривалість переходу між дочірніми об'єктами" + +#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3034 +msgid "The child transition animation duration, in milliseconds" +msgstr "Тривалість анімації переходу між дочірніми об'єктами у мілісекундах" + +#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3040 +msgid "Child transition running" +msgstr "Виконання переходу між дочірніми об'єктами" + +#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3041 +msgid "Whether or not the child transition is currently running" +msgstr "Чи виконується перехід між дочірніми об'єктами цієї миті" + +#: src/hdy-leaflet.c:1129 +msgid "Navigatable" +msgstr "Придатний до навігації" + +#: src/hdy-leaflet.c:1130 +msgid "Whether the child can be navigated to" +msgstr "Визначає, чи можна переходити до дочірнього об'єкта" + +#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252 +msgid "Description" +msgstr "Опис" + +#: src/hdy-preferences-row.c:116 +msgid "The title of the preference" +msgstr "Заголовок налаштувань" + +#: src/hdy-preferences-window.c:141 +msgid "Untitled page" +msgstr "Сторінка без назви" + +#: src/hdy-preferences-window.c:483 +msgid "Search enabled" +msgstr "Пошук увімкнено" + +#: src/hdy-preferences-window.c:484 +msgid "Whether search is enabled" +msgstr "Визначає, чи увімкнено пошук" + +#: src/hdy-preferences-window.c:498 +msgid "" +"Whether or not swipe gesture can be used to switch from a subpage to the " +"preferences" +msgstr "" +"Визначає, чи можна скористатися жестом змахування для перемикання з " +"допоміжної сторінки до сторінки параметрів" + +#: src/hdy-preferences-window.ui:9 +msgid "Preferences" +msgstr "Налаштування" + +#: src/hdy-preferences-window.ui:78 +msgid "Search" +msgstr "Пошук" + +#: src/hdy-preferences-window.ui:201 +msgid "No Results Found" +msgstr "Нічого не знайдено" + +#: src/hdy-preferences-window.ui:216 +msgid "Try a different search" +msgstr "Спробувати інші критерії пошуку" + +#: src/hdy-search-bar.c:451 +msgid "Search Mode Enabled" +msgstr "Режим пошуку увімкнено" + +#: src/hdy-search-bar.c:452 +msgid "Whether the search mode is on and the search bar shown" +msgstr "Чи увімкнено режим пошуку й чи відкрита панель пошуку" + +#: src/hdy-search-bar.c:463 +msgid "Show Close Button" +msgstr "Показувати кнопку закривання" + +#: src/hdy-search-bar.c:464 +msgid "Whether to show the close button in the toolbar" +msgstr "Чи показувати кнопку закривання на панелі інструментів" + +#: src/hdy-shadow-helper.c:254 +msgid "Widget" +msgstr "Віджет" + +#: src/hdy-shadow-helper.c:255 +msgid "The widget the shadow will be drawn for" +msgstr "Віджет, для якого буде намальовано тінь" + +#: src/hdy-squeezer.c:1086 +msgid "Homogeneous" +msgstr "Однорідний" + +#: src/hdy-squeezer.c:1087 +msgid "Homogeneous sizing" +msgstr "Однорідний розмір" + +#: src/hdy-squeezer.c:1094 +msgid "The widget currently visible in the squeezer" +msgstr "Віджет, який зараз показано у відтискачі" + +#: src/hdy-squeezer.c:1108 +msgid "The type of animation used to transition" +msgstr "Тип анімації, яку буде використано для переходу" + +#: src/hdy-squeezer.c:1143 +msgid "X align" +msgstr "Вирівнювання за X" + +#: src/hdy-squeezer.c:1144 +msgid "The horizontal alignment, from 0 (start) to 1 (end)" +msgstr "Вирівнювання за горизонталлю, від 0 (початок) до 1 (кінець)" + +#: src/hdy-squeezer.c:1165 +msgid "Y align" +msgstr "Вирівнювання за Y" + +#: src/hdy-squeezer.c:1166 +msgid "The vertical alignment, from 0 (top) to 1 (bottom)" +msgstr "Вирівнювання за вертикаллю, від 0 (верх) до 1 (низ)" + +#: src/hdy-squeezer.c:1175 src/hdy-swipe-tracker.c:772 +msgid "Enabled" +msgstr "Увімкнено" + +#: src/hdy-squeezer.c:1176 +msgid "" +"Whether the child can be picked or should be ignored when looking for the " +"child fitting the available size best" +msgstr "" +"Визначає, можна вибирати дочірній об'єкт чи його слід ігнорувати при пошуку " +"найкращої відповідності розмірів дочірнього об'єкта" + +#: src/hdy-stackable-box.c:2951 +msgid "Horizontally homogeneous sizing when the widget is folded" +msgstr "Однорідний за розміром горизонтально, якщо віджет згорнуто" + +#: src/hdy-stackable-box.c:2963 +msgid "Vertically homogeneous sizing when the widget is folded" +msgstr "Однорідний за розміром вертикально, якщо віджет згорнуто" + +#: src/hdy-stackable-box.c:2975 +msgid "Horizontally homogeneous sizing when the widget is unfolded" +msgstr "Однорідний за розміром горизонтально, якщо віджет розгорнуто" + +#: src/hdy-stackable-box.c:2987 +msgid "Vertically homogeneous sizing when the widget is unfolded" +msgstr "Однорідний за розміром вертикально, якщо віджет розгорнуто" + +#: src/hdy-stackable-box.c:2994 +msgid "The widget currently visible when the widget is folded" +msgstr "Віджет є видимим, коли віджет згорнуто" + +#: src/hdy-stackable-box.c:3084 src/hdy-stackable-box.c:3085 +msgid "Orientation" +msgstr "Орієнтація" + +#: src/hdy-swipe-tracker.c:757 +msgid "Swipeable" +msgstr "Змахуваний" + +#: src/hdy-swipe-tracker.c:758 +msgid "The swipeable the swipe tracker is attached to" +msgstr "Змахуваний об'єкт, до якого долучено стеження за змахуванням" + +#: src/hdy-swipe-tracker.c:773 +msgid "Whether the swipe tracker processes events" +msgstr "Визначає, чи стеження за змахуванням обробляє події" + +#: src/hdy-swipe-tracker.c:787 +msgid "Reversed" +msgstr "Обернене" + +#: src/hdy-swipe-tracker.c:788 +msgid "Whether swipe direction is reversed" +msgstr "Визначає, що напрям змахування є оберненим" + +#: src/hdy-title-bar.c:308 +msgid "Selection mode" +msgstr "Режим вибирання" + +#: src/hdy-title-bar.c:309 +msgid "Whether or not the title bar is in selection mode" +msgstr "Визначає, чи смужка заголовка перебуває у режимі вибору" + +#: src/hdy-value-object.c:191 +msgctxt "HdyValueObjectClass" +msgid "Value" +msgstr "Значення" + +#: src/hdy-value-object.c:192 +msgctxt "HdyValueObjectClass" +msgid "The contained value" +msgstr "Вміщене значення" + +#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:511 +#: src/hdy-view-switcher-title.c:237 +msgid "Policy" +msgstr "Поведінка" + +#: src/hdy-view-switcher-bar.c:187 src/hdy-view-switcher.c:512 +#: src/hdy-view-switcher-title.c:238 +msgid "The policy to determine the mode to use" +msgstr "Правила для визначення режиму, яким слід скористатися" + +#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:217 +#: src/hdy-view-switcher.c:526 src/hdy-view-switcher-title.c:252 +msgid "Icon Size" +msgstr "Розмір піктограм" + +#: src/hdy-view-switcher-bar.c:202 src/hdy-view-switcher-button.c:218 +#: src/hdy-view-switcher.c:527 src/hdy-view-switcher-title.c:253 +msgid "Symbolic size to use for named icon" +msgstr "Символьний розмір, використовуваний для іменованої піктограми" + +#: src/hdy-view-switcher-bar.c:215 src/hdy-view-switcher-bar.c:216 +#: src/hdy-view-switcher.c:561 src/hdy-view-switcher.c:562 +#: src/hdy-view-switcher-title.c:266 src/hdy-view-switcher-title.c:267 +msgid "Stack" +msgstr "Стос" + +#: src/hdy-view-switcher-bar.c:229 +msgid "Reveal" +msgstr "Показати" + +#: src/hdy-view-switcher-bar.c:230 +msgid "Whether the view switcher is revealed" +msgstr "Визначає, чи показано перемикач перегляду" + +#: src/hdy-view-switcher-button.c:203 +msgid "Icon Name" +msgstr "Назва значка" + +#: src/hdy-view-switcher-button.c:204 +msgid "Icon name for image" +msgstr "Назва піктограми для зображення" + +#: src/hdy-view-switcher-button.c:234 +msgid "Needs attention" +msgstr "Потребує уваги" + +#: src/hdy-view-switcher-button.c:235 +msgid "Hint the view needs attention" +msgstr "Підказка про те, що перегляд потребує уваги" + +#: src/hdy-view-switcher.c:546 +msgid "Narrow ellipsize" +msgstr "Багатокрапка при звуженні" + +#: src/hdy-view-switcher.c:547 +msgid "" +"The preferred place to ellipsize the string, if the narrow mode label does " +"not have enough room to display the entire string" +msgstr "" +"Бажане місце для обривання рядка багатокрапкою, якщо мітка у вузькому режимі " +"не має достатньо місця для показу цілого рядка" + +#: src/hdy-view-switcher-title.c:308 +msgid "View switcher enabled" +msgstr "Увімкнено перемикач перегляду" + +#: src/hdy-view-switcher-title.c:309 +msgid "Whether the view switcher is enabled" +msgstr "Визначає, чи увімкнено перемикач перегляду" + +#: src/hdy-view-switcher-title.c:322 +msgid "Title visible" +msgstr "Заголовок видимий" + +#: src/hdy-view-switcher-title.c:323 +msgid "Whether the title label is visible" +msgstr "Визначає, чи видимою є мітка заголовка" + +#: src/hdy-window-handle-controller.c:259 +msgid "Move" +msgstr "Пересунути" + +#: src/hdy-window-handle-controller.c:267 +msgid "Resize" +msgstr "Змінити розмір" + +#: src/hdy-window-handle-controller.c:298 +msgid "Always on Top" +msgstr "Завжди зверху" + +#~ msgid "Only Digits" +#~ msgstr "Лише цифри" + +#~ msgid "" +#~ "Whether the keypad should show only digits or also extra buttons for #, *" +#~ msgstr "" +#~ "Визначає, слід показувати на цифровій панелі лише цифри чи додаткові " +#~ "кнопки #, *" + +#~ msgid "Entry widget" +#~ msgstr "Віджет введення" + +#~ msgid "Right action widget" +#~ msgstr "Віджет дії праворуч" + +#~ msgid "Left action widget" +#~ msgstr "Віджет дії ліворуч" diff --git a/subprojects/libhandy/run.in b/subprojects/libhandy/run.in new file mode 100755 index 0000000..629897b --- /dev/null +++ b/subprojects/libhandy/run.in @@ -0,0 +1,12 @@ +#!/bin/sh + +ABS_BUILDDIR='@ABS_BUILDDIR@' +ABS_SRCDIR='@ABS_SRCDIR@' + +export GLADE_CATALOG_SEARCH_PATH="${ABS_SRCDIR}/glade/:${GLADE_CATALOG_SEARCH_PATH}" +export GLADE_MODULE_SEARCH_PATH="${ABS_BUILDDIR}/glade:${GLADE_MODULE_SEARCH_PATH}" +export GI_TYPELIB_PATH="${ABS_BUILDDIR}/src:$GI_TYPELIB_PATH" +export LD_LIBRARY_PATH="${ABS_BUILDDIR}/src:${ABS_BUILDDIR}/glade:$LD_LIBRARY_PATH" +export PKG_CONFIG_PATH="${ABS_BUILDDIR}/src:$PKG_CONFIG_PATH" + +exec "$@" 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'; diff --git a/subprojects/libhandy/tests/meson.build b/subprojects/libhandy/tests/meson.build new file mode 100644 index 0000000..c80e96e --- /dev/null +++ b/subprojects/libhandy/tests/meson.build @@ -0,0 +1,59 @@ +if get_option('tests') + +test_env = [ + 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()), + 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()), + 'G_DEBUG=gc-friendly,fatal-warnings', + 'GSETTINGS_BACKEND=memory', + 'PYTHONDONTWRITEBYTECODE=yes', + 'MALLOC_CHECK_=2', +] + +test_cflags = [ + '-DHDY_LOG_DOMAIN="Handy"', + '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir()), +] + +test_link_args = [ + '-fPIC', +] + +test_names = [ + 'test-action-row', + 'test-application-window', + 'test-avatar', + 'test-carousel', + 'test-carousel-indicator-dots', + 'test-carousel-indicator-lines', + 'test-combo-row', + 'test-deck', + 'test-expander-row', + 'test-header-bar', + 'test-header-group', + 'test-keypad', + 'test-leaflet', + 'test-preferences-group', + 'test-preferences-page', + 'test-preferences-row', + 'test-preferences-window', + 'test-search-bar', + 'test-squeezer', + 'test-swipe-group', + 'test-value-object', + 'test-view-switcher', + 'test-view-switcher-bar', + 'test-window', + 'test-window-handle', +] + +foreach test_name : test_names + t = executable(test_name, [test_name + '.c'] + libhandy_generated_headers, + c_args: test_cflags, + link_args: test_link_args, + dependencies: libhandy_deps + [libhandy_dep], + pie: true, + ) + test(test_name, t, env: test_env) +endforeach + +endif diff --git a/subprojects/libhandy/tests/test-action-row.c b/subprojects/libhandy/tests/test-action-row.c new file mode 100644 index 0000000..b6ae3c6 --- /dev/null +++ b/subprojects/libhandy/tests/test-action-row.c @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +gint activated; + +static void +activated_cb (GtkWidget *widget, gpointer data) +{ + activated++; +} + + +static void +test_hdy_action_row_add (void) +{ + g_autoptr (HdyActionRow) row = NULL; + GtkWidget *sw; + + row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ())); + g_assert_nonnull (row); + + sw = gtk_switch_new (); + g_assert_nonnull (sw); + + gtk_container_add (GTK_CONTAINER (row), sw); +} + + +static void +test_hdy_action_row_add_prefix (void) +{ + g_autoptr (HdyActionRow) row = NULL; + GtkWidget *radio; + + row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ())); + g_assert_nonnull (row); + + radio = gtk_radio_button_new (NULL); + g_assert_nonnull (radio); + + hdy_action_row_add_prefix (row, radio); +} + + +static void +test_hdy_action_row_subtitle (void) +{ + g_autoptr (HdyActionRow) row = NULL; + + row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ())); + g_assert_nonnull (row); + + g_assert_cmpstr (hdy_action_row_get_subtitle (row), ==, ""); + + hdy_action_row_set_subtitle (row, "Dummy subtitle"); + g_assert_cmpstr (hdy_action_row_get_subtitle (row), ==, "Dummy subtitle"); +} + + +static void +test_hdy_action_row_icon_name (void) +{ + g_autoptr (HdyActionRow) row = NULL; + + row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ())); + g_assert_nonnull (row); + + g_assert_null (hdy_action_row_get_icon_name (row)); + + hdy_action_row_set_icon_name (row, "dummy-icon-name"); + g_assert_cmpstr (hdy_action_row_get_icon_name (row), ==, "dummy-icon-name"); +} + + +static void +test_hdy_action_row_use_undeline (void) +{ + g_autoptr (HdyActionRow) row = NULL; + + row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ())); + g_assert_nonnull (row); + + g_assert_false (hdy_action_row_get_use_underline (row)); + + hdy_action_row_set_use_underline (row, TRUE); + g_assert_true (hdy_action_row_get_use_underline (row)); + + hdy_action_row_set_use_underline (row, FALSE); + g_assert_false (hdy_action_row_get_use_underline (row)); +} + + +static void +test_hdy_action_row_activate (void) +{ + g_autoptr (HdyActionRow) row = NULL; + + row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ())); + g_assert_nonnull (row); + + activated = 0; + g_signal_connect (row, "activated", G_CALLBACK (activated_cb), NULL); + + hdy_action_row_activate (row); + g_assert_cmpint (activated, ==, 1); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ActionRow/add", test_hdy_action_row_add); + g_test_add_func("/Handy/ActionRow/add_prefix", test_hdy_action_row_add_prefix); + g_test_add_func("/Handy/ActionRow/subtitle", test_hdy_action_row_subtitle); + g_test_add_func("/Handy/ActionRow/icon_name", test_hdy_action_row_icon_name); + g_test_add_func("/Handy/ActionRow/use_underline", test_hdy_action_row_use_undeline); + g_test_add_func("/Handy/ActionRow/activate", test_hdy_action_row_activate); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-application-window.c b/subprojects/libhandy/tests/test-application-window.c new file mode 100644 index 0000000..a745de6 --- /dev/null +++ b/subprojects/libhandy/tests/test-application-window.c @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_application_window_new (void) +{ + g_autoptr (GtkWidget) window = NULL; + + window = g_object_ref_sink (hdy_application_window_new ()); + g_assert_nonnull (window); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ApplicationWindow/new", test_hdy_application_window_new); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-avatar.c b/subprojects/libhandy/tests/test-avatar.c new file mode 100644 index 0000000..ad330ae --- /dev/null +++ b/subprojects/libhandy/tests/test-avatar.c @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +#define TEST_ICON_NAME "avatar-default-symbolic" +#define TEST_STRING "Mario Rossi" +#define TEST_SIZE 128 + + +static gboolean +is_surface_empty (cairo_surface_t *surface) +{ + unsigned char * data; + guint length; + + cairo_surface_flush (surface); + data = cairo_image_surface_get_data (surface); + length = cairo_image_surface_get_width (surface) * cairo_image_surface_get_height (surface); + + for (int i = 0; i < length; i++) { + if (data[i] != 0) + return FALSE; + } + return TRUE; +} + +static GdkPixbuf * +load_null_image_func (gint size, + gpointer data) +{ + return NULL; +} + +static GdkPixbuf * +load_image_func (gint size, + GdkRGBA *color) +{ + GdkPixbuf *pixbuf; + cairo_surface_t *surface; + cairo_t *cr; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + cr = cairo_create (surface); + if (color != NULL) { + gdk_cairo_set_source_rgba (cr, color); + cairo_paint (cr); + } + pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); + + cairo_surface_destroy (surface); + cairo_destroy (cr); + return pixbuf; +} + + +static void +map_event_cb (GtkWidget *widget, GdkEvent *event, cairo_surface_t **surface) +{ + cairo_t *cr; + + g_assert (surface != NULL); + + *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, TEST_SIZE, TEST_SIZE); + cr = cairo_create (*surface); + gtk_widget_draw (widget, cr); + cairo_destroy (cr); + gtk_main_quit (); +} + + +static gboolean +did_draw_something (GtkWidget *widget) +{ + GtkWidget *window; + gboolean empty; + cairo_surface_t *surface; + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + + gtk_widget_set_events (widget, GDK_STRUCTURE_MASK); + g_signal_connect (widget, "map-event", G_CALLBACK (map_event_cb), &surface); + + gtk_window_resize (GTK_WINDOW (window), TEST_SIZE, TEST_SIZE); + gtk_container_add (GTK_CONTAINER (window), widget); + + gtk_widget_show (widget); + gtk_widget_show (window); + + gtk_main (); + + g_assert (surface); + g_assert (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS); + empty = is_surface_empty (surface); + + cairo_surface_destroy (surface); + gtk_widget_destroy (window); + + return !empty; +} + + +static void +test_hdy_avatar_generate (void) +{ + GtkWidget *avatar = hdy_avatar_new (TEST_SIZE, "", TRUE); + g_assert (HDY_IS_AVATAR (avatar)); + + g_assert_true (did_draw_something (GTK_WIDGET (avatar))); +} + + +static void +test_hdy_avatar_icon_name (void) +{ + HdyAvatar *avatar = HDY_AVATAR (hdy_avatar_new (128, NULL, TRUE)); + + g_assert_null (hdy_avatar_get_icon_name (avatar)); + hdy_avatar_set_icon_name (avatar, TEST_ICON_NAME); + g_assert_cmpstr (hdy_avatar_get_icon_name (avatar), ==, TEST_ICON_NAME); + + g_assert_true (did_draw_something (GTK_WIDGET (avatar))); +} + +static void +test_hdy_avatar_text (void) +{ + HdyAvatar *avatar = HDY_AVATAR (hdy_avatar_new (128, NULL, TRUE)); + + g_assert_null (hdy_avatar_get_text (avatar)); + hdy_avatar_set_text (avatar, TEST_STRING); + g_assert_cmpstr (hdy_avatar_get_text (avatar), ==, TEST_STRING); + + g_assert_true (did_draw_something (GTK_WIDGET (avatar))); +} + +static void +test_hdy_avatar_size (void) +{ + HdyAvatar *avatar = HDY_AVATAR (hdy_avatar_new (TEST_SIZE, NULL, TRUE)); + + g_assert_cmpint (hdy_avatar_get_size (avatar), ==, TEST_SIZE); + hdy_avatar_set_size (avatar, TEST_SIZE / 2); + g_assert_cmpint (hdy_avatar_get_size (avatar), ==, TEST_SIZE / 2); + + g_assert_true (did_draw_something (GTK_WIDGET (avatar))); +} + +static void +test_hdy_avatar_custom_image (void) +{ + GtkWidget *avatar; + GdkRGBA color; + + avatar = hdy_avatar_new (TEST_SIZE, NULL, TRUE); + + g_assert (HDY_IS_AVATAR (avatar)); + + hdy_avatar_set_image_load_func (HDY_AVATAR (avatar), + (HdyAvatarImageLoadFunc) load_image_func, + NULL, + NULL); + + g_object_ref (avatar); + g_assert_false (did_draw_something (avatar)); + + hdy_avatar_set_image_load_func (HDY_AVATAR (avatar), + NULL, + NULL, + NULL); + + g_assert_true (did_draw_something (avatar)); + + gdk_rgba_parse (&color, "#F00"); + hdy_avatar_set_image_load_func (HDY_AVATAR (avatar), + (HdyAvatarImageLoadFunc) load_image_func, + &color, + NULL); + + g_assert_true (did_draw_something (avatar)); + + hdy_avatar_set_image_load_func (HDY_AVATAR (avatar), + (HdyAvatarImageLoadFunc) load_null_image_func, + NULL, + NULL); + + g_assert_true (did_draw_something (avatar)); + + g_object_unref (avatar); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func ("/Handy/Avatar/generate", test_hdy_avatar_generate); + g_test_add_func ("/Handy/Avatar/custom_image", test_hdy_avatar_custom_image); + g_test_add_func ("/Handy/Avatar/icon_name", test_hdy_avatar_icon_name); + g_test_add_func ("/Handy/Avatar/text", test_hdy_avatar_text); + g_test_add_func ("/Handy/Avatar/size", test_hdy_avatar_size); + + return g_test_run (); +} diff --git a/subprojects/libhandy/tests/test-carousel-indicator-dots.c b/subprojects/libhandy/tests/test-carousel-indicator-dots.c new file mode 100644 index 0000000..bd02d10 --- /dev/null +++ b/subprojects/libhandy/tests/test-carousel-indicator-dots.c @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +gint notified; + +static void +notify_cb (GtkWidget *widget, gpointer data) +{ + notified++; +} + +static void +test_hdy_carousel_indicator_dots_carousel (void) +{ + g_autoptr (HdyCarouselIndicatorDots) dots = NULL; + HdyCarousel *carousel; + + dots = g_object_ref_sink (HDY_CAROUSEL_INDICATOR_DOTS (hdy_carousel_indicator_dots_new ())); + g_assert_nonnull (dots); + + notified = 0; + g_signal_connect (dots, "notify::carousel", G_CALLBACK (notify_cb), NULL); + + carousel = HDY_CAROUSEL (hdy_carousel_new ()); + g_assert_nonnull (carousel); + + g_assert_null (hdy_carousel_indicator_dots_get_carousel (dots)); + g_assert_cmpint (notified, ==, 0); + + hdy_carousel_indicator_dots_set_carousel (dots, carousel); + g_assert (hdy_carousel_indicator_dots_get_carousel (dots) == carousel); + g_assert_cmpint (notified, ==, 1); + + hdy_carousel_indicator_dots_set_carousel (dots, NULL); + g_assert_null (hdy_carousel_indicator_dots_get_carousel (dots)); + g_assert_cmpint (notified, ==, 2); +} + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/CarouselIndicatorDots/carousel", test_hdy_carousel_indicator_dots_carousel); + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-carousel-indicator-lines.c b/subprojects/libhandy/tests/test-carousel-indicator-lines.c new file mode 100644 index 0000000..dfccdd2 --- /dev/null +++ b/subprojects/libhandy/tests/test-carousel-indicator-lines.c @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +gint notified; + +static void +notify_cb (GtkWidget *widget, gpointer data) +{ + notified++; +} + +static void +test_hdy_carousel_indicator_lines_carousel (void) +{ + g_autoptr (HdyCarouselIndicatorLines) lines = NULL; + HdyCarousel *carousel; + + lines = g_object_ref_sink (HDY_CAROUSEL_INDICATOR_LINES (hdy_carousel_indicator_lines_new ())); + g_assert_nonnull (lines); + + notified = 0; + g_signal_connect (lines, "notify::carousel", G_CALLBACK (notify_cb), NULL); + + carousel = HDY_CAROUSEL (hdy_carousel_new ()); + g_assert_nonnull (carousel); + + g_assert_null (hdy_carousel_indicator_lines_get_carousel (lines)); + g_assert_cmpint (notified, ==, 0); + + hdy_carousel_indicator_lines_set_carousel (lines, carousel); + g_assert (hdy_carousel_indicator_lines_get_carousel (lines) == carousel); + g_assert_cmpint (notified, ==, 1); + + hdy_carousel_indicator_lines_set_carousel (lines, NULL); + g_assert_null (hdy_carousel_indicator_lines_get_carousel (lines)); + g_assert_cmpint (notified, ==, 2); +} + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/CarouselInidicatorLines/carousel", test_hdy_carousel_indicator_lines_carousel); + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-carousel.c b/subprojects/libhandy/tests/test-carousel.c new file mode 100644 index 0000000..45b2fec --- /dev/null +++ b/subprojects/libhandy/tests/test-carousel.c @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +gint notified; + +static void +notify_cb (GtkWidget *widget, gpointer data) +{ + notified++; +} + +static void +test_hdy_carousel_add_remove (void) +{ + HdyCarousel *carousel; + GtkWidget *child1, *child2, *child3; + + carousel = HDY_CAROUSEL (hdy_carousel_new ()); + + child1 = gtk_label_new (""); + child2 = gtk_label_new (""); + child3 = gtk_label_new (""); + + notified = 0; + g_signal_connect (carousel, "notify::n-pages", G_CALLBACK (notify_cb), NULL); + + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 0); + + gtk_container_add (GTK_CONTAINER (carousel), child1); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 1); + g_assert_cmpint (notified, ==, 1); + + hdy_carousel_prepend (carousel, child2); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 2); + g_assert_cmpint (notified, ==, 2); + + hdy_carousel_insert (carousel, child3, 1); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 3); + g_assert_cmpint (notified, ==, 3); + + hdy_carousel_reorder (carousel, child3, 0); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 3); + g_assert_cmpint (notified, ==, 3); + + gtk_container_remove (GTK_CONTAINER (carousel), child2); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 2); + g_assert_cmpint (notified, ==, 4); + + gtk_container_remove (GTK_CONTAINER (carousel), child1); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 1); + g_assert_cmpint (notified, ==, 5); + + gtk_container_remove (GTK_CONTAINER (carousel), child3); + g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 0); + g_assert_cmpint (notified, ==, 6); + + g_object_unref (carousel); +} + +static void +test_hdy_carousel_scroll_to (void) +{ + HdyCarousel *carousel; + GtkWidget *child1, *child2, *child3; + + carousel = HDY_CAROUSEL (hdy_carousel_new ()); + + child1 = gtk_label_new (""); + child2 = gtk_label_new (""); + child3 = gtk_label_new (""); + + notified = 0; + g_signal_connect (carousel, "notify::position", G_CALLBACK (notify_cb), NULL); + + gtk_container_add (GTK_CONTAINER (carousel), child1); + gtk_container_add (GTK_CONTAINER (carousel), child2); + gtk_container_add (GTK_CONTAINER (carousel), child3); + + /* Since tests are done synchronously, avoid animations */ + hdy_carousel_set_animation_duration (carousel, 0); + + g_assert_cmpfloat(hdy_carousel_get_position (carousel), ==, 0); + g_assert_cmpint (notified, ==, 0); + + hdy_carousel_scroll_to (carousel, child3); + g_assert_cmpfloat(hdy_carousel_get_position (carousel), ==, 2); + g_assert_cmpint (notified, ==, 1); + + hdy_carousel_scroll_to (carousel, child2); + g_assert_cmpfloat(hdy_carousel_get_position (carousel), ==, 1); + g_assert_cmpint (notified, ==, 2); + + g_object_unref (carousel); +} + +static void +test_hdy_carousel_interactive (void) +{ + HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ()); + gboolean interactive; + + notified = 0; + g_signal_connect (carousel, "notify::interactive", G_CALLBACK (notify_cb), NULL); + + /* Accessors */ + g_assert_true (hdy_carousel_get_interactive (carousel)); + hdy_carousel_set_interactive (carousel, FALSE); + g_assert_false (hdy_carousel_get_interactive (carousel)); + g_assert_cmpint (notified, ==, 1); + + /* Property */ + g_object_set (carousel, "interactive", TRUE, NULL); + g_object_get (carousel, "interactive", &interactive, NULL); + g_assert_true (interactive); + g_assert_cmpint (notified, ==, 2); + + /* Setting the same value should not notify */ + hdy_carousel_set_interactive (carousel, TRUE); + g_assert_cmpint (notified, ==, 2); +} + +static void +test_hdy_carousel_spacing (void) +{ + HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ()); + guint spacing; + + notified = 0; + g_signal_connect (carousel, "notify::spacing", G_CALLBACK (notify_cb), NULL); + + /* Accessors */ + g_assert_cmpuint (hdy_carousel_get_spacing (carousel), ==, 0); + hdy_carousel_set_spacing (carousel, 12); + g_assert_cmpuint (hdy_carousel_get_spacing (carousel), ==, 12); + g_assert_cmpint (notified, ==, 1); + + /* Property */ + g_object_set (carousel, "spacing", 6, NULL); + g_object_get (carousel, "spacing", &spacing, NULL); + g_assert_cmpuint (spacing, ==, 6); + g_assert_cmpint (notified, ==, 2); + + /* Setting the same value should not notify */ + hdy_carousel_set_spacing (carousel, 6); + g_assert_cmpint (notified, ==, 2); +} + +static void +test_hdy_carousel_animation_duration (void) +{ + HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ()); + guint duration; + + notified = 0; + g_signal_connect (carousel, "notify::animation-duration", G_CALLBACK (notify_cb), NULL); + + /* Accessors */ + g_assert_cmpuint (hdy_carousel_get_animation_duration (carousel), ==, 250); + hdy_carousel_set_animation_duration (carousel, 200); + g_assert_cmpuint (hdy_carousel_get_animation_duration (carousel), ==, 200); + g_assert_cmpint (notified, ==, 1); + + /* Property */ + g_object_set (carousel, "animation-duration", 500, NULL); + g_object_get (carousel, "animation-duration", &duration, NULL); + g_assert_cmpuint (duration, ==, 500); + g_assert_cmpint (notified, ==, 2); + + /* Setting the same value should not notify */ + hdy_carousel_set_animation_duration (carousel, 500); + g_assert_cmpint (notified, ==, 2); +} + +static void +test_hdy_carousel_allow_mouse_drag (void) +{ + HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ()); + gboolean allow_mouse_drag; + + notified = 0; + g_signal_connect (carousel, "notify::allow-mouse-drag", G_CALLBACK (notify_cb), NULL); + + /* Accessors */ + g_assert_true (hdy_carousel_get_allow_mouse_drag (carousel)); + hdy_carousel_set_allow_mouse_drag (carousel, FALSE); + g_assert_false (hdy_carousel_get_allow_mouse_drag (carousel)); + g_assert_cmpint (notified, ==, 1); + + /* Property */ + g_object_set (carousel, "allow-mouse-drag", TRUE, NULL); + g_object_get (carousel, "allow-mouse-drag", &allow_mouse_drag, NULL); + g_assert_true (allow_mouse_drag); + g_assert_cmpint (notified, ==, 2); + + /* Setting the same value should not notify */ + hdy_carousel_set_allow_mouse_drag (carousel, TRUE); + g_assert_cmpint (notified, ==, 2); +} + +static void +test_hdy_carousel_reveal_duration (void) +{ + HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ()); + guint duration; + + notified = 0; + g_signal_connect (carousel, "notify::reveal-duration", G_CALLBACK (notify_cb), NULL); + + /* Accessors */ + g_assert_cmpuint (hdy_carousel_get_reveal_duration (carousel), ==, 0); + hdy_carousel_set_reveal_duration (carousel, 200); + g_assert_cmpuint (hdy_carousel_get_reveal_duration (carousel), ==, 200); + g_assert_cmpint (notified, ==, 1); + + /* Property */ + g_object_set (carousel, "reveal-duration", 500, NULL); + g_object_get (carousel, "reveal-duration", &duration, NULL); + g_assert_cmpuint (duration, ==, 500); + g_assert_cmpint (notified, ==, 2); + + /* Setting the same value should not notify */ + hdy_carousel_set_reveal_duration (carousel, 500); + g_assert_cmpint (notified, ==, 2); +} + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/Carousel/add_remove", test_hdy_carousel_add_remove); + g_test_add_func("/Handy/Carousel/scroll_to", test_hdy_carousel_scroll_to); + g_test_add_func("/Handy/Carousel/interactive", test_hdy_carousel_interactive); + g_test_add_func("/Handy/Carousel/spacing", test_hdy_carousel_spacing); + g_test_add_func("/Handy/Carousel/animation_duration", test_hdy_carousel_animation_duration); + g_test_add_func("/Handy/Carousel/allow_mouse_drag", test_hdy_carousel_allow_mouse_drag); + g_test_add_func("/Handy/Carousel/reveal_duration", test_hdy_carousel_reveal_duration); + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-combo-row.c b/subprojects/libhandy/tests/test-combo-row.c new file mode 100644 index 0000000..12feea6 --- /dev/null +++ b/subprojects/libhandy/tests/test-combo-row.c @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_combo_row_set_for_enum (void) +{ + g_autoptr (HdyComboRow) row = NULL; + GListModel *model; + HdyEnumValueObject *value; + + row = g_object_ref_sink (HDY_COMBO_ROW (hdy_combo_row_new ())); + g_assert_nonnull (row); + + g_assert_null (hdy_combo_row_get_model (row)); + + hdy_combo_row_set_for_enum (row, GTK_TYPE_ORIENTATION, hdy_enum_value_row_name, NULL, NULL); + model = hdy_combo_row_get_model (row); + g_assert_true (G_IS_LIST_MODEL (model)); + + g_assert_cmpuint (g_list_model_get_n_items (model), ==, 2); + + value = g_list_model_get_item (model, 0); + g_assert_true (HDY_IS_ENUM_VALUE_OBJECT (value)); + g_assert_cmpstr (hdy_enum_value_object_get_nick (value), ==, "horizontal"); + + value = g_list_model_get_item (model, 1); + g_assert_true (HDY_IS_ENUM_VALUE_OBJECT (value)); + g_assert_cmpstr (hdy_enum_value_object_get_nick (value), ==, "vertical"); +} + + +static void +test_hdy_combo_row_use_subtitle (void) +{ + g_autoptr (HdyComboRow) row = NULL; + + row = g_object_ref_sink (HDY_COMBO_ROW (hdy_combo_row_new ())); + g_assert_nonnull (row); + + g_assert_false (hdy_combo_row_get_use_subtitle (row)); + + hdy_combo_row_set_use_subtitle (row, TRUE); + g_assert_true (hdy_combo_row_get_use_subtitle (row)); + + hdy_combo_row_set_use_subtitle (row, FALSE); + g_assert_false (hdy_combo_row_get_use_subtitle (row)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ComboRow/set_for_enum", test_hdy_combo_row_set_for_enum); + g_test_add_func("/Handy/ComboRow/use_subtitle", test_hdy_combo_row_use_subtitle); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-deck.c b/subprojects/libhandy/tests/test-deck.c new file mode 100644 index 0000000..e2c5677 --- /dev/null +++ b/subprojects/libhandy/tests/test-deck.c @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_deck_adjacent_child (void) +{ + g_autoptr (HdyDeck) deck = NULL; + GtkWidget *children[2]; + gint i; + GtkWidget *result; + + deck = HDY_DECK (hdy_deck_new ()); + g_assert_nonnull (deck); + + for (i = 0; i < 2; i++) { + children[i] = gtk_label_new (""); + g_assert_nonnull (children[i]); + + gtk_container_add (GTK_CONTAINER (deck), children[i]); + } + + hdy_deck_set_visible_child (deck, children[0]); + + result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_null (result); + + result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_true (result == children[1]); + + hdy_deck_set_visible_child (deck, children[1]); + + result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_true (result == children[0]); + + result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_null (result); +} + + +static void +test_hdy_deck_navigate (void) +{ + g_autoptr (HdyDeck) deck = NULL; + GtkWidget *children[2]; + gint i; + gboolean result; + + deck = HDY_DECK (hdy_deck_new ()); + g_assert_nonnull (deck); + + for (i = 0; i < 2; i++) { + children[i] = gtk_label_new (""); + g_assert_nonnull (children[i]); + + gtk_container_add (GTK_CONTAINER (deck), children[i]); + } + + hdy_deck_set_visible_child (deck, children[0]); + + result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_false (result); + + result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_true (result); + g_assert_true (hdy_deck_get_visible_child (deck) == children[1]); + + result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_false (result); + + result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_true (result); + g_assert_true (hdy_deck_get_visible_child (deck) == children[0]); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func ("/Handy/Deck/adjacent_child", test_hdy_deck_adjacent_child); + g_test_add_func ("/Handy/Deck/navigate", test_hdy_deck_navigate); + + return g_test_run (); +} diff --git a/subprojects/libhandy/tests/test-expander-row.c b/subprojects/libhandy/tests/test-expander-row.c new file mode 100644 index 0000000..b0625d9 --- /dev/null +++ b/subprojects/libhandy/tests/test-expander-row.c @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_expander_row_add (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + GtkWidget *sw; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + sw = gtk_switch_new (); + g_assert_nonnull (sw); + + gtk_container_add (GTK_CONTAINER (row), sw); +} + + +static void +test_hdy_expander_row_subtitle (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + g_assert_cmpstr (hdy_expander_row_get_subtitle (row), ==, ""); + + hdy_expander_row_set_subtitle (row, "Dummy subtitle"); + g_assert_cmpstr (hdy_expander_row_get_subtitle (row), ==, "Dummy subtitle"); +} + + +static void +test_hdy_expander_row_icon_name (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + g_assert_null (hdy_expander_row_get_icon_name (row)); + + hdy_expander_row_set_icon_name (row, "dummy-icon-name"); + g_assert_cmpstr (hdy_expander_row_get_icon_name (row), ==, "dummy-icon-name"); +} + + +static void +test_hdy_expander_row_use_undeline (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + g_assert_false (hdy_expander_row_get_use_underline (row)); + + hdy_expander_row_set_use_underline (row, TRUE); + g_assert_true (hdy_expander_row_get_use_underline (row)); + + hdy_expander_row_set_use_underline (row, FALSE); + g_assert_false (hdy_expander_row_get_use_underline (row)); +} + + +static void +test_hdy_expander_row_expanded (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + g_assert_false (hdy_expander_row_get_expanded (row)); + + hdy_expander_row_set_expanded (row, TRUE); + g_assert_true (hdy_expander_row_get_expanded (row)); + + hdy_expander_row_set_expanded (row, FALSE); + g_assert_false (hdy_expander_row_get_expanded (row)); +} + + +static void +test_hdy_expander_row_enable_expansion (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + g_assert_true (hdy_expander_row_get_enable_expansion (row)); + g_assert_false (hdy_expander_row_get_expanded (row)); + + hdy_expander_row_set_expanded (row, TRUE); + g_assert_true (hdy_expander_row_get_expanded (row)); + + hdy_expander_row_set_enable_expansion (row, FALSE); + g_assert_false (hdy_expander_row_get_enable_expansion (row)); + g_assert_false (hdy_expander_row_get_expanded (row)); + + hdy_expander_row_set_expanded (row, TRUE); + g_assert_false (hdy_expander_row_get_expanded (row)); + + hdy_expander_row_set_enable_expansion (row, TRUE); + g_assert_true (hdy_expander_row_get_enable_expansion (row)); + g_assert_true (hdy_expander_row_get_expanded (row)); +} + + +static void +test_hdy_expander_row_show_enable_switch (void) +{ + g_autoptr (HdyExpanderRow) row = NULL; + + row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ())); + g_assert_nonnull (row); + + g_assert_false (hdy_expander_row_get_show_enable_switch (row)); + + hdy_expander_row_set_show_enable_switch (row, TRUE); + g_assert_true (hdy_expander_row_get_show_enable_switch (row)); + + hdy_expander_row_set_show_enable_switch (row, FALSE); + g_assert_false (hdy_expander_row_get_show_enable_switch (row)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ExpanderRow/add", test_hdy_expander_row_add); + g_test_add_func("/Handy/ExpanderRow/subtitle", test_hdy_expander_row_subtitle); + g_test_add_func("/Handy/ExpanderRow/icon_name", test_hdy_expander_row_icon_name); + g_test_add_func("/Handy/ExpanderRow/use_underline", test_hdy_expander_row_use_undeline); + g_test_add_func("/Handy/ExpanderRow/expanded", test_hdy_expander_row_expanded); + g_test_add_func("/Handy/ExpanderRow/enable_expansion", test_hdy_expander_row_enable_expansion); + g_test_add_func("/Handy/ExpanderRow/show_enable_switch", test_hdy_expander_row_show_enable_switch); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-header-bar.c b/subprojects/libhandy/tests/test-header-bar.c new file mode 100644 index 0000000..15064bf --- /dev/null +++ b/subprojects/libhandy/tests/test-header-bar.c @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_header_bar_pack (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + GtkWidget *widget; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + widget = gtk_switch_new (); + g_assert_nonnull (widget); + + hdy_header_bar_pack_start (bar, widget); + + widget = gtk_switch_new (); + g_assert_nonnull (widget); + + hdy_header_bar_pack_end (bar, widget); +} + + +static void +test_hdy_header_bar_title (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_null (hdy_header_bar_get_title (bar)); + + hdy_header_bar_set_title (bar, "Dummy title"); + g_assert_cmpstr (hdy_header_bar_get_title (bar), ==, "Dummy title"); + + hdy_header_bar_set_title (bar, NULL); + g_assert_null (hdy_header_bar_get_title (bar)); +} + + +static void +test_hdy_header_bar_subtitle (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_null (hdy_header_bar_get_subtitle (bar)); + + hdy_header_bar_set_subtitle (bar, "Dummy subtitle"); + g_assert_cmpstr (hdy_header_bar_get_subtitle (bar), ==, "Dummy subtitle"); + + hdy_header_bar_set_subtitle (bar, NULL); + g_assert_null (hdy_header_bar_get_subtitle (bar)); +} + + +static void +test_hdy_header_bar_custom_title (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + GtkWidget *widget; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_null (hdy_header_bar_get_custom_title (bar)); + + widget = gtk_switch_new (); + g_assert_nonnull (widget); + hdy_header_bar_set_custom_title (bar, widget); + g_assert (hdy_header_bar_get_custom_title (bar) == widget); + + hdy_header_bar_set_custom_title (bar, NULL); + g_assert_null (hdy_header_bar_get_custom_title (bar)); +} + + +static void +test_hdy_header_bar_show_close_button (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_false (hdy_header_bar_get_show_close_button (bar)); + + hdy_header_bar_set_show_close_button (bar, TRUE); + g_assert_true (hdy_header_bar_get_show_close_button (bar)); + + hdy_header_bar_set_show_close_button (bar, FALSE); + g_assert_false (hdy_header_bar_get_show_close_button (bar)); +} + + +static void +test_hdy_header_bar_has_subtitle (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_true (hdy_header_bar_get_has_subtitle (bar)); + + hdy_header_bar_set_has_subtitle (bar, FALSE); + g_assert_false (hdy_header_bar_get_has_subtitle (bar)); + + hdy_header_bar_set_has_subtitle (bar, TRUE); + g_assert_true (hdy_header_bar_get_has_subtitle (bar)); +} + + +static void +test_hdy_header_bar_decoration_layout (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_null (hdy_header_bar_get_decoration_layout (bar)); + + hdy_header_bar_set_decoration_layout (bar, ":"); + g_assert_cmpstr (hdy_header_bar_get_decoration_layout (bar), ==, ":"); + + hdy_header_bar_set_decoration_layout (bar, NULL); + g_assert_null (hdy_header_bar_get_decoration_layout (bar)); +} + + +static void +test_hdy_header_bar_centering_policy (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_cmpint (hdy_header_bar_get_centering_policy (bar), ==, HDY_CENTERING_POLICY_LOOSE); + + hdy_header_bar_set_centering_policy (bar, HDY_CENTERING_POLICY_STRICT); + g_assert_cmpint (hdy_header_bar_get_centering_policy (bar), ==, HDY_CENTERING_POLICY_STRICT); + + hdy_header_bar_set_centering_policy (bar, HDY_CENTERING_POLICY_LOOSE); + g_assert_cmpint (hdy_header_bar_get_centering_policy (bar), ==, HDY_CENTERING_POLICY_LOOSE); +} + + +static void +test_hdy_header_bar_transition_duration (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_cmpuint (hdy_header_bar_get_transition_duration (bar), ==, 200); + + hdy_header_bar_set_transition_duration (bar, 0); + g_assert_cmpuint (hdy_header_bar_get_transition_duration (bar), ==, 0); + + hdy_header_bar_set_transition_duration (bar, 1000); + g_assert_cmpuint (hdy_header_bar_get_transition_duration (bar), ==, 1000); +} + + +static void +test_hdy_header_bar_interpolate_size (void) +{ + g_autoptr (HdyHeaderBar) bar = NULL; + + bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ())); + g_assert_nonnull (bar); + + g_assert_false (hdy_header_bar_get_interpolate_size (bar)); + + hdy_header_bar_set_interpolate_size (bar, TRUE); + g_assert_true (hdy_header_bar_get_interpolate_size (bar)); + + hdy_header_bar_set_interpolate_size (bar, FALSE); + g_assert_false (hdy_header_bar_get_interpolate_size (bar)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/HeaderBar/pack", test_hdy_header_bar_pack); + g_test_add_func("/Handy/HeaderBar/title", test_hdy_header_bar_title); + g_test_add_func("/Handy/HeaderBar/subtitle", test_hdy_header_bar_subtitle); + g_test_add_func("/Handy/HeaderBar/custom_title", test_hdy_header_bar_custom_title); + g_test_add_func("/Handy/HeaderBar/show_close_button", test_hdy_header_bar_show_close_button); + g_test_add_func("/Handy/HeaderBar/has_subtitle", test_hdy_header_bar_has_subtitle); + g_test_add_func("/Handy/HeaderBar/decoration_layout", test_hdy_header_bar_decoration_layout); + g_test_add_func("/Handy/HeaderBar/centering_policy", test_hdy_header_bar_centering_policy); + g_test_add_func("/Handy/HeaderBar/transition_duration", test_hdy_header_bar_transition_duration); + g_test_add_func("/Handy/HeaderBar/interpolate_size", test_hdy_header_bar_interpolate_size); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-header-group.c b/subprojects/libhandy/tests/test-header-group.c new file mode 100644 index 0000000..632c1ff --- /dev/null +++ b/subprojects/libhandy/tests/test-header-group.c @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_header_group_decorate_all (void) +{ + g_autoptr (HdyHeaderGroup) hg = HDY_HEADER_GROUP (hdy_header_group_new ()); + gboolean decorate_all = FALSE; + + g_assert_false (hdy_header_group_get_decorate_all (hg)); + g_object_get (hg, "decorate-all", &decorate_all, NULL); + g_assert_false (decorate_all); + + hdy_header_group_set_decorate_all (hg, TRUE); + + g_assert_true (hdy_header_group_get_decorate_all (hg)); + g_object_get (hg, "decorate-all", &decorate_all, NULL); + g_assert_true (decorate_all); + + g_object_set (hg, "decorate-all", FALSE, NULL); + + g_assert_false (hdy_header_group_get_decorate_all (hg)); + g_object_get (hg, "decorate-all", &decorate_all, NULL); + g_assert_false (decorate_all); +} + + +static void +test_hdy_header_group_add_remove (void) +{ + g_autoptr (HdyHeaderGroup) hg = HDY_HEADER_GROUP (hdy_header_group_new ()); + g_autoptr (HdyHeaderBar) bar1 = HDY_HEADER_BAR (g_object_ref_sink (hdy_header_bar_new ())); + g_autoptr (GtkHeaderBar) bar2 = GTK_HEADER_BAR (g_object_ref_sink (gtk_header_bar_new ())); + + g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 0); + + hdy_header_group_add_header_bar (hg, bar1); + g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 1); + + hdy_header_group_add_gtk_header_bar (hg, bar2); + g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 2); + + hdy_header_group_remove_gtk_header_bar (hg, bar2); + g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 1); + + hdy_header_group_remove_header_bar (hg, bar1); + + g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 0); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/HeaderGroup/decorate_all", test_hdy_header_group_decorate_all); + g_test_add_func("/Handy/HeaderGroup/add_remove", test_hdy_header_group_add_remove); + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-keypad.c b/subprojects/libhandy/tests/test-keypad.c new file mode 100644 index 0000000..f33037a --- /dev/null +++ b/subprojects/libhandy/tests/test-keypad.c @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +gint notified; + + +static void +notify_cb (GtkWidget *widget, + gpointer data) +{ + notified++; +} + + +static void +test_hdy_keypad_row_spacing (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + guint row_spacing = 0; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + + notified = 0; + g_signal_connect (keypad, "notify::row-spacing", G_CALLBACK (notify_cb), NULL); + + g_assert_cmpuint (hdy_keypad_get_row_spacing (keypad), ==, 6); + g_object_get (keypad, "row-spacing", &row_spacing, NULL); + g_assert_cmpuint (row_spacing, ==, 6); + + hdy_keypad_set_row_spacing (keypad, 0); + g_assert_cmpint (notified, ==, 1); + + g_assert_cmpuint (hdy_keypad_get_row_spacing (keypad), ==, 0); + g_object_get (keypad, "row-spacing", &row_spacing, NULL); + g_assert_cmpuint (row_spacing, ==, 0); + + g_object_set (keypad, "row-spacing", 12, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_cmpuint (hdy_keypad_get_row_spacing (keypad), ==, 12); + g_object_get (keypad, "row-spacing", &row_spacing, NULL); + g_assert_cmpuint (row_spacing, ==, 12); + + g_assert_cmpint (notified, ==, 2); +} + + +static void +test_hdy_keypad_column_spacing (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + guint column_spacing = 0; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + + notified = 0; + g_signal_connect (keypad, "notify::column-spacing", G_CALLBACK (notify_cb), NULL); + + g_assert_cmpuint (hdy_keypad_get_column_spacing (keypad), ==, 6); + g_object_get (keypad, "column-spacing", &column_spacing, NULL); + g_assert_cmpuint (column_spacing, ==, 6); + + hdy_keypad_set_column_spacing (keypad, 0); + g_assert_cmpint (notified, ==, 1); + + g_assert_cmpuint (hdy_keypad_get_column_spacing (keypad), ==, 0); + g_object_get (keypad, "column-spacing", &column_spacing, NULL); + g_assert_cmpuint (column_spacing, ==, 0); + + g_object_set (keypad, "column-spacing", 12, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_cmpuint (hdy_keypad_get_column_spacing (keypad), ==, 12); + g_object_get (keypad, "column-spacing", &column_spacing, NULL); + g_assert_cmpuint (column_spacing, ==, 12); + + g_assert_cmpint (notified, ==, 2); +} + + +static void +test_hdy_keypad_letters_visible (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + gboolean letters_visible = FALSE; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + + notified = 0; + g_signal_connect (keypad, "notify::letters-visible", G_CALLBACK (notify_cb), NULL); + + g_assert_true (hdy_keypad_get_letters_visible (keypad)); + g_object_get (keypad, "letters-visible", &letters_visible, NULL); + g_assert_true (letters_visible); + + hdy_keypad_set_letters_visible (keypad, FALSE); + g_assert_cmpint (notified, ==, 1); + + g_assert_false (hdy_keypad_get_letters_visible (keypad)); + g_object_get (keypad, "letters-visible", &letters_visible, NULL); + g_assert_false (letters_visible); + + g_object_set (keypad, "letters-visible", TRUE, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_true (hdy_keypad_get_letters_visible (keypad)); + g_object_get (keypad, "letters-visible", &letters_visible, NULL); + g_assert_true (letters_visible); + + g_assert_cmpint (notified, ==, 2); +} + + +static void +test_hdy_keypad_symbols_visible (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + gboolean symbols_visible = TRUE; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + + notified = 0; + g_signal_connect (keypad, "notify::symbols-visible", G_CALLBACK (notify_cb), NULL); + + g_assert_false (hdy_keypad_get_symbols_visible (keypad)); + g_object_get (keypad, "symbols-visible", &symbols_visible, NULL); + g_assert_false (symbols_visible); + + hdy_keypad_set_symbols_visible (keypad, TRUE); + g_assert_cmpint (notified, ==, 1); + + g_assert_true (hdy_keypad_get_symbols_visible (keypad)); + g_object_get (keypad, "symbols-visible", &symbols_visible, NULL); + g_assert_true (symbols_visible); + + g_object_set (keypad, "symbols-visible", FALSE, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_false (hdy_keypad_get_symbols_visible (keypad)); + g_object_get (keypad, "symbols-visible", &symbols_visible, NULL); + g_assert_false (symbols_visible); + + g_assert_cmpint (notified, ==, 2); +} + + +static void +test_hdy_keypad_entry (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + g_autoptr (GtkEntry) entry = NULL; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + entry = g_object_ref_sink (GTK_ENTRY (gtk_entry_new ())); + + notified = 0; + g_signal_connect (keypad, "notify::entry", G_CALLBACK (notify_cb), NULL); + + g_assert_null (hdy_keypad_get_entry (keypad)); + + hdy_keypad_set_entry (keypad, entry); + g_assert_cmpint (notified, ==, 1); + + g_assert_true (hdy_keypad_get_entry (keypad) == entry); + + g_object_set (keypad, "entry", NULL, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_null (hdy_keypad_get_entry (keypad)); +} + + +static void +test_hdy_keypad_start_action (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + g_autoptr (GtkWidget) button = NULL; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + button = g_object_ref_sink (gtk_button_new ()); + + notified = 0; + g_signal_connect (keypad, "notify::start-action", G_CALLBACK (notify_cb), NULL); + + g_assert_nonnull (hdy_keypad_get_start_action (keypad)); + + hdy_keypad_set_start_action (keypad, button); + g_assert_cmpint (notified, ==, 1); + + g_assert_true (hdy_keypad_get_start_action (keypad) == button); + + g_object_set (keypad, "start-action", NULL, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_null (hdy_keypad_get_start_action (keypad)); +} + + +static void +test_hdy_keypad_end_action (void) +{ + g_autoptr (HdyKeypad) keypad = NULL; + g_autoptr (GtkWidget) button = NULL; + + keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE))); + button = g_object_ref_sink (gtk_button_new ()); + + notified = 0; + g_signal_connect (keypad, "notify::end-action", G_CALLBACK (notify_cb), NULL); + + g_assert_nonnull (hdy_keypad_get_end_action (keypad)); + + hdy_keypad_set_end_action (keypad, button); + g_assert_cmpint (notified, ==, 1); + + g_assert_true (hdy_keypad_get_end_action (keypad) == button); + + g_object_set (keypad, "end-action", NULL, NULL); + g_assert_cmpint (notified, ==, 2); + + g_assert_null (hdy_keypad_get_end_action (keypad)); +} + + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func ("/Handy/Keypad/row_spacing", test_hdy_keypad_row_spacing); + g_test_add_func ("/Handy/Keypad/column_spacing", test_hdy_keypad_column_spacing); + g_test_add_func ("/Handy/Keypad/letters_visible", test_hdy_keypad_letters_visible); + g_test_add_func ("/Handy/Keypad/symbols_visible", test_hdy_keypad_symbols_visible); + g_test_add_func ("/Handy/Keypad/entry", test_hdy_keypad_entry); + g_test_add_func ("/Handy/Keypad/start_action", test_hdy_keypad_start_action); + g_test_add_func ("/Handy/Keypad/end_action", test_hdy_keypad_end_action); + + return g_test_run (); +} diff --git a/subprojects/libhandy/tests/test-leaflet.c b/subprojects/libhandy/tests/test-leaflet.c new file mode 100644 index 0000000..43afb3c --- /dev/null +++ b/subprojects/libhandy/tests/test-leaflet.c @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_leaflet_adjacent_child (void) +{ + g_autoptr (HdyLeaflet) leaflet = NULL; + GtkWidget *children[3]; + gint i; + GtkWidget *result; + + leaflet = HDY_LEAFLET (hdy_leaflet_new ()); + g_assert_nonnull (leaflet); + + for (i = 0; i < 3; i++) { + children[i] = gtk_label_new (""); + g_assert_nonnull (children[i]); + + gtk_container_add (GTK_CONTAINER (leaflet), children[i]); + } + + gtk_container_child_set (GTK_CONTAINER (leaflet), children[1], + "navigatable", FALSE, + NULL); + + hdy_leaflet_set_visible_child (leaflet, children[0]); + + result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_null (result); + + result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_true (result == children[2]); + + hdy_leaflet_set_visible_child (leaflet, children[1]); + + result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_true (result == children[0]); + + result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_true (result == children[2]); + + hdy_leaflet_set_visible_child (leaflet, children[2]); + + result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_true (result == children[0]); + + result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_null (result); +} + + +static void +test_hdy_leaflet_navigate (void) +{ + g_autoptr (HdyLeaflet) leaflet = NULL; + GtkWidget *children[3]; + gint i; + gboolean result; + + leaflet = HDY_LEAFLET (hdy_leaflet_new ()); + g_assert_nonnull (leaflet); + + for (i = 0; i < 3; i++) { + children[i] = gtk_label_new (""); + g_assert_nonnull (children[i]); + + gtk_container_add (GTK_CONTAINER (leaflet), children[i]); + } + + gtk_container_child_set (GTK_CONTAINER (leaflet), children[1], + "navigatable", FALSE, + NULL); + + hdy_leaflet_set_visible_child (leaflet, children[0]); + + result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_false (result); + + result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_true (result); + g_assert_true (hdy_leaflet_get_visible_child (leaflet) == children[2]); + + result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD); + g_assert_false (result); + + result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_BACK); + g_assert_true (result); + g_assert_true (hdy_leaflet_get_visible_child (leaflet) == children[0]); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func ("/Handy/Leaflet/adjacent_child", test_hdy_leaflet_adjacent_child); + g_test_add_func ("/Handy/Leaflet/navigate", test_hdy_leaflet_navigate); + + return g_test_run (); +} diff --git a/subprojects/libhandy/tests/test-preferences-group.c b/subprojects/libhandy/tests/test-preferences-group.c new file mode 100644 index 0000000..d5c69ad --- /dev/null +++ b/subprojects/libhandy/tests/test-preferences-group.c @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_preferences_group_add (void) +{ + g_autoptr (HdyPreferencesGroup) group = NULL; + HdyPreferencesRow *row; + GtkWidget *widget; + + group = g_object_ref_sink (HDY_PREFERENCES_GROUP (hdy_preferences_group_new ())); + g_assert_nonnull (group); + + row = HDY_PREFERENCES_ROW (hdy_preferences_row_new ()); + g_assert_nonnull (row); + gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (row)); + + widget = gtk_switch_new (); + g_assert_nonnull (widget); + gtk_container_add (GTK_CONTAINER (group), widget); + + g_assert (G_TYPE_CHECK_INSTANCE_TYPE (gtk_widget_get_parent (GTK_WIDGET (row)), GTK_TYPE_LIST_BOX)); + g_assert (G_TYPE_CHECK_INSTANCE_TYPE (gtk_widget_get_parent (widget), GTK_TYPE_BOX)); +} + + +static void +test_hdy_preferences_group_title (void) +{ + g_autoptr (HdyPreferencesGroup) group = NULL; + + group = g_object_ref_sink (HDY_PREFERENCES_GROUP (hdy_preferences_group_new ())); + g_assert_nonnull (group); + + g_assert_cmpstr (hdy_preferences_group_get_title (group), ==, ""); + + hdy_preferences_group_set_title (group, "Dummy title"); + g_assert_cmpstr (hdy_preferences_group_get_title (group), ==, "Dummy title"); + + hdy_preferences_group_set_title (group, NULL); + g_assert_cmpstr (hdy_preferences_group_get_title (group), ==, ""); +} + + +static void +test_hdy_preferences_group_description (void) +{ + g_autoptr (HdyPreferencesGroup) group = NULL; + + group = g_object_ref_sink (HDY_PREFERENCES_GROUP (hdy_preferences_group_new ())); + g_assert_nonnull (group); + + g_assert_cmpstr (hdy_preferences_group_get_description (group), ==, ""); + + hdy_preferences_group_set_description (group, "Dummy description"); + g_assert_cmpstr (hdy_preferences_group_get_description (group), ==, "Dummy description"); + + hdy_preferences_group_set_description (group, NULL); + g_assert_cmpstr (hdy_preferences_group_get_description (group), ==, ""); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/PreferencesGroup/add", test_hdy_preferences_group_add); + g_test_add_func("/Handy/PreferencesGroup/title", test_hdy_preferences_group_title); + g_test_add_func("/Handy/PreferencesGroup/description", test_hdy_preferences_group_description); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-preferences-page.c b/subprojects/libhandy/tests/test-preferences-page.c new file mode 100644 index 0000000..5f6d15c --- /dev/null +++ b/subprojects/libhandy/tests/test-preferences-page.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_preferences_page_add (void) +{ + g_autoptr (HdyPreferencesPage) page = NULL; + HdyPreferencesGroup *group; + GtkWidget *widget; + + page = g_object_ref_sink (HDY_PREFERENCES_PAGE (hdy_preferences_page_new ())); + g_assert_nonnull (page); + + group = HDY_PREFERENCES_GROUP (hdy_preferences_group_new ()); + g_assert_nonnull (group); + gtk_container_add (GTK_CONTAINER (page), GTK_WIDGET (group)); + + widget = gtk_switch_new (); + g_assert_nonnull (widget); + g_test_expect_message (HDY_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Can't add children of type GtkSwitch to HdyPreferencesPage"); + gtk_container_add (GTK_CONTAINER (page), widget); + g_test_assert_expected_messages (); +} + + +static void +test_hdy_preferences_page_title (void) +{ + g_autoptr (HdyPreferencesPage) page = NULL; + + page = g_object_ref_sink (HDY_PREFERENCES_PAGE (hdy_preferences_page_new ())); + g_assert_nonnull (page); + + g_assert_null (hdy_preferences_page_get_title (page)); + + hdy_preferences_page_set_title (page, "Dummy title"); + g_assert_cmpstr (hdy_preferences_page_get_title (page), ==, "Dummy title"); + + hdy_preferences_page_set_title (page, NULL); + g_assert_null (hdy_preferences_page_get_title (page)); +} + + +static void +test_hdy_preferences_page_icon_name (void) +{ + g_autoptr (HdyPreferencesPage) page = NULL; + + page = g_object_ref_sink (HDY_PREFERENCES_PAGE (hdy_preferences_page_new ())); + g_assert_nonnull (page); + + g_assert_null (hdy_preferences_page_get_icon_name (page)); + + hdy_preferences_page_set_icon_name (page, "dummy-icon-name"); + g_assert_cmpstr (hdy_preferences_page_get_icon_name (page), ==, "dummy-icon-name"); + + hdy_preferences_page_set_icon_name (page, NULL); + g_assert_null (hdy_preferences_page_get_icon_name (page)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/PreferencesPage/add", test_hdy_preferences_page_add); + g_test_add_func("/Handy/PreferencesPage/title", test_hdy_preferences_page_title); + g_test_add_func("/Handy/PreferencesPage/icon_name", test_hdy_preferences_page_icon_name); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-preferences-row.c b/subprojects/libhandy/tests/test-preferences-row.c new file mode 100644 index 0000000..c4f1769 --- /dev/null +++ b/subprojects/libhandy/tests/test-preferences-row.c @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_preferences_row_title (void) +{ + g_autoptr (HdyPreferencesRow) row = NULL; + + row = g_object_ref_sink (HDY_PREFERENCES_ROW (hdy_preferences_row_new ())); + g_assert_nonnull (row); + + g_assert_null (hdy_preferences_row_get_title (row)); + + hdy_preferences_row_set_title (row, "Dummy title"); + g_assert_cmpstr (hdy_preferences_row_get_title (row), ==, "Dummy title"); + + hdy_preferences_row_set_title (row, NULL); + g_assert_null (hdy_preferences_row_get_title (row)); +} + + +static void +test_hdy_preferences_row_use_undeline (void) +{ + g_autoptr (HdyPreferencesRow) row = NULL; + + row = g_object_ref_sink (HDY_PREFERENCES_ROW (hdy_preferences_row_new ())); + g_assert_nonnull (row); + + g_assert_false (hdy_preferences_row_get_use_underline (row)); + + hdy_preferences_row_set_use_underline (row, TRUE); + g_assert_true (hdy_preferences_row_get_use_underline (row)); + + hdy_preferences_row_set_use_underline (row, FALSE); + g_assert_false (hdy_preferences_row_get_use_underline (row)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/PreferencesRow/title", test_hdy_preferences_row_title); + g_test_add_func("/Handy/PreferencesRow/use_underline", test_hdy_preferences_row_use_undeline); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-preferences-window.c b/subprojects/libhandy/tests/test-preferences-window.c new file mode 100644 index 0000000..32a0f8d --- /dev/null +++ b/subprojects/libhandy/tests/test-preferences-window.c @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_preferences_window_add (void) +{ + g_autoptr (HdyPreferencesWindow) window = NULL; + HdyPreferencesPage *page; + GtkWidget *widget; + + window = g_object_ref_sink (HDY_PREFERENCES_WINDOW (hdy_preferences_window_new ())); + g_assert_nonnull (window); + + page = HDY_PREFERENCES_PAGE (hdy_preferences_page_new ()); + g_assert_nonnull (page); + gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (page)); + + widget = gtk_switch_new (); + g_assert_nonnull (widget); + g_test_expect_message (HDY_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Can't add children of type GtkSwitch to HdyPreferencesWindow"); + gtk_container_add (GTK_CONTAINER (window), widget); + g_test_assert_expected_messages (); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/PreferencesWindow/add", test_hdy_preferences_window_add); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-search-bar.c b/subprojects/libhandy/tests/test-search-bar.c new file mode 100644 index 0000000..9f90721 --- /dev/null +++ b/subprojects/libhandy/tests/test-search-bar.c @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_search_bar_add (void) +{ + g_autoptr (HdySearchBar) bar = NULL; + GtkWidget *entry; + + bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ())); + g_assert_nonnull (bar); + + entry = gtk_entry_new (); + g_assert_nonnull (entry); + + gtk_container_add (GTK_CONTAINER (bar), entry); +} + + +static void +test_hdy_search_bar_connect_entry (void) +{ + g_autoptr (HdySearchBar) bar = NULL; + GtkWidget *box, *entry; + + bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ())); + g_assert_nonnull (bar); + + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + g_assert_nonnull (box); + + entry = gtk_entry_new (); + g_assert_nonnull (entry); + + gtk_container_add (GTK_CONTAINER (box), entry); + gtk_container_add (GTK_CONTAINER (bar), box); + hdy_search_bar_connect_entry (bar, GTK_ENTRY (entry)); +} + + +static void +test_hdy_search_bar_search_mode (void) +{ + g_autoptr (HdySearchBar) bar = NULL; + + bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ())); + g_assert_nonnull (bar); + + g_assert_false (hdy_search_bar_get_search_mode (bar)); + + hdy_search_bar_set_search_mode (bar, TRUE); + g_assert_true (hdy_search_bar_get_search_mode (bar)); + + hdy_search_bar_set_search_mode (bar, FALSE); + g_assert_false (hdy_search_bar_get_search_mode (bar)); +} + + +static void +test_hdy_search_bar_show_close_button (void) +{ + g_autoptr (HdySearchBar) bar = NULL; + + bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ())); + g_assert_nonnull (bar); + + g_assert_false (hdy_search_bar_get_show_close_button (bar)); + + hdy_search_bar_set_show_close_button (bar, TRUE); + g_assert_true (hdy_search_bar_get_show_close_button (bar)); + + hdy_search_bar_set_show_close_button (bar, FALSE); + g_assert_false (hdy_search_bar_get_show_close_button (bar)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/SearchBar/add", test_hdy_search_bar_add); + g_test_add_func("/Handy/SearchBar/connect_entry", test_hdy_search_bar_connect_entry); + g_test_add_func("/Handy/SearchBar/search_mode", test_hdy_search_bar_search_mode); + g_test_add_func("/Handy/SearchBar/show_close_button", test_hdy_search_bar_show_close_button); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-squeezer.c b/subprojects/libhandy/tests/test-squeezer.c new file mode 100644 index 0000000..90a12e7 --- /dev/null +++ b/subprojects/libhandy/tests/test-squeezer.c @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_squeezer_homogeneous (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + g_assert_true (hdy_squeezer_get_homogeneous (squeezer)); + + hdy_squeezer_set_homogeneous (squeezer, FALSE); + g_assert_false (hdy_squeezer_get_homogeneous (squeezer)); + + hdy_squeezer_set_homogeneous (squeezer, TRUE); + g_assert_true (hdy_squeezer_get_homogeneous (squeezer)); +} + + +static void +test_hdy_squeezer_transition_duration (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + g_assert_cmpuint (hdy_squeezer_get_transition_duration (squeezer), ==, 200); + + hdy_squeezer_set_transition_duration (squeezer, 400); + g_assert_cmpuint (hdy_squeezer_get_transition_duration (squeezer), ==, 400); + + hdy_squeezer_set_transition_duration (squeezer, -1); + g_assert_cmpuint (hdy_squeezer_get_transition_duration (squeezer), ==, G_MAXUINT); +} + + +static void +test_hdy_squeezer_transition_type (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + g_assert_cmpuint (hdy_squeezer_get_transition_type (squeezer), ==, HDY_SQUEEZER_TRANSITION_TYPE_NONE); + + hdy_squeezer_set_transition_type (squeezer, HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE); + g_assert_cmpuint (hdy_squeezer_get_transition_type (squeezer), ==, HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE); + + hdy_squeezer_set_transition_type (squeezer, HDY_SQUEEZER_TRANSITION_TYPE_NONE); + g_assert_cmpuint (hdy_squeezer_get_transition_type (squeezer), ==, HDY_SQUEEZER_TRANSITION_TYPE_NONE); +} + + +static void +test_hdy_squeezer_transition_running (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + g_assert_false (hdy_squeezer_get_transition_running (squeezer)); +} + + +static void +test_hdy_squeezer_show_hide_child (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + GtkWidget *child; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + g_assert_null (hdy_squeezer_get_visible_child (squeezer)); + + child = gtk_label_new (""); + gtk_container_add (GTK_CONTAINER (squeezer), child); + g_assert_null (hdy_squeezer_get_visible_child (squeezer)); + + gtk_widget_show (child); + g_assert (hdy_squeezer_get_visible_child (squeezer) == child); + + gtk_widget_hide (child); + g_assert_null (hdy_squeezer_get_visible_child (squeezer)); + + gtk_widget_show (child); + g_assert (hdy_squeezer_get_visible_child (squeezer) == child); + + gtk_container_remove (GTK_CONTAINER (squeezer), child); + g_assert_null (hdy_squeezer_get_visible_child (squeezer)); +} + + +static void +test_hdy_squeezer_interpolate_size (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + g_assert_false (hdy_squeezer_get_interpolate_size (squeezer)); + + hdy_squeezer_set_interpolate_size (squeezer, TRUE); + g_assert_true (hdy_squeezer_get_interpolate_size (squeezer)); + + hdy_squeezer_set_interpolate_size (squeezer, FALSE); + g_assert_false (hdy_squeezer_get_interpolate_size (squeezer)); +} + + +static void +test_hdy_squeezer_child_enabled (void) +{ + g_autoptr (HdySqueezer) squeezer = NULL; + GtkWidget *child; + + squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ())); + g_assert_nonnull (squeezer); + + child = gtk_label_new (""); + gtk_widget_show (child); + gtk_container_add (GTK_CONTAINER (squeezer), child); + g_assert_true (hdy_squeezer_get_child_enabled (squeezer, child)); + + hdy_squeezer_set_child_enabled (squeezer, child, FALSE); + g_assert_false (hdy_squeezer_get_child_enabled (squeezer, child)); + + hdy_squeezer_set_child_enabled (squeezer, child, TRUE); + g_assert_true (hdy_squeezer_get_child_enabled (squeezer, child)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ViewSwitcher/homogeneous", test_hdy_squeezer_homogeneous); + g_test_add_func("/Handy/ViewSwitcher/transition_duration", test_hdy_squeezer_transition_duration); + g_test_add_func("/Handy/ViewSwitcher/transition_type", test_hdy_squeezer_transition_type); + g_test_add_func("/Handy/ViewSwitcher/transition_running", test_hdy_squeezer_transition_running); + g_test_add_func("/Handy/ViewSwitcher/show_hide_child", test_hdy_squeezer_show_hide_child); + g_test_add_func("/Handy/ViewSwitcher/interpolate_size", test_hdy_squeezer_interpolate_size); + g_test_add_func("/Handy/ViewSwitcher/child_enabled", test_hdy_squeezer_child_enabled); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-swipe-group.c b/subprojects/libhandy/tests/test-swipe-group.c new file mode 100644 index 0000000..41b174f --- /dev/null +++ b/subprojects/libhandy/tests/test-swipe-group.c @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + +static void +test_hdy_swipe_group_add_remove (void) +{ + g_autoptr (HdySwipeGroup) group = NULL; + g_autoptr (HdySwipeable) swipeable1 = NULL; + g_autoptr (HdySwipeable) swipeable2 = NULL; + + group = hdy_swipe_group_new (); + + swipeable1 = HDY_SWIPEABLE (hdy_carousel_new ()); + swipeable2 = HDY_SWIPEABLE (hdy_carousel_new ()); + + g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 0); + + hdy_swipe_group_add_swipeable (group, swipeable1); + g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 1); + + hdy_swipe_group_add_swipeable (group, swipeable2); + g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 2); + + hdy_swipe_group_remove_swipeable (group, swipeable2); + g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 1); + + hdy_swipe_group_remove_swipeable (group, swipeable1); + g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 0); +} + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/SwipeGroup/add_remove", test_hdy_swipe_group_add_remove); + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-value-object.c b/subprojects/libhandy/tests/test-value-object.c new file mode 100644 index 0000000..3b9f03c --- /dev/null +++ b/subprojects/libhandy/tests/test-value-object.c @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 Red Hat Inc. + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_value_object_init (void) +{ + HdyValueObject *obj; + GValue value = G_VALUE_INIT; + gchar *str; + + g_value_init (&value, G_TYPE_STRING); + g_value_set_string (&value, "asdfasdf"); + obj = hdy_value_object_new (&value); + g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf"); + g_clear_object (&obj); + + obj = hdy_value_object_new_string ("asdfasdf"); + g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf"); + g_clear_object (&obj); + + obj = hdy_value_object_new_take_string (g_strdup ("asdfasdf")); + g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf"); + g_clear_object (&obj); + + obj = hdy_value_object_new_collect (G_TYPE_STRING, "asdfasdf"); + g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf"); + + /* And check that _dup_string works too */ + str = hdy_value_object_dup_string (obj); + g_assert_cmpstr (str, ==, "asdfasdf"); + g_clear_pointer (&str, g_free); + g_clear_object (&obj); + + g_value_unset (&value); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ValueObject/init", test_hdy_value_object_init); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-view-switcher-bar.c b/subprojects/libhandy/tests/test-view-switcher-bar.c new file mode 100644 index 0000000..54538a6 --- /dev/null +++ b/subprojects/libhandy/tests/test-view-switcher-bar.c @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_view_switcher_bar_policy (void) +{ + g_autoptr (HdyViewSwitcherBar) bar = NULL; + + bar = g_object_ref_sink (HDY_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_new ())); + g_assert_nonnull (bar); + + g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_NARROW); + + hdy_view_switcher_bar_set_policy (bar, HDY_VIEW_SWITCHER_POLICY_AUTO); + g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_AUTO); + + hdy_view_switcher_bar_set_policy (bar, HDY_VIEW_SWITCHER_POLICY_WIDE); + g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_WIDE); + + hdy_view_switcher_bar_set_policy (bar, HDY_VIEW_SWITCHER_POLICY_NARROW); + g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_NARROW); +} + + +static void +test_hdy_view_switcher_bar_stack (void) +{ + g_autoptr (HdyViewSwitcherBar) bar = NULL; + GtkStack *stack; + + bar = g_object_ref_sink (HDY_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_new ())); + g_assert_nonnull (bar); + + stack = GTK_STACK (gtk_stack_new ()); + g_assert_nonnull (stack); + + g_assert_null (hdy_view_switcher_bar_get_stack (bar)); + + hdy_view_switcher_bar_set_stack (bar, stack); + g_assert (hdy_view_switcher_bar_get_stack (bar) == stack); + + hdy_view_switcher_bar_set_stack (bar, NULL); + g_assert_null (hdy_view_switcher_bar_get_stack (bar)); +} + + +static void +test_hdy_view_switcher_bar_reveal (void) +{ + g_autoptr (HdyViewSwitcherBar) bar = NULL; + + bar = g_object_ref_sink (HDY_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_new ())); + g_assert_nonnull (bar); + + g_assert_false (hdy_view_switcher_bar_get_reveal (bar)); + + hdy_view_switcher_bar_set_reveal (bar, TRUE); + g_assert_true (hdy_view_switcher_bar_get_reveal (bar)); + + hdy_view_switcher_bar_set_reveal (bar, FALSE); + g_assert_false (hdy_view_switcher_bar_get_reveal (bar)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ViewSwitcherBar/policy", test_hdy_view_switcher_bar_policy); + g_test_add_func("/Handy/ViewSwitcherBar/stack", test_hdy_view_switcher_bar_stack); + g_test_add_func("/Handy/ViewSwitcherBar/reveal", test_hdy_view_switcher_bar_reveal); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-view-switcher.c b/subprojects/libhandy/tests/test-view-switcher.c new file mode 100644 index 0000000..2f6c89d --- /dev/null +++ b/subprojects/libhandy/tests/test-view-switcher.c @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_view_switcher_policy (void) +{ + g_autoptr (HdyViewSwitcher) view_switcher = NULL; + + view_switcher = g_object_ref_sink (HDY_VIEW_SWITCHER (hdy_view_switcher_new ())); + g_assert_nonnull (view_switcher); + + g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_AUTO); + + hdy_view_switcher_set_policy (view_switcher, HDY_VIEW_SWITCHER_POLICY_NARROW); + g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_NARROW); + + hdy_view_switcher_set_policy (view_switcher, HDY_VIEW_SWITCHER_POLICY_WIDE); + g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_WIDE); + + hdy_view_switcher_set_policy (view_switcher, HDY_VIEW_SWITCHER_POLICY_AUTO); + g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_AUTO); +} + + +static void +test_hdy_view_switcher_narrow_ellipsize (void) +{ + g_autoptr (HdyViewSwitcher) view_switcher = NULL; + + view_switcher = g_object_ref_sink (HDY_VIEW_SWITCHER (hdy_view_switcher_new ())); + g_assert_nonnull (view_switcher); + + g_assert_cmpint (hdy_view_switcher_get_narrow_ellipsize (view_switcher), ==, PANGO_ELLIPSIZE_NONE); + + hdy_view_switcher_set_narrow_ellipsize (view_switcher, PANGO_ELLIPSIZE_END); + g_assert_cmpint (hdy_view_switcher_get_narrow_ellipsize (view_switcher), ==, PANGO_ELLIPSIZE_END); + + hdy_view_switcher_set_narrow_ellipsize (view_switcher, PANGO_ELLIPSIZE_NONE); + g_assert_cmpint (hdy_view_switcher_get_narrow_ellipsize (view_switcher), ==, PANGO_ELLIPSIZE_NONE); +} + + +static void +test_hdy_view_switcher_stack (void) +{ + g_autoptr (HdyViewSwitcher) view_switcher = NULL; + GtkStack *stack; + + view_switcher = g_object_ref_sink (HDY_VIEW_SWITCHER (hdy_view_switcher_new ())); + g_assert_nonnull (view_switcher); + + stack = GTK_STACK (gtk_stack_new ()); + g_assert_nonnull (stack); + + g_assert_null (hdy_view_switcher_get_stack (view_switcher)); + + hdy_view_switcher_set_stack (view_switcher, stack); + g_assert (hdy_view_switcher_get_stack (view_switcher) == stack); + + hdy_view_switcher_set_stack (view_switcher, NULL); + g_assert_null (hdy_view_switcher_get_stack (view_switcher)); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/ViewSwitcher/policy", test_hdy_view_switcher_policy); + g_test_add_func("/Handy/ViewSwitcher/narrow_ellipsize", test_hdy_view_switcher_narrow_ellipsize); + g_test_add_func("/Handy/ViewSwitcher/stack", test_hdy_view_switcher_stack); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-window-handle.c b/subprojects/libhandy/tests/test-window-handle.c new file mode 100644 index 0000000..190c667 --- /dev/null +++ b/subprojects/libhandy/tests/test-window-handle.c @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_window_handle_new (void) +{ + g_autoptr (GtkWidget) handle = NULL; + + handle = g_object_ref_sink (hdy_window_handle_new ()); + g_assert_nonnull (handle); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/WindowHandle/new", test_hdy_window_handle_new); + + return g_test_run(); +} diff --git a/subprojects/libhandy/tests/test-window.c b/subprojects/libhandy/tests/test-window.c new file mode 100644 index 0000000..4b286de --- /dev/null +++ b/subprojects/libhandy/tests/test-window.c @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include <handy.h> + + +static void +test_hdy_window_new (void) +{ + g_autoptr (GtkWidget) window = NULL; + + window = g_object_ref_sink (hdy_window_new ()); + g_assert_nonnull (window); +} + + +gint +main (gint argc, + gchar *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + hdy_init (); + + g_test_add_func("/Handy/Window/new", test_hdy_window_new); + + return g_test_run(); +} |