diff options
Diffstat (limited to '')
-rw-r--r-- | subprojects/libhandy/src/hdy-action-row.c | 774 |
1 files changed, 774 insertions, 0 deletions
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); +} |