[libadwaita/wip/exalm/borderless: 2/15] Add AdwSplitButton
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/borderless: 2/15] Add AdwSplitButton
- Date: Thu, 9 Sep 2021 15:56:06 +0000 (UTC)
commit 6eee9aef06c688d2fc435d83c7b32c80a58e8e37
Author: Alexander Mikhaylenko <alexm gnome org>
Date: Sun Jul 4 18:57:13 2021 +0500
Add AdwSplitButton
With the new header bar button design, it will be quite difficult to create
split buttons by hand, let's provide a widget for that.
Fixes https://gitlab.gnome.org/GNOME/libadwaita/-/issues/110
src/adw-split-button.c | 926 +++++++++++++++++++++++++++++++++++
src/adw-split-button.h | 74 +++
src/adwaita.h | 1 +
src/meson.build | 2 +
src/stylesheet/widgets/_buttons.scss | 123 +++++
tests/meson.build | 1 +
tests/test-split-button.c | 283 +++++++++++
7 files changed, 1410 insertions(+)
---
diff --git a/src/adw-split-button.c b/src/adw-split-button.c
new file mode 100644
index 00000000..9f620b6e
--- /dev/null
+++ b/src/adw-split-button.c
@@ -0,0 +1,926 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "adw-split-button.h"
+
+#include "adw-widget-utils-private.h"
+
+/**
+ * AdwSplitButton:
+ *
+ * A combined button and dropdown widget.
+ *
+ * `AdwSplitButton` is typically used to present a set of actions in a menu,
+ * but allow access to one of them with a single click.
+ *
+ * The API is very similar to [class@Gtk.Button] and [class@Gtk.MenuButton], see
+ * their documentation for details.
+ *
+ * ## CSS nodes
+ *
+ * ```
+ * splitbutton[.image-button][.text-button]
+ * ├── button
+ * │ ╰── <content>
+ * ├── separator
+ * ╰── menubutton
+ * ╰── button.toggle
+ * ╰── arrow
+ * ```
+ *
+ * `AdwSplitButton`'s CSS node is called `splitbutton`. It contains the css
+ * nodes: `button`, `separator`, `menubutton`. See [class@Gtk.MenuButton]
+ * documentation for the `menubutton` contents.
+ *
+ * The main CSS node will contain the `.image-button` or `.text-button` style
+ * classes matching the button contents. The nested button nodes will never
+ * contain them.
+ *
+ * ## Accessibility
+ *
+ * `AdwSplitButton` uses the `GTK_ACCESSIBLE_ROLE_BUTTON` role.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_LABEL,
+ PROP_USE_UNDERLINE,
+ PROP_ICON_NAME,
+ PROP_CHILD,
+ PROP_MENU_MODEL,
+ PROP_POPOVER,
+ PROP_DIRECTION,
+
+ /* actionable properties */
+ PROP_ACTION_NAME,
+ PROP_ACTION_TARGET,
+ LAST_PROP = PROP_ACTION_NAME
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_CLICKED,
+ SIGNAL_ACTIVATE,
+ SIGNAL_LAST_SIGNAL
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+struct _AdwSplitButton
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *button;
+ GtkWidget *menu_button;
+ GtkWidget *arrow_button;
+ GtkWidget *separator;
+};
+
+static void adw_split_button_actionable_init (GtkActionableInterface *iface);
+static void adw_split_button_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (AdwSplitButton, adw_split_button, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, adw_split_button_actionable_init)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_split_button_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+static void
+update_state (AdwSplitButton *self)
+{
+ GtkStateFlags flags;
+ gboolean keyboard_activating;
+
+ flags = gtk_widget_get_state_flags (self->button) |
+ gtk_widget_get_state_flags (self->arrow_button);
+
+ keyboard_activating =
+ gtk_widget_has_css_class (self->button, "keyboard-activating") ||
+ gtk_widget_has_css_class (self->arrow_button, "keyboard-activating");
+
+ if (flags & GTK_STATE_FLAG_ACTIVE || keyboard_activating)
+ gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_ACTIVE, FALSE);
+ else
+ gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_ACTIVE);
+
+ if (flags & GTK_STATE_FLAG_CHECKED)
+ 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);
+}
+
+static void
+update_style_classes (AdwSplitButton *self)
+{
+ const char *label = gtk_button_get_label (GTK_BUTTON (self->button));
+ const char *icon_name = gtk_button_get_icon_name (GTK_BUTTON (self->button));
+
+ if (icon_name && icon_name[0])
+ gtk_widget_add_css_class (GTK_WIDGET (self), "image-button");
+ else
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "image-button");
+
+ if (label && label[0])
+ gtk_widget_add_css_class (GTK_WIDGET (self), "text-button");
+ else
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "text-button");
+
+ gtk_widget_remove_css_class (self->button, "text-button");
+ gtk_widget_remove_css_class (self->button, "image-button");
+
+ gtk_widget_remove_css_class (self->arrow_button, "image-button");
+}
+
+static void
+clicked_cb (AdwSplitButton *self)
+{
+ g_signal_emit (self, signals[SIGNAL_CLICKED], 0);
+}
+
+static void
+activate_cb (AdwSplitButton *self)
+{
+ g_signal_emit_by_name (self->button, "activate");
+}
+
+static void
+adw_split_button_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (object);
+
+ switch (prop_id) {
+ case PROP_LABEL:
+ g_value_set_string (value, adw_split_button_get_label (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, adw_split_button_get_use_underline (self));
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, adw_split_button_get_icon_name (self));
+ break;
+ case PROP_CHILD:
+ g_value_set_object (value, adw_split_button_get_child (self));
+ break;
+ case PROP_MENU_MODEL:
+ g_value_set_object (value, adw_split_button_get_menu_model (self));
+ break;
+ case PROP_POPOVER:
+ g_value_set_object (value, adw_split_button_get_popover (self));
+ break;
+ case PROP_DIRECTION:
+ g_value_set_enum (value, adw_split_button_get_direction (self));
+ break;
+ case PROP_ACTION_NAME:
+ g_value_set_string (value, gtk_actionable_get_action_name (GTK_ACTIONABLE (self)));
+ break;
+ case PROP_ACTION_TARGET:
+ g_value_set_variant (value, gtk_actionable_get_action_target_value (GTK_ACTIONABLE (self)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+adw_split_button_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (object);
+
+ switch (prop_id) {
+ case PROP_LABEL:
+ adw_split_button_set_label (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ adw_split_button_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ case PROP_ICON_NAME:
+ adw_split_button_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_CHILD:
+ adw_split_button_set_child (self, g_value_get_object (value));
+ break;
+ case PROP_MENU_MODEL:
+ adw_split_button_set_menu_model (self, g_value_get_object (value));
+ break;
+ case PROP_POPOVER:
+ adw_split_button_set_popover (self, g_value_get_object (value));
+ break;
+ case PROP_DIRECTION:
+ adw_split_button_set_direction (self, g_value_get_enum (value));
+ break;
+ case PROP_ACTION_NAME:
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self), g_value_get_string (value));
+ break;
+ case PROP_ACTION_TARGET:
+ gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self), g_value_get_variant (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+adw_split_button_dispose (GObject *object)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (object);
+
+ g_clear_pointer (&self->button, gtk_widget_unparent);
+ g_clear_pointer (&self->menu_button, gtk_widget_unparent);
+ g_clear_pointer (&self->separator, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (adw_split_button_parent_class)->dispose (object);
+}
+
+static void
+adw_split_button_class_init (AdwSplitButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = adw_split_button_get_property;
+ object_class->set_property = adw_split_button_set_property;
+ object_class->dispose = adw_split_button_dispose;
+
+ widget_class->focus = adw_widget_focus_child;
+ widget_class->grab_focus = adw_widget_grab_focus_child;
+ widget_class->compute_expand = adw_widget_compute_expand;
+
+ /**
+ * AdwSplitButton:label: (attributes org.gtk.Property.get=adw_split_button_get_label
org.gtk.Property.set=adw_split_button_set_label)
+ *
+ * The label for the button.
+ *
+ * Setting the label will set [property@Adw.SplitButton:icon-name] and
+ * [property@Adw.SplitButton:child] to `NULL`.
+ *
+ * Since: 1.0
+ */
+ props[PROP_LABEL] =
+ g_param_spec_string ("label",
+ "Label",
+ "The label for the button",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwSplitButton:use-underline: (attributes org.gtk.Property.get=adw_split_button_get_use_underline
org.gtk.Property.set=adw_split_button_set_use_underline)
+ *
+ * Whether an underline in the text indicates a mnemonic.
+ *
+ * See [property@Adw.SplitButton:label].
+ *
+ * Since: 1.0
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ "Use underline",
+ "Whether an underline in the text indicates a mnemonic",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwSplitButton:icon-name: (attributes org.gtk.Property.get=adw_split_button_get_icon_name
org.gtk.Property.set=adw_split_button_set_icon_name)
+ *
+ * The name of the icon used to automatically populate the button.
+ *
+ * Setting the icon name will set [property@Adw.SplitButton:label] and
+ * [property@Adw.SplitButton:child] to `NULL`.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "The name of the icon used to automatically populate the button",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwSplitButton:child: (attributes org.gtk.Property.get=adw_split_button_get_child
org.gtk.Property.set=adw_split_button_set_child)
+ *
+ * The child widget.
+ *
+ * Setting the child widget will set [property@Adw.SplitButton:label] and
+ * [property@Adw.SplitButton:icon-name] to `NULL`.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CHILD] =
+ g_param_spec_object ("child",
+ "Child",
+ "The child widget",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwSplitButton:menu-model: (attributes org.gtk.Property.get=adw_split_button_get_menu_model
org.gtk.Property.set=adw_split_button_set_menu_model)
+ *
+ * The `GMenuModel` from which the popup will be created.
+ *
+ * If the menu model is `NULL`, the dropdown is disabled.
+ *
+ * A [class@Gtk.Popover] will be created from the menu model with
+ * [ctor@Gtk.PopoverMenu.new_from_model]. Actions will be connected
+ * as documented for this function.
+ *
+ * If [property@Adw.SplitButton:popover] is already set, it will be
+ * dissociated from the button, and the property is set to `NULL`.
+ *
+ * Since: 1.0
+ */
+ props[PROP_MENU_MODEL] =
+ g_param_spec_object ("menu-model",
+ "Menu model",
+ "The model from which the popup is made",
+ G_TYPE_MENU_MODEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwSplitButton:popover: (attributes org.gtk.Property.get=adw_split_button_get_popover
org.gtk.Property.set=adw_split_button_set_popover)
+ *
+ * The `GtkPopover` that will be popped up when the dropdown is clicked.
+ *
+ * If the popover is `NULL`, the dropdown is disabled.
+ *
+ * If [property@Adw.SplitButton:menu-model] is set, the menu model is
+ * dissociated from the button, and the property is set to `NULL`.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POPOVER] =
+ g_param_spec_object ("popover",
+ "Popover",
+ "The popover that will be popped up when the dropdown is clicked",
+ GTK_TYPE_POPOVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwSplitButton:direction: (attributes org.gtk.Property.get=adw_split_button_get_direction
org.gtk.Property.set=adw_split_button_set_direction)
+ *
+ * The direction in which the popup will be popped up.
+ *
+ * The dropdown arrow icon will point at the same direction.
+ *
+ * If the does not fit in the available space in the given direction,
+ * GTK will its best to keep it inside the screen and fully visible.
+ *
+ * If you pass `GTK_ARROW_NONE`, it's equivalent to `GTK_ARROW_DOWN`.
+ *
+ * Since: 1.0
+ */
+ props[PROP_DIRECTION] =
+ g_param_spec_enum ("direction",
+ "Direction",
+ "The direction in which the popup will be popped up",
+ GTK_TYPE_ARROW_TYPE,
+ GTK_ARROW_DOWN,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ g_object_class_override_property (object_class, PROP_ACTION_NAME, "action-name");
+ g_object_class_override_property (object_class, PROP_ACTION_TARGET, "action-target");
+
+ /**
+ * AdwSplitButton::clicked:
+ *
+ * Emitted when the button has been activated (pressed and released).
+ */
+ signals[SIGNAL_CLICKED] =
+ g_signal_new ("clicked",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ /**
+ * AdwSplitButton::activate:
+ *
+ * Emitted to animate press then release.
+ *
+ * This is an action signal. Applications should never connect
+ * to this signal, but use the [signal@Adw.SplitButton::clicked] signal.
+ */
+ signals[SIGNAL_ACTIVATE] =
+ g_signal_new ("activate",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ gtk_widget_class_set_activate_signal (widget_class, signals[SIGNAL_ACTIVATE]);
+
+ g_signal_override_class_handler ("activate",
+ G_TYPE_FROM_CLASS (klass),
+ G_CALLBACK (activate_cb));
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "splitbutton");
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (GObject *object) { \
+ g_object_notify_by_pspec (object, props[prop]); \
+}
+
+NOTIFY (notify_use_underline_cb, PROP_USE_UNDERLINE);
+NOTIFY (notify_menu_model_cb, PROP_MENU_MODEL);
+NOTIFY (notify_popover_cb, PROP_POPOVER);
+NOTIFY (notify_direction_cb, PROP_DIRECTION);
+
+static void
+notify_action_name_cb (GObject *object)
+{
+ g_object_notify (object, "action-name");
+}
+
+static void
+notify_action_target_cb (GObject *object)
+{
+ g_object_notify (object, "action-target");
+}
+
+static void
+adw_split_button_init (AdwSplitButton *self)
+{
+ gtk_widget_set_hexpand (GTK_WIDGET (self), FALSE);
+
+ self->button = gtk_button_new ();
+ gtk_widget_set_parent (self->button, GTK_WIDGET (self));
+ gtk_widget_set_hexpand (self->button, TRUE);
+
+ self->separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
+ gtk_widget_set_parent (self->separator, GTK_WIDGET (self));
+
+ self->menu_button = gtk_menu_button_new ();
+ gtk_widget_set_parent (self->menu_button, GTK_WIDGET (self));
+
+ /* FIXME: This is iffy, but we don't have any other way to do it */
+ self->arrow_button = gtk_widget_get_first_child (self->menu_button);
+
+ g_signal_connect_swapped (self->button, "clicked", G_CALLBACK (clicked_cb), self);
+
+ g_signal_connect_swapped (self->button, "notify::css-classes", G_CALLBACK (update_state), self);
+ g_signal_connect_swapped (self->button, "state-flags-changed", G_CALLBACK (update_state), self);
+ g_signal_connect_swapped (self->arrow_button, "notify::css-classes", G_CALLBACK (update_state), self);
+ g_signal_connect_swapped (self->arrow_button, "state-flags-changed", G_CALLBACK (update_state), self);
+
+ g_signal_connect_swapped (self->button, "notify::use-underline", G_CALLBACK (notify_use_underline_cb),
self);
+ g_signal_connect_swapped (self->button, "notify::action-name", G_CALLBACK (notify_action_name_cb), self);
+ g_signal_connect_swapped (self->button, "notify::action-target", G_CALLBACK (notify_action_target_cb),
self);
+ g_signal_connect_swapped (self->menu_button, "notify::menu-model", G_CALLBACK (notify_menu_model_cb),
self);
+ g_signal_connect_swapped (self->menu_button, "notify::popover", G_CALLBACK (notify_popover_cb), self);
+ g_signal_connect_swapped (self->menu_button, "notify::direction", G_CALLBACK (notify_direction_cb), self);
+
+ g_object_bind_property (self->button, "sensitive",
+ self, "sensitive",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
+
+ update_style_classes (self);
+}
+
+static const char *
+adw_split_button_get_action_name (GtkActionable *actionable)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (actionable);
+
+ return gtk_actionable_get_action_name (GTK_ACTIONABLE (self->button));
+}
+
+static void
+adw_split_button_set_action_name (GtkActionable *actionable,
+ const char *action_name)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (actionable);
+
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self->button), action_name);
+}
+
+static GVariant *
+adw_split_button_get_action_target_value (GtkActionable *actionable)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (actionable);
+
+ return gtk_actionable_get_action_target_value (GTK_ACTIONABLE (self->button));
+}
+
+static void
+adw_split_button_set_action_target_value (GtkActionable *actionable,
+ GVariant *action_target)
+{
+ AdwSplitButton *self = ADW_SPLIT_BUTTON (actionable);
+
+ gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->button), action_target);
+}
+
+static void
+adw_split_button_actionable_init (GtkActionableInterface *iface)
+{
+ iface->get_action_name = adw_split_button_get_action_name;
+ iface->set_action_name = adw_split_button_set_action_name;
+ iface->get_action_target_value = adw_split_button_get_action_target_value;
+ iface->set_action_target_value = adw_split_button_set_action_target_value;
+}
+
+static void
+adw_split_button_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *type)
+{
+ if (GTK_IS_POPOVER (child))
+ adw_split_button_set_popover (ADW_SPLIT_BUTTON (buildable), GTK_POPOVER (child));
+ else if (GTK_IS_WIDGET (child))
+ adw_split_button_set_child (ADW_SPLIT_BUTTON (buildable), GTK_WIDGET (child));
+ else
+ parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_split_button_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+ iface->add_child = adw_split_button_buildable_add_child;
+}
+
+/**
+ * adw_split_button_new:
+ *
+ * Creates a new `AdwSplitButton`.
+ *
+ * Returns: the newly created `AdwSplitButton`
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_split_button_new (void)
+{
+ return g_object_new (ADW_TYPE_SPLIT_BUTTON, NULL);
+}
+
+/**
+ * adw_split_button_get_label: (attributes org.gtk.Method.get_property=label)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets the label for @self.
+ *
+ * Returns: (nullable): the label for @self
+ *
+ * Since: 1.0
+ */
+const char *
+adw_split_button_get_label (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), NULL);
+
+ return gtk_button_get_label (GTK_BUTTON (self->button));
+}
+
+/**
+ * adw_split_button_set_label: (attributes org.gtk.Method.set_property=label)
+ * @self: a `AdwSplitButton`
+ * @label: the label to set
+ *
+ * Sets the label for @self.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_label (AdwSplitButton *self,
+ const char *label)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+ g_return_if_fail (label != NULL);
+
+ if (!g_strcmp0 (label, adw_split_button_get_label (self)))
+ return;
+
+ g_object_freeze_notify (G_OBJECT (self));
+ if (adw_split_button_get_icon_name (self))
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+ if (adw_split_button_get_child (self))
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+
+ gtk_button_set_label (GTK_BUTTON (self->button), label);
+ update_style_classes (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LABEL]);
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_split_button_get_use_underline: (attributes org.gtk.Method.set_property=use-underline)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets whether an underline in the text indicates a mnemonic.
+ *
+ * Returns: whether an underline in the text indicates a mnemonic
+ *
+ * Since: 1.0
+ */
+gboolean
+adw_split_button_get_use_underline (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), FALSE);
+
+ return gtk_button_get_use_underline (GTK_BUTTON (self->button));
+}
+
+/**
+ * adw_split_button_set_use_underline: (attributes org.gtk.Method.set_property=use-underline)
+ * @self: a `AdwSplitButton`
+ * @use_underline: whether an underline in the text indicates a mnemonic
+ *
+ * Sets whether an underline in the text indicates a mnemonic.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_use_underline (AdwSplitButton *self,
+ gboolean use_underline)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ use_underline = !!use_underline;
+
+ if (use_underline == adw_split_button_get_use_underline (self))
+ return;
+
+ gtk_button_set_use_underline (GTK_BUTTON (self->button), use_underline);
+}
+
+/**
+ * adw_split_button_get_icon_name: (attributes org.gtk.Method.get_property=icon-name)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets the name of the icon used to automatically populate the button.
+ *
+ * If the icon name has not been set with [method@Adw.SplitButton.set_icon_name]
+ * the return value will be `NULL`.
+ *
+ * Returns: (nullable): the icon name
+ *
+ * Since: 1.0
+ */
+const char *
+adw_split_button_get_icon_name (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), NULL);
+
+ return gtk_button_get_icon_name (GTK_BUTTON (self->button));
+}
+
+/**
+ * adw_split_button_set_icon_name: (attributes org.gtk.Method.set_property=icon-name)
+ * @self: a `AdwSplitButton`
+ * @icon_name: the icon name to set
+ *
+ * Sets the name of the icon used to automatically populate the button.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_icon_name (AdwSplitButton *self,
+ const char *icon_name)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+ g_return_if_fail (icon_name != NULL);
+
+ if (!g_strcmp0 (icon_name, adw_split_button_get_icon_name (self)))
+ return;
+
+ g_object_freeze_notify (G_OBJECT (self));
+ if (adw_split_button_get_label (self))
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LABEL]);
+ if (adw_split_button_get_child (self))
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+
+ gtk_button_set_icon_name (GTK_BUTTON (self->button), icon_name);
+
+ update_style_classes (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_split_button_get_child: (attributes org.gtk.Method.get_property=child)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets the child widget.
+ *
+ * Returns: (transfer none) (nullable): the child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_split_button_get_child (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), NULL);
+
+ return gtk_button_get_child (GTK_BUTTON (self->button));
+}
+
+/**
+ * adw_split_button_set_child: (attributes org.gtk.Method.set_property=child)
+ * @self: a `AdwSplitButton`
+ * @child: (nullable): the new child widget
+ *
+ * Sets the child widget.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_child (AdwSplitButton *self,
+ GtkWidget *child)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ if (child == adw_split_button_get_child (self))
+ return;
+
+ g_object_freeze_notify (G_OBJECT (self));
+ if (adw_split_button_get_label (self))
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LABEL]);
+ if (adw_split_button_get_icon_name (self))
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+
+ gtk_button_set_child (GTK_BUTTON (self->button), child);
+
+ update_style_classes (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * adw_split_button_get_menu_model: (attributes org.gtk.Method.get_property=menu-model)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets the menu model from which the popup will be created.
+ *
+ * Returns: (transfer none) (nullable): the menu model
+ *
+ * Since: 1.0
+ */
+GMenuModel *
+adw_split_button_get_menu_model (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), NULL);
+
+ return gtk_menu_button_get_menu_model (GTK_MENU_BUTTON (self->menu_button));
+}
+
+/**
+ * adw_split_button_set_menu_model: (attributes org.gtk.Method.set_property=menu-model)
+ * @self: a `AdwSplitButton`
+ * @menu_model: (nullable): the menu model
+ *
+ * Sets the menu model from which the popup will be created.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_menu_model (AdwSplitButton *self,
+ GMenuModel *menu_model)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ if (menu_model == adw_split_button_get_menu_model (self))
+ return;
+
+ gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (self->menu_button), menu_model);
+}
+
+/**
+ * adw_split_button_get_popover: (attributes org.gtk.Method.get_property=popover)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets the popover that will be popped up when the dropdown is clicked.
+ *
+ * Returns: (transfer none) (nullable): the popover
+ *
+ * Since: 1.0
+ */
+GtkPopover *
+adw_split_button_get_popover (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), NULL);
+
+ return gtk_menu_button_get_popover (GTK_MENU_BUTTON (self->menu_button));
+}
+
+/**
+ * adw_split_button_set_popover: (attributes org.gtk.Method.set_property=popover)
+ * @self: a `AdwSplitButton`
+ * @popover: (nullable): the popover
+ *
+ * Sets the popover that will be popped up when the dropdown is clicked.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_popover (AdwSplitButton *self,
+ GtkPopover *popover)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ if (popover == adw_split_button_get_popover (self))
+ return;
+
+ gtk_menu_button_set_popover (GTK_MENU_BUTTON (self->menu_button), GTK_WIDGET (popover));
+}
+
+/**
+ * adw_split_button_get_direction: (attributes org.gtk.Method.get_property=direction)
+ * @self: a `AdwSplitButton`
+ *
+ * Gets the direction in which the popup will be popped up.
+ *
+ * Returns: the direction
+ *
+ * Since: 1.0
+ */
+GtkArrowType
+adw_split_button_get_direction (AdwSplitButton *self)
+{
+ g_return_val_if_fail (ADW_IS_SPLIT_BUTTON (self), GTK_ARROW_DOWN);
+
+ return gtk_menu_button_get_direction (GTK_MENU_BUTTON (self->menu_button));
+}
+
+/**
+ * adw_split_button_set_direction: (attributes org.gtk.Method.set_property=direction)
+ * @self: a `AdwSplitButton`
+ * @direction: the direction
+ *
+ * Sets the direction in which the popup will be popped up.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_set_direction (AdwSplitButton *self,
+ GtkArrowType direction)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ if (direction == adw_split_button_get_direction (self))
+ return;
+
+ gtk_menu_button_set_direction (GTK_MENU_BUTTON (self->menu_button), direction);
+
+ update_style_classes (self);
+}
+
+/**
+ * adw_split_button_popup:
+ * @self: a `AdwSplitButton`
+ *
+ * Pops up the menu.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_popup (AdwSplitButton *self)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ gtk_menu_button_popup (GTK_MENU_BUTTON (self->menu_button));
+}
+
+/**
+ * adw_split_button_popdown:
+ * @self: a `AdwSplitButton`
+ *
+ * Dismisses the menu.
+ *
+ * Since: 1.0
+ */
+void
+adw_split_button_popdown (AdwSplitButton *self)
+{
+ g_return_if_fail (ADW_IS_SPLIT_BUTTON (self));
+
+ gtk_menu_button_popdown (GTK_MENU_BUTTON (self->menu_button));
+}
diff --git a/src/adw-split-button.h b/src/adw-split-button.h
new file mode 100644
index 00000000..55686e2e
--- /dev/null
+++ b/src/adw-split-button.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_SPLIT_BUTTON (adw_split_button_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwSplitButton, adw_split_button, ADW, SPLIT_BUTTON, GtkWidget)
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_split_button_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_split_button_get_label (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_label (AdwSplitButton *self,
+ const char *label);
+
+ADW_AVAILABLE_IN_ALL
+gboolean adw_split_button_get_use_underline (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_use_underline (AdwSplitButton *self,
+ gboolean use_underline);
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_split_button_get_icon_name (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_icon_name (AdwSplitButton *self,
+ const char *icon_name);
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_split_button_get_child (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_child (AdwSplitButton *self,
+ GtkWidget *child);
+
+ADW_AVAILABLE_IN_ALL
+GMenuModel *adw_split_button_get_menu_model (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_menu_model (AdwSplitButton *self,
+ GMenuModel *menu_model);
+
+ADW_AVAILABLE_IN_ALL
+GtkPopover *adw_split_button_get_popover (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_popover (AdwSplitButton *self,
+ GtkPopover *popover);
+
+ADW_AVAILABLE_IN_ALL
+GtkArrowType adw_split_button_get_direction (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_set_direction (AdwSplitButton *self,
+ GtkArrowType direction);
+
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_popup (AdwSplitButton *self);
+ADW_AVAILABLE_IN_ALL
+void adw_split_button_popdown (AdwSplitButton *self);
+
+G_END_DECLS
diff --git a/src/adwaita.h b/src/adwaita.h
index 916b83b1..82a55c48 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -47,6 +47,7 @@ G_BEGIN_DECLS
#include "adw-preferences-page.h"
#include "adw-preferences-row.h"
#include "adw-preferences-window.h"
+#include "adw-split-button.h"
#include "adw-squeezer.h"
#include "adw-status-page.h"
#include "adw-swipe-tracker.h"
diff --git a/src/meson.build b/src/meson.build
index 8390bbc7..f32a00d8 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -94,6 +94,7 @@ src_headers = [
'adw-preferences-page.h',
'adw-preferences-row.h',
'adw-preferences-window.h',
+ 'adw-split-button.h',
'adw-squeezer.h',
'adw-status-page.h',
'adw-swipe-tracker.h',
@@ -151,6 +152,7 @@ src_sources = [
'adw-preferences-row.c',
'adw-preferences-window.c',
'adw-shadow-helper.c',
+ 'adw-split-button.c',
'adw-squeezer.c',
'adw-status-page.c',
'adw-swipe-tracker.c',
diff --git a/src/stylesheet/widgets/_buttons.scss b/src/stylesheet/widgets/_buttons.scss
index 4ca8b309..be63789c 100644
--- a/src/stylesheet/widgets/_buttons.scss
+++ b/src/stylesheet/widgets/_buttons.scss
@@ -380,3 +380,126 @@ menubutton {
}
}
}
+
+splitbutton {
+ border-radius: $button_radius;
+
+ &, & > separator {
+ transition: $button_transition;
+ transition-property: background;
+ }
+
+ > separator {
+ margin-top: 6px;
+ margin-bottom: 6px;
+ background: none;
+ }
+
+ > menubutton > button {
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+
+ // Since the inner button doesn't have any style classes on it,
+ // we have to add them manually
+ &.image-button > button {
+ min-width: 24px;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ &.text-button.image-button > button {
+ padding-left: 10px;
+ padding-right: 10px;
+
+ > box {
+ border-spacing: 6px;
+ }
+ }
+
+ // Reimplementing linked so we don't blow up css
+ > button:dir(ltr),
+ > menubutton > button:dir(rtl) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ margin-right: -1px;
+ }
+
+ > button:dir(rtl),
+ > menubutton > button:dir(ltr) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ margin-left: -1px;
+ }
+
+ &.flat {
+ > separator {
+ background: gtkalpha(currentColor, .3);
+ }
+
+ &:hover,
+ &:active,
+ &:checked {
+ background: gtkalpha(currentColor, .05);
+
+ > separator {
+ background: none;
+ }
+ }
+
+ &:focus-within:focus-visible > separator {
+ background: none;
+ }
+
+ > button,
+ > menubutton > button {
+ @extend %button_basic_flat;
+
+ border-radius: $button_radius;
+ }
+ }
+
+ &.outline {
+ > button,
+ > menubutton > button {
+ @extend %outline_button;
+ }
+ }
+
+ &.suggested-action {
+ > button, > menubutton > button {
+ @extend %filled_button;
+
+ color: $accent_fg_color;
+
+ &, &:checked {
+ background-color: $accent_bg_color;
+ }
+ }
+ }
+
+ &.destructive-action {
+ > button, > menubutton > button {
+ @extend %filled_button;
+
+ color: $destructive_fg_color;
+
+ &, &:checked {
+ background-color: $destructive_bg_color;
+ }
+ }
+ }
+
+ &.suggested-action,
+ &.destructive-action {
+ $_separator_color: gtkalpha(currentColor, if($contrast == 'high', .6, .3));
+ > menubutton > button {
+ &:dir(ltr) { box-shadow: inset 1px 0 $_separator_color; }
+ &:dir(rtl) { box-shadow: inset -1px 0 $_separator_color; }
+ }
+ }
+
+ > menubutton > button > arrow.none {
+ -gtk-icon-source: -gtk-icontheme('pan-down-symbolic');
+ }
+}
diff --git a/tests/meson.build b/tests/meson.build
index 4ec38955..895d13a0 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -36,6 +36,7 @@ test_names = [
'test-preferences-page',
'test-preferences-row',
'test-preferences-window',
+ 'test-split-button',
'test-squeezer',
'test-status-page',
'test-tab-bar',
diff --git a/tests/test-split-button.c b/tests/test-split-button.c
new file mode 100644
index 00000000..884102aa
--- /dev/null
+++ b/tests/test-split-button.c
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include <adwaita.h>
+
+int notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_adw_split_button_icon_name (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ const char *icon_name;
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ notified = 0;
+ g_signal_connect (button, "notify::icon-name", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "icon-name", &icon_name, NULL);
+ g_assert_null (icon_name);
+
+ adw_split_button_set_icon_name (button, "document-open-symbolic");
+ g_assert_cmpint (notified, ==, 1);
+
+ adw_split_button_set_icon_name (button, "document-open-symbolic");
+ g_assert_cmpstr (adw_split_button_get_icon_name (button), ==, "document-open-symbolic");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "icon-name", "edit-find-symbolic", NULL);
+ g_assert_cmpstr (adw_split_button_get_icon_name (button), ==, "edit-find-symbolic");
+ g_assert_cmpint (notified, ==, 2);
+
+ adw_split_button_set_label (button, "Open");
+ g_assert_null (adw_split_button_get_icon_name (button));
+ g_assert_cmpint (notified, ==, 3);
+
+ adw_split_button_set_icon_name (button, "document-open-symbolic");
+ g_assert_cmpstr (adw_split_button_get_icon_name (button), ==, "document-open-symbolic");
+ g_assert_cmpint (notified, ==, 4);
+
+ adw_split_button_set_child (button, gtk_button_new ());
+ g_assert_null (adw_split_button_get_icon_name (button));
+ g_assert_cmpint (notified, ==, 5);
+}
+
+static void
+test_adw_split_button_label (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ const char *label;
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ notified = 0;
+ g_signal_connect (button, "notify::label", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "label", &label, NULL);
+ g_assert_null (label);
+
+ adw_split_button_set_label (button, "Open");
+ g_assert_cmpint (notified, ==, 1);
+
+ adw_split_button_set_label (button, "Open");
+ g_assert_cmpstr (adw_split_button_get_label (button), ==, "Open");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "label", "Find", NULL);
+ g_assert_cmpstr (adw_split_button_get_label (button), ==, "Find");
+ g_assert_cmpint (notified, ==, 2);
+
+ adw_split_button_set_icon_name (button, "document-open-symbolic");
+ g_assert_null (adw_split_button_get_label (button));
+ g_assert_cmpint (notified, ==, 3);
+
+ adw_split_button_set_label (button, "Open");
+ g_assert_cmpstr (adw_split_button_get_label (button), ==, "Open");
+ g_assert_cmpint (notified, ==, 4);
+
+ adw_split_button_set_child (button, gtk_button_new ());
+ g_assert_null (adw_split_button_get_label (button));
+ g_assert_cmpint (notified, ==, 5);
+}
+
+static void
+test_adw_split_button_use_underline (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ gboolean use_underline;
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ notified = 0;
+ g_signal_connect (button, "notify::use-underline", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "use-underline", &use_underline, NULL);
+ g_assert_false (use_underline);
+
+ adw_split_button_set_use_underline (button, FALSE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_split_button_set_use_underline (button, TRUE);
+ g_assert_true (adw_split_button_get_use_underline (button));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "use-underline", FALSE, NULL);
+ g_assert_false (adw_split_button_get_use_underline (button));
+ g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_adw_split_button_child (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ GtkWidget *child1, *child2, *child3, *child;
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ child1 = gtk_button_new ();
+ child2 = gtk_button_new ();
+ child3 = gtk_button_new ();
+
+ notified = 0;
+ g_signal_connect (button, "notify::child", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "child", &child, NULL);
+ g_assert_null (child);
+
+ adw_split_button_set_child (button, NULL);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_split_button_set_child (button, child1);
+ g_assert_true (adw_split_button_get_child (button) == child1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "child", child2, NULL);
+ g_assert_true (adw_split_button_get_child (button) == child2);
+ g_assert_cmpint (notified, ==, 2);
+
+ adw_split_button_set_label (button, "Open");
+ /* adw_split_button_get_child() will return button's internal child, as will
+ * gtk_button_get_child(). We can check that it's not same as the one we had
+ * just set */
+ g_assert_false (adw_split_button_get_child (button) == child2);
+ g_assert_cmpint (notified, ==, 3);
+
+ adw_split_button_set_child (button, child3);
+ g_assert_true (adw_split_button_get_child (button) == child3);
+ g_assert_cmpint (notified, ==, 4);
+
+ adw_split_button_set_icon_name (button, "document-open-symbolic");
+ g_assert_false (adw_split_button_get_child (button) == child3);
+ g_assert_cmpint (notified, ==, 5);
+}
+
+static void
+test_adw_split_button_menu_model (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ GMenuModel *model = NULL;
+ g_autoptr (GMenuModel) model1 = G_MENU_MODEL (g_menu_new ());
+ g_autoptr (GMenuModel) model2 = G_MENU_MODEL (g_menu_new ());
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ notified = 0;
+ g_signal_connect (button, "notify::menu-model", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "menu-model", &model, NULL);
+ g_assert_null (model);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_split_button_set_menu_model (button, model1);
+ g_object_get (button, "menu-model", &model, NULL);
+ g_assert_true (model == model1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "menu-model", model2, NULL);
+ g_assert_true (adw_split_button_get_menu_model (button) == model2);
+ g_assert_cmpint (notified, ==, 2);
+
+ adw_split_button_set_popover (button, GTK_POPOVER (gtk_popover_new ()));
+ g_assert_null (adw_split_button_get_menu_model (button));
+ g_assert_cmpint (notified, ==, 3);
+}
+
+static void
+test_adw_split_button_popover (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ GtkPopover *popover, *popover1, *popover2;
+ g_autoptr (GMenuModel) model = NULL;
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ popover1 = GTK_POPOVER (gtk_popover_new ());
+ popover2 = GTK_POPOVER (gtk_popover_new ());
+
+ notified = 0;
+ g_signal_connect (button, "notify::popover", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "popover", &popover, NULL);
+ g_assert_null (popover);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_split_button_set_popover (button, popover1);
+ g_object_get (button, "popover", &popover, NULL);
+ g_assert_true (popover == popover1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "popover", popover2, NULL);
+ g_assert_true (adw_split_button_get_popover (button) == popover2);
+ g_assert_cmpint (notified, ==, 2);
+
+ model = G_MENU_MODEL (g_menu_new ());
+ adw_split_button_set_menu_model (button, model);
+ /* When a menu model is set, we can still access popover, and what exactly
+ * popover that is is an implementation detail. However, what we know for
+ * sure is it's not the same one as we had just set */
+ g_assert_false (adw_split_button_get_popover (button) == popover2);
+ g_assert_cmpint (notified, ==, 3);
+}
+
+static void
+test_adw_split_button_direction (void)
+{
+ g_autoptr (AdwSplitButton) button = NULL;
+ GtkArrowType direction;
+
+ button = g_object_ref_sink (ADW_SPLIT_BUTTON (adw_split_button_new ()));
+ g_assert_nonnull (button);
+
+ notified = 0;
+ g_signal_connect (button, "notify::direction", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (button, "direction", &direction, NULL);
+ g_assert_cmpint (direction, ==, GTK_ARROW_DOWN);
+
+ adw_split_button_set_direction (button, GTK_ARROW_DOWN);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_split_button_set_direction (button, GTK_ARROW_UP);
+ g_assert_cmpint (adw_split_button_get_direction (button), ==, GTK_ARROW_UP);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (button, "direction", GTK_ARROW_DOWN, NULL);
+ g_assert_cmpint (adw_split_button_get_direction (button), ==, GTK_ARROW_DOWN);
+ g_assert_cmpint (notified, ==, 2);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func ("/Adwaita/SplitButton/icon_name", test_adw_split_button_icon_name);
+ g_test_add_func ("/Adwaita/SplitButton/label", test_adw_split_button_label);
+ g_test_add_func ("/Adwaita/SplitButton/use_underline", test_adw_split_button_use_underline);
+ g_test_add_func ("/Adwaita/SplitButton/child", test_adw_split_button_child);
+ g_test_add_func ("/Adwaita/SplitButton/menu_model", test_adw_split_button_menu_model);
+ g_test_add_func ("/Adwaita/SplitButton/popover", test_adw_split_button_popover);
+ g_test_add_func ("/Adwaita/SplitButton/direction", test_adw_split_button_direction);
+
+ return g_test_run ();
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]