diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml
index 8c49516cb1..510ae21b9c 100644
--- a/docs/reference/gtk/gtk4-docs.xml
+++ b/docs/reference/gtk/gtk4-docs.xml
@@ -76,6 +76,7 @@
+
diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt
index 5fbfb9c2a3..be3cdfca7d 100644
--- a/docs/reference/gtk/gtk4-sections.txt
+++ b/docs/reference/gtk/gtk4-sections.txt
@@ -3208,6 +3208,26 @@ GTK_TREE_LIST_MODEL_GET_CLASS
gtk_tree_list_row_get_type
+
+gtktreeexpander
+GtkTreeExpander
+gtk_tree_expander_new
+gtk_tree_expander_get_child
+gtk_tree_expander_set_child
+gtk_tree_expander_get_item
+gtk_tree_expander_get_list_row
+gtk_tree_expander_set_list_row
+
+GTK_TREE_EXPANDER
+GTK_IS_TREE_EXPANDER
+GTK_TYPE_TREE_EXPANDER
+GTK_TREE_EXPANDER_CLASS
+GTK_IS_TREE_EXPANDER_CLASS
+GTK_TREE_EXPANDER_GET_CLASS
+
+gtk_tree_expander_get_type
+
+
gtktreemodel
GtkTreeModel
diff --git a/gtk/gtk.h b/gtk/gtk.h
index 0ad9b5c412..1a2f2111f4 100644
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
@@ -250,6 +250,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/gtk/gtktreeexpander.c b/gtk/gtktreeexpander.c
new file mode 100644
index 0000000000..7dcd67ad27
--- /dev/null
+++ b/gtk/gtktreeexpander.c
@@ -0,0 +1,449 @@
+/*
+ * Copyright © 2019 Benjamin Otte
+ *
+ * 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 .
+ *
+ * Authors: Benjamin Otte
+ */
+
+#include "config.h"
+
+#include "gtktreeexpander.h"
+
+#include "gtkboxlayout.h"
+#include "gtkbuiltiniconprivate.h"
+#include "gtkintl.h"
+#include "gtktreelistmodel.h"
+
+/**
+ * SECTION:gtktreeexpander
+ * @title: GtkTreeExpander
+ * @short_description: An indenting expander button for use in a tree list
+ * @see_also: #GtkTreeListModel
+ *
+ * GtkTreeExpander is a widget that provides an expander for a list.
+ *
+ * It is typically placed as a bottommost child into a #GtkListView to allow
+ * users to expand and collapse children in a list with a #GtkTreeListModel.
+ * It will provide the common UI elements, gestures and keybindings for this
+ * purpose.
+ *
+ * On top of this, the "listitem.expand", "listitem.collapse" and
+ * "listitem.toggle-expand" actions are provided to allow adding custom UI
+ * for managing expanded state.
+ *
+ * The #GtkTreeListModel must be set to not be passthrough. Then it will provide
+ * #GtkTreeListRow items which can be set via gtk_tree_expander_set_list_row()
+ * on the expander. The expander will then watch that row item automatically.
+ * gtk_tree_expander_set_child() sets the widget that displays the actual row
+ * contents.
+ *
+ * # CSS nodes
+ *
+ * |[
+ * treeexpander
+ * ├── [indent]*
+ * ├── [expander]
+ * ╰──
+ * ]|
+ *
+ * GtkTreeExpander has zero or one CSS nodes with the name "expander" that should
+ * display the expander icon. The node will be `:checked` when it is expanded.
+ * If the node is not expandable, an "indent" node will be displayed instead.
+ *
+ * For every level of depth, another "indent" node is prepended.
+ */
+
+struct _GtkTreeExpander
+{
+ GtkWidget parent_instance;
+
+ GtkTreeListRow *list_row;
+ GtkWidget *child;
+
+ GtkWidget *expander;
+ guint notify_handler;
+};
+
+enum
+{
+ PROP_0,
+ PROP_CHILD,
+ PROP_ITEM,
+ PROP_LIST_ROW,
+
+ N_PROPS
+};
+
+G_DEFINE_TYPE (GtkTreeExpander, gtk_tree_expander, GTK_TYPE_WIDGET)
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+static void
+gtk_tree_expander_update_for_list_row (GtkTreeExpander *self)
+{
+ if (self->list_row == NULL)
+ {
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self));
+ child != self->child;
+ child = gtk_widget_get_first_child (GTK_WIDGET (self)))
+ {
+ gtk_widget_unparent (child);
+ }
+ self->expander = NULL;
+ }
+ else
+ {
+ GtkWidget *child;
+ guint i, depth;
+
+ depth = gtk_tree_list_row_get_depth (self->list_row);
+ if (gtk_tree_list_row_is_expandable (self->list_row))
+ {
+ if (self->expander == NULL)
+ {
+ self->expander = gtk_builtin_icon_new ("expander");
+ gtk_widget_insert_before (self->expander,
+ GTK_WIDGET (self),
+ self->child);
+ }
+ if (gtk_tree_list_row_get_expanded (self->list_row))
+ gtk_widget_set_state_flags (self->expander, GTK_STATE_FLAG_CHECKED, FALSE);
+ else
+ gtk_widget_unset_state_flags (self->expander, GTK_STATE_FLAG_CHECKED);
+ child = gtk_widget_get_prev_sibling (self->expander);
+ }
+ else
+ {
+ g_clear_pointer (&self->expander, gtk_widget_unparent);
+ depth++;
+ if (self->child)
+ child = gtk_widget_get_prev_sibling (self->child);
+ else
+ child = gtk_widget_get_last_child (GTK_WIDGET (self));
+ }
+
+ for (i = 0; i < depth; i++)
+ {
+ if (child)
+ child = gtk_widget_get_prev_sibling (child);
+ else
+ gtk_widget_insert_after (gtk_builtin_icon_new ("indent"), GTK_WIDGET (self), NULL);
+ }
+
+ while (child)
+ {
+ GtkWidget *prev = gtk_widget_get_prev_sibling (child);
+ gtk_widget_unparent (child);
+ child = prev;
+ }
+ }
+}
+
+static void
+gtk_tree_expander_list_row_notify_cb (GtkTreeListRow *list_row,
+ GParamSpec *pspec,
+ GtkTreeExpander *self)
+{
+ if (pspec->name == g_intern_static_string ("expanded"))
+ {
+ if (self->expander)
+ {
+ if (gtk_tree_list_row_get_expanded (list_row))
+ gtk_widget_set_state_flags (self->expander, GTK_STATE_FLAG_CHECKED, FALSE);
+ else
+ gtk_widget_unset_state_flags (self->expander, GTK_STATE_FLAG_CHECKED);
+ }
+ }
+ else if (pspec->name == g_intern_static_string ("item"))
+ {
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ITEM]);
+ }
+ else
+ {
+ /* can this happen other than when destroying the row? */
+ gtk_tree_expander_update_for_list_row (self);
+ }
+}
+
+static void
+gtk_tree_expander_clear_list_row (GtkTreeExpander *self)
+{
+ if (self->list_row == NULL)
+ return;
+
+ g_signal_handler_disconnect (self->list_row, self->notify_handler);
+ g_clear_object (&self->list_row);
+}
+
+static void
+gtk_tree_expander_dispose (GObject *object)
+{
+ GtkTreeExpander *self = GTK_TREE_EXPANDER (object);
+
+ gtk_tree_expander_clear_list_row (self);
+ gtk_tree_expander_update_for_list_row (self);
+
+ g_clear_pointer (&self->child, gtk_widget_unparent);
+
+ g_assert (self->expander == NULL);
+
+ G_OBJECT_CLASS (gtk_tree_expander_parent_class)->dispose (object);
+}
+
+static void
+gtk_tree_expander_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtkTreeExpander *self = GTK_TREE_EXPANDER (object);
+
+ switch (property_id)
+ {
+ case PROP_CHILD:
+ g_value_set_object (value, self->child);
+ break;
+
+ case PROP_ITEM:
+ g_value_set_object (value, gtk_tree_expander_get_item (self));
+ break;
+
+ case PROP_LIST_ROW:
+ g_value_set_object (value, self->list_row);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gtk_tree_expander_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtkTreeExpander *self = GTK_TREE_EXPANDER (object);
+
+ switch (property_id)
+ {
+ case PROP_CHILD:
+ gtk_tree_expander_set_child (self, g_value_get_object (value));
+ break;
+
+ case PROP_LIST_ROW:
+ gtk_tree_expander_set_list_row (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gtk_tree_expander_class_init (GtkTreeExpanderClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->dispose = gtk_tree_expander_dispose;
+ gobject_class->get_property = gtk_tree_expander_get_property;
+ gobject_class->set_property = gtk_tree_expander_set_property;
+
+ /**
+ * GtkTreeExpander:child:
+ *
+ * The child widget with the actual contents
+ */
+ properties[PROP_CHILD] =
+ g_param_spec_object ("child",
+ P_("Child"),
+ P_("The child widget with the actual contents"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtkTreeExpander:item:
+ *
+ * The item held by this expander's row
+ */
+ properties[PROP_ITEM] =
+ g_param_spec_object ("item",
+ P_("Item"),
+ P_("The item held by this expander's row"),
+ G_TYPE_OBJECT,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtkTreeExpander:list-row:
+ *
+ * The list row to track for expander state
+ */
+ properties[PROP_LIST_ROW] =
+ g_param_spec_object ("list-row",
+ P_("List row"),
+ P_("The list row to track for expander state"),
+ GTK_TYPE_TREE_LIST_ROW,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, N_PROPS, properties);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, I_("treeexpander"));
+}
+
+static void
+gtk_tree_expander_init (GtkTreeExpander *self)
+{
+}
+
+/**
+ * gtk_tree_expander_new:
+ *
+ * Creates a new #GtkTreeExpander
+ *
+ * Returns: a new #GtkTreeExpander
+ **/
+GtkWidget *
+gtk_tree_expander_new (void)
+{
+ return g_object_new (GTK_TYPE_TREE_EXPANDER,
+ NULL);
+}
+
+/**
+ * gtk_tree_expander_get_child
+ * @self: a #GtkTreeExpander
+ *
+ * Gets the child widget displayed by @self.
+ *
+ * Returns: (nullable) (transfer none): The child displayed by @self
+ **/
+GtkWidget *
+gtk_tree_expander_get_child (GtkTreeExpander *self)
+{
+ g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL);
+
+ return self->child;
+}
+
+/**
+ * gtk_tree_expander_set_child:
+ * @self: a #GtkTreeExpander widget
+ * @child: (nullable): a #GtkWidget, or %NULL
+ *
+ * Sets the content widget to display.
+ */
+void
+gtk_tree_expander_set_child (GtkTreeExpander *self,
+ GtkWidget *child)
+{
+ g_return_if_fail (GTK_IS_TREE_EXPANDER (self));
+ g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));
+
+ if (self->child == child)
+ return;
+
+ g_clear_pointer (&self->child, gtk_widget_unparent);
+
+ if (child)
+ {
+ self->child = child;
+ gtk_widget_set_parent (child, GTK_WIDGET (self));
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CHILD]);
+}
+
+/**
+ * gtk_tree_expander_get_item
+ * @self: a #GtkTreeExpander
+ *
+ * Forwards the item set on the #GtkTreeListRow that @self is managing.
+ *
+ * This call is essentially equivalent to calling
+ * `gtk_tree_list_row_get_item (gtk_tree_expander_get_list_row (@self))`.
+ *
+ * Returns: (nullable) (transfer none): The item of the row
+ **/
+gpointer
+gtk_tree_expander_get_item (GtkTreeExpander *self)
+{
+ g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL);
+
+ if (self->list_row == NULL)
+ return NULL;
+
+ return gtk_tree_list_row_get_item (self->list_row);
+}
+
+/**
+ * gtk_tree_expander_get_list_row
+ * @self: a #GtkTreeExpander
+ *
+ * Gets the list row managed by @self.
+ *
+ * Returns: (nullable) (transfer none): The list row displayed by @self
+ **/
+GtkTreeListRow *
+gtk_tree_expander_get_list_row (GtkTreeExpander *self)
+{
+ g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL);
+
+ return self->list_row;
+}
+
+/**
+ * gtk_tree_expander_set_list_row:
+ * @self: a #GtkTreeExpander widget
+ * @list_row: (nullable): a #GtkTreeListRow, or %NULL
+ *
+ * Sets the tree list row that this expander should manage.
+ */
+void
+gtk_tree_expander_set_list_row (GtkTreeExpander *self,
+ GtkTreeListRow *list_row)
+{
+ g_return_if_fail (GTK_IS_TREE_EXPANDER (self));
+ g_return_if_fail (list_row == NULL || GTK_IS_TREE_LIST_ROW (list_row));
+
+ if (self->list_row == list_row)
+ return;
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ gtk_tree_expander_clear_list_row (self);
+
+ if (list_row)
+ {
+ self->list_row = g_object_ref (list_row);
+ self->notify_handler = g_signal_connect (list_row,
+ "notify",
+ G_CALLBACK (gtk_tree_expander_list_row_notify_cb),
+ self);
+ }
+
+ gtk_tree_expander_update_for_list_row (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LIST_ROW]);
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ITEM]);
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
diff --git a/gtk/gtktreeexpander.h b/gtk/gtktreeexpander.h
new file mode 100644
index 0000000000..4ee36405c2
--- /dev/null
+++ b/gtk/gtktreeexpander.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2019 Benjamin Otte
+ *
+ * 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 .
+ *
+ * Authors: Benjamin Otte
+ */
+
+#ifndef __GTK_TREE_EXPANDER_H__
+#define __GTK_TREE_EXPANDER_H__
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_TREE_EXPANDER (gtk_tree_expander_get_type ())
+
+GDK_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GtkTreeExpander, gtk_tree_expander, GTK, TREE_EXPANDER, GtkWidget)
+
+GDK_AVAILABLE_IN_ALL
+GtkWidget * gtk_tree_expander_new (void);
+
+GDK_AVAILABLE_IN_ALL
+GtkWidget * gtk_tree_expander_get_child (GtkTreeExpander *self);
+GDK_AVAILABLE_IN_ALL
+void gtk_tree_expander_set_child (GtkTreeExpander *self,
+ GtkWidget *child);
+GDK_AVAILABLE_IN_ALL
+gpointer gtk_tree_expander_get_item (GtkTreeExpander *self);
+GDK_AVAILABLE_IN_ALL
+GtkTreeListRow * gtk_tree_expander_get_list_row (GtkTreeExpander *self);
+GDK_AVAILABLE_IN_ALL
+void gtk_tree_expander_set_list_row (GtkTreeExpander *self,
+ GtkTreeListRow *list_row);
+
+
+G_END_DECLS
+
+#endif /* __GTK_TREE_EXPANDER_H__ */
diff --git a/gtk/meson.build b/gtk/meson.build
index d6b2135b14..c52e78ea6f 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -389,6 +389,7 @@ gtk_public_sources = files([
'gtktooltip.c',
'gtktooltipwindow.c',
'gtktreednd.c',
+ 'gtktreeexpander.c',
'gtktreelistmodel.c',
'gtktreemodel.c',
'gtktreemodelfilter.c',
@@ -638,6 +639,7 @@ gtk_public_headers = files([
'gtktogglebutton.h',
'gtktooltip.h',
'gtktreednd.h',
+ 'gtktreeexpander.h',
'gtktreelistmodel.h',
'gtktreemodel.h',
'gtktreemodelfilter.h',
diff --git a/tests/testlistview.c b/tests/testlistview.c
index 0234b9bdc5..c82cb5f8fb 100644
--- a/tests/testlistview.c
+++ b/tests/testlistview.c
@@ -338,14 +338,12 @@ create_list_model_for_directory (gpointer file)
typedef struct _RowData RowData;
struct _RowData
{
- GtkWidget *depth_box;
GtkWidget *expander;
GtkWidget *icon;
GtkWidget *name;
GCancellable *cancellable;
GtkTreeListRow *current_item;
- GBinding *expander_binding;
};
static void row_data_notify_item (GtkListItem *item,
@@ -363,8 +361,6 @@ row_data_unbind (RowData *data)
g_clear_object (&data->cancellable);
}
- g_binding_unbind (data->expander_binding);
-
g_clear_object (&data->current_item);
}
@@ -437,7 +433,6 @@ row_data_bind (RowData *data,
GtkTreeListRow *item)
{
GFileInfo *info;
- guint depth;
row_data_unbind (data);
@@ -446,11 +441,7 @@ row_data_bind (RowData *data,
data->current_item = g_object_ref (item);
- depth = gtk_tree_list_row_get_depth (item);
- gtk_widget_set_size_request (data->depth_box, 16 * depth, 0);
-
- gtk_widget_set_sensitive (data->expander, gtk_tree_list_row_is_expandable (item));
- data->expander_binding = g_object_bind_property (item, "expanded", data->expander, "active", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+ gtk_tree_expander_set_list_row (GTK_TREE_EXPANDER (data->expander), item);
info = gtk_tree_list_row_get_item (item);
@@ -512,15 +503,11 @@ setup_widget (GtkListItem *list_item,
gtk_label_set_width_chars (GTK_LABEL (child), 5);
gtk_box_append (GTK_BOX (box), child);
- data->depth_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
- gtk_box_append (GTK_BOX (box), data->depth_box);
-
- data->expander = g_object_new (GTK_TYPE_TOGGLE_BUTTON, "css-name", "expander-widget", NULL);
- gtk_button_set_has_frame (GTK_BUTTON (data->expander), FALSE);
+ data->expander = gtk_tree_expander_new ();
gtk_box_append (GTK_BOX (box), data->expander);
- child = g_object_new (GTK_TYPE_SPINNER, "css-name", "expander", NULL);
- g_object_bind_property (data->expander, "active", child, "spinning", G_BINDING_SYNC_CREATE);
- gtk_button_set_child (GTK_BUTTON (data->expander), child);
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_tree_expander_set_child (GTK_TREE_EXPANDER (data->expander), box);
data->icon = gtk_image_new ();
gtk_box_append (GTK_BOX (box), data->icon);