From 66f798366c9ccdb3c413646bc4414268841de92a Mon Sep 17 00:00:00 2001 From: Gerben Jan Dijkman Date: Fri, 26 May 2023 17:25:04 +0200 Subject: [PATCH] Added 2nd patch --- gui-libs/gtk/files/4447.patch | 14 +- gui-libs/gtk/files/5818.patch | 6139 +++++++++++++++++++++++++++++ gui-libs/gtk/gtk-4.11.2-r1.ebuild | 1 + 3 files changed, 6141 insertions(+), 13 deletions(-) create mode 100644 gui-libs/gtk/files/5818.patch diff --git a/gui-libs/gtk/files/4447.patch b/gui-libs/gtk/files/4447.patch index 4d79cb0..5f27cdd 100644 --- a/gui-libs/gtk/files/4447.patch +++ b/gui-libs/gtk/files/4447.patch @@ -1,14 +1,5 @@ -From 8cee572ac34d29d852dfabbf4f42c4843fb5b892 Mon Sep 17 00:00:00 2001 -From: Marco Melorio -Date: Fri, 10 Jun 2022 14:39:24 +0200 -Subject: [PATCH] listbase: Add "reversed" property - ---- - gtk/gtklistbase.c | 93 ++++++++++++++++++++++++++++++++++++++++------- - 1 file changed, 80 insertions(+), 13 deletions(-) - diff --git a/gtk/gtklistbase.c b/gtk/gtklistbase.c -index 85827363ced..b23e5a58078 100644 +index 85827363ced093ffcf94597a5ce02b405a380965..b23e5a58078915319045102e83e254929c75df03 100644 --- a/gtk/gtklistbase.c +++ b/gtk/gtklistbase.c @@ -64,6 +64,7 @@ struct _GtkListBasePrivate @@ -207,6 +198,3 @@ index 85827363ced..b23e5a58078 100644 gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), across_out, NULL, NULL); gtk_list_base_get_adjustment_values (self, priv->orientation, along_out, NULL, NULL); --- -GitLab - diff --git a/gui-libs/gtk/files/5818.patch b/gui-libs/gtk/files/5818.patch new file mode 100644 index 0000000..201dbf0 --- /dev/null +++ b/gui-libs/gtk/files/5818.patch @@ -0,0 +1,6139 @@ +diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml +index 615d782bfef56242d6be7258f7435f466144da55..ba87e860cef4b3cc1241c6180d63b088ab7a9181 100644 +--- a/demos/gtk-demo/demo.gresource.xml ++++ b/demos/gtk-demo/demo.gresource.xml +@@ -195,6 +195,9 @@ + + listview_settings.ui + ++ ++ listview_settings2.ui ++ + + ucdnames.data + +@@ -312,6 +315,7 @@ + listview_minesweeper.c + listview_selections.c + listview_settings.c ++ listview_settings2.c + listview_ucd.c + listview_weather.c + listview_words.c +diff --git a/demos/gtk-demo/listview_settings.c b/demos/gtk-demo/listview_settings.c +index 4e8e69bfbf4b8dd9548360e99fcaa623e0ca205d..6a7779ba12b0690777e5c88143d48238a100ee74 100644 +--- a/demos/gtk-demo/listview_settings.c ++++ b/demos/gtk-demo/listview_settings.c +@@ -14,138 +14,7 @@ + + #include + +-#include +- +-/* Create an object that wraps GSettingsSchemaKey because that's a boxed type */ +-typedef struct _SettingsKey SettingsKey; +-struct _SettingsKey +-{ +- GObject parent_instance; +- +- GSettings *settings; +- GSettingsSchemaKey *key; +-}; +- +-enum { +- PROP_0, +- PROP_NAME, +- PROP_SUMMARY, +- PROP_DESCRIPTION, +- PROP_VALUE, +- PROP_TYPE, +- PROP_DEFAULT_VALUE, +- +- N_PROPS +-}; +- +-#define SETTINGS_TYPE_KEY (settings_key_get_type ()) +-G_DECLARE_FINAL_TYPE (SettingsKey, settings_key, SETTINGS, KEY, GObject); +- +-G_DEFINE_TYPE (SettingsKey, settings_key, G_TYPE_OBJECT); +-static GParamSpec *properties[N_PROPS] = { NULL, }; +- +-static void +-settings_key_get_property (GObject *object, +- guint property_id, +- GValue *value, +- GParamSpec *pspec) +-{ +- SettingsKey *self = SETTINGS_KEY (object); +- +- switch (property_id) +- { +- case PROP_DESCRIPTION: +- g_value_set_string (value, g_settings_schema_key_get_description (self->key)); +- break; +- +- case PROP_NAME: +- g_value_set_string (value, g_settings_schema_key_get_name (self->key)); +- break; +- +- case PROP_SUMMARY: +- g_value_set_string (value, g_settings_schema_key_get_summary (self->key)); +- break; +- +- case PROP_VALUE: +- { +- GVariant *variant = g_settings_get_value (self->settings, g_settings_schema_key_get_name (self->key)); +- g_value_take_string (value, g_variant_print (variant, FALSE)); +- g_variant_unref (variant); +- } +- break; +- +- case PROP_TYPE: +- { +- const GVariantType *type = g_settings_schema_key_get_value_type (self->key); +- g_value_set_string (value, g_variant_type_peek_string (type)); +- } +- break; +- +- case PROP_DEFAULT_VALUE: +- { +- GVariant *variant = g_settings_schema_key_get_default_value (self->key); +- g_value_take_string (value, g_variant_print (variant, FALSE)); +- g_variant_unref (variant); +- } +- break; +- +- default: +- G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +- break; +- } +-} +- +-static void +-settings_key_finalize (GObject *object) +-{ +- SettingsKey *self = SETTINGS_KEY (object); +- +- g_object_unref (self->settings); +- g_settings_schema_key_unref (self->key); +- +- G_OBJECT_CLASS (settings_key_parent_class)->finalize (object); +-} +- +-static void +-settings_key_class_init (SettingsKeyClass *klass) +-{ +- GObjectClass *gobject_class = G_OBJECT_CLASS (klass); +- +- gobject_class->finalize = settings_key_finalize; +- gobject_class->get_property = settings_key_get_property; +- +- properties[PROP_DESCRIPTION] = +- g_param_spec_string ("description", NULL, NULL, NULL, G_PARAM_READABLE); +- properties[PROP_NAME] = +- g_param_spec_string ("name", NULL, NULL, NULL, G_PARAM_READABLE); +- properties[PROP_SUMMARY] = +- g_param_spec_string ("summary", NULL, NULL, NULL, G_PARAM_READABLE); +- properties[PROP_VALUE] = +- g_param_spec_string ("value", NULL, NULL, NULL, G_PARAM_READABLE); +- properties[PROP_TYPE] = +- g_param_spec_string ("type", NULL, NULL, NULL, G_PARAM_READABLE); +- properties[PROP_DEFAULT_VALUE] = +- g_param_spec_string ("default-value", NULL, NULL, NULL, G_PARAM_READABLE); +- +- g_object_class_install_properties (gobject_class, N_PROPS, properties); +-} +- +-static void +-settings_key_init (SettingsKey *self) +-{ +-} +- +-static SettingsKey * +-settings_key_new (GSettings *settings, +- GSettingsSchemaKey *key) +-{ +- SettingsKey *result = g_object_new (SETTINGS_TYPE_KEY, NULL); +- +- result->settings = g_object_ref (settings); +- result->key = g_settings_schema_key_ref (key); +- +- return result; +-} ++#include "settings-key.h" + + static void + item_value_changed (GtkEditableLabel *label, +@@ -153,6 +22,7 @@ item_value_changed (GtkEditableLabel *label, + GtkColumnViewCell *cell) + { + SettingsKey *self; ++ GSettingsSchemaKey *key; + const char *text; + const GVariantType *type; + GVariant *variant; +@@ -163,9 +33,10 @@ item_value_changed (GtkEditableLabel *label, + text = gtk_editable_get_text (GTK_EDITABLE (label)); + + self = gtk_column_view_cell_get_item (cell); ++ key = settings_key_get_key (self); + +- type = g_settings_schema_key_get_value_type (self->key); +- name = g_settings_schema_key_get_name (self->key); ++ type = g_settings_schema_key_get_value_type (key); ++ name = g_settings_schema_key_get_name (key); + + variant = g_variant_parse (type, text, NULL, NULL, &error); + if (!variant) +@@ -175,13 +46,13 @@ item_value_changed (GtkEditableLabel *label, + goto revert; + } + +- if (!g_settings_schema_key_range_check (self->key, variant)) ++ if (!g_settings_schema_key_range_check (key, variant)) + { + g_warning ("Not a valid value for %s", name); + goto revert; + } + +- g_settings_set_value (self->settings, name, variant); ++ g_settings_set_value (settings_key_get_settings (self), name, variant); + g_variant_unref (variant); + return; + +diff --git a/demos/gtk-demo/listview_settings2.c b/demos/gtk-demo/listview_settings2.c +new file mode 100644 +index 0000000000000000000000000000000000000000..4f3e87fb68f69c7a2fbf44c5276599bfa06cd704 +--- /dev/null ++++ b/demos/gtk-demo/listview_settings2.c +@@ -0,0 +1,230 @@ ++/* Lists/Settings v2 ++ * #Keywords: GtkListHeaderFactory, GtkSectionModel ++ * ++ * This demo shows a settings viewer for GSettings. ++ * ++ * It demonstrates how to implement support for sections with GtkListView. ++ * ++ * It also shows how to quickly flatten a large tree of items into a list ++ * that can be filtered to find the itmes one is looking for. ++ */ ++ ++#include ++ ++#include "settings-key.h" ++ ++static void ++item_value_changed (GtkEditableLabel *label, ++ GParamSpec *pspec, ++ GtkColumnViewCell *cell) ++{ ++ SettingsKey *self; ++ GSettingsSchemaKey *key; ++ const char *text; ++ const GVariantType *type; ++ GVariant *variant; ++ GError *error = NULL; ++ const char *name; ++ char *value; ++ ++ text = gtk_editable_get_text (GTK_EDITABLE (label)); ++ ++ self = gtk_column_view_cell_get_item (cell); ++ key = settings_key_get_key (self); ++ ++ type = g_settings_schema_key_get_value_type (key); ++ name = g_settings_schema_key_get_name (key); ++ ++ variant = g_variant_parse (type, text, NULL, NULL, &error); ++ if (!variant) ++ { ++ g_warning ("%s", error->message); ++ g_clear_error (&error); ++ goto revert; ++ } ++ ++ if (!g_settings_schema_key_range_check (key, variant)) ++ { ++ g_warning ("Not a valid value for %s", name); ++ goto revert; ++ } ++ ++ g_settings_set_value (settings_key_get_settings (self), name, variant); ++ g_variant_unref (variant); ++ return; ++ ++revert: ++ gtk_widget_error_bell (GTK_WIDGET (label)); ++ ++ g_object_get (self, "value", &value, NULL); ++ gtk_editable_set_text (GTK_EDITABLE (label), value); ++ g_free (value); ++} ++ ++static int ++strvcmp (gconstpointer p1, ++ gconstpointer p2) ++{ ++ const char * const *s1 = p1; ++ const char * const *s2 = p2; ++ ++ return strcmp (*s1, *s2); ++} ++ ++static gpointer ++map_settings_to_keys (gpointer item, ++ gpointer unused) ++{ ++ GSettings *settings = item; ++ GSettingsSchema *schema; ++ GListStore *store; ++ char **keys; ++ guint i; ++ ++ g_object_get (settings, "settings-schema", &schema, NULL); ++ ++ store = g_list_store_new (SETTINGS_TYPE_KEY); ++ ++ keys = g_settings_schema_list_keys (schema); ++ ++ for (i = 0; keys[i] != NULL; i++) ++ { ++ GSettingsSchemaKey *almost_there = g_settings_schema_get_key (schema, keys[i]); ++ SettingsKey *finally = settings_key_new (settings, almost_there); ++ g_list_store_append (store, finally); ++ g_object_unref (finally); ++ g_settings_schema_key_unref (almost_there); ++ } ++ ++ g_strfreev (keys); ++ g_settings_schema_unref (schema); ++ g_object_unref (settings); ++ ++ return store; ++} ++ ++static GListModel * ++create_settings_model (gpointer item, ++ gpointer unused) ++{ ++ GSettings *settings = item; ++ char **schemas; ++ GListStore *result; ++ guint i; ++ ++ if (settings == NULL) ++ { ++ g_settings_schema_source_list_schemas (g_settings_schema_source_get_default (), ++ TRUE, ++ &schemas, ++ NULL); ++ } ++ else ++ { ++ schemas = g_settings_list_children (settings); ++ } ++ ++ if (schemas == NULL || schemas[0] == NULL) ++ { ++ g_free (schemas); ++ return NULL; ++ } ++ ++ qsort (schemas, g_strv_length (schemas), sizeof (char *), strvcmp); ++ ++ result = g_list_store_new (G_TYPE_SETTINGS); ++ for (i = 0; schemas[i] != NULL; i++) ++ { ++ GSettings *child; ++ ++ if (settings == NULL) ++ child = g_settings_new (schemas[i]); ++ else ++ child = g_settings_get_child (settings, schemas[i]); ++ ++ g_list_store_append (result, child); ++ g_object_unref (child); ++ } ++ ++ g_strfreev (schemas); ++ ++ return G_LIST_MODEL (result); ++} ++ ++static void ++search_enabled (GtkSearchEntry *entry) ++{ ++ gtk_editable_set_text (GTK_EDITABLE (entry), ""); ++} ++ ++static void ++stop_search (GtkSearchEntry *entry, ++ gpointer data) ++{ ++ gtk_editable_set_text (GTK_EDITABLE (entry), ""); ++} ++ ++static GtkWidget *window = NULL; ++ ++GtkWidget * ++do_listview_settings2 (GtkWidget *do_widget) ++{ ++ if (window == NULL) ++ { ++ GtkListView *listview; ++ GListModel *model; ++ GtkTreeListModel *treemodel; ++ GtkNoSelection *selection; ++ GtkBuilderScope *scope; ++ GtkBuilder *builder; ++ GError *error = NULL; ++ GtkFilter *filter; ++ ++ g_type_ensure (SETTINGS_TYPE_KEY); ++ ++ scope = gtk_builder_cscope_new (); ++ gtk_builder_cscope_add_callback (scope, search_enabled); ++ gtk_builder_cscope_add_callback (scope, stop_search); ++ gtk_builder_cscope_add_callback (scope, settings_key_get_search_string); ++ gtk_builder_cscope_add_callback (scope, item_value_changed); ++ ++ builder = gtk_builder_new (); ++ gtk_builder_set_scope (builder, scope); ++ g_object_unref (scope); ++ ++ gtk_builder_add_from_resource (builder, "/listview_settings2/listview_settings2.ui", &error); ++ g_assert_no_error (error); ++ ++ window = GTK_WIDGET (gtk_builder_get_object (builder, "window")); ++ gtk_window_set_display (GTK_WINDOW (window), ++ gtk_widget_get_display (do_widget)); ++ g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); ++ ++ listview = GTK_LIST_VIEW (gtk_builder_get_object (builder, "listview")); ++ filter = GTK_FILTER (gtk_builder_get_object (builder, "filter")); ++ ++ model = create_settings_model (NULL, NULL); ++ treemodel = gtk_tree_list_model_new (model, ++ TRUE, ++ TRUE, ++ create_settings_model, ++ NULL, ++ NULL); ++ model = G_LIST_MODEL (gtk_map_list_model_new (G_LIST_MODEL (treemodel), map_settings_to_keys, NULL, NULL)); ++ model = G_LIST_MODEL (gtk_flatten_list_model_new (model)); ++ model = G_LIST_MODEL (gtk_filter_list_model_new (model, g_object_ref (filter))); ++ selection = gtk_no_selection_new (model); ++ ++ gtk_list_view_set_model (GTK_LIST_VIEW (listview), GTK_SELECTION_MODEL (selection)); ++ g_object_unref (selection); ++ ++ g_object_unref (builder); ++ } ++ ++ if (!gtk_widget_get_visible (window)) ++ gtk_widget_set_visible (window, TRUE); ++ else ++ gtk_window_destroy (GTK_WINDOW (window)); ++ ++ return window; ++} +diff --git a/demos/gtk-demo/listview_settings2.ui b/demos/gtk-demo/listview_settings2.ui +new file mode 100644 +index 0000000000000000000000000000000000000000..0600f6358263f0e21f742fb6798e794c455a4117 +--- /dev/null ++++ b/demos/gtk-demo/listview_settings2.ui +@@ -0,0 +1,130 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ Settings ++ 640 ++ 480 ++ ++ ++ ++ ++ system-search-symbolic ++ ++ ++ ++ ++ ++ ++ vertical ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ 1 ++ ++ ++ ++ ++ ++ ++ ++ ]]> ++ ++ ++ ++ ++ ++ ++ ++ ++ ]]> ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build +index c03beb39409d18db2e93801b7d6c6d5c4eced859..976c3ec997da08737259f478d72840f84abaa523 100644 +--- a/demos/gtk-demo/meson.build ++++ b/demos/gtk-demo/meson.build +@@ -57,6 +57,7 @@ demos = files([ + 'listview_minesweeper.c', + 'listview_selections.c', + 'listview_settings.c', ++ 'listview_settings2.c', + 'listview_ucd.c', + 'listview_weather.c', + 'listview_words.c', +@@ -131,6 +132,7 @@ extra_demo_sources = files([ + 'demo4widget.c', + 'pixbufpaintable.c', + 'script-names.c', ++ 'settings-key.c', + 'unicode-names.c', + 'suggestionentry.c', + 'language-names.c', +diff --git a/demos/gtk-demo/settings-key.c b/demos/gtk-demo/settings-key.c +new file mode 100644 +index 0000000000000000000000000000000000000000..13c0a5d559f35d8d8e9a91529008d563d8910b8e +--- /dev/null ++++ b/demos/gtk-demo/settings-key.c +@@ -0,0 +1,165 @@ ++#include "settings-key.h" ++ ++/* Create an object that wraps GSettingsSchemaKey because that's a boxed type */ ++struct _SettingsKey ++{ ++ GObject parent_instance; ++ ++ GSettings *settings; ++ GSettingsSchemaKey *key; ++}; ++ ++enum { ++ PROP_0, ++ PROP_NAME, ++ PROP_SETTINGS, ++ PROP_SUMMARY, ++ PROP_DESCRIPTION, ++ PROP_VALUE, ++ PROP_TYPE, ++ PROP_DEFAULT_VALUE, ++ ++ N_PROPS ++}; ++ ++G_DEFINE_TYPE (SettingsKey, settings_key, G_TYPE_OBJECT); ++static GParamSpec *properties[N_PROPS] = { NULL, }; ++ ++static void ++settings_key_get_property (GObject *object, ++ guint property_id, ++ GValue *value, ++ GParamSpec *pspec) ++{ ++ SettingsKey *self = SETTINGS_KEY (object); ++ ++ switch (property_id) ++ { ++ case PROP_DESCRIPTION: ++ g_value_set_string (value, g_settings_schema_key_get_description (self->key)); ++ break; ++ ++ case PROP_NAME: ++ g_value_set_string (value, g_settings_schema_key_get_name (self->key)); ++ break; ++ ++ case PROP_SUMMARY: ++ g_value_set_string (value, g_settings_schema_key_get_summary (self->key)); ++ break; ++ ++ case PROP_VALUE: ++ { ++ GVariant *variant = g_settings_get_value (self->settings, g_settings_schema_key_get_name (self->key)); ++ g_value_take_string (value, g_variant_print (variant, FALSE)); ++ g_variant_unref (variant); ++ } ++ break; ++ ++ case PROP_TYPE: ++ { ++ const GVariantType *type = g_settings_schema_key_get_value_type (self->key); ++ g_value_set_string (value, g_variant_type_peek_string (type)); ++ } ++ break; ++ ++ case PROP_DEFAULT_VALUE: ++ { ++ GVariant *variant = g_settings_schema_key_get_default_value (self->key); ++ g_value_take_string (value, g_variant_print (variant, FALSE)); ++ g_variant_unref (variant); ++ } ++ break; ++ ++ case PROP_SETTINGS: ++ g_value_set_object (value, self->settings); ++ break; ++ ++ default: ++ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); ++ break; ++ } ++} ++ ++static void ++settings_key_finalize (GObject *object) ++{ ++ SettingsKey *self = SETTINGS_KEY (object); ++ ++ g_object_unref (self->settings); ++ g_settings_schema_key_unref (self->key); ++ ++ G_OBJECT_CLASS (settings_key_parent_class)->finalize (object); ++} ++ ++static void ++settings_key_class_init (SettingsKeyClass *klass) ++{ ++ GObjectClass *gobject_class = G_OBJECT_CLASS (klass); ++ ++ gobject_class->finalize = settings_key_finalize; ++ gobject_class->get_property = settings_key_get_property; ++ ++ properties[PROP_DESCRIPTION] = ++ g_param_spec_string ("description", NULL, NULL, NULL, G_PARAM_READABLE); ++ properties[PROP_NAME] = ++ g_param_spec_string ("name", NULL, NULL, NULL, G_PARAM_READABLE); ++ properties[PROP_SETTINGS] = ++ g_param_spec_object ("settings", NULL, NULL, G_TYPE_SETTINGS, G_PARAM_READABLE); ++ properties[PROP_SUMMARY] = ++ g_param_spec_string ("summary", NULL, NULL, NULL, G_PARAM_READABLE); ++ properties[PROP_VALUE] = ++ g_param_spec_string ("value", NULL, NULL, NULL, G_PARAM_READABLE); ++ properties[PROP_TYPE] = ++ g_param_spec_string ("type", NULL, NULL, NULL, G_PARAM_READABLE); ++ properties[PROP_DEFAULT_VALUE] = ++ g_param_spec_string ("default-value", NULL, NULL, NULL, G_PARAM_READABLE); ++ ++ g_object_class_install_properties (gobject_class, N_PROPS, properties); ++} ++ ++static void ++settings_key_init (SettingsKey *self) ++{ ++} ++ ++SettingsKey * ++settings_key_new (GSettings *settings, ++ GSettingsSchemaKey *key) ++{ ++ SettingsKey *result = g_object_new (SETTINGS_TYPE_KEY, NULL); ++ ++ result->settings = g_object_ref (settings); ++ result->key = g_settings_schema_key_ref (key); ++ ++ return result; ++} ++ ++GSettingsSchemaKey * ++settings_key_get_key (SettingsKey *self) ++{ ++ return self->key; ++} ++ ++GSettings * ++settings_key_get_settings (SettingsKey *self) ++{ ++ return self->settings; ++} ++ ++char * ++settings_key_get_search_string (SettingsKey *self) ++{ ++ char *schema, *result; ++ ++ g_object_get (self->settings, "schema-id", &schema, NULL); ++ ++ result = g_strconcat (g_settings_schema_key_get_name (self->key), " ", ++ g_settings_schema_key_get_summary (self->key), " ", ++ schema, ++ NULL); ++ ++ g_free (schema); ++ ++ return result; ++} ++ +diff --git a/demos/gtk-demo/settings-key.h b/demos/gtk-demo/settings-key.h +new file mode 100644 +index 0000000000000000000000000000000000000000..5260a84516873f2e84cab6affb41f90784daf174 +--- /dev/null ++++ b/demos/gtk-demo/settings-key.h +@@ -0,0 +1,17 @@ ++#pragma once ++ ++#include ++ ++#include ++ ++/* Create an object that wraps GSettingsSchemaKey because that's a boxed type */ ++typedef struct _SettingsKey SettingsKey; ++#define SETTINGS_TYPE_KEY (settings_key_get_type ()) ++G_DECLARE_FINAL_TYPE (SettingsKey, settings_key, SETTINGS, KEY, GObject); ++ ++SettingsKey * settings_key_new (GSettings *settings, ++ GSettingsSchemaKey *key); ++ ++GSettingsSchemaKey * settings_key_get_key (SettingsKey *self); ++GSettings * settings_key_get_settings (SettingsKey *self); ++char * settings_key_get_search_string (SettingsKey *self); +diff --git a/gtk/gtk.h b/gtk/gtk.h +index a3973437f3e68df9b3fdc73d2c73a1b903a7b546..8ad4e38902a844c4ae367f29877cce627be921ed 100644 +--- a/gtk/gtk.h ++++ b/gtk/gtk.h +@@ -174,6 +174,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -225,6 +226,7 @@ + #include + #include + #include ++#include + #include + #include + #include +diff --git a/gtk/gtkfilterlistmodel.c b/gtk/gtkfilterlistmodel.c +index 1804b7d12a37d20519c62a926ded3b20a902914d..7e53e52c1190a02d96ac92fbc40710e7426acfa3 100644 +--- a/gtk/gtkfilterlistmodel.c ++++ b/gtk/gtkfilterlistmodel.c +@@ -23,6 +23,7 @@ + + #include "gtkbitset.h" + #include "gtkprivate.h" ++#include "gtksectionmodelprivate.h" + + /** + * GtkFilterListModel: +@@ -135,8 +136,67 @@ gtk_filter_list_model_model_init (GListModelInterface *iface) + iface->get_item = gtk_filter_list_model_get_item; + } + ++static void ++gtk_filter_list_model_get_section (GtkSectionModel *model, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (model); ++ guint n_items; ++ guint pos, start, end; ++ ++ switch (self->strictness) ++ { ++ case GTK_FILTER_MATCH_NONE: ++ *out_start = 0; ++ *out_end = G_MAXUINT; ++ return; ++ ++ case GTK_FILTER_MATCH_ALL: ++ gtk_list_model_get_section (self->model, position, out_start, out_end); ++ return; ++ ++ case GTK_FILTER_MATCH_SOME: ++ n_items = gtk_bitset_get_size (self->matches); ++ if (position >= n_items) ++ { ++ *out_start = n_items; ++ *out_end = G_MAXUINT; ++ return; ++ } ++ if (!GTK_IS_SECTION_MODEL (self->model)) ++ { ++ *out_start = 0; ++ *out_end = n_items; ++ return; ++ } ++ break; ++ ++ default: ++ g_assert_not_reached (); ++ } ++ ++ /* if we get here, we have a section model, and are MATCH_SOME */ ++ ++ pos = gtk_bitset_get_nth (self->matches, position); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (self->model), pos, &start, &end); ++ if (start == 0) ++ *out_start = 0; ++ else ++ *out_start = gtk_bitset_get_size_in_range (self->matches, 0, start - 1); ++ *out_end = *out_start + gtk_bitset_get_size_in_range (self->matches, start, end - 1); ++} ++ ++static void ++gtk_filter_list_model_section_model_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_filter_list_model_get_section; ++} ++ + G_DEFINE_TYPE_WITH_CODE (GtkFilterListModel, gtk_filter_list_model, G_TYPE_OBJECT, +- G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_filter_list_model_model_init)) ++ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_filter_list_model_model_init) ++ G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL, gtk_filter_list_model_section_model_init)) + + static gboolean + gtk_filter_list_model_run_filter_on_item (GtkFilterListModel *self, +@@ -164,7 +224,7 @@ gtk_filter_list_model_run_filter (GtkFilterListModel *self, + gboolean more; + + g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); +- ++ + if (self->pending == NULL) + return; + +@@ -355,7 +415,7 @@ gtk_filter_list_model_set_property (GObject *object, + } + } + +-static void ++static void + gtk_filter_list_model_get_property (GObject *object, + guint prop_id, + GValue *value, +@@ -503,7 +563,7 @@ gtk_filter_list_model_refilter (GtkFilterListModel *self, + case GTK_FILTER_MATCH_SOME: + { + GtkBitset *old, *pending; +- ++ + if (self->matches == NULL) + { + if (self->strictness == GTK_FILTER_MATCH_ALL) +diff --git a/gtk/gtkflattenlistmodel.c b/gtk/gtkflattenlistmodel.c +index 04cf04c6fd0ee3a8694a8d5b85ab5a1fa6d9ec5d..f80e80ff6d5512fc1dcb1d39dc358039f2f2b8d4 100644 +--- a/gtk/gtkflattenlistmodel.c ++++ b/gtk/gtkflattenlistmodel.c +@@ -21,8 +21,8 @@ + + #include "gtkflattenlistmodel.h" + ++#include "gtksectionmodel.h" + #include "gtkrbtreeprivate.h" +-#include "gtkprivate.h" + + /** + * GtkFlattenListModel: +@@ -200,8 +200,39 @@ gtk_flatten_list_model_model_init (GListModelInterface *iface) + iface->get_item = gtk_flatten_list_model_get_item; + } + ++static void ++gtk_flatten_list_model_get_section (GtkSectionModel *model, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkFlattenListModel *self = GTK_FLATTEN_LIST_MODEL (model); ++ FlattenNode *node; ++ guint model_pos; ++ ++ node = gtk_flatten_list_model_get_nth (self->items, position, &model_pos); ++ if (node == NULL) ++ { ++ *out_start = gtk_flatten_list_model_get_n_items (G_LIST_MODEL (self)); ++ *out_end = G_MAXUINT; ++ return; ++ } ++ ++ *out_start = position - model_pos; ++ *out_end = position - model_pos + g_list_model_get_n_items (node->model); ++} ++ ++static void ++gtk_flatten_list_model_section_model_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_flatten_list_model_get_section; ++} ++ + G_DEFINE_TYPE_WITH_CODE (GtkFlattenListModel, gtk_flatten_list_model, G_TYPE_OBJECT, +- G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_flatten_list_model_model_init)) ++ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, ++ gtk_flatten_list_model_model_init) ++ G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL, ++ gtk_flatten_list_model_section_model_init)) + + static void + gtk_flatten_list_model_items_changed_cb (GListModel *model, +@@ -433,7 +464,7 @@ gtk_flatten_list_model_class_init (GtkFlattenListModelClass *class) + properties[PROP_MODEL] = + g_param_spec_object ("model", NULL, NULL, + G_TYPE_LIST_MODEL, +- GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); ++ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkFlattenListModel:n-items: +diff --git a/gtk/gtkgridview.c b/gtk/gtkgridview.c +index ead43a8c8bc5a8714bce880b69d60e0a7a2a0380..274f34c3c9502a4e954d549760d10f9c673abb8f 100644 +--- a/gtk/gtkgridview.c ++++ b/gtk/gtkgridview.c +@@ -264,8 +264,7 @@ gtk_grid_view_is_inert (GtkGridView *self) + GtkWidget *widget = GTK_WIDGET (self); + + return !gtk_widget_get_visible (widget) || +- gtk_widget_get_root (widget) == NULL || +- self->factory == NULL; ++ gtk_widget_get_root (widget) == NULL; + } + + static void +@@ -886,7 +885,7 @@ gtk_grid_view_size_allocate (GtkWidget *widget, + { + GtkListTile *filler; + tile = gtk_list_item_manager_get_last (self->item_manager); +- filler = gtk_list_tile_split (self->item_manager, tile, tile->n_items); ++ filler = gtk_list_tile_append_filler (self->item_manager, tile); + gtk_list_tile_set_area_position (self->item_manager, + filler, + column_start (self, xspacing, i), +@@ -1330,19 +1329,12 @@ void + gtk_grid_view_set_factory (GtkGridView *self, + GtkListItemFactory *factory) + { +- gboolean was_inert; +- + g_return_if_fail (GTK_IS_GRID_VIEW (self)); + g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); + +- was_inert = gtk_grid_view_is_inert (self); +- + if (!g_set_object (&self->factory, factory)) + return; + +- if (!was_inert || !gtk_grid_view_is_inert (self)) +- gtk_grid_view_update_factories (self); +- + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]); + } + +diff --git a/gtk/gtklistbase.c b/gtk/gtklistbase.c +index 0a7dbb0b025d7080dd67783cddf409c31bda774c..00ac9c571d9b8dbcf2e8c67b9d402689df2941d9 100644 +--- a/gtk/gtklistbase.c ++++ b/gtk/gtklistbase.c +@@ -1959,11 +1959,25 @@ gtk_list_base_split_func (GtkWidget *widget, + } + + static GtkListItemBase * +-gtk_list_base_create_widget_func (GtkWidget *widget) ++gtk_list_base_create_list_widget_func (GtkWidget *widget) + { + return GTK_LIST_BASE_GET_CLASS (widget)->create_list_widget (GTK_LIST_BASE (widget)); + } + ++static void ++gtk_list_base_prepare_section_func (GtkWidget *widget, ++ GtkListTile *tile, ++ guint pos) ++{ ++ GTK_LIST_BASE_GET_CLASS (widget)->prepare_section (GTK_LIST_BASE (widget), tile, pos); ++} ++ ++static GtkListHeaderBase * ++gtk_list_base_create_header_widget_func (GtkWidget *widget) ++{ ++ return GTK_LIST_BASE_GET_CLASS (widget)->create_header_widget (GTK_LIST_BASE (widget)); ++} ++ + static void + gtk_list_base_init_real (GtkListBase *self, + GtkListBaseClass *g_class) +@@ -1973,7 +1987,9 @@ gtk_list_base_init_real (GtkListBase *self, + + priv->item_manager = gtk_list_item_manager_new (GTK_WIDGET (self), + gtk_list_base_split_func, +- gtk_list_base_create_widget_func); ++ gtk_list_base_create_list_widget_func, ++ gtk_list_base_prepare_section_func, ++ gtk_list_base_create_header_widget_func); + priv->anchor = gtk_list_item_tracker_new (priv->item_manager); + priv->anchor_side_along = GTK_PACK_START; + priv->anchor_side_across = GTK_PACK_START; +diff --git a/gtk/gtklistbaseprivate.h b/gtk/gtklistbaseprivate.h +index 7aa596ffde423557ea924003305aeb65531f1b60..fad3872cdc09433de5e255a687a3c98fe58640bf 100644 +--- a/gtk/gtklistbaseprivate.h ++++ b/gtk/gtklistbaseprivate.h +@@ -37,6 +37,10 @@ struct _GtkListBaseClass + GtkListTile *tile, + guint n_items); + GtkListItemBase * (* create_list_widget) (GtkListBase *self); ++ void (* prepare_section) (GtkListBase *self, ++ GtkListTile *tile, ++ guint position); ++ GtkListHeaderBase * (* create_header_widget) (GtkListBase *self); + + gboolean (* get_allocation) (GtkListBase *self, + guint pos, +diff --git a/gtk/gtklistheader.c b/gtk/gtklistheader.c +new file mode 100644 +index 0000000000000000000000000000000000000000..62cfd5b4c9b63cd3835c14a8298ae72ce8ccc509 +--- /dev/null ++++ b/gtk/gtklistheader.c +@@ -0,0 +1,381 @@ ++/* ++ * Copyright © 2023 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 "gtklistheaderprivate.h" ++ ++/** ++ * GtkListHeader: ++ * ++ * `GtkListHeader` is used by list widgets to represent the headers they ++ * display. ++ * ++ * The `GtkListHeader`s are managed just like [class@gtk.ListItem]s via ++ * their factory, but provide a different set of properties suitable for ++ * managing the header instead of individual items. ++ * ++ * Since: 4.12 ++ */ ++ ++enum ++{ ++ PROP_0, ++ PROP_CHILD, ++ PROP_END, ++ PROP_ITEM, ++ PROP_N_ITEMS, ++ PROP_START, ++ ++ N_PROPS ++}; ++ ++G_DEFINE_TYPE (GtkListHeader, gtk_list_header, G_TYPE_OBJECT) ++ ++static GParamSpec *properties[N_PROPS] = { NULL, }; ++ ++static void ++gtk_list_header_dispose (GObject *object) ++{ ++ GtkListHeader *self = GTK_LIST_HEADER (object); ++ ++ g_assert (self->owner == NULL); /* would hold a reference */ ++ g_clear_object (&self->child); ++ ++ G_OBJECT_CLASS (gtk_list_header_parent_class)->dispose (object); ++} ++ ++static void ++gtk_list_header_get_property (GObject *object, ++ guint property_id, ++ GValue *value, ++ GParamSpec *pspec) ++{ ++ GtkListHeader *self = GTK_LIST_HEADER (object); ++ ++ switch (property_id) ++ { ++ case PROP_CHILD: ++ g_value_set_object (value, self->child); ++ break; ++ ++ case PROP_END: ++ if (self->owner) ++ g_value_set_uint (value, gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self->owner))); ++ else ++ g_value_set_uint (value, GTK_INVALID_LIST_POSITION); ++ break; ++ ++ case PROP_ITEM: ++ if (self->owner) ++ g_value_set_object (value, gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self->owner))); ++ break; ++ ++ case PROP_N_ITEMS: ++ g_value_set_uint (value, gtk_list_header_get_n_items (self)); ++ break; ++ ++ case PROP_START: ++ if (self->owner) ++ g_value_set_uint (value, gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self->owner))); ++ else ++ g_value_set_uint (value, GTK_INVALID_LIST_POSITION); ++ break; ++ ++ default: ++ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); ++ break; ++ } ++} ++ ++static void ++gtk_list_header_set_property (GObject *object, ++ guint property_id, ++ const GValue *value, ++ GParamSpec *pspec) ++{ ++ GtkListHeader *self = GTK_LIST_HEADER (object); ++ ++ switch (property_id) ++ { ++ case PROP_CHILD: ++ gtk_list_header_set_child (self, g_value_get_object (value)); ++ break; ++ ++ default: ++ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); ++ break; ++ } ++} ++ ++static void ++gtk_list_header_class_init (GtkListHeaderClass *klass) ++{ ++ GObjectClass *gobject_class = G_OBJECT_CLASS (klass); ++ ++ gobject_class->dispose = gtk_list_header_dispose; ++ gobject_class->get_property = gtk_list_header_get_property; ++ gobject_class->set_property = gtk_list_header_set_property; ++ ++ /** ++ * GtkListHeader:child: (attributes org.gtk.Property.get=gtk_list_header_get_child org.gtk.Property.set=gtk_list_header_set_child) ++ * ++ * Widget used for display. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_CHILD] = ++ g_param_spec_object ("child", NULL, NULL, ++ GTK_TYPE_WIDGET, ++ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ ++ /** ++ * GtkListHeader:end: (attributes org.gtk.Property.get=gtk_list_header_get_end) ++ * ++ * The first position no longer part of this section. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_END] = ++ g_param_spec_uint ("end", NULL, NULL, ++ 0, G_MAXUINT, GTK_INVALID_LIST_POSITION, ++ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ ++ /** ++ * GtkListHeader:item: (attributes org.gtk.Property.get=gtk_list_header_get_item) ++ * ++ * The item at the start of the section. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_ITEM] = ++ g_param_spec_object ("item", NULL, NULL, ++ G_TYPE_OBJECT, ++ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ ++ /** ++ * GtkListHeader:n-items: (attributes org.gtk.Property.get=gtk_list_header_get_n_items) ++ * ++ * Number of items in this section. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_N_ITEMS] = ++ g_param_spec_uint ("n-items", NULL, NULL, ++ 0, G_MAXUINT, 0, ++ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ ++ /** ++ * GtkListHeader:start: (attributes org.gtk.Property.get=gtk_list_header_get_start) ++ * ++ * First position of items in this section. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_START] = ++ g_param_spec_uint ("start", NULL, NULL, ++ 0, G_MAXUINT, GTK_INVALID_LIST_POSITION, ++ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ ++ g_object_class_install_properties (gobject_class, N_PROPS, properties); ++} ++ ++static void ++gtk_list_header_init (GtkListHeader *self) ++{ ++} ++ ++GtkListHeader * ++gtk_list_header_new (void) ++{ ++ return g_object_new (GTK_TYPE_LIST_HEADER, NULL); ++} ++ ++void ++gtk_list_header_do_notify (GtkListHeader *list_header, ++ gboolean notify_item, ++ gboolean notify_start, ++ gboolean notify_end, ++ gboolean notify_n_items) ++{ ++ GObject *object = G_OBJECT (list_header); ++ ++ if (notify_item) ++ g_object_notify_by_pspec (object, properties[PROP_ITEM]); ++ if (notify_start) ++ g_object_notify_by_pspec (object, properties[PROP_START]); ++ if (notify_end) ++ g_object_notify_by_pspec (object, properties[PROP_END]); ++ if (notify_n_items) ++ g_object_notify_by_pspec (object, properties[PROP_N_ITEMS]); ++} ++ ++/** ++ * gtk_list_header_get_item: (attributes org.gtk.Method.get_property=item) ++ * @self: a `GtkListHeader` ++ * ++ * Gets the model item at the start of the section. ++ * This is the item that occupies the list model at position ++ * [property@Gtk.ListHeader:start]. ++ * ++ * If @self is unbound, this function returns %NULL. ++ * ++ * Returns: (nullable) (transfer none) (type GObject): The item displayed ++ * ++ * Since: 4.12 ++ **/ ++gpointer ++gtk_list_header_get_item (GtkListHeader *self) ++{ ++ g_return_val_if_fail (GTK_IS_LIST_HEADER (self), NULL); ++ ++ if (self->owner) ++ return gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self->owner)); ++ else ++ return NULL; ++} ++ ++/** ++ * gtk_list_header_get_child: (attributes org.gtk.Method.get_property=child) ++ * @self: a `GtkListHeader` ++ * ++ * Gets the child previously set via gtk_list_header_set_child() or ++ * %NULL if none was set. ++ * ++ * Returns: (transfer none) (nullable): The child ++ * ++ * Since: 4.12 ++ */ ++GtkWidget * ++gtk_list_header_get_child (GtkListHeader *self) ++{ ++ g_return_val_if_fail (GTK_IS_LIST_HEADER (self), NULL); ++ ++ return self->child; ++} ++ ++/** ++ * gtk_list_header_set_child: (attributes org.gtk.Method.set_property=child) ++ * @self: a `GtkListHeader` ++ * @child: (nullable): The list item's child or %NULL to unset ++ * ++ * Sets the child to be used for this listitem. ++ * ++ * This function is typically called by applications when ++ * setting up a header so that the widget can be reused when ++ * binding it multiple times. ++ * ++ * Since: 4.12 ++ */ ++void ++gtk_list_header_set_child (GtkListHeader *self, ++ GtkWidget *child) ++{ ++ g_return_if_fail (GTK_IS_LIST_HEADER (self)); ++ g_return_if_fail (child == NULL || gtk_widget_get_parent (child) == NULL); ++ ++ if (self->child == child) ++ return; ++ ++ g_clear_object (&self->child); ++ ++ if (child) ++ { ++ g_object_ref_sink (child); ++ self->child = child; ++ } ++ ++ if (self->owner) ++ gtk_list_header_widget_set_child (self->owner, child); ++ ++ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CHILD]); ++} ++ ++/** ++ * gtk_list_header_get_start: (attributes org.gtk.Method.get_property=start) ++ * @self: a `GtkListHeader` ++ * ++ * Gets the start position in the model of the section that @self is ++ * currently the header for. ++ * ++ * If @self is unbound, %GTK_INVALID_LIST_POSITION is returned. ++ * ++ * Returns: The start position of the section ++ * ++ * Since: 4.12 ++ */ ++guint ++gtk_list_header_get_start (GtkListHeader *self) ++{ ++ g_return_val_if_fail (GTK_IS_LIST_HEADER (self), GTK_INVALID_LIST_POSITION); ++ ++ if (self->owner) ++ return gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self->owner)); ++ else ++ return GTK_INVALID_LIST_POSITION; ++} ++ ++/** ++ * gtk_list_header_get_end: (attributes org.gtk.Method.get_property=end) ++ * @self: a `GtkListHeader` ++ * ++ * Gets the end position in the model of the section that @self is ++ * currently the header for. ++ * ++ * If @self is unbound, %GTK_INVALID_LIST_POSITION is returned. ++ * ++ * Returns: The end position of the section ++ * ++ * Since: 4.12 ++ */ ++guint ++gtk_list_header_get_end (GtkListHeader *self) ++{ ++ g_return_val_if_fail (GTK_IS_LIST_HEADER (self), GTK_INVALID_LIST_POSITION); ++ ++ if (self->owner) ++ return gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self->owner)); ++ else ++ return GTK_INVALID_LIST_POSITION; ++} ++ ++/** ++ * gtk_list_header_get_n_items: (attributes org.gtk.Method.get_property=n-items) ++ * @self: a `GtkListHeader` ++ * ++ * Gets the the number of items in the section. ++ * ++ * If @self is unbound, 0 is returned. ++ * ++ * Returns: The number of items in the section ++ * ++ * Since: 4.12 ++ */ ++guint ++gtk_list_header_get_n_items (GtkListHeader *self) ++{ ++ g_return_val_if_fail (GTK_IS_LIST_HEADER (self), GTK_INVALID_LIST_POSITION); ++ ++ if (self->owner) ++ return gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self->owner)) - ++ gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self->owner)); ++ else ++ return 0; ++} ++ +diff --git a/gtk/gtklistheader.h b/gtk/gtklistheader.h +new file mode 100644 +index 0000000000000000000000000000000000000000..87862bab5967eff0af92996dd20b79fe1edcd5cc +--- /dev/null ++++ b/gtk/gtklistheader.h +@@ -0,0 +1,50 @@ ++/* ++ * Copyright © 2023 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 ++ */ ++ ++#pragma once ++ ++#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) ++#error "Only can be included directly." ++#endif ++ ++#include ++ ++G_BEGIN_DECLS ++ ++#define GTK_TYPE_LIST_HEADER (gtk_list_header_get_type ()) ++GDK_AVAILABLE_IN_4_12 ++GDK_DECLARE_INTERNAL_TYPE (GtkListHeader, gtk_list_header, GTK, LIST_HEADER, GObject) ++ ++GDK_AVAILABLE_IN_4_12 ++gpointer gtk_list_header_get_item (GtkListHeader *self); ++GDK_AVAILABLE_IN_4_12 ++guint gtk_list_header_get_start (GtkListHeader *self) G_GNUC_PURE; ++GDK_AVAILABLE_IN_4_12 ++guint gtk_list_header_get_end (GtkListHeader *self) G_GNUC_PURE; ++GDK_AVAILABLE_IN_4_12 ++guint gtk_list_header_get_n_items (GtkListHeader *self) G_GNUC_PURE; ++ ++GDK_AVAILABLE_IN_4_12 ++void gtk_list_header_set_child (GtkListHeader *self, ++ GtkWidget *child); ++GDK_AVAILABLE_IN_4_12 ++GtkWidget * gtk_list_header_get_child (GtkListHeader *self); ++ ++G_END_DECLS ++ +diff --git a/gtk/gtklistheaderbase.c b/gtk/gtklistheaderbase.c +new file mode 100644 +index 0000000000000000000000000000000000000000..a2b5b72e1e9cd1d4173c421a28898cbb0fbf09cf +--- /dev/null ++++ b/gtk/gtklistheaderbase.c +@@ -0,0 +1,112 @@ ++/* ++ * Copyright © 2023 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 "gtklistheaderbaseprivate.h" ++ ++typedef struct _GtkListHeaderBasePrivate GtkListHeaderBasePrivate; ++struct _GtkListHeaderBasePrivate ++{ ++ GObject *item; ++ guint start; ++ guint end; ++}; ++ ++G_DEFINE_TYPE_WITH_PRIVATE (GtkListHeaderBase, gtk_list_header_base, GTK_TYPE_WIDGET) ++ ++static void ++gtk_list_header_base_default_update (GtkListHeaderBase *self, ++ gpointer item, ++ guint start, ++ guint end) ++{ ++ GtkListHeaderBasePrivate *priv = gtk_list_header_base_get_instance_private (self); ++ ++ g_set_object (&priv->item, item); ++ priv->start = start; ++ priv->end = end; ++} ++ ++static void ++gtk_list_header_base_dispose (GObject *object) ++{ ++ GtkListHeaderBase *self = GTK_LIST_HEADER_BASE (object); ++ GtkListHeaderBasePrivate *priv = gtk_list_header_base_get_instance_private (self); ++ ++ g_clear_object (&priv->item); ++ ++ G_OBJECT_CLASS (gtk_list_header_base_parent_class)->dispose (object); ++} ++ ++static void ++gtk_list_header_base_class_init (GtkListHeaderBaseClass *klass) ++{ ++ GObjectClass *gobject_class = G_OBJECT_CLASS (klass); ++ ++ klass->update = gtk_list_header_base_default_update; ++ ++ gobject_class->dispose = gtk_list_header_base_dispose; ++} ++ ++static void ++gtk_list_header_base_init (GtkListHeaderBase *self) ++{ ++} ++ ++void ++gtk_list_header_base_update (GtkListHeaderBase *self, ++ gpointer item, ++ guint start, ++ guint end) ++{ ++ GtkListHeaderBasePrivate *priv = gtk_list_header_base_get_instance_private (self); ++ ++ if (priv->item == item && ++ priv->start == start && ++ priv->end == end) ++ return; ++ ++ GTK_LIST_HEADER_BASE_GET_CLASS (self)->update (self, item, start, end); ++} ++ ++guint ++gtk_list_header_base_get_start (GtkListHeaderBase *self) ++{ ++ GtkListHeaderBasePrivate *priv = gtk_list_header_base_get_instance_private (self); ++ ++ return priv->start; ++} ++ ++guint ++gtk_list_header_base_get_end (GtkListHeaderBase *self) ++{ ++ GtkListHeaderBasePrivate *priv = gtk_list_header_base_get_instance_private (self); ++ ++ return priv->end; ++} ++ ++gpointer ++gtk_list_header_base_get_item (GtkListHeaderBase *self) ++{ ++ GtkListHeaderBasePrivate *priv = gtk_list_header_base_get_instance_private (self); ++ ++ return priv->item; ++} ++ +diff --git a/gtk/gtklistheaderbaseprivate.h b/gtk/gtklistheaderbaseprivate.h +new file mode 100644 +index 0000000000000000000000000000000000000000..fa3fa45f22dce2d9472be38db834f64ee98cbb83 +--- /dev/null ++++ b/gtk/gtklistheaderbaseprivate.h +@@ -0,0 +1,63 @@ ++/* ++ * Copyright © 2023 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 ++ */ ++ ++#pragma once ++ ++#include "gtkwidget.h" ++ ++G_BEGIN_DECLS ++ ++#define GTK_TYPE_LIST_HEADER_BASE (gtk_list_header_base_get_type ()) ++#define GTK_LIST_HEADER_BASE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GTK_TYPE_LIST_HEADER_BASE, GtkListHeaderBase)) ++#define GTK_LIST_HEADER_BASE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GTK_TYPE_LIST_HEADER_BASE, GtkListHeaderBaseClass)) ++#define GTK_IS_LIST_HEADER_BASE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GTK_TYPE_LIST_HEADER_BASE)) ++#define GTK_IS_LIST_HEADER_BASE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GTK_TYPE_LIST_HEADER_BASE)) ++#define GTK_LIST_HEADER_BASE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GTK_TYPE_LIST_HEADER_BASE, GtkListHeaderBaseClass)) ++ ++typedef struct _GtkListHeaderBase GtkListHeaderBase; ++typedef struct _GtkListHeaderBaseClass GtkListHeaderBaseClass; ++ ++struct _GtkListHeaderBase ++{ ++ GtkWidget parent_instance; ++}; ++ ++struct _GtkListHeaderBaseClass ++{ ++ GtkWidgetClass parent_class; ++ ++ void (* update) (GtkListHeaderBase *self, ++ gpointer item, ++ guint start, ++ guint end); ++}; ++ ++GType gtk_list_header_base_get_type (void) G_GNUC_CONST; ++ ++void gtk_list_header_base_update (GtkListHeaderBase *self, ++ gpointer item, ++ guint start, ++ guint end); ++ ++guint gtk_list_header_base_get_start (GtkListHeaderBase *self); ++guint gtk_list_header_base_get_end (GtkListHeaderBase *self); ++gpointer gtk_list_header_base_get_item (GtkListHeaderBase *self); ++ ++G_END_DECLS ++ +diff --git a/gtk/gtklistheaderprivate.h b/gtk/gtklistheaderprivate.h +new file mode 100644 +index 0000000000000000000000000000000000000000..81aa850c8af87f402012a5e0f56a43b8f4e95518 +--- /dev/null ++++ b/gtk/gtklistheaderprivate.h +@@ -0,0 +1,52 @@ ++/* ++ * Copyright © 2023 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 ++ */ ++ ++#pragma once ++ ++#include "gtklistheader.h" ++ ++#include "gtklistheaderwidgetprivate.h" ++ ++G_BEGIN_DECLS ++ ++struct _GtkListHeader ++{ ++ GObject parent_instance; ++ ++ GtkListHeaderWidget *owner; /* has a reference */ ++ ++ GtkWidget *child; ++}; ++ ++struct _GtkListHeaderClass ++{ ++ GObjectClass parent_class; ++}; ++ ++GtkListHeader * gtk_list_header_new (void); ++ ++void gtk_list_header_do_notify (GtkListHeader *list_header, ++ gboolean notify_item, ++ gboolean notify_start, ++ gboolean notify_end, ++ gboolean notify_n_items); ++ ++ ++G_END_DECLS ++ +diff --git a/gtk/gtklistheaderwidget.c b/gtk/gtklistheaderwidget.c +new file mode 100644 +index 0000000000000000000000000000000000000000..5a1a2559cedb3b669f3625ec57db83a49a630c56 +--- /dev/null ++++ b/gtk/gtklistheaderwidget.c +@@ -0,0 +1,295 @@ ++/* ++ * Copyright © 2023 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 "gtklistheaderwidgetprivate.h" ++ ++#include "gtkbinlayout.h" ++#include "gtklistheaderprivate.h" ++#include "gtklistitemfactoryprivate.h" ++#include "gtklistbaseprivate.h" ++#include "gtkwidget.h" ++ ++typedef struct _GtkListHeaderWidgetPrivate GtkListHeaderWidgetPrivate; ++struct _GtkListHeaderWidgetPrivate ++{ ++ GtkListItemFactory *factory; ++ ++ GtkListHeader *header; ++}; ++ ++enum { ++ PROP_0, ++ PROP_FACTORY, ++ ++ N_PROPS ++}; ++ ++G_DEFINE_TYPE_WITH_PRIVATE (GtkListHeaderWidget, gtk_list_header_widget, GTK_TYPE_LIST_HEADER_BASE) ++ ++static GParamSpec *properties[N_PROPS] = { NULL, }; ++ ++static void ++gtk_list_header_widget_setup_func (gpointer object, ++ gpointer data) ++{ ++ GtkListHeaderWidget *self = GTK_LIST_HEADER_WIDGET (data); ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ GtkListHeader *header = object; ++ ++ priv->header = header; ++ header->owner = self; ++ ++ gtk_list_header_widget_set_child (self, header->child); ++ ++ gtk_list_header_do_notify (header, ++ gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self)) != NULL, ++ gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self)) != GTK_INVALID_LIST_POSITION, ++ gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self)) != GTK_INVALID_LIST_POSITION, ++ gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self)) != gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self))); ++} ++ ++static void ++gtk_list_header_widget_setup_factory (GtkListHeaderWidget *self) ++{ ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ GtkListHeader *header; ++ ++ header = gtk_list_header_new (); ++ ++ gtk_list_item_factory_setup (priv->factory, ++ G_OBJECT (header), ++ gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self)) != NULL, ++ gtk_list_header_widget_setup_func, ++ self); ++ ++ g_assert (priv->header == header); ++} ++ ++static void ++gtk_list_header_widget_teardown_func (gpointer object, ++ gpointer data) ++{ ++ GtkListHeaderWidget *self = GTK_LIST_HEADER_WIDGET (data); ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ GtkListHeader *header = object; ++ ++ header->owner = NULL; ++ priv->header = NULL; ++ ++ gtk_list_header_widget_set_child (self, NULL); ++ ++ gtk_list_header_do_notify (header, ++ gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self)) != NULL, ++ gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self)) != GTK_INVALID_LIST_POSITION, ++ gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self)) != GTK_INVALID_LIST_POSITION, ++ gtk_list_header_base_get_start (GTK_LIST_HEADER_BASE (self)) != gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (self))); ++} ++ ++static void ++gtk_list_header_widget_teardown_factory (GtkListHeaderWidget *self) ++{ ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ gpointer header = priv->header; ++ ++ gtk_list_item_factory_teardown (priv->factory, ++ header, ++ gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self)) != NULL, ++ gtk_list_header_widget_teardown_func, ++ self); ++ ++ g_assert (priv->header == NULL); ++ g_object_unref (header); ++} ++ ++typedef struct { ++ GtkListHeaderWidget *widget; ++ gpointer item; ++ guint start; ++ guint end; ++} GtkListHeaderWidgetUpdate; ++ ++static void ++gtk_list_header_widget_update_func (gpointer object, ++ gpointer data) ++{ ++ GtkListHeaderWidgetUpdate *update = data; ++ GtkListHeaderWidget *self = update->widget; ++ GtkListHeaderBase *base = GTK_LIST_HEADER_BASE (self); ++ /* Track notify manually instead of freeze/thaw_notify for performance reasons. */ ++ gboolean notify_item, notify_start, notify_end, notify_n_items; ++ ++ /* FIXME: It's kinda evil to notify external objects from here... */ ++ notify_item = gtk_list_header_base_get_item (base) != update->item; ++ notify_start = gtk_list_header_base_get_start (base) != update->start; ++ notify_end = gtk_list_header_base_get_end (base) != update->end; ++ notify_n_items = gtk_list_header_base_get_end (base) - gtk_list_header_base_get_start (base) != update->end - update->start; ++ ++ GTK_LIST_HEADER_BASE_CLASS (gtk_list_header_widget_parent_class)->update (base, ++ update->item, ++ update->start, ++ update->end); ++ ++ if (object) ++ gtk_list_header_do_notify (object, notify_item, notify_start, notify_end, notify_n_items); ++} ++ ++static void ++gtk_list_header_widget_update (GtkListHeaderBase *base, ++ gpointer item, ++ guint start, ++ guint end) ++{ ++ GtkListHeaderWidget *self = GTK_LIST_HEADER_WIDGET (base); ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ GtkListHeaderWidgetUpdate update = { self, item, start, end }; ++ ++ if (priv->header) ++ { ++ gtk_list_item_factory_update (priv->factory, ++ G_OBJECT (priv->header), ++ gtk_list_header_base_get_item (GTK_LIST_HEADER_BASE (self)) != NULL, ++ item != NULL, ++ gtk_list_header_widget_update_func, ++ &update); ++ } ++ else ++ { ++ gtk_list_header_widget_update_func (NULL, &update); ++ } ++} ++ ++static void ++gtk_list_header_widget_set_property (GObject *object, ++ guint property_id, ++ const GValue *value, ++ GParamSpec *pspec) ++{ ++ GtkListHeaderWidget *self = GTK_LIST_HEADER_WIDGET (object); ++ ++ switch (property_id) ++ { ++ case PROP_FACTORY: ++ gtk_list_header_widget_set_factory (self, g_value_get_object (value)); ++ break; ++ ++ default: ++ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); ++ break; ++ } ++} ++ ++static void ++gtk_list_header_widget_clear_factory (GtkListHeaderWidget *self) ++{ ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ ++ if (priv->factory == NULL) ++ return; ++ ++ if (priv->header) ++ gtk_list_header_widget_teardown_factory (self); ++ ++ g_clear_object (&priv->factory); ++} ++ ++static void ++gtk_list_header_widget_dispose (GObject *object) ++{ ++ GtkListHeaderWidget *self = GTK_LIST_HEADER_WIDGET (object); ++ ++ gtk_list_header_widget_clear_factory (self); ++ ++ G_OBJECT_CLASS (gtk_list_header_widget_parent_class)->dispose (object); ++} ++ ++static void ++gtk_list_header_widget_class_init (GtkListHeaderWidgetClass *klass) ++{ ++ GtkListHeaderBaseClass *base_class = GTK_LIST_HEADER_BASE_CLASS (klass); ++ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); ++ GObjectClass *gobject_class = G_OBJECT_CLASS (klass); ++ ++ base_class->update = gtk_list_header_widget_update; ++ ++ gobject_class->set_property = gtk_list_header_widget_set_property; ++ gobject_class->dispose = gtk_list_header_widget_dispose; ++ ++ properties[PROP_FACTORY] = ++ g_param_spec_object ("factory", NULL, NULL, ++ GTK_TYPE_LIST_ITEM_FACTORY, ++ G_PARAM_WRITABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ ++ g_object_class_install_properties (gobject_class, N_PROPS, properties); ++ ++ gtk_widget_class_set_css_name (widget_class, I_("header")); ++ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_ROW_HEADER); ++ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); ++} ++ ++static void ++gtk_list_header_widget_init (GtkListHeaderWidget *self) ++{ ++} ++ ++void ++gtk_list_header_widget_set_factory (GtkListHeaderWidget *self, ++ GtkListItemFactory *factory) ++{ ++ GtkListHeaderWidgetPrivate *priv = gtk_list_header_widget_get_instance_private (self); ++ ++ if (priv->factory == factory) ++ return; ++ ++ gtk_list_header_widget_clear_factory (self); ++ ++ if (factory) ++ { ++ priv->factory = g_object_ref (factory); ++ ++ gtk_list_header_widget_setup_factory (self); ++ } ++ ++ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]); ++} ++ ++GtkWidget * ++gtk_list_header_widget_new (GtkListItemFactory *factory) ++{ ++ return g_object_new (GTK_TYPE_LIST_HEADER_WIDGET, ++ "factory", factory, ++ NULL); ++} ++ ++void ++gtk_list_header_widget_set_child (GtkListHeaderWidget *self, ++ GtkWidget *child) ++{ ++ GtkWidget *cur_child = gtk_widget_get_first_child (GTK_WIDGET (self)); ++ ++ if (cur_child == child) ++ return; ++ ++ g_clear_pointer (&cur_child, gtk_widget_unparent); ++ ++ if (child) ++ gtk_widget_set_parent (child, GTK_WIDGET (self)); ++} ++ +diff --git a/gtk/gtklistheaderwidgetprivate.h b/gtk/gtklistheaderwidgetprivate.h +new file mode 100644 +index 0000000000000000000000000000000000000000..54f3c666d8f017f5ebf405b7492f93d16a36a6a9 +--- /dev/null ++++ b/gtk/gtklistheaderwidgetprivate.h +@@ -0,0 +1,61 @@ ++/* ++ * Copyright © 2023 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 ++ */ ++ ++#pragma once ++ ++#include "gtklistheaderbaseprivate.h" ++ ++#include "gtklistitemfactory.h" ++ ++G_BEGIN_DECLS ++ ++#define GTK_TYPE_LIST_HEADER_WIDGET (gtk_list_header_widget_get_type ()) ++#define GTK_LIST_HEADER_WIDGET(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GTK_TYPE_LIST_HEADER_WIDGET, GtkListHeaderWidget)) ++#define GTK_LIST_HEADER_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GTK_TYPE_LIST_HEADER_WIDGET, GtkListHeaderWidgetClass)) ++#define GTK_IS_LIST_HEADER_WIDGET(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GTK_TYPE_LIST_HEADER_WIDGET)) ++#define GTK_IS_LIST_HEADER_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GTK_TYPE_LIST_HEADER_WIDGET)) ++#define GTK_LIST_HEADER_WIDGET_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GTK_TYPE_LIST_HEADER_WIDGET, GtkListHeaderWidgetClass)) ++ ++typedef struct _GtkListHeaderWidget GtkListHeaderWidget; ++typedef struct _GtkListHeaderWidgetClass GtkListHeaderWidgetClass; ++ ++struct _GtkListHeaderWidget ++{ ++ GtkListHeaderBase parent_instance; ++}; ++ ++struct _GtkListHeaderWidgetClass ++{ ++ GtkListHeaderBaseClass parent_class; ++}; ++ ++GType gtk_list_header_widget_get_type (void) G_GNUC_CONST; ++ ++GtkWidget * gtk_list_header_widget_new (GtkListItemFactory *factory); ++ ++void gtk_list_header_widget_set_factory (GtkListHeaderWidget *self, ++ GtkListItemFactory *factory); ++GtkListItemFactory * gtk_list_header_widget_get_factory (GtkListHeaderWidget *self); ++ ++void gtk_list_header_widget_set_child (GtkListHeaderWidget *self, ++ GtkWidget *child); ++ ++ ++G_END_DECLS ++ +diff --git a/gtk/gtklistitembase.c b/gtk/gtklistitembase.c +index 67bbf959120b51844409f53b9b7c8d6a068e682d..cdd8fe9703dcf948c9cc1006b4d6cff650834952 100644 +--- a/gtk/gtklistitembase.c ++++ b/gtk/gtklistitembase.c +@@ -81,6 +81,11 @@ gtk_list_item_base_update (GtkListItemBase *self, + GtkListItemBasePrivate *priv = gtk_list_item_base_get_instance_private (self); + gboolean was_selected; + ++ if (priv->position == position && ++ priv->item == item && ++ priv->selected == selected) ++ return; ++ + was_selected = priv->selected; + + GTK_LIST_ITEM_BASE_GET_CLASS (self)->update (self, position, item, selected); +diff --git a/gtk/gtklistitemmanager.c b/gtk/gtklistitemmanager.c +index 8ef33b5def00a13aea32b4f9ffa20a5dd8538027..2ca7f9eaf8a93ae3116d972ab0b08dc3d9730a0f 100644 +--- a/gtk/gtklistitemmanager.c ++++ b/gtk/gtklistitemmanager.c +@@ -23,20 +23,26 @@ + + #include "gtklistitembaseprivate.h" + #include "gtklistitemwidgetprivate.h" ++#include "gtksectionmodel.h" + #include "gtkwidgetprivate.h" + ++typedef struct _GtkListItemChange GtkListItemChange; ++ + struct _GtkListItemManager + { + GObject parent_instance; + + GtkWidget *widget; + GtkSelectionModel *model; ++ gboolean has_sections; + + GtkRbTree *items; + GSList *trackers; + + GtkListTile * (* split_func) (GtkWidget *, GtkListTile *, guint); + GtkListItemBase * (* create_widget) (GtkWidget *); ++ void (* prepare_section) (GtkWidget *, GtkListTile *, guint); ++ GtkListHeaderBase * (* create_header_widget) (GtkWidget *); + }; + + struct _GtkListItemManagerClass +@@ -52,26 +58,104 @@ struct _GtkListItemTracker + guint n_after; + }; + +-static GtkWidget * gtk_list_item_manager_acquire_list_item (GtkListItemManager *self, +- guint position, +- GtkWidget *prev_sibling); +-static GtkWidget * gtk_list_item_manager_try_reacquire_list_item +- (GtkListItemManager *self, +- GHashTable *change, +- guint position, +- GtkWidget *prev_sibling); +-static void gtk_list_item_manager_update_list_item (GtkListItemManager *self, +- GtkWidget *item, +- guint position); +-static void gtk_list_item_manager_move_list_item (GtkListItemManager *self, +- GtkWidget *list_item, +- guint position, +- GtkWidget *prev_sibling); +-static void gtk_list_item_manager_release_list_item (GtkListItemManager *self, +- GHashTable *change, +- GtkWidget *widget); ++struct _GtkListItemChange ++{ ++ GHashTable *deleted_items; ++ GQueue recycled_items; ++ GQueue recycled_headers; ++}; ++ + G_DEFINE_TYPE (GtkListItemManager, gtk_list_item_manager, G_TYPE_OBJECT) + ++static void ++gtk_list_item_change_init (GtkListItemChange *change) ++{ ++ change->deleted_items = NULL; ++ g_queue_init (&change->recycled_items); ++ g_queue_init (&change->recycled_headers); ++} ++ ++static void ++gtk_list_item_change_finish (GtkListItemChange *change) ++{ ++ GtkWidget *widget; ++ ++ g_clear_pointer (&change->deleted_items, g_hash_table_destroy); ++ ++ while ((widget = g_queue_pop_head (&change->recycled_items))) ++ gtk_widget_unparent (widget); ++ while ((widget = g_queue_pop_head (&change->recycled_headers))) ++ gtk_widget_unparent (widget); ++} ++ ++static void ++gtk_list_item_change_recycle (GtkListItemChange *change, ++ GtkListItemBase *widget) ++{ ++ g_queue_push_tail (&change->recycled_items, widget); ++} ++ ++static void ++gtk_list_item_change_clear_header (GtkListItemChange *change, ++ GtkWidget **widget) ++{ ++ if (*widget == NULL) ++ return; ++ ++ g_assert (GTK_IS_LIST_HEADER_BASE (*widget)); ++ g_queue_push_tail (&change->recycled_headers, *widget); ++ *widget = NULL; ++} ++ ++static void ++gtk_list_item_change_release (GtkListItemChange *change, ++ GtkListItemBase *widget) ++{ ++ if (change->deleted_items == NULL) ++ change->deleted_items = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) gtk_widget_unparent); ++ ++ if (!g_hash_table_replace (change->deleted_items, gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (widget)), widget)) ++ { ++ g_warning ("Duplicate item detected in list. Picking one randomly."); ++ gtk_list_item_change_recycle (change, widget); ++ } ++} ++ ++static GtkListItemBase * ++gtk_list_item_change_find (GtkListItemChange *change, ++ gpointer item) ++{ ++ gpointer result; ++ ++ if (change->deleted_items && g_hash_table_steal_extended (change->deleted_items, item, NULL, &result)) ++ return result; ++ ++ return NULL; ++} ++ ++static GtkListItemBase * ++gtk_list_item_change_get (GtkListItemChange *change, ++ gpointer item) ++{ ++ GtkListItemBase *result; ++ ++ result = gtk_list_item_change_find (change, item); ++ if (result) ++ return result; ++ ++ result = g_queue_pop_head (&change->recycled_items); ++ if (result) ++ return result; ++ ++ return NULL; ++} ++ ++static GtkListHeaderBase * ++gtk_list_item_change_get_header (GtkListItemChange *change) ++{ ++ return g_queue_pop_head (&change->recycled_headers); ++} ++ + static void + potentially_empty_rectangle_union (cairo_rectangle_int_t *self, + const cairo_rectangle_int_t *area) +@@ -101,11 +185,36 @@ gtk_list_item_manager_augment_node (GtkRbTree *tree, + aug->n_items = tile->n_items; + aug->area = tile->area; + ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_HEADER: ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ aug->has_header = TRUE; ++ aug->has_footer = FALSE; ++ break; ++ case GTK_LIST_TILE_FOOTER: ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ aug->has_header = FALSE; ++ aug->has_footer = TRUE; ++ break; ++ case GTK_LIST_TILE_ITEM: ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ aug->has_header = FALSE; ++ aug->has_footer = FALSE; ++ break; ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ + if (left) + { + GtkListTileAugment *left_aug = gtk_rb_tree_get_augment (tree, left); + + aug->n_items += left_aug->n_items; ++ aug->has_header |= left_aug->has_header; ++ aug->has_footer |= left_aug->has_footer; + potentially_empty_rectangle_union (&aug->area, &left_aug->area); + } + +@@ -114,6 +223,8 @@ gtk_list_item_manager_augment_node (GtkRbTree *tree, + GtkListTileAugment *right_aug = gtk_rb_tree_get_augment (tree, right); + + aug->n_items += right_aug->n_items; ++ aug->has_header |= right_aug->has_header; ++ aug->has_footer |= right_aug->has_footer; + potentially_empty_rectangle_union (&aug->area, &right_aug->area); + } + } +@@ -127,9 +238,11 @@ gtk_list_item_manager_clear_node (gpointer _tile) + } + + GtkListItemManager * +-gtk_list_item_manager_new (GtkWidget *widget, +- GtkListTile * (* split_func) (GtkWidget *, GtkListTile *, guint), +- GtkListItemBase * (* create_widget) (GtkWidget *)) ++gtk_list_item_manager_new (GtkWidget *widget, ++ GtkListTile * (* split_func) (GtkWidget *, GtkListTile *, guint), ++ GtkListItemBase * (* create_widget) (GtkWidget *), ++ void (* prepare_section) (GtkWidget *, GtkListTile *, guint), ++ GtkListHeaderBase * (* create_header_widget) (GtkWidget *)) + { + GtkListItemManager *self; + +@@ -141,6 +254,8 @@ gtk_list_item_manager_new (GtkWidget *widget, + self->widget = widget; + self->split_func = split_func; + self->create_widget = create_widget; ++ self->prepare_section = prepare_section; ++ self->create_header_widget = create_header_widget; + + self->items = gtk_rb_tree_new_for_size (sizeof (GtkListTile), + sizeof (GtkListTileAugment), +@@ -151,6 +266,15 @@ gtk_list_item_manager_new (GtkWidget *widget, + return self; + } + ++static gboolean ++gtk_list_item_manager_has_sections (GtkListItemManager *self) ++{ ++ if (self->model == NULL || !self->has_sections) ++ return FALSE; ++ ++ return GTK_IS_SECTION_MODEL (self->model); ++} ++ + void + gtk_list_item_manager_get_tile_bounds (GtkListItemManager *self, + GdkRectangle *out_bounds) +@@ -238,6 +362,110 @@ gtk_list_item_manager_get_nth (GtkListItemManager *self, + return tile; + } + ++static GtkListTile * ++gtk_list_tile_get_header (GtkListItemManager *self, ++ GtkListTile *tile) ++{ ++ GtkListTileAugment *aug; ++ GtkListTile *other; ++ gboolean check_right = FALSE; ++ ++ while (TRUE) ++ { ++ if (check_right) ++ { ++ other = gtk_rb_tree_node_get_right (tile); ++ if (other) ++ { ++ aug = gtk_rb_tree_get_augment (self->items, other); ++ if (aug->has_header) ++ { ++ check_right = TRUE; ++ tile = other; ++ continue; ++ } ++ } ++ } ++ ++ if (tile->type == GTK_LIST_TILE_HEADER || ++ tile->type == GTK_LIST_TILE_UNMATCHED_HEADER) ++ return tile; ++ ++ other = gtk_rb_tree_node_get_left (tile); ++ if (other) ++ { ++ aug = gtk_rb_tree_get_augment (self->items, other); ++ if (aug->has_header) ++ { ++ check_right = TRUE; ++ tile = other; ++ continue; ++ } ++ } ++ ++ while ((other = gtk_rb_tree_node_get_parent (tile))) ++ { ++ if (gtk_rb_tree_node_get_right (other) == tile) ++ break; ++ tile = other; ++ } ++ tile = other; ++ check_right = FALSE; ++ } ++} ++ ++static GtkListTile * ++gtk_list_tile_get_footer (GtkListItemManager *self, ++ GtkListTile *tile) ++{ ++ GtkListTileAugment *aug; ++ GtkListTile *other; ++ gboolean check_left = FALSE; ++ ++ while (TRUE) ++ { ++ if (check_left) ++ { ++ other = gtk_rb_tree_node_get_left (tile); ++ if (other) ++ { ++ aug = gtk_rb_tree_get_augment (self->items, other); ++ if (aug->has_footer) ++ { ++ check_left = TRUE; ++ tile = other; ++ continue; ++ } ++ } ++ } ++ ++ if (tile->type == GTK_LIST_TILE_FOOTER || ++ tile->type == GTK_LIST_TILE_UNMATCHED_FOOTER) ++ return tile; ++ ++ other = gtk_rb_tree_node_get_right (tile); ++ if (other) ++ { ++ aug = gtk_rb_tree_get_augment (self->items, other); ++ if (aug->has_footer) ++ { ++ check_left = TRUE; ++ tile = other; ++ continue; ++ } ++ } ++ ++ while ((other = gtk_rb_tree_node_get_parent (tile))) ++ { ++ if (gtk_rb_tree_node_get_left (other) == tile) ++ break; ++ tile = other; ++ } ++ tile = other; ++ check_left = FALSE; ++ } ++} ++ + /* This computes Manhattan distance */ + static int + rectangle_distance (const cairo_rectangle_int_t *rect, +@@ -401,6 +629,28 @@ gtk_list_tile_get_augment (GtkListItemManager *self, + return gtk_rb_tree_get_augment (self->items, tile); + } + ++static GtkListTile * ++gtk_list_tile_get_next_skip (GtkListTile *tile) ++{ ++ for (tile = gtk_rb_tree_node_get_next (tile); ++ tile && (tile->type == GTK_LIST_TILE_FILLER || tile->type == GTK_LIST_TILE_REMOVED); ++ tile = gtk_rb_tree_node_get_next (tile)) ++ { } ++ ++ return tile; ++} ++ ++static GtkListTile * ++gtk_list_tile_get_previous_skip (GtkListTile *tile) ++{ ++ for (tile = gtk_rb_tree_node_get_previous (tile); ++ tile && (tile->type == GTK_LIST_TILE_FILLER || tile->type == GTK_LIST_TILE_REMOVED); ++ tile = gtk_rb_tree_node_get_previous (tile)) ++ { } ++ ++ return tile; ++} ++ + /* + * gtk_list_tile_set_area: + * @self: the list item manager +@@ -461,6 +711,18 @@ gtk_list_tile_set_area_size (GtkListItemManager *self, + gtk_rb_tree_node_mark_dirty (tile); + } + ++static void ++gtk_list_tile_set_type (GtkListTile *tile, ++ GtkListTileType type) ++{ ++ if (tile->type == type) ++ return; ++ ++ g_assert (tile->widget == NULL); ++ tile->type = type; ++ gtk_rb_tree_node_mark_dirty (tile); ++} ++ + static void + gtk_list_item_tracker_unset_position (GtkListItemManager *self, + GtkListItemTracker *tracker) +@@ -569,11 +831,11 @@ gtk_list_item_manager_ensure_split (GtkListItemManager *self, + + static void + gtk_list_item_manager_remove_items (GtkListItemManager *self, +- GHashTable *change, ++ GtkListItemChange *change, + guint position, + guint n_items) + { +- GtkListTile *tile, *next; ++ GtkListTile *tile, *header; + guint offset; + + if (n_items == 0) +@@ -582,24 +844,63 @@ gtk_list_item_manager_remove_items (GtkListItemManager *self, + tile = gtk_list_item_manager_get_nth (self, position, &offset); + if (offset) + tile = gtk_list_item_manager_ensure_split (self, tile, offset); ++ header = gtk_list_tile_get_previous_skip (tile); ++ if (header->type != GTK_LIST_TILE_HEADER && header->type != GTK_LIST_TILE_UNMATCHED_HEADER) ++ header = NULL; + + while (n_items > 0) + { +- if (tile->n_items > n_items) ++ switch (tile->type) + { +- gtk_list_item_manager_ensure_split (self, tile, n_items); +- g_assert (tile->n_items <= n_items); ++ case GTK_LIST_TILE_HEADER: ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ g_assert (header == NULL); ++ header = tile; ++ break; ++ ++ case GTK_LIST_TILE_FOOTER: ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ if (header) ++ { ++ gtk_list_item_change_clear_header (change, &header->widget); ++ gtk_list_tile_set_type (header, GTK_LIST_TILE_REMOVED); ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); ++ header = NULL; ++ } ++ break; ++ ++ case GTK_LIST_TILE_ITEM: ++ if (tile->n_items > n_items) ++ { ++ gtk_list_item_manager_ensure_split (self, tile, n_items); ++ g_assert (tile->n_items <= n_items); ++ } ++ if (tile->widget) ++ gtk_list_item_change_release (change, GTK_LIST_ITEM_BASE (tile->widget)); ++ tile->widget = NULL; ++ n_items -= tile->n_items; ++ tile->n_items = 0; ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); ++ break; ++ ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ default: ++ g_assert_not_reached (); ++ break; + } + +- next = gtk_rb_tree_node_get_next (tile); +- if (tile->widget) +- gtk_list_item_manager_release_list_item (self, change, tile->widget); +- tile->widget = NULL; +- n_items -= tile->n_items; +- tile->n_items = 0; +- gtk_rb_tree_node_mark_dirty (tile); ++ tile = gtk_list_tile_get_next_skip (tile); ++ } + +- tile = next; ++ if (header) ++ { ++ if (tile->type == GTK_LIST_TILE_FOOTER || tile->type == GTK_LIST_TILE_UNMATCHED_FOOTER) ++ { ++ gtk_list_item_change_clear_header (change, &header->widget); ++ gtk_list_tile_set_type (header, GTK_LIST_TILE_REMOVED); ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); ++ } + } + + gtk_widget_queue_resize (GTK_WIDGET (self->widget)); +@@ -607,23 +908,70 @@ gtk_list_item_manager_remove_items (GtkListItemManager *self, + + static void + gtk_list_item_manager_add_items (GtkListItemManager *self, ++ GtkListItemChange *change, + guint position, + guint n_items) + { + GtkListTile *tile; + guint offset; ++ gboolean has_sections; + + if (n_items == 0) + return; + ++ has_sections = gtk_list_item_manager_has_sections (self); ++ + tile = gtk_list_item_manager_get_nth (self, position, &offset); ++ if (tile == NULL) ++ { ++ /* at end of list, pick the footer */ ++ for (tile = gtk_rb_tree_get_last (self->items); ++ tile && (tile->type == GTK_LIST_TILE_REMOVED || tile->type == GTK_LIST_TILE_FILLER); ++ tile = gtk_rb_tree_node_get_previous (tile)) ++ { } ++ ++ if (tile == NULL) ++ { ++ /* empty list, there isn't even a footer yet */ ++ tile = gtk_rb_tree_insert_after (self->items, NULL); ++ tile->type = GTK_LIST_TILE_UNMATCHED_HEADER; ++ ++ tile = gtk_rb_tree_insert_after (self->items, tile); ++ tile->type = GTK_LIST_TILE_UNMATCHED_FOOTER; ++ } ++ else if (has_sections && tile->type == GTK_LIST_TILE_FOOTER) ++ { ++ GtkListTile *header; ++ ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_UNMATCHED_FOOTER); ++ ++ header = gtk_list_tile_get_header (self, tile); ++ gtk_list_item_change_clear_header (change, &header->widget); ++ gtk_list_tile_set_type (header, GTK_LIST_TILE_UNMATCHED_HEADER); ++ } ++ } + if (offset) + tile = gtk_list_item_manager_ensure_split (self, tile, offset); +- ++ + tile = gtk_rb_tree_insert_before (self->items, tile); ++ tile->type = GTK_LIST_TILE_ITEM; + tile->n_items = n_items; + gtk_rb_tree_node_mark_dirty (tile); + ++ if (has_sections) ++ { ++ GtkListTile *section = gtk_list_tile_get_previous_skip (tile); ++ ++ if (section->type == GTK_LIST_TILE_HEADER) ++ { ++ gtk_list_item_change_clear_header (change, §ion->widget); ++ gtk_list_tile_set_type (section, ++ GTK_LIST_TILE_UNMATCHED_HEADER); ++ gtk_list_tile_set_type (gtk_list_tile_get_footer (self, section), ++ GTK_LIST_TILE_UNMATCHED_FOOTER); ++ } ++ } ++ + gtk_widget_queue_resize (GTK_WIDGET (self->widget)); + } + +@@ -632,7 +980,8 @@ gtk_list_item_manager_merge_list_items (GtkListItemManager *self, + GtkListTile *first, + GtkListTile *second) + { +- if (first->widget || second->widget) ++ if (first->widget || second->widget || ++ first->type != GTK_LIST_TILE_ITEM || second->type != GTK_LIST_TILE_ITEM) + return FALSE; + + first->n_items += second->n_items; +@@ -653,7 +1002,7 @@ gtk_list_item_manager_merge_list_items (GtkListItemManager *self, + * items will be given to the new tile, which will be + * nserted after the tile. + * +- * It is valid for either tile to have 0 items after ++ * It is not valid for either tile to have 0 items after + * the split. + * + * Returns: The new tile +@@ -665,9 +1014,12 @@ gtk_list_tile_split (GtkListItemManager *self, + { + GtkListTile *result; + +- g_assert (n_items <= tile->n_items); ++ g_assert (n_items > 0); ++ g_assert (n_items < tile->n_items); ++ g_assert (tile->type == GTK_LIST_TILE_ITEM); + + result = gtk_rb_tree_insert_after (self->items, tile); ++ result->type = GTK_LIST_TILE_ITEM; + result->n_items = tile->n_items - n_items; + tile->n_items = n_items; + gtk_rb_tree_node_mark_dirty (tile); +@@ -675,10 +1027,38 @@ gtk_list_tile_split (GtkListItemManager *self, + return result; + } + ++/* ++ * gtk_list_tile_append_filler: ++ * @self: the listitemmanager ++ * @previous: tile to append to ++ * ++ * Appends a filler tile. ++ * ++ * Filler tiles don't refer to any items or header and exist ++ * just to take up space, so that finding items by position gets ++ * easier. ++ * ++ * They ave a special garbage-collection behavior, see ++ * gtk_list_tile_gc(). ++ * ++ * Returns: The new filler tile ++ **/ ++GtkListTile * ++gtk_list_tile_append_filler (GtkListItemManager *self, ++ GtkListTile *previous) ++{ ++ GtkListTile *result; ++ ++ result = gtk_rb_tree_insert_after (self->items, previous); ++ result->type = GTK_LIST_TILE_FILLER; ++ ++ return result; ++} ++ + /* + * gtk_list_tile_gc: + * @self: the listitemmanager +- * @tile: a tile ++ * @tile: a tile or NULL + * + * Tries to get rid of tiles when they aren't needed anymore, + * either because their referenced listitems were deleted or +@@ -686,7 +1066,11 @@ gtk_list_tile_split (GtkListItemManager *self, + * + * Note that this only looks forward, but never backward. + * +- * Returns: The next tile ++ * A special case here are filler tiles. They only get ++ * collected, when they are explicitly passed in, but never ++ * otherwise. ++ * ++ * Returns: The next tile or NULL if everything was gc'ed + **/ + GtkListTile * + gtk_list_tile_gc (GtkListItemManager *self, +@@ -694,22 +1078,51 @@ gtk_list_tile_gc (GtkListItemManager *self, + { + GtkListTile *next; + ++ if (tile == NULL) ++ return NULL; ++ ++ if (tile->type == GTK_LIST_TILE_FILLER) ++ { ++ next = gtk_rb_tree_node_get_next (tile); ++ gtk_rb_tree_remove (self->items, tile); ++ tile = next; ++ } ++ + while (tile) + { + next = gtk_rb_tree_node_get_next (tile); ++ while (next && next->type == GTK_LIST_TILE_REMOVED) ++ { ++ gtk_rb_tree_remove (self->items, next); ++ next = gtk_rb_tree_node_get_next (tile); ++ } + +- if (tile->n_items == 0) ++ switch (tile->type) + { ++ case GTK_LIST_TILE_ITEM: ++ g_assert (tile->n_items > 0); ++ if (next == NULL) ++ break; ++ if (gtk_list_item_manager_merge_list_items (self, tile, next)) ++ continue; ++ break; ++ ++ case GTK_LIST_TILE_HEADER: ++ case GTK_LIST_TILE_FOOTER: ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ case GTK_LIST_TILE_FILLER: ++ break; ++ ++ case GTK_LIST_TILE_REMOVED: + gtk_rb_tree_remove (self->items, tile); + tile = next; + continue; +- } + +- if (next == NULL) +- break; +- +- if (gtk_list_item_manager_merge_list_items (self, tile, next)) +- continue; ++ default: ++ g_assert_not_reached (); ++ break; ++ } + + break; + } +@@ -719,14 +1132,15 @@ gtk_list_tile_gc (GtkListItemManager *self, + + static void + gtk_list_item_manager_release_items (GtkListItemManager *self, +- GQueue *released) ++ GtkListItemChange *change) + { + GtkListTile *tile; + guint position, i, n_items, query_n_items; +- gboolean tracked; ++ gboolean tracked, deleted_section; + + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model)); + position = 0; ++ deleted_section = FALSE; + + while (position < n_items) + { +@@ -742,36 +1156,134 @@ gtk_list_item_manager_release_items (GtkListItemManager *self, + while (i < position + query_n_items) + { + g_assert (tile != NULL); +- if (tile->widget) ++ switch (tile->type) + { +- g_queue_push_tail (released, tile->widget); +- tile->widget = NULL; ++ case GTK_LIST_TILE_ITEM: ++ if (tile->widget) ++ { ++ gtk_list_item_change_recycle (change, GTK_LIST_ITEM_BASE (tile->widget)); ++ tile->widget = NULL; ++ } ++ i += tile->n_items; ++ break; ++ ++ case GTK_LIST_TILE_HEADER: ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ g_assert (deleted_section); ++ gtk_list_item_change_clear_header (change, &tile->widget); ++ G_GNUC_FALLTHROUGH; ++ case GTK_LIST_TILE_FOOTER: ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); ++ deleted_section = TRUE; ++ break; ++ ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ default: ++ g_assert_not_reached (); ++ break; + } +- i += tile->n_items; +- tile = gtk_rb_tree_node_get_next (tile); ++ tile = gtk_list_tile_get_next_skip (tile); ++ } ++ if (deleted_section) ++ { ++ tile = gtk_list_tile_get_header (self, tile); ++ gtk_list_item_change_clear_header (change, &tile->widget); ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_UNMATCHED_HEADER); ++ ++ tile = gtk_list_tile_get_footer (self, tile); ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_UNMATCHED_FOOTER); + } + position += query_n_items; + } + } + ++static GtkListTile * ++gtk_list_item_manager_insert_section (GtkListItemManager *self, ++ guint pos, ++ GtkListTileType footer_type, ++ GtkListTileType header_type) ++{ ++ GtkListTile *tile, *footer, *header; ++ guint offset; ++ ++ tile = gtk_list_item_manager_get_nth (self, pos, &offset); ++ if (tile == NULL) ++ { ++ if (footer_type == GTK_LIST_TILE_FOOTER) ++ { ++ footer = gtk_rb_tree_get_last (self->items); ++ if (footer->type != GTK_LIST_TILE_FOOTER && footer->type != GTK_LIST_TILE_UNMATCHED_FOOTER) ++ footer = gtk_list_tile_get_previous_skip (footer); ++ gtk_list_tile_set_type (footer, footer_type); ++ } ++ return NULL; ++ } ++ ++ if (offset) ++ tile = gtk_list_item_manager_ensure_split (self, tile, offset); ++ ++ header = gtk_list_tile_get_previous_skip (tile); ++ if (header->type == GTK_LIST_TILE_HEADER || header->type == GTK_LIST_TILE_UNMATCHED_HEADER) ++ { ++ if (header_type == GTK_LIST_TILE_HEADER) ++ gtk_list_tile_set_type (header, header_type); ++ if (footer_type == GTK_LIST_TILE_FOOTER) ++ { ++ footer = gtk_list_tile_get_previous_skip (header); ++ if (footer) ++ gtk_list_tile_set_type (footer, footer_type); ++ } ++ } ++ else ++ { ++ self->prepare_section (self->widget, tile, pos); ++ ++ header = gtk_rb_tree_insert_before (self->items, tile); ++ gtk_list_tile_set_type (header, header_type); ++ footer = gtk_rb_tree_insert_before (self->items, header); ++ gtk_list_tile_set_type (footer, footer_type); ++ } ++ ++ return header; ++} ++ ++static GtkWidget * ++gtk_list_tile_find_widget_before (GtkListTile *tile) ++{ ++ GtkListTile *other; ++ ++ for (other = gtk_rb_tree_node_get_previous (tile); ++ other; ++ other = gtk_rb_tree_node_get_previous (other)) ++ { ++ if (other->widget) ++ return other->widget; ++ } ++ ++ return NULL; ++} ++ + static void + gtk_list_item_manager_ensure_items (GtkListItemManager *self, +- GHashTable *change, +- guint update_start) ++ GtkListItemChange *change, ++ guint update_start, ++ int update_diff) + { +- GtkListTile *tile, *other_tile; +- GtkWidget *widget, *insert_after; ++ GtkListTile *tile, *header; ++ GtkWidget *insert_after; + guint position, i, n_items, query_n_items, offset; +- GQueue released = G_QUEUE_INIT; +- gboolean tracked; ++ gboolean tracked, has_sections; + + if (self->model == NULL) + return; + + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model)); + position = 0; ++ has_sections = gtk_list_item_manager_has_sections (self); + +- gtk_list_item_manager_release_items (self, &released); ++ gtk_list_item_manager_release_items (self, change); + + while (position < n_items) + { +@@ -783,66 +1295,133 @@ gtk_list_item_manager_ensure_items (GtkListItemManager *self, + } + + tile = gtk_list_item_manager_get_nth (self, position, &offset); +- for (other_tile = tile; +- other_tile && other_tile->widget == NULL; +- other_tile = gtk_rb_tree_node_get_previous (other_tile)) +- { /* do nothing */ } +- insert_after = other_tile ? other_tile->widget : NULL; +- + if (offset > 0) + tile = gtk_list_item_manager_ensure_split (self, tile, offset); + +- for (i = 0; i < query_n_items; i++) ++ if (has_sections) + { +- g_assert (tile != NULL); ++ header = gtk_list_tile_get_header (self, tile); ++ if (header->type == GTK_LIST_TILE_UNMATCHED_HEADER) ++ { ++ guint start, end; ++ gpointer item; ++ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (self->model), position, &start, &end); ++ header = gtk_list_item_manager_insert_section (self, ++ start, ++ GTK_LIST_TILE_UNMATCHED_FOOTER, ++ GTK_LIST_TILE_HEADER); ++ g_assert (header->widget == NULL); ++ header->widget = GTK_WIDGET (gtk_list_item_change_get_header (change)); ++ if (header->widget == NULL) ++ header->widget = GTK_WIDGET (self->create_header_widget (self->widget)); ++ item = g_list_model_get_item (G_LIST_MODEL (self->model), start); ++ gtk_list_header_base_update (GTK_LIST_HEADER_BASE (header->widget), ++ item, ++ start, end); ++ g_object_unref (item); ++ gtk_widget_insert_after (header->widget, ++ self->widget, ++ gtk_list_tile_find_widget_before (header)); ++ ++ gtk_list_item_manager_insert_section (self, ++ end, ++ GTK_LIST_TILE_FOOTER, ++ GTK_LIST_TILE_UNMATCHED_HEADER); ++ } ++ else if (gtk_list_header_base_get_end (GTK_LIST_HEADER_BASE (header->widget)) > update_start) ++ { ++ GtkListHeaderBase *base = GTK_LIST_HEADER_BASE (header->widget); ++ guint start = gtk_list_header_base_get_start (base); ++ gtk_list_header_base_update (base, ++ gtk_list_header_base_get_item (base), ++ start > update_start ? start + update_diff : start, ++ gtk_list_header_base_get_end (base) + update_diff); ++ } ++ } + +- while (tile->n_items == 0) +- tile = gtk_rb_tree_node_get_next (tile); ++ insert_after = gtk_list_tile_find_widget_before (tile); + +- if (tile->n_items > 1) +- gtk_list_item_manager_ensure_split (self, tile, 1); ++ for (i = 0; i < query_n_items;) ++ { ++ g_assert (tile != NULL); + +- if (tile->widget == NULL) +- { +- if (change) ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_ITEM: ++ if (tile->n_items > 1) ++ gtk_list_item_manager_ensure_split (self, tile, 1); ++ ++ if (tile->widget == NULL) + { +- tile->widget = gtk_list_item_manager_try_reacquire_list_item (self, +- change, +- position + i, +- insert_after); ++ gpointer item = g_list_model_get_item (G_LIST_MODEL (self->model), position + i); ++ tile->widget = GTK_WIDGET (gtk_list_item_change_get (change, item)); ++ if (tile->widget == NULL) ++ tile->widget = GTK_WIDGET (self->create_widget (self->widget)); ++ gtk_list_item_base_update (GTK_LIST_ITEM_BASE (tile->widget), ++ position + i, ++ item, ++ gtk_selection_model_is_selected (self->model, position + i)); ++ gtk_widget_insert_after (tile->widget, self->widget, insert_after); + } +- if (tile->widget == NULL) ++ else + { +- tile->widget = g_queue_pop_head (&released); +- if (tile->widget) ++ if (update_start <= position + i) + { +- gtk_list_item_manager_move_list_item (self, +- tile->widget, +- position + i, +- insert_after); +- } +- else +- { +- tile->widget = gtk_list_item_manager_acquire_list_item (self, +- position + i, +- insert_after); ++ gtk_list_item_base_update (GTK_LIST_ITEM_BASE (tile->widget), ++ position + i, ++ gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (tile->widget)), ++ gtk_selection_model_is_selected (self->model, position + i)); + } + } +- } +- else +- { +- if (update_start <= position + i) +- gtk_list_item_manager_update_list_item (self, tile->widget, position + i); +- } +- insert_after = tile->widget; ++ insert_after = tile->widget; ++ i++; ++ break; + +- tile = gtk_rb_tree_node_get_next (tile); ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ if (has_sections) ++ { ++ guint start, end; ++ gpointer item; ++ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (self->model), position + i, &start, &end); ++ ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_HEADER); ++ g_assert (tile->widget == NULL); ++ tile->widget = GTK_WIDGET (gtk_list_item_change_get_header (change)); ++ if (tile->widget == NULL) ++ tile->widget = GTK_WIDGET (self->create_header_widget (self->widget)); ++ item = g_list_model_get_item (G_LIST_MODEL (self->model), start); ++ gtk_list_header_base_update (GTK_LIST_HEADER_BASE (tile->widget), ++ item, ++ start, end); ++ g_object_unref (item); ++ gtk_widget_insert_after (tile->widget, self->widget, insert_after); ++ insert_after = tile->widget; ++ ++ gtk_list_item_manager_insert_section (self, ++ end, ++ GTK_LIST_TILE_FOOTER, ++ GTK_LIST_TILE_UNMATCHED_HEADER); ++ } ++ break; ++ ++ case GTK_LIST_TILE_HEADER: ++ case GTK_LIST_TILE_FOOTER: ++ break; ++ ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ tile = gtk_list_tile_get_next_skip (tile); + } ++ + position += query_n_items; + } +- +- while ((widget = g_queue_pop_head (&released))) +- gtk_list_item_manager_release_list_item (self, NULL, widget); + } + + static void +@@ -852,15 +1431,15 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + guint added, + GtkListItemManager *self) + { +- GHashTable *change; ++ GtkListItemChange change; + GSList *l; + guint n_items; + ++ gtk_list_item_change_init (&change); + n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model)); +- change = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify )gtk_widget_unparent); + +- gtk_list_item_manager_remove_items (self, change, position, removed); +- gtk_list_item_manager_add_items (self, position, added); ++ gtk_list_item_manager_remove_items (self, &change, position, removed); ++ gtk_list_item_manager_add_items (self, &change, position, added); + + /* Check if any tracked item was removed */ + for (l = self->trackers; l; l = l->next) +@@ -870,7 +1449,7 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + if (tracker->widget == NULL) + continue; + +- if (g_hash_table_lookup (change, gtk_list_item_base_get_item (tracker->widget))) ++ if (tracker->position >= position && tracker->position < position + removed) + break; + } + +@@ -894,12 +1473,14 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + + for (i = 0; i < added; i++) + { +- GtkWidget *widget; ++ GtkListItemBase *widget; ++ gpointer item; ++ ++ /* XXX: can we avoid temporarily allocating items on failure? */ ++ item = g_list_model_get_item (G_LIST_MODEL (self->model), position + i); ++ widget = gtk_list_item_change_find (&change, item); ++ g_object_unref (item); + +- widget = gtk_list_item_manager_try_reacquire_list_item (self, +- change, +- position + i, +- insert_after); + if (widget == NULL) + { + offset++; +@@ -923,8 +1504,13 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + else + tile = gtk_list_item_manager_ensure_split (self, tile, 1); + +- new_tile->widget = widget; +- insert_after = widget; ++ new_tile->widget = GTK_WIDGET (widget); ++ gtk_list_item_base_update (widget, ++ position + i, ++ item, ++ gtk_selection_model_is_selected (self->model, position + i)); ++ gtk_widget_insert_after (new_tile->widget, self->widget, insert_after); ++ insert_after = new_tile->widget; + } + } + +@@ -948,9 +1534,13 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + } + else if (tracker->position >= position) + { +- if (g_hash_table_lookup (change, gtk_list_item_base_get_item (tracker->widget))) ++ GtkListItemBase *widget = gtk_list_item_change_find (&change, gtk_list_item_base_get_item (tracker->widget)); ++ if (widget) + { +- /* The item is gone. Guess a good new position */ ++ /* The item is still in the recycling pool, which means it got deleted. ++ * Put the widget back and then guess a good new position */ ++ gtk_list_item_change_release (&change, widget); ++ + tracker->position = position + (tracker->position - position) * added / removed; + if (tracker->position >= n_items) + { +@@ -975,7 +1565,7 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + } + } + +- gtk_list_item_manager_ensure_items (self, change, position + added); ++ gtk_list_item_manager_ensure_items (self, &change, position + added, added - removed); + + /* final loop through the trackers: Grab the missing widgets. + * For items that had been removed and a new position was set, grab +@@ -996,7 +1586,7 @@ gtk_list_item_manager_model_items_changed_cb (GListModel *model, + tracker->widget = GTK_LIST_ITEM_BASE (tile->widget); + } + +- g_hash_table_unref (change); ++ gtk_list_item_change_finish (&change); + + gtk_widget_queue_resize (self->widget); + } +@@ -1024,23 +1614,32 @@ gtk_list_item_manager_model_selection_changed_cb (GListModel *model, + + while (n_items > 0) + { +- if (tile->widget) +- gtk_list_item_manager_update_list_item (self, tile->widget, position); ++ if (tile->widget && tile->type == GTK_LIST_TILE_ITEM) ++ { ++ gtk_list_item_base_update (GTK_LIST_ITEM_BASE (tile->widget), ++ position, ++ gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (tile->widget)), ++ gtk_selection_model_is_selected (self->model, position)); ++ } + position += tile->n_items; + n_items -= MIN (n_items, tile->n_items); +- tile = gtk_rb_tree_node_get_next (tile); ++ tile = gtk_list_tile_get_next_skip (tile); + } + } + + static void + gtk_list_item_manager_clear_model (GtkListItemManager *self) + { ++ GtkListItemChange change; ++ GtkListTile *tile; + GSList *l; + + if (self->model == NULL) + return; + +- gtk_list_item_manager_remove_items (self, NULL, 0, g_list_model_get_n_items (G_LIST_MODEL (self->model))); ++ gtk_list_item_change_init (&change); ++ gtk_list_item_manager_remove_items (self, &change, 0, g_list_model_get_n_items (G_LIST_MODEL (self->model))); ++ gtk_list_item_change_finish (&change); + for (l = self->trackers; l; l = l->next) + { + gtk_list_item_tracker_unset_position (self, l->data); +@@ -1053,6 +1652,15 @@ gtk_list_item_manager_clear_model (GtkListItemManager *self) + gtk_list_item_manager_model_items_changed_cb, + self); + g_clear_object (&self->model); ++ ++ /* really empty the tiles */ ++ for (tile = gtk_list_tile_gc (self, gtk_list_item_manager_get_first (self)); ++ tile; ++ tile = gtk_list_tile_gc (self, tile)) ++ { ++ g_assert (tile->type == GTK_LIST_TILE_FILLER); ++ } ++ g_assert (gtk_rb_tree_get_root (self->items) == NULL); + } + + static void +@@ -1094,6 +1702,8 @@ gtk_list_item_manager_set_model (GtkListItemManager *self, + + if (model) + { ++ GtkListItemChange change; ++ + self->model = g_object_ref (model); + + g_signal_connect (model, +@@ -1105,7 +1715,10 @@ gtk_list_item_manager_set_model (GtkListItemManager *self, + G_CALLBACK (gtk_list_item_manager_model_selection_changed_cb), + self); + +- gtk_list_item_manager_add_items (self, 0, g_list_model_get_n_items (G_LIST_MODEL (model))); ++ gtk_list_item_change_init (&change); ++ gtk_list_item_manager_add_items (self, &change, 0, g_list_model_get_n_items (G_LIST_MODEL (model))); ++ gtk_list_item_manager_ensure_items (self, &change, G_MAXUINT, 0); ++ gtk_list_item_change_finish (&change); + } + } + +@@ -1117,185 +1730,73 @@ gtk_list_item_manager_get_model (GtkListItemManager *self) + return self->model; + } + +-/* +- * gtk_list_item_manager_acquire_list_item: +- * @self: a `GtkListItemManager` +- * @position: the row in the model to create a list item for +- * @prev_sibling: the widget this widget should be inserted before or %NULL +- * if it should be the first widget +- * +- * Creates a list item widget to use for @position. No widget may +- * yet exist that is used for @position. +- * +- * When the returned item is no longer needed, the caller is responsible +- * for calling gtk_list_item_manager_release_list_item(). +- * A particular case is when the row at @position is removed. In that case, +- * all list items in the removed range must be released before +- * gtk_list_item_manager_model_changed() is called. +- * +- * Returns: a properly setup widget to use in @position +- **/ +-static GtkWidget * +-gtk_list_item_manager_acquire_list_item (GtkListItemManager *self, +- guint position, +- GtkWidget *prev_sibling) ++void ++gtk_list_item_manager_set_has_sections (GtkListItemManager *self, ++ gboolean has_sections) + { +- GtkListItemBase *result; +- gpointer item; +- gboolean selected; ++ GtkListItemChange change; ++ GtkListTile *tile; ++ gboolean had_sections; + +- g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); +- g_return_val_if_fail (prev_sibling == NULL || GTK_IS_WIDGET (prev_sibling), NULL); ++ if (self->has_sections == has_sections) ++ return; + +- result = self->create_widget (self->widget); ++ had_sections = gtk_list_item_manager_has_sections (self); + +- item = g_list_model_get_item (G_LIST_MODEL (self->model), position); +- selected = gtk_selection_model_is_selected (self->model, position); +- gtk_list_item_base_update (result, position, item, selected); +- g_object_unref (item); +- gtk_widget_insert_after (GTK_WIDGET (result), self->widget, prev_sibling); ++ self->has_sections = has_sections; + +- return GTK_WIDGET (result); +-} ++ gtk_list_item_change_init (&change); + +-/** +- * gtk_list_item_manager_try_acquire_list_item_from_change: +- * @self: a `GtkListItemManager` +- * @position: the row in the model to create a list item for +- * @prev_sibling: the widget this widget should be inserted after or %NULL +- * if it should be the first widget +- * +- * Like gtk_list_item_manager_acquire_list_item(), but only tries to acquire list +- * items from those previously released as part of @change. +- * If no matching list item is found, %NULL is returned and the caller should use +- * gtk_list_item_manager_acquire_list_item(). +- * +- * Returns: (nullable): a properly setup widget to use in @position or %NULL if +- * no item for reuse existed +- **/ +-static GtkWidget * +-gtk_list_item_manager_try_reacquire_list_item (GtkListItemManager *self, +- GHashTable *change, +- guint position, +- GtkWidget *prev_sibling) +-{ +- GtkWidget *result; +- gpointer item; +- +- g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); +- g_return_val_if_fail (prev_sibling == NULL || GTK_IS_WIDGET (prev_sibling), NULL); +- +- /* XXX: can we avoid temporarily allocating items on failure? */ +- item = g_list_model_get_item (G_LIST_MODEL (self->model), position); +- if (g_hash_table_steal_extended (change, item, NULL, (gpointer *) &result)) +- { +- GtkListItemBase *list_item = GTK_LIST_ITEM_BASE (result); +- gtk_list_item_base_update (list_item, +- position, +- gtk_list_item_base_get_item (list_item), +- gtk_selection_model_is_selected (self->model, position)); +- gtk_widget_insert_after (result, self->widget, prev_sibling); +- /* XXX: Should we let the listview do this? */ +- gtk_widget_queue_resize (result); +- } +- else ++ if (had_sections && !gtk_list_item_manager_has_sections (self)) + { +- result = NULL; +- } +- g_object_unref (item); +- +- return result; +-} ++ GtkListTile *header = NULL, *footer = NULL; + +-/** +- * gtk_list_item_manager_move_list_item: +- * @self: a `GtkListItemManager` +- * @list_item: an acquired `GtkListItem` that should be moved to represent +- * a different row +- * @position: the new position of that list item +- * @prev_sibling: the new previous sibling +- * +- * Moves the widget to represent a new position in the listmodel without +- * releasing the item. +- * +- * This is most useful when scrolling. +- **/ +-static void +-gtk_list_item_manager_move_list_item (GtkListItemManager *self, +- GtkWidget *list_item, +- guint position, +- GtkWidget *prev_sibling) +-{ +- gpointer item; +- gboolean selected; +- +- item = g_list_model_get_item (G_LIST_MODEL (self->model), position); +- selected = gtk_selection_model_is_selected (self->model, position); +- gtk_list_item_base_update (GTK_LIST_ITEM_BASE (list_item), +- position, +- item, +- selected); +- gtk_widget_insert_after (list_item, _gtk_widget_get_parent (list_item), prev_sibling); +- g_object_unref (item); +-} +- +-/** +- * gtk_list_item_manager_update_list_item: +- * @self: a `GtkListItemManager` +- * @item: a `GtkListItem` that has been acquired +- * @position: the new position of that list item +- * +- * Updates the position of the given @item. This function must be called whenever +- * the position of an item changes, like when new items are added before it. +- **/ +-static void +-gtk_list_item_manager_update_list_item (GtkListItemManager *self, +- GtkWidget *item, +- guint position) +-{ +- GtkListItemBase *list_item = GTK_LIST_ITEM_BASE (item); +- gboolean selected; ++ for (tile = gtk_rb_tree_get_first (self->items); ++ tile; ++ tile = gtk_list_tile_get_next_skip (tile)) ++ { ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_HEADER: ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ gtk_list_item_change_clear_header (&change, &tile->widget); ++ if (!header) ++ header = tile; ++ else ++ gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); ++ break; ++ case GTK_LIST_TILE_FOOTER: ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ if (footer) ++ gtk_list_tile_set_type (footer, GTK_LIST_TILE_REMOVED); ++ footer = tile; ++ break; ++ case GTK_LIST_TILE_ITEM: ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ break; ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ } ++ if (header) ++ { ++ gtk_list_tile_set_type (header, GTK_LIST_TILE_UNMATCHED_HEADER); ++ gtk_list_tile_set_type (footer, GTK_LIST_TILE_UNMATCHED_FOOTER); ++ } ++ } + +- g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); +- g_return_if_fail (GTK_IS_LIST_ITEM_BASE (item)); ++ gtk_list_item_manager_ensure_items (self, &change, G_MAXUINT, 0); ++ gtk_list_item_change_finish (&change); + +- selected = gtk_selection_model_is_selected (self->model, position); +- gtk_list_item_base_update (list_item, +- position, +- gtk_list_item_base_get_item (list_item), +- selected); ++ gtk_widget_queue_resize (self->widget); + } + +-/* +- * gtk_list_item_manager_release_list_item: +- * @self: a `GtkListItemManager` +- * @change: (nullable): The change associated with this release or +- * %NULL if this is a final removal +- * @item: an item previously acquired with +- * gtk_list_item_manager_acquire_list_item() +- * +- * Releases an item that was previously acquired via +- * gtk_list_item_manager_acquire_list_item() and is no longer in use. +- **/ +-static void +-gtk_list_item_manager_release_list_item (GtkListItemManager *self, +- GHashTable *change, +- GtkWidget *item) ++gboolean ++gtk_list_item_manager_get_has_sections (GtkListItemManager *self) + { +- g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); +- g_return_if_fail (GTK_IS_LIST_ITEM_BASE (item)); +- +- if (change != NULL) +- { +- if (!g_hash_table_replace (change, gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (item)), item)) +- { +- g_warning ("Duplicate item detected in list. Picking one randomly."); +- } +- +- return; +- } +- +- gtk_widget_unparent (item); ++ return self->has_sections; + } + + GtkListItemTracker * +@@ -1318,13 +1819,17 @@ void + gtk_list_item_tracker_free (GtkListItemManager *self, + GtkListItemTracker *tracker) + { ++ GtkListItemChange change; ++ + gtk_list_item_tracker_unset_position (self, tracker); + + self->trackers = g_slist_remove (self->trackers, tracker); + + g_free (tracker); + +- gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT); ++ gtk_list_item_change_init (&change); ++ gtk_list_item_manager_ensure_items (self, &change, G_MAXUINT, 0); ++ gtk_list_item_change_finish (&change); + + gtk_widget_queue_resize (self->widget); + } +@@ -1336,6 +1841,7 @@ gtk_list_item_tracker_set_position (GtkListItemManager *self, + guint n_before, + guint n_after) + { ++ GtkListItemChange change; + GtkListTile *tile; + guint n_items; + +@@ -1352,7 +1858,9 @@ gtk_list_item_tracker_set_position (GtkListItemManager *self, + tracker->n_before = n_before; + tracker->n_after = n_after; + +- gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT); ++ gtk_list_item_change_init (&change); ++ gtk_list_item_manager_ensure_items (self, &change, G_MAXUINT, 0); ++ gtk_list_item_change_finish (&change); + + tile = gtk_list_item_manager_get_nth (self, position, NULL); + if (tile) +diff --git a/gtk/gtklistitemmanagerprivate.h b/gtk/gtklistitemmanagerprivate.h +index 44a9ac82ea4a4deeb60fa7486a5cd1168d5aff7f..cc92ef74a9db6145a7ffa0496d170cd4d32cd9f3 100644 +--- a/gtk/gtklistitemmanagerprivate.h ++++ b/gtk/gtklistitemmanagerprivate.h +@@ -24,6 +24,7 @@ + #include "gtk/gtkenums.h" + + #include "gtk/gtklistitembaseprivate.h" ++#include "gtk/gtklistheaderbaseprivate.h" + #include "gtk/gtklistitemfactory.h" + #include "gtk/gtkrbtreeprivate.h" + #include "gtk/gtkselectionmodel.h" +@@ -43,8 +44,20 @@ typedef struct _GtkListTile GtkListTile; + typedef struct _GtkListTileAugment GtkListTileAugment; + typedef struct _GtkListItemTracker GtkListItemTracker; + ++typedef enum ++{ ++ GTK_LIST_TILE_ITEM, ++ GTK_LIST_TILE_HEADER, ++ GTK_LIST_TILE_FOOTER, ++ GTK_LIST_TILE_UNMATCHED_HEADER, ++ GTK_LIST_TILE_UNMATCHED_FOOTER, ++ GTK_LIST_TILE_FILLER, ++ GTK_LIST_TILE_REMOVED, ++} GtkListTileType; ++ + struct _GtkListTile + { ++ GtkListTileType type; + GtkWidget *widget; + guint n_items; + /* area occupied by tile. May be empty if tile has no allcoation */ +@@ -54,6 +67,10 @@ struct _GtkListTile + struct _GtkListTileAugment + { + guint n_items; ++ ++ guint has_header :1; ++ guint has_footer :1; ++ + /* union of all areas of tile and children */ + cairo_rectangle_int_t area; + }; +@@ -63,7 +80,9 @@ GType gtk_list_item_manager_get_type (void) G_GNUC_CO + + GtkListItemManager * gtk_list_item_manager_new (GtkWidget *widget, + GtkListTile * (* split_func) (GtkWidget *, GtkListTile *, guint), +- GtkListItemBase * (* create_widget) (GtkWidget *)); ++ GtkListItemBase * (* create_widget) (GtkWidget *), ++ void (* prepare_section) (GtkWidget *, GtkListTile *, guint), ++ GtkListHeaderBase * (* create_header_widget) (GtkWidget *)); + + void gtk_list_item_manager_get_tile_bounds (GtkListItemManager *self, + GdkRectangle *out_bounds); +@@ -97,12 +116,17 @@ void gtk_list_tile_set_area_size (GtkListItemMana + GtkListTile * gtk_list_tile_split (GtkListItemManager *self, + GtkListTile *tile, + guint n_items); ++GtkListTile * gtk_list_tile_append_filler (GtkListItemManager *self, ++ GtkListTile *previous); + GtkListTile * gtk_list_tile_gc (GtkListItemManager *self, + GtkListTile *tile); + + void gtk_list_item_manager_set_model (GtkListItemManager *self, + GtkSelectionModel *model); + GtkSelectionModel * gtk_list_item_manager_get_model (GtkListItemManager *self); ++void gtk_list_item_manager_set_has_sections (GtkListItemManager *self, ++ gboolean has_sections); ++gboolean gtk_list_item_manager_get_has_sections (GtkListItemManager *self); + + GtkListItemTracker * gtk_list_item_tracker_new (GtkListItemManager *self); + void gtk_list_item_tracker_free (GtkListItemManager *self, +diff --git a/gtk/gtklistview.c b/gtk/gtklistview.c +index f6709be181a312bdccc00c4f4b9f09291ea719fa..ef274d93a4797afbf911e68bd9dd20a11a864377 100644 +--- a/gtk/gtklistview.c ++++ b/gtk/gtklistview.c +@@ -23,6 +23,7 @@ + + #include "gtkbitset.h" + #include "gtklistbaseprivate.h" ++#include "gtklistheaderwidgetprivate.h" + #include "gtklistitemmanagerprivate.h" + #include "gtklistitemwidgetprivate.h" + #include "gtkmultiselection.h" +@@ -145,6 +146,7 @@ enum + PROP_0, + PROP_ENABLE_RUBBERBAND, + PROP_FACTORY, ++ PROP_HEADER_FACTORY, + PROP_MODEL, + PROP_SHOW_SEPARATORS, + PROP_SINGLE_CLICK_ACTIVATE, +@@ -163,29 +165,6 @@ G_DEFINE_TYPE (GtkListView, gtk_list_view, GTK_TYPE_LIST_BASE) + static GParamSpec *properties[N_PROPS] = { NULL, }; + static guint signals[LAST_SIGNAL] = { 0 }; + +-static void G_GNUC_UNUSED +-dump (GtkListView *self) +-{ +- GtkListTile *tile; +- guint n_widgets, n_list_rows; +- +- n_widgets = 0; +- n_list_rows = 0; +- //g_print ("ANCHOR: %u - %u\n", self->anchor_start, self->anchor_end); +- for (tile = gtk_list_item_manager_get_first (self->item_manager); +- tile; +- tile = gtk_rb_tree_node_get_next (tile)) +- { +- if (tile->widget) +- n_widgets++; +- n_list_rows++; +- g_print (" %4u%s %d,%d,%d,%d\n", tile->n_items, tile->widget ? " (widget)" : "", +- tile->area.x, tile->area.y, tile->area.width, tile->area.height); +- } +- +- g_print (" => %u widgets in %u list rows\n", n_widgets, n_list_rows); +-} +- + static GtkListTile * + gtk_list_view_split (GtkListBase *base, + GtkListTile *tile, +@@ -215,6 +194,13 @@ gtk_list_view_split (GtkListBase *base, + return new_tile; + } + ++static void ++gtk_list_view_prepare_section (GtkListBase *base, ++ GtkListTile *tile, ++ guint position) ++{ ++} ++ + /* We define the listview as **inert** when the factory isn't used. */ + static gboolean + gtk_list_view_is_inert (GtkListView *self) +@@ -222,13 +208,13 @@ gtk_list_view_is_inert (GtkListView *self) + GtkWidget *widget = GTK_WIDGET (self); + + return !gtk_widget_get_visible (widget) || +- gtk_widget_get_root (widget) == NULL || +- self->factory == NULL; ++ gtk_widget_get_root (widget) == NULL; + } + + static void + gtk_list_view_update_factories_with (GtkListView *self, +- GtkListItemFactory *factory) ++ GtkListItemFactory *factory, ++ GtkListItemFactory *header_factory) + { + GtkListTile *tile; + +@@ -236,8 +222,27 @@ gtk_list_view_update_factories_with (GtkListView *self, + tile != NULL; + tile = gtk_rb_tree_node_get_next (tile)) + { +- if (tile->widget) +- gtk_list_factory_widget_set_factory (GTK_LIST_FACTORY_WIDGET (tile->widget), factory); ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_ITEM: ++ if (tile->widget) ++ gtk_list_factory_widget_set_factory (GTK_LIST_FACTORY_WIDGET (tile->widget), factory); ++ break; ++ case GTK_LIST_TILE_HEADER: ++ if (tile->widget) ++ gtk_list_header_widget_set_factory (GTK_LIST_HEADER_WIDGET (tile->widget), header_factory); ++ break; ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ case GTK_LIST_TILE_FOOTER: ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ g_assert (tile->widget == NULL); ++ break; ++ default: ++ g_assert_not_reached(); ++ break; ++ } + } + } + +@@ -245,13 +250,14 @@ static void + gtk_list_view_update_factories (GtkListView *self) + { + gtk_list_view_update_factories_with (self, +- gtk_list_view_is_inert (self) ? NULL : self->factory); ++ gtk_list_view_is_inert (self) ? NULL : self->factory, ++ gtk_list_view_is_inert (self) ? NULL : self->header_factory); + } + + static void + gtk_list_view_clear_factories (GtkListView *self) + { +- gtk_list_view_update_factories_with (self, NULL); ++ gtk_list_view_update_factories_with (self, NULL, NULL); + } + + static GtkListItemBase * +@@ -275,6 +281,20 @@ gtk_list_view_create_list_widget (GtkListBase *base) + return GTK_LIST_ITEM_BASE (result); + } + ++static GtkListHeaderBase * ++gtk_list_view_create_header_widget (GtkListBase *base) ++{ ++ GtkListView *self = GTK_LIST_VIEW (base); ++ GtkListItemFactory *factory; ++ ++ if (gtk_list_view_is_inert (self)) ++ factory = NULL; ++ else ++ factory = self->header_factory; ++ ++ return GTK_LIST_HEADER_BASE (gtk_list_header_widget_new (factory)); ++} ++ + static gboolean + gtk_list_view_get_allocation (GtkListBase *base, + guint pos, +@@ -527,8 +547,11 @@ gtk_list_view_measure_list (GtkWidget *widget, + gtk_widget_measure (tile->widget, + orientation, for_size, + &child_min, &child_nat, NULL, NULL); +- g_array_append_val (min_heights, child_min); +- g_array_append_val (nat_heights, child_nat); ++ if (tile->type == GTK_LIST_TILE_ITEM) ++ { ++ g_array_append_val (min_heights, child_min); ++ g_array_append_val (nat_heights, child_nat); ++ } + min += child_min; + nat += child_nat; + } +@@ -622,7 +645,8 @@ gtk_list_view_size_allocate (GtkWidget *widget, + else + row_height = nat; + gtk_list_tile_set_area_size (self->item_manager, tile, list_width, row_height); +- g_array_append_val (heights, row_height); ++ if (tile->type == GTK_LIST_TILE_ITEM) ++ g_array_append_val (heights, row_height); + } + + /* step 3: determine height of unknown items and set the positions */ +@@ -723,6 +747,10 @@ gtk_list_view_get_property (GObject *object, + g_value_set_object (value, self->factory); + break; + ++ case PROP_HEADER_FACTORY: ++ g_value_set_object (value, self->header_factory); ++ break; ++ + case PROP_MODEL: + g_value_set_object (value, gtk_list_base_get_model (GTK_LIST_BASE (self))); + break; +@@ -763,6 +791,10 @@ gtk_list_view_set_property (GObject *object, + gtk_list_view_set_factory (self, g_value_get_object (value)); + break; + ++ case PROP_HEADER_FACTORY: ++ gtk_list_view_set_header_factory (self, g_value_get_object (value)); ++ break; ++ + case PROP_MODEL: + gtk_list_view_set_model (self, g_value_get_object (value)); + break; +@@ -812,6 +844,8 @@ gtk_list_view_class_init (GtkListViewClass *klass) + + list_base_class->split = gtk_list_view_split; + list_base_class->create_list_widget = gtk_list_view_create_list_widget; ++ list_base_class->prepare_section = gtk_list_view_prepare_section; ++ list_base_class->create_header_widget = gtk_list_view_create_header_widget; + list_base_class->get_allocation = gtk_list_view_get_allocation; + list_base_class->get_items_in_rect = gtk_list_view_get_items_in_rect; + list_base_class->get_position_from_allocation = gtk_list_view_get_position_from_allocation; +@@ -849,6 +883,18 @@ gtk_list_view_class_init (GtkListViewClass *klass) + GTK_TYPE_LIST_ITEM_FACTORY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + ++ /** ++ * GtkListView:header-factory: (attributes org.gtk.Property.get=gtk_list_view_get_header_factory org.gtk.Property.set=gtk_list_view_set_header_factory) ++ * ++ * Factory for creating header widgets. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_HEADER_FACTORY] = ++ g_param_spec_object ("header-factory", NULL, NULL, ++ GTK_TYPE_LIST_ITEM_FACTORY, ++ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); ++ + /** + * GtkListView:model: (attributes org.gtk.Property.get=gtk_list_view_get_model org.gtk.Property.set=gtk_list_view_set_model) + * +@@ -1054,22 +1100,80 @@ void + gtk_list_view_set_factory (GtkListView *self, + GtkListItemFactory *factory) + { +- gboolean was_inert; +- + g_return_if_fail (GTK_IS_LIST_VIEW (self)); + g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); + +- was_inert = gtk_list_view_is_inert (self); +- + if (!g_set_object (&self->factory, factory)) + return; + +- if (!was_inert || !gtk_list_view_is_inert (self)) +- gtk_list_view_update_factories (self); ++ gtk_list_view_update_factories (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]); + } + ++/** ++ * gtk_list_view_get_header_factory: (attributes org.gtk.Method.get_property=header-factory) ++ * @self: a `GtkListView` ++ * ++ * Gets the factory that's currently used to populate section headers. ++ * ++ * Returns: (nullable) (transfer none): The factory in use ++ * ++ * Since: 4.12 ++ */ ++GtkListItemFactory * ++gtk_list_view_get_header_factory (GtkListView *self) ++{ ++ g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL); ++ ++ return self->header_factory; ++} ++ ++/** ++ * gtk_list_view_set_header_factory: (attributes org.gtk.Method.set_property=header-factory) ++ * @self: a `GtkListView` ++ * @factory: (nullable) (transfer none): the factory to use ++ * ++ * Sets the `GtkListItemFactory` to use for populating the ++ * [class@Gtk.ListHeader] objects used in section headers. ++ * ++ * If this factory is set to %NULL, the list will not show section headers. ++ * ++ * Since: 4.12 ++ */ ++void ++gtk_list_view_set_header_factory (GtkListView *self, ++ GtkListItemFactory *factory) ++{ ++ gboolean had_sections; ++ ++ g_return_if_fail (GTK_IS_LIST_VIEW (self)); ++ g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); ++ ++ had_sections = gtk_list_item_manager_get_has_sections (self->item_manager); ++ ++ if (!g_set_object (&self->header_factory, factory)) ++ return; ++ ++ gtk_list_item_manager_set_has_sections (self->item_manager, factory != NULL); ++ ++ if (!gtk_list_view_is_inert (self) && ++ had_sections && gtk_list_item_manager_get_has_sections (self->item_manager)) ++ { ++ GtkListTile *tile; ++ ++ for (tile = gtk_list_item_manager_get_first (self->item_manager); ++ tile != NULL; ++ tile = gtk_rb_tree_node_get_next (tile)) ++ { ++ if (tile->widget && tile->type == GTK_LIST_TILE_HEADER) ++ gtk_list_header_widget_set_factory (GTK_LIST_HEADER_WIDGET (tile->widget), factory); ++ } ++ } ++ ++ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HEADER_FACTORY]); ++} ++ + /** + * gtk_list_view_set_show_separators: (attributes org.gtk.Method.set_property=show-separators) + * @self: a `GtkListView` +@@ -1139,7 +1243,7 @@ gtk_list_view_set_single_click_activate (GtkListView *self, + tile != NULL; + tile = gtk_rb_tree_node_get_next (tile)) + { +- if (tile->widget) ++ if (tile->widget && tile->type == GTK_LIST_TILE_ITEM) + gtk_list_factory_widget_set_single_click_activate (GTK_LIST_FACTORY_WIDGET (tile->widget), single_click_activate); + } + +diff --git a/gtk/gtklistview.h b/gtk/gtklistview.h +index b3e3afb60a1fc6634e1574edbfe87f3b89539c3a..df4901e319a1e2d84f1b3ebc62c62fa8142d5fb3 100644 +--- a/gtk/gtklistview.h ++++ b/gtk/gtklistview.h +@@ -57,6 +57,13 @@ GDK_AVAILABLE_IN_ALL + GtkListItemFactory * + gtk_list_view_get_factory (GtkListView *self); + ++GDK_AVAILABLE_IN_4_12 ++void gtk_list_view_set_header_factory (GtkListView *self, ++ GtkListItemFactory *factory); ++GDK_AVAILABLE_IN_4_12 ++GtkListItemFactory * ++ gtk_list_view_get_header_factory (GtkListView *self); ++ + GDK_AVAILABLE_IN_ALL + void gtk_list_view_set_show_separators (GtkListView *self, + gboolean show_separators); +diff --git a/gtk/gtklistviewprivate.h b/gtk/gtklistviewprivate.h +index 002376fc1e52f1cbdee692ad34504280c6e3d16b..7dd02ce3f83db126016d2617d36a871a597988e1 100644 +--- a/gtk/gtklistviewprivate.h ++++ b/gtk/gtklistviewprivate.h +@@ -30,6 +30,7 @@ struct _GtkListView + + GtkListItemManager *item_manager; + GtkListItemFactory *factory; ++ GtkListItemFactory *header_factory; + gboolean show_separators; + gboolean single_click_activate; + }; +diff --git a/gtk/gtkmultiselection.c b/gtk/gtkmultiselection.c +index 055472d3d45cb3aec091acc48c48e61ad2d0a97d..f549541baa6d7779f98b38ed75f4b7a08bcdda58 100644 +--- a/gtk/gtkmultiselection.c ++++ b/gtk/gtkmultiselection.c +@@ -22,6 +22,7 @@ + #include "gtkmultiselection.h" + + #include "gtkbitset.h" ++#include "gtksectionmodelprivate.h" + #include "gtkselectionmodel.h" + + /** +@@ -94,6 +95,23 @@ gtk_multi_selection_list_model_init (GListModelInterface *iface) + iface->get_item = gtk_multi_selection_get_item; + } + ++static void ++gtk_multi_selection_get_section (GtkSectionModel *model, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkMultiSelection *self = GTK_MULTI_SELECTION (model); ++ ++ gtk_list_model_get_section (self->model, position, out_start, out_end); ++} ++ ++static void ++gtk_multi_selection_section_model_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_multi_selection_get_section; ++} ++ + static gboolean + gtk_multi_selection_is_selected (GtkSelectionModel *model, + guint position) +@@ -205,6 +223,8 @@ gtk_multi_selection_selection_model_init (GtkSelectionModelInterface *iface) + G_DEFINE_TYPE_EXTENDED (GtkMultiSelection, gtk_multi_selection, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, + gtk_multi_selection_list_model_init) ++ G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL, ++ gtk_multi_selection_section_model_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, + gtk_multi_selection_selection_model_init)) + +diff --git a/gtk/gtknoselection.c b/gtk/gtknoselection.c +index c0848f7c0e860479dc7efbe17e9878f05da8ca28..20bff24df292306bf787d5e4743974716a7eddf3 100644 +--- a/gtk/gtknoselection.c ++++ b/gtk/gtknoselection.c +@@ -22,6 +22,7 @@ + #include "gtknoselection.h" + + #include "gtkbitset.h" ++#include "gtksectionmodelprivate.h" + #include "gtkselectionmodel.h" + + /** +@@ -92,6 +93,23 @@ gtk_no_selection_list_model_init (GListModelInterface *iface) + iface->get_item = gtk_no_selection_get_item; + } + ++static void ++gtk_no_selection_get_section (GtkSectionModel *model, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkNoSelection *self = GTK_NO_SELECTION (model); ++ ++ gtk_list_model_get_section (self->model, position, out_start, out_end); ++} ++ ++static void ++gtk_no_selection_section_model_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_no_selection_get_section; ++} ++ + static gboolean + gtk_no_selection_is_selected (GtkSelectionModel *model, + guint position) +@@ -117,6 +135,8 @@ gtk_no_selection_selection_model_init (GtkSelectionModelInterface *iface) + G_DEFINE_TYPE_EXTENDED (GtkNoSelection, gtk_no_selection, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, + gtk_no_selection_list_model_init) ++ G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL, ++ gtk_no_selection_section_model_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, + gtk_no_selection_selection_model_init)) + +diff --git a/gtk/gtksectionmodel.c b/gtk/gtksectionmodel.c +new file mode 100644 +index 0000000000000000000000000000000000000000..a5084ab340ab3bd6356cfb256f16746df298b987 +--- /dev/null ++++ b/gtk/gtksectionmodel.c +@@ -0,0 +1,225 @@ ++/* ++ * Copyright © 2022 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 "gtksectionmodelprivate.h" ++ ++#include "gtkmarshalers.h" ++ ++/** ++ * GtkSectionModel: ++ * ++ * `GtkSectionModel` is an interface that adds support for section to list models. ++ * ++ * This support is then used by widgets using list models to be able to group their ++ * items into sections. ++ * ++ * Many GTK list models support sections inherently, or they pass through the sections ++ * of a model they are wrapping. ++ * ++ * A `GtkSectionModel` groups successive items into so-called sections. List widgets ++ * like `GtkListView` then allow displaying section headers for these sections. ++ * ++ * When the section groupings of a model changes, the model will emit the ++ * [signal@Gtk.SectionModel::sections-changed] signal by calling the ++ * [method@Gtk.SectionModel.sections_changed] function. All sections in the given range ++ * now need to be queried again. ++ * The [signal@Gio.ListModel::items-changed] signal has the same effect, all sections in ++ * that range are invalidated, too. ++ * ++ * Since: 4.12 ++ */ ++ ++G_DEFINE_INTERFACE (GtkSectionModel, gtk_section_model, G_TYPE_LIST_MODEL) ++ ++enum { ++ SECTIONS_CHANGED, ++ LAST_SIGNAL ++}; ++ ++static guint signals[LAST_SIGNAL] = { 0 }; ++ ++static void ++gtk_section_model_default_get_section (GtkSectionModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self)); ++ ++ if (position >= n_items) ++ { ++ *out_start = n_items; ++ *out_end = G_MAXUINT; ++ } ++ ++ *out_start = 0; ++ *out_end = n_items; ++} ++ ++static void ++gtk_section_model_default_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_section_model_default_get_section; ++ ++ /** ++ * GtkSectionModel::sections-changed ++ * @model: a `GtkSectionModel` ++ * @position: The first item that may have changed ++ * @n_items: number of items with changes ++ * ++ * Emitted when the start-of-section state of some of the items in @model changes. ++ * ++ * Note that this signal does not specify the new section state of the ++ * items, they need to be queried manually. It is also not necessary for ++ * a model to change the section state of any of the items in the section ++ * model, though it would be rather useless to emit such a signal. ++ * ++ * The [signal@Gio.ListModel::items-changed] implies the effect of the ++ * [signal@Gtk.SectionModel::sections-changed] signal for all the items ++ * it covers. ++ * ++ * Since: 4.12 ++ */ ++ signals[SECTIONS_CHANGED] = ++ g_signal_new ("sections-changed", ++ GTK_TYPE_SECTION_MODEL, ++ G_SIGNAL_RUN_LAST, ++ 0, ++ NULL, NULL, ++ _gtk_marshal_VOID__UINT_UINT, ++ G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT); ++ g_signal_set_va_marshaller (signals[SECTIONS_CHANGED], ++ GTK_TYPE_SECTION_MODEL, ++ _gtk_marshal_VOID__UINT_UINTv); ++} ++ ++/** ++ * gtk_section_model_get_section: ++ * @self: a `GtkSectionModel` ++ * @position: the position of the item to query ++ * @out_start: (out caller-allocates): the position of the first ++ * item in the section ++ * @out_end: (out caller-allocates): the position of the first ++ * item not part of the section anymore. ++ * ++ * Query the section that covers the given position. The number of ++ * items in the section can be computed by `out_end - out_start`. ++ * ++ * If the position is larger than the number of items, a single ++ * range from n_items to G_MAXUINT will be returned. ++ * ++ * Since: 4.12 ++ */ ++void ++gtk_section_model_get_section (GtkSectionModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkSectionModelInterface *iface; ++ ++ g_return_if_fail (GTK_IS_SECTION_MODEL (self)); ++ g_return_if_fail (out_start != NULL); ++ g_return_if_fail (out_end != NULL); ++ ++ iface = GTK_SECTION_MODEL_GET_IFACE (self); ++ iface->get_section (self, position, out_start, out_end); ++ ++ g_warn_if_fail (*out_start < *out_end); ++} ++ ++/* A version of gtk_section_model_get_section() that handles NULL ++ * (treats it as the empty list) and any GListModel (treats it as ++ * a single section). ++ **/ ++void ++gtk_list_model_get_section (GListModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ g_return_if_fail (out_start != NULL); ++ g_return_if_fail (out_end != NULL); ++ ++ if (self == NULL) ++ { ++ *out_start = 0; ++ *out_end = G_MAXUINT; ++ return; ++ } ++ ++ g_return_if_fail (G_IS_LIST_MODEL (self)); ++ ++ if (!GTK_IS_SECTION_MODEL (self)) ++ { ++ guint n_items = g_list_model_get_n_items (self); ++ ++ if (position < n_items) ++ { ++ *out_start = 0; ++ *out_end = G_MAXUINT; ++ } ++ else ++ { ++ *out_start = n_items; ++ *out_end = G_MAXUINT; ++ } ++ ++ return; ++ } ++ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (self), position, out_start, out_end); ++} ++ ++/** ++ * gtk_section_model_section_changed: ++ * @self: a `GtkSectionModel` ++ * @position: the first changed item ++ * @n_items: the number of changed items ++ * ++ * This function emits the [signal@Gtk.SectionModel::section-changed] ++ * signal to notify about changes to sections. It must cover all ++ * positions that used to be a section start or that are now a section ++ * start. It does not have to cover all positions for which the section ++ * has changed. ++ * ++ * The [signal@Gio.ListModel::items-changed] implies the effect of the ++ * [signal@Gtk.SectionModel::section-changed] signal for all the items ++ * it covers. ++ * ++ * It is recommended that when changes to the items cause section changes ++ * in a larger range, that the larger range is included in the emission ++ * of the [signal@Gio.ListModel::items-changed] instead of emitting ++ * two signals. ++ * ++ * Since: 4.12 ++ */ ++void ++gtk_section_model_sections_changed (GtkSectionModel *self, ++ guint position, ++ guint n_items) ++{ ++ g_return_if_fail (GTK_IS_SECTION_MODEL (self)); ++ g_return_if_fail (n_items > 0); ++ g_return_if_fail (position + n_items <= g_list_model_get_n_items (G_LIST_MODEL (self))); ++ ++ g_signal_emit (self, signals[SECTIONS_CHANGED], 0, position, n_items); ++} +diff --git a/gtk/gtksectionmodel.h b/gtk/gtksectionmodel.h +new file mode 100644 +index 0000000000000000000000000000000000000000..a242b42c20ec07c3eaae2faac3f8ecbc2879c13e +--- /dev/null ++++ b/gtk/gtksectionmodel.h +@@ -0,0 +1,72 @@ ++/* ++ * Copyright © 2022 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 ++ */ ++ ++#pragma once ++ ++#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) ++#error "Only can be included directly." ++#endif ++ ++#include ++ ++G_BEGIN_DECLS ++ ++#define GTK_TYPE_SECTION_MODEL (gtk_section_model_get_type ()) ++ ++GDK_AVAILABLE_IN_4_12 ++G_DECLARE_INTERFACE (GtkSectionModel, gtk_section_model, GTK, SECTION_MODEL, GListModel) ++ ++/** ++ * GtkSectionModelInterface: ++ * @get_section: Return the section that covers the given position. If ++ * the position is outside the number of items, returns a single range from ++ * n_items to G_MAXUINT ++ * ++ * The list of virtual functions for the `GtkSectionModel` interface. ++ * No function must be implemented, but unless `GtkSectionModel::get_section()` ++ * is implemented, the whole model will just be a single section. ++ * ++ * Since: 4.12 ++ */ ++struct _GtkSectionModelInterface ++{ ++ /*< private >*/ ++ GTypeInterface g_iface; ++ ++ /*< public >*/ ++ void (* get_section) (GtkSectionModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end); ++}; ++ ++GDK_AVAILABLE_IN_4_12 ++void gtk_section_model_get_section (GtkSectionModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end); ++ ++/* for implementations only */ ++GDK_AVAILABLE_IN_4_12 ++void gtk_section_model_sections_changed (GtkSectionModel *self, ++ guint position, ++ guint n_items); ++ ++G_END_DECLS ++ +diff --git a/gtk/gtksectionmodelprivate.h b/gtk/gtksectionmodelprivate.h +new file mode 100644 +index 0000000000000000000000000000000000000000..e6cfb0cb1b105e3e409b531ae80bb9bb1186f155 +--- /dev/null ++++ b/gtk/gtksectionmodelprivate.h +@@ -0,0 +1,14 @@ ++#pragma once ++ ++#include "gtksectionmodel.h" ++ ++G_BEGIN_DECLS ++ ++void gtk_list_model_get_section (GListModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end); ++ ++ ++G_END_DECLS ++ +diff --git a/gtk/gtksingleselection.c b/gtk/gtksingleselection.c +index e4e148ec83fcb6f07a86b3d803bb9778494394b9..07f0c7eaee634cdfbf313e1338b558e49a5a7f90 100644 +--- a/gtk/gtksingleselection.c ++++ b/gtk/gtksingleselection.c +@@ -22,6 +22,7 @@ + #include "gtksingleselection.h" + + #include "gtkbitset.h" ++#include "gtksectionmodelprivate.h" + #include "gtkselectionmodel.h" + + /** +@@ -103,6 +104,23 @@ gtk_single_selection_list_model_init (GListModelInterface *iface) + iface->get_item = gtk_single_selection_get_item; + } + ++static void ++gtk_single_selection_get_section (GtkSectionModel *model, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkSingleSelection *self = GTK_SINGLE_SELECTION (model); ++ ++ gtk_list_model_get_section (self->model, position, out_start, out_end); ++} ++ ++static void ++gtk_single_selection_section_model_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_single_selection_get_section; ++} ++ + static gboolean + gtk_single_selection_is_selected (GtkSelectionModel *model, + guint position) +@@ -167,6 +185,8 @@ gtk_single_selection_selection_model_init (GtkSelectionModelInterface *iface) + G_DEFINE_TYPE_EXTENDED (GtkSingleSelection, gtk_single_selection, G_TYPE_OBJECT, 0, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, + gtk_single_selection_list_model_init) ++ G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL, ++ gtk_single_selection_section_model_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL, + gtk_single_selection_selection_model_init)) + +diff --git a/gtk/gtksortlistmodel.c b/gtk/gtksortlistmodel.c +index c2feb9e2502c1b5a0bb902e24c5384dcb02c43df..105dfd435853847c36cf8adffeafde27323019fd 100644 +--- a/gtk/gtksortlistmodel.c ++++ b/gtk/gtksortlistmodel.c +@@ -22,7 +22,9 @@ + #include "gtksortlistmodel.h" + + #include "gtkbitset.h" ++#include "gtkmultisorter.h" + #include "gtkprivate.h" ++#include "gtksectionmodel.h" + #include "gtksorterprivate.h" + #include "timsort/gtktimsortprivate.h" + +@@ -73,6 +75,13 @@ + * If you run into performance issues with `GtkSortListModel`, + * it is strongly recommended that you write your own sorting list + * model. ++ * ++ * `GtkSortListModel` allows sorting the items into sections. It ++ * implements `GtkSectionModel` and when [property@Gtk.SortListModel:section-sorter] ++ * is set, it will sort all items with that sorter and items comparing ++ * equal with it will be put into the same section. ++ * The [property@Gtk.SortListModel:sorter] will then be used to sort items ++ * inside their sections. + */ + + enum { +@@ -82,6 +91,7 @@ enum { + PROP_MODEL, + PROP_N_ITEMS, + PROP_PENDING, ++ PROP_SECTION_SORTER, + PROP_SORTER, + NUM_PROPERTIES + }; +@@ -92,6 +102,8 @@ struct _GtkSortListModel + + GListModel *model; + GtkSorter *sorter; ++ GtkSorter *section_sorter; ++ GtkSorter *real_sorter; + gboolean incremental; + + GtkTimSort sort; /* ongoing sort operation */ +@@ -99,6 +111,7 @@ struct _GtkSortListModel + + guint n_items; + GtkSortKeys *sort_keys; ++ GtkSortKeys *section_sort_keys; /* we assume they are compatible with the sort keys because they're the first element */ + gsize key_size; + gpointer keys; + GtkBitset *missing_keys; +@@ -174,8 +187,159 @@ gtk_sort_list_model_model_init (GListModelInterface *iface) + iface->get_item = gtk_sort_list_model_get_item; + } + ++static void ++gtk_sort_list_model_ensure_key (GtkSortListModel *self, ++ guint pos) ++{ ++ gpointer item; ++ ++ if (!gtk_bitset_contains (self->missing_keys, pos)) ++ return; ++ ++ item = g_list_model_get_item (self->model, pos); ++ gtk_sort_keys_init_key (self->sort_keys, item, key_from_pos (self, pos)); ++ g_object_unref (item); ++ ++ gtk_bitset_remove (self->missing_keys, pos); ++} ++ ++static void ++gtk_sort_list_model_get_section_unsorted (GtkSortListModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ gpointer *pos, *start, *end; ++ ++ pos = &self->positions[position]; ++ gtk_sort_list_model_ensure_key (self, pos_from_key (self, *pos)); ++ ++ for (start = pos; ++ start > self->positions; ++ start--) ++ { ++ gtk_sort_list_model_ensure_key (self, pos_from_key (self, start[-1])); ++ if (gtk_sort_keys_compare (self->section_sort_keys, start[-1], *pos) != GTK_ORDERING_EQUAL) ++ break; ++ } ++ ++ for (end = pos + 1; ++ end < &self->positions[self->n_items]; ++ end++) ++ { ++ gtk_sort_list_model_ensure_key (self, pos_from_key (self, *end)); ++ if (gtk_sort_keys_compare (self->section_sort_keys, *end, *pos) != GTK_ORDERING_EQUAL) ++ break; ++ } ++ ++ *out_start = start - self->positions; ++ *out_end = end - self->positions; ++} ++ ++static void ++gtk_sort_list_model_get_section_sorted (GtkSortListModel *self, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ gpointer *pos; ++ guint step, min, max, mid; ++ ++ pos = &self->positions[position]; ++ ++ max = position; ++ step = 1; ++ while (max > 0) ++ { ++ min = max - MIN (max, step); ++ step *= 2; ++ if (gtk_sort_keys_compare (self->section_sort_keys, self->positions[min], *pos) == GTK_ORDERING_EQUAL) ++ { ++ max = min; ++ continue; ++ } ++ /* now min is different, max is equal, bsearch where that changes */ ++ while (max - min > 1) ++ { ++ mid = (max + min) / 2; ++ if (gtk_sort_keys_compare (self->section_sort_keys, self->positions[mid], *pos) == GTK_ORDERING_EQUAL) ++ max = mid; ++ else ++ min = mid; ++ } ++ break; ++ } ++ *out_start = max; ++ ++ min = position; ++ step = 1; ++ while (min < self->n_items - 1) ++ { ++ max = min + MIN (self->n_items - 1 - min, step); ++ step *= 2; ++ if (gtk_sort_keys_compare (self->section_sort_keys, self->positions[max], *pos) == GTK_ORDERING_EQUAL) ++ { ++ min = max; ++ continue; ++ } ++ /* now min is equal, max is different, bsearch where that changes */ ++ while (max - min > 1) ++ { ++ mid = (max + min) / 2; ++ if (gtk_sort_keys_compare (self->section_sort_keys, self->positions[mid], *pos) == GTK_ORDERING_EQUAL) ++ min = mid; ++ else ++ max = mid; ++ } ++ break; ++ } ++ *out_end = min + 1; ++} ++ ++static void ++gtk_sort_list_model_get_section (GtkSectionModel *model, ++ guint position, ++ guint *out_start, ++ guint *out_end) ++{ ++ GtkSortListModel *self = GTK_SORT_LIST_MODEL (model); ++ ++ if (position >= self->n_items) ++ { ++ *out_start = self->n_items; ++ *out_end = G_MAXUINT; ++ return; ++ } ++ ++ if (self->section_sort_keys == NULL) ++ { ++ *out_start = 0; ++ *out_end = self->n_items; ++ return; ++ } ++ ++ /* When the list is not sorted: ++ * - keys may not exist yet ++ * - equal items may not be adjacent ++ * So add a slow path that can deal with that, but is O(N). ++ * The fast path is O(log N) and will be used for I guess ++ * 99% of cases. ++ */ ++ if (self->sort_cb) ++ gtk_sort_list_model_get_section_unsorted (self, position, out_start, out_end); ++ else ++ gtk_sort_list_model_get_section_sorted (self, position, out_start, out_end); ++} ++ ++static void ++gtk_sort_list_model_section_model_init (GtkSectionModelInterface *iface) ++{ ++ iface->get_section = gtk_sort_list_model_get_section; ++} ++ + G_DEFINE_TYPE_WITH_CODE (GtkSortListModel, gtk_sort_list_model, G_TYPE_OBJECT, +- G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_sort_list_model_model_init)) ++ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_sort_list_model_model_init) ++ G_IMPLEMENT_INTERFACE (GTK_TYPE_SECTION_MODEL, gtk_sort_list_model_section_model_init)) + + static gboolean + gtk_sort_list_model_is_sorting (GtkSortListModel *self) +@@ -379,6 +543,7 @@ gtk_sort_list_model_clear_keys (GtkSortListModel *self) + g_clear_pointer (&self->missing_keys, gtk_bitset_unref); + g_clear_pointer (&self->keys, g_free); + g_clear_pointer (&self->sort_keys, gtk_sort_keys_unref); ++ g_clear_pointer (&self->section_sort_keys, gtk_sort_keys_unref); + self->key_size = 0; + } + +@@ -426,9 +591,9 @@ gtk_sort_list_model_clear_items (GtkSortListModel *self, + static gboolean + gtk_sort_list_model_should_sort (GtkSortListModel *self) + { +- return self->sorter != NULL && ++ return self->real_sorter != NULL && + self->model != NULL && +- gtk_sorter_get_order (self->sorter) != GTK_SORTER_ORDER_NONE; ++ gtk_sorter_get_order (self->real_sorter) != GTK_SORTER_ORDER_NONE; + } + + static void +@@ -436,9 +601,12 @@ gtk_sort_list_model_create_keys (GtkSortListModel *self) + { + g_assert (self->keys == NULL); + g_assert (self->sort_keys == NULL); ++ g_assert (self->section_sort_keys == NULL); + g_assert (self->key_size == 0); + +- self->sort_keys = gtk_sorter_get_keys (self->sorter); ++ self->sort_keys = gtk_sorter_get_keys (self->real_sorter); ++ if (self->section_sorter) ++ self->section_sort_keys = gtk_sorter_get_keys (self->section_sorter); + self->key_size = gtk_sort_keys_get_key_size (self->sort_keys); + self->keys = g_malloc_n (self->n_items, self->key_size); + self->missing_keys = gtk_bitset_new_range (0, self->n_items); +@@ -646,6 +814,10 @@ gtk_sort_list_model_set_property (GObject *object, + gtk_sort_list_model_set_model (self, g_value_get_object (value)); + break; + ++ case PROP_SECTION_SORTER: ++ gtk_sort_list_model_set_section_sorter (self, g_value_get_object (value)); ++ break; ++ + case PROP_SORTER: + gtk_sort_list_model_set_sorter (self, g_value_get_object (value)); + break; +@@ -686,6 +858,10 @@ gtk_sort_list_model_get_property (GObject *object, + g_value_set_uint (value, gtk_sort_list_model_get_pending (self)); + break; + ++ case PROP_SECTION_SORTER: ++ g_value_set_object (value, self->section_sorter); ++ break; ++ + case PROP_SORTER: + g_value_set_object (value, self->sorter); + break; +@@ -763,13 +939,42 @@ gtk_sort_list_model_clear_model (GtkSortListModel *self) + } + + static void +-gtk_sort_list_model_clear_sorter (GtkSortListModel *self) ++gtk_sort_list_model_clear_real_sorter (GtkSortListModel *self) + { +- if (self->sorter == NULL) ++ if (self->real_sorter == NULL) + return; + +- g_signal_handlers_disconnect_by_func (self->sorter, gtk_sort_list_model_sorter_changed_cb, self); +- g_clear_object (&self->sorter); ++ g_signal_handlers_disconnect_by_func (self->real_sorter, gtk_sort_list_model_sorter_changed_cb, self); ++ g_clear_object (&self->real_sorter); ++} ++ ++static void ++gtk_sort_list_model_ensure_real_sorter (GtkSortListModel *self) ++{ ++ if (self->sorter) ++ { ++ if (self->section_sorter) ++ { ++ GtkMultiSorter *multi; ++ ++ multi = gtk_multi_sorter_new (); ++ self->real_sorter = GTK_SORTER (multi); ++ gtk_multi_sorter_append (multi, g_object_ref (self->section_sorter)); ++ gtk_multi_sorter_append (multi, g_object_ref (self->sorter)); ++ } ++ else ++ self->real_sorter = g_object_ref (self->sorter); ++ } ++ else ++ { ++ if (self->section_sorter) ++ self->real_sorter = g_object_ref (self->section_sorter); ++ } ++ ++ if (self->real_sorter) ++ g_signal_connect (self->real_sorter, "changed", G_CALLBACK (gtk_sort_list_model_sorter_changed_cb), self); ++ ++ gtk_sort_list_model_sorter_changed_cb (self->real_sorter, GTK_SORTER_CHANGE_DIFFERENT, self); + } + + static void +@@ -778,7 +983,9 @@ gtk_sort_list_model_dispose (GObject *object) + GtkSortListModel *self = GTK_SORT_LIST_MODEL (object); + + gtk_sort_list_model_clear_model (self); +- gtk_sort_list_model_clear_sorter (self); ++ gtk_sort_list_model_clear_real_sorter (self); ++ g_clear_object (&self->section_sorter); ++ g_clear_object (&self->sorter); + + G_OBJECT_CLASS (gtk_sort_list_model_parent_class)->dispose (object); + }; +@@ -846,6 +1053,18 @@ gtk_sort_list_model_class_init (GtkSortListModelClass *class) + 0, G_MAXUINT, 0, + GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + ++ /** ++ * GtkSortListModel:section-sorter: (attributes org.gtk.Property.get=gtk_sort_list_model_get_section_sorter org.gtk.Property.set=gtk_sort_list_model_set_section_sorter) ++ * ++ * The section sorter for this model, if one is set. ++ * ++ * Since: 4.12 ++ */ ++ properties[PROP_SECTION_SORTER] = ++ g_param_spec_object ("section-sorter", NULL, NULL, ++ GTK_TYPE_SORTER, ++ GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); ++ + /** + * GtkSortListModel:sorter: (attributes org.gtk.Property.get=gtk_sort_list_model_get_sorter org.gtk.Property.set=gtk_sort_list_model_set_sorter) + * +@@ -972,15 +1191,16 @@ gtk_sort_list_model_set_sorter (GtkSortListModel *self, + g_return_if_fail (GTK_IS_SORT_LIST_MODEL (self)); + g_return_if_fail (sorter == NULL || GTK_IS_SORTER (sorter)); + +- gtk_sort_list_model_clear_sorter (self); ++ if (self->sorter == sorter) ++ return; ++ ++ gtk_sort_list_model_clear_real_sorter (self); ++ g_clear_object (&self->sorter); + + if (sorter) +- { +- self->sorter = g_object_ref (sorter); +- g_signal_connect (sorter, "changed", G_CALLBACK (gtk_sort_list_model_sorter_changed_cb), self); +- } ++ self->sorter = g_object_ref (sorter); + +- gtk_sort_list_model_sorter_changed_cb (sorter, GTK_SORTER_CHANGE_DIFFERENT, self); ++ gtk_sort_list_model_ensure_real_sorter (self); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SORTER]); + } +@@ -1001,6 +1221,55 @@ gtk_sort_list_model_get_sorter (GtkSortListModel *self) + return self->sorter; + } + ++/** ++ * gtk_sort_list_model_set_section_sorter: (attributes org.gtk.Method.set_property=section-sorter) ++ * @self: a `GtkSortListModel` ++ * @sorter: (nullable): the `GtkSorter` to sort @model with ++ * ++ * Sets a new section sorter on @self. ++ * ++ * Since: 4.12 ++ */ ++void ++gtk_sort_list_model_set_section_sorter (GtkSortListModel *self, ++ GtkSorter *sorter) ++{ ++ g_return_if_fail (GTK_IS_SORT_LIST_MODEL (self)); ++ g_return_if_fail (sorter == NULL || GTK_IS_SORTER (sorter)); ++ ++ if (self->section_sorter == sorter) ++ return; ++ ++ gtk_sort_list_model_clear_real_sorter (self); ++ g_clear_object (&self->section_sorter); ++ ++ if (sorter) ++ self->section_sorter = g_object_ref (sorter); ++ ++ gtk_sort_list_model_ensure_real_sorter (self); ++ ++ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SECTION_SORTER]); ++} ++ ++/** ++ * gtk_sort_list_model_get_section_sorter: (attributes org.gtk.Method.get_property=section-sorter) ++ * @self: a `GtkSortListModel` ++ * ++ * Gets the section sorter that is used to sort items of @self into ++ * sections. ++ * ++ * Returns: (nullable) (transfer none): the sorter of #self ++ * ++ * Since: 4.12 ++ */ ++GtkSorter * ++gtk_sort_list_model_get_section_sorter (GtkSortListModel *self) ++{ ++ g_return_val_if_fail (GTK_IS_SORT_LIST_MODEL (self), NULL); ++ ++ return self->section_sorter; ++} ++ + /** + * gtk_sort_list_model_set_incremental: (attributes org.gtk.Method.set_property=incremental) + * @self: a `GtkSortListModel` +diff --git a/gtk/gtksortlistmodel.h b/gtk/gtksortlistmodel.h +index 957f5b0019f176f3cf937ea75743db44173ba197..64bb0d019cabacad124779f1648208ed4dd0972e 100644 +--- a/gtk/gtksortlistmodel.h ++++ b/gtk/gtksortlistmodel.h +@@ -45,6 +45,12 @@ void gtk_sort_list_model_set_sorter (GtkSortListMode + GDK_AVAILABLE_IN_ALL + GtkSorter * gtk_sort_list_model_get_sorter (GtkSortListModel *self); + ++GDK_AVAILABLE_IN_4_12 ++void gtk_sort_list_model_set_section_sorter (GtkSortListModel *self, ++ GtkSorter *sorter); ++GDK_AVAILABLE_IN_4_12 ++GtkSorter * gtk_sort_list_model_get_section_sorter (GtkSortListModel *self); ++ + GDK_AVAILABLE_IN_ALL + void gtk_sort_list_model_set_model (GtkSortListModel *self, + GListModel *model); +diff --git a/gtk/meson.build b/gtk/meson.build +index b3975fb3fcd0a67eb15f0a9e6812b7c8bd44282c..ec214f0d741979ee4ff1489cd57a4d83d31befab 100644 +--- a/gtk/meson.build ++++ b/gtk/meson.build +@@ -271,6 +271,9 @@ gtk_public_sources = files([ + 'gtklinkbutton.c', + 'gtklistbox.c', + 'gtklistfactorywidget.c', ++ 'gtklistheader.c', ++ 'gtklistheaderbase.c', ++ 'gtklistheaderwidget.c', + 'gtklistitem.c', + 'gtklistitembase.c', + 'gtklistitemfactory.c', +@@ -333,6 +336,7 @@ gtk_public_sources = files([ + 'gtkscrolledwindow.c', + 'gtksearchbar.c', + 'gtksearchentry.c', ++ 'gtksectionmodel.c', + 'gtkselectionfiltermodel.c', + 'gtkselectionmodel.c', + 'gtkseparator.c', +@@ -518,6 +522,7 @@ gtk_public_headers = files([ + 'gtklinkbutton.h', + 'gtklistbase.h', + 'gtklistbox.h', ++ 'gtklistheader.h', + 'gtklistitem.h', + 'gtklistitemfactory.h', + 'gtklistview.h', +@@ -565,6 +570,7 @@ gtk_public_headers = files([ + 'gtkscrolledwindow.h', + 'gtksearchbar.h', + 'gtksearchentry.h', ++ 'gtksectionmodel.h', + 'gtkselectionfiltermodel.h', + 'gtkselectionmodel.h', + 'gtkseparator.h', +diff --git a/gtk/theme/Default/_common.scss b/gtk/theme/Default/_common.scss +index 33b7172914d5e27c3896fa706a013b53739f5873..231600ecf87e8e6cf64327024ba10d669db60609 100644 +--- a/gtk/theme/Default/_common.scss ++++ b/gtk/theme/Default/_common.scss +@@ -3377,7 +3377,7 @@ columnview row:not(:selected) cell editablelabel.editing text selection { + + + .rich-list { /* rich lists usually containing other widgets than just labels/text */ +- & > row { ++ & > row, & > header { + padding: 8px 12px; + min-height: 32px; /* should be tall even when only containing a label */ + +@@ -3385,6 +3385,14 @@ columnview row:not(:selected) cell editablelabel.editing text selection { + border-spacing: 12px; + } + } ++ & > header { ++ @extend %osd; ++ background-color: $osd_bg_color; ++ ++ border-bottom: 1px solid $borders-color; ++ border-top: 1px solid $borders-color; ++ font-weight: bold; ++ } + } + + /******************************************************** +diff --git a/testsuite/gtk/filterlistmodel-exhaustive.c b/testsuite/gtk/filterlistmodel-exhaustive.c +index 084f1d301d9c055e30155bca9309f5a83da10be4..2af32072f58e507d56dfb036afb2283ff21a246b 100644 +--- a/testsuite/gtk/filterlistmodel-exhaustive.c ++++ b/testsuite/gtk/filterlistmodel-exhaustive.c +@@ -43,6 +43,23 @@ + } \ + }G_STMT_END + ++#define assert_sections_equal(model1, model2) G_STMT_START{ \ ++ guint _i, _n, _start1, _end1, _start2, _end2; \ ++ g_assert_cmpint (g_list_model_get_n_items (G_LIST_MODEL (model1)), ==, g_list_model_get_n_items (G_LIST_MODEL (model2))); \ ++ _n = g_list_model_get_n_items (G_LIST_MODEL (model1)); \ ++ for (_i = 0; _i < _n; _i = _end1) \ ++ { \ ++ gtk_section_model_get_section (model1, _i, &_start1, &_end1); \ ++ gtk_section_model_get_section (model2, _i, &_start2, &_end2); \ ++ g_assert_cmpint (_start1, <, _end1); \ ++ g_assert_cmpint (_start2, <, _end2); \ ++ g_assert_cmpint (_start1, ==, _start2); \ ++ g_assert_cmpint (_end1, ==, _end2); \ ++ g_assert_cmpint (_i, ==, _start1); \ ++ g_assert_cmpint (_end1, <=, _n); \ ++ } \ ++}G_STMT_END ++ + G_GNUC_UNUSED static char * + model_to_string (GListModel *model) + { +@@ -469,6 +486,7 @@ test_model_changes (gconstpointer model_id) + { + ensure_updated (); + assert_model_equal (G_LIST_MODEL (flatten1), G_LIST_MODEL (model2)); ++ assert_sections_equal (GTK_SECTION_MODEL (flatten1), GTK_SECTION_MODEL (model2)); + } + } + +diff --git a/testsuite/gtk/filterlistmodel.c b/testsuite/gtk/filterlistmodel.c +index b3c8303daa448f9f7519250b0b41b4de5f0e1c72..6f1465b3d1437fe028e8dcb116a07960be4c9787 100644 +--- a/testsuite/gtk/filterlistmodel.c ++++ b/testsuite/gtk/filterlistmodel.c +@@ -323,7 +323,7 @@ test_change_filter (void) + { + GtkFilterListModel *filter; + GtkFilter *custom; +- ++ + filter = new_model (10, is_not_near, GUINT_TO_POINTER (5)); + assert_model (filter, "1 2 8 9 10"); + assert_changes (filter, ""); +@@ -457,6 +457,94 @@ test_add_remove_item (void) + g_object_unref (filter); + } + ++static int ++sort_func (gconstpointer p1, ++ gconstpointer p2, ++ gpointer data) ++{ ++ const char *s1 = gtk_string_object_get_string ((GtkStringObject *)p1); ++ const char *s2 = gtk_string_object_get_string ((GtkStringObject *)p2); ++ ++ /* compare just the first byte */ ++ return (int)(s1[0]) - (int)(s2[0]); ++} ++ ++static gboolean ++filter_func (gpointer item, ++ gpointer data) ++{ ++ const char *s = gtk_string_object_get_string ((GtkStringObject *)item); ++ ++ return s[0] == s[1]; ++} ++ ++static void ++test_sections (void) ++{ ++ GtkStringList *list; ++ const char *strings[] = { ++ "aaa", ++ "aab", ++ "abc", ++ "bbb", ++ "bq1", ++ "bq2", ++ "cc", ++ "cx", ++ NULL ++ }; ++ GtkSorter *sorter; ++ GtkSortListModel *sorted; ++ GtkSorter *section_sorter; ++ guint s, e; ++ GtkFilterListModel *filtered; ++ GtkFilter *filter; ++ ++ list = gtk_string_list_new (strings); ++ sorter = GTK_SORTER (gtk_string_sorter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string"))); ++ sorted = gtk_sort_list_model_new (G_LIST_MODEL (list), sorter); ++ section_sorter = GTK_SORTER (gtk_custom_sorter_new (sort_func, NULL, NULL)); ++ gtk_sort_list_model_set_section_sorter (GTK_SORT_LIST_MODEL (sorted), section_sorter); ++ g_object_unref (section_sorter); ++ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sorted), 0, &s, &e); ++ g_assert_cmpint (s, ==, 0); ++ g_assert_cmpint (e, ==, 3); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sorted), 3, &s, &e); ++ g_assert_cmpint (s, ==, 3); ++ g_assert_cmpint (e, ==, 6); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sorted), 6, &s, &e); ++ g_assert_cmpint (s, ==, 6); ++ g_assert_cmpint (e, ==, 8); ++ ++ filtered = gtk_filter_list_model_new (NULL, NULL); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (filtered), 0, &s, &e); ++ g_assert_cmpint (s, ==, 0); ++ g_assert_cmpint (e, ==, G_MAXUINT); ++ ++ gtk_filter_list_model_set_model (filtered, G_LIST_MODEL (sorted)); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (filtered), 0, &s, &e); ++ g_assert_cmpint (s, ==, 0); ++ g_assert_cmpint (e, ==, 3); ++ ++ filter = GTK_FILTER (gtk_custom_filter_new (filter_func, NULL, NULL)); ++ gtk_filter_list_model_set_filter (filtered, filter); ++ g_object_unref (filter); ++ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (filtered), 0, &s, &e); ++ g_assert_cmpint (s, ==, 0); ++ g_assert_cmpint (e, ==, 2); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (filtered), 2, &s, &e); ++ g_assert_cmpint (s, ==, 2); ++ g_assert_cmpint (e, ==, 3); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (filtered), 3, &s, &e); ++ g_assert_cmpint (s, ==, 3); ++ g_assert_cmpint (e, ==, 4); ++ ++ g_object_unref (filtered); ++ g_object_unref (sorted); ++} ++ + int + main (int argc, char *argv[]) + { +@@ -472,6 +560,7 @@ main (int argc, char *argv[]) + g_test_add_func ("/filterlistmodel/incremental", test_incremental); + g_test_add_func ("/filterlistmodel/empty", test_empty); + g_test_add_func ("/filterlistmodel/add_remove_item", test_add_remove_item); ++ g_test_add_func ("/filterlistmodel/sections", test_sections); + + return g_test_run (); + } +diff --git a/testsuite/gtk/listitemmanager.c b/testsuite/gtk/listitemmanager.c +new file mode 100644 +index 0000000000000000000000000000000000000000..ba5fbd54baf01a2280eebe3e519306c01c8c4718 +--- /dev/null ++++ b/testsuite/gtk/listitemmanager.c +@@ -0,0 +1,497 @@ ++/* ++ * Copyright © 2023 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 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 . ++ */ ++ ++#include ++ ++#include ++#include "gtk/gtklistitemmanagerprivate.h" ++#include "gtk/gtklistbaseprivate.h" ++ ++static GListModel * ++create_source_model (guint min_size, guint max_size) ++{ ++ GtkStringList *list; ++ guint i, size; ++ ++ size = g_test_rand_int_range (min_size, max_size + 1); ++ list = gtk_string_list_new (NULL); ++ ++ for (i = 0; i < size; i++) ++ gtk_string_list_append (list, g_test_rand_bit () ? "A" : "B"); ++ ++ return G_LIST_MODEL (list); ++} ++ ++void ++print_list_item_manager_tiles (GtkListItemManager *items) ++{ ++ GString *string; ++ GtkListTile *tile; ++ ++ string = g_string_new (""); ++ ++ for (tile = gtk_list_item_manager_get_first (items); ++ tile != NULL; ++ tile = gtk_rb_tree_node_get_next (tile)) ++ { ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_ITEM: ++ if (tile->widget) ++ g_string_append_c (string, 'W'); ++ else if (tile->n_items == 1) ++ g_string_append_c (string, 'x'); ++ else ++ g_string_append_printf (string, "%u,", tile->n_items); ++ break; ++ case GTK_LIST_TILE_HEADER: ++ g_string_append_c (string, '['); ++ break; ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ g_string_append_c (string, '('); ++ break; ++ case GTK_LIST_TILE_FOOTER: ++ g_string_append_c (string, ']'); ++ break; ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ g_string_append_c (string, ')'); ++ break; ++ ++ case GTK_LIST_TILE_FILLER: ++ g_string_append_c (string, '_'); ++ break; ++ case GTK_LIST_TILE_REMOVED: ++ g_string_append_c (string, '.'); ++ break; ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ } ++ ++ g_print ("%s\n", string->str); ++ ++ g_string_free (string, TRUE); ++} ++ ++static void ++check_list_item_manager (GtkListItemManager *items, ++ GtkListItemTracker **trackers, ++ gsize n_trackers) ++{ ++ GListModel *model = G_LIST_MODEL (gtk_list_item_manager_get_model (items)); ++ GtkListTile *tile; ++ guint n_items = 0; ++ guint i; ++ gboolean has_sections; ++ enum { ++ NO_SECTION, ++ MATCHED_SECTION, ++ UNMATCHED_SECTION ++ } section_state = NO_SECTION; ++ ++ has_sections = gtk_list_item_manager_get_has_sections (items); ++ ++ for (tile = gtk_list_item_manager_get_first (items); ++ tile != NULL; ++ tile = gtk_rb_tree_node_get_next (tile)) ++ { ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_HEADER: ++ g_assert_cmpint (section_state, ==, NO_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_true (has_sections); ++ g_assert_true (tile->widget); ++ section_state = MATCHED_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ g_assert_cmpint (section_state, ==, NO_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_false (tile->widget); ++ section_state = UNMATCHED_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_FOOTER: ++ g_assert_cmpint (section_state, ==, MATCHED_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_true (has_sections); ++ g_assert_false (tile->widget); ++ section_state = NO_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ g_assert_cmpint (section_state, ==, UNMATCHED_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_false (tile->widget); ++ section_state = NO_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_ITEM: ++ g_assert_cmpint (section_state, !=, NO_SECTION); ++ if (tile->widget) ++ { ++ GObject *item = g_list_model_get_item (model, n_items); ++ if (has_sections) ++ g_assert_cmpint (section_state, ==, MATCHED_SECTION); ++ else ++ g_assert_cmpint (section_state, ==, UNMATCHED_SECTION); ++ g_assert_cmphex (GPOINTER_TO_SIZE (item), ==, GPOINTER_TO_SIZE (gtk_list_item_base_get_item (GTK_LIST_ITEM_BASE (tile->widget)))); ++ g_object_unref (item); ++ g_assert_cmpint (n_items, ==, gtk_list_item_base_get_position (GTK_LIST_ITEM_BASE (tile->widget))); ++ g_assert_cmpint (tile->n_items, ==, 1); ++ } ++ if (tile->n_items) ++ n_items += tile->n_items; ++ break; ++ ++ case GTK_LIST_TILE_FILLER: ++ /* We don't add fillers */ ++ g_assert_not_reached (); ++ break; ++ ++ case GTK_LIST_TILE_REMOVED: ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_false (tile->widget); ++ break; ++ ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ } ++ ++ g_assert_cmpint (section_state, ==, NO_SECTION); ++ g_assert_cmpint (n_items, ==, g_list_model_get_n_items (model)); ++ ++ for (i = 0; i < n_trackers; i++) ++ { ++ guint pos, offset; ++ ++ pos = gtk_list_item_tracker_get_position (items, trackers[i]); ++ if (pos == GTK_INVALID_LIST_POSITION) ++ continue; ++ tile = gtk_list_item_manager_get_nth (items, pos, &offset); ++ g_assert_cmpint (tile->n_items, ==, 1); ++ g_assert_cmpint (offset, ==, 0); ++ g_assert_true (tile->widget); ++ } ++ ++ for (tile = gtk_list_tile_gc (items, gtk_list_item_manager_get_first (items)); ++ tile != NULL; ++ tile = gtk_list_tile_gc (items, gtk_rb_tree_node_get_next (tile))) ++ ; ++ ++ n_items = 0; ++ ++ for (tile = gtk_list_item_manager_get_first (items); ++ tile != NULL; ++ tile = gtk_rb_tree_node_get_next (tile)) ++ { ++ switch (tile->type) ++ { ++ case GTK_LIST_TILE_HEADER: ++ g_assert_cmpint (section_state, ==, NO_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_true (has_sections); ++ g_assert_true (tile->widget); ++ section_state = MATCHED_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_UNMATCHED_HEADER: ++ g_assert_cmpint (section_state, ==, NO_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_false (tile->widget); ++ section_state = UNMATCHED_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_FOOTER: ++ g_assert_cmpint (section_state, ==, MATCHED_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_true (has_sections); ++ g_assert_false (tile->widget); ++ section_state = NO_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_UNMATCHED_FOOTER: ++ g_assert_cmpint (section_state, ==, UNMATCHED_SECTION); ++ g_assert_cmpint (tile->n_items, ==, 0); ++ g_assert_false (tile->widget); ++ section_state = NO_SECTION; ++ break; ++ ++ case GTK_LIST_TILE_ITEM: ++ g_assert_cmpint (section_state, !=, NO_SECTION); ++ if (tile->widget) ++ { ++ g_assert_cmpint (tile->n_items, ==, 1); ++ } ++ if (tile->n_items) ++ n_items += tile->n_items; ++ break; ++ ++ case GTK_LIST_TILE_FILLER: ++ case GTK_LIST_TILE_REMOVED: ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ } ++ ++ g_assert_cmpint (section_state, ==, NO_SECTION); ++ g_assert_cmpint (n_items, ==, g_list_model_get_n_items (model)); ++ ++ for (i = 0; i < n_trackers; i++) ++ { ++ guint pos, offset; ++ ++ pos = gtk_list_item_tracker_get_position (items, trackers[i]); ++ if (pos == GTK_INVALID_LIST_POSITION) ++ continue; ++ tile = gtk_list_item_manager_get_nth (items, pos, &offset); ++ g_assert_cmpint (tile->n_items, ==, 1); ++ g_assert_cmpint (offset, ==, 0); ++ g_assert_true (tile->widget); ++ } ++} ++ ++static GtkListTile * ++split_simple (GtkWidget *widget, ++ GtkListTile *tile, ++ guint n_items) ++{ ++ GtkListItemManager *items = g_object_get_data (G_OBJECT (widget), "the-items"); ++ ++ return gtk_list_tile_split (items, tile, n_items); ++} ++ ++static void ++prepare_simple (GtkWidget *widget, ++ GtkListTile *tile, ++ guint n_items) ++{ ++} ++ ++static GtkListItemBase * ++create_simple_item (GtkWidget *widget) ++{ ++ return g_object_new (GTK_TYPE_LIST_ITEM_BASE, NULL); ++} ++ ++static GtkListHeaderBase * ++create_simple_header (GtkWidget *widget) ++{ ++ return g_object_new (GTK_TYPE_LIST_HEADER_BASE, NULL); ++} ++ ++static void ++test_create (void) ++{ ++ GtkListItemManager *items; ++ GtkWidget *widget; ++ ++ widget = gtk_window_new (); ++ items = gtk_list_item_manager_new (widget, ++ split_simple, ++ create_simple_item, ++ prepare_simple, ++ create_simple_header); ++ g_object_set_data_full (G_OBJECT (widget), "the-items", items, g_object_unref); ++ ++ gtk_window_destroy (GTK_WINDOW (widget)); ++} ++ ++static void ++test_create_with_items (void) ++{ ++ GListModel *source; ++ GtkNoSelection *selection; ++ GtkListItemManager *items; ++ GtkWidget *widget; ++ ++ widget = gtk_window_new (); ++ items = gtk_list_item_manager_new (widget, ++ split_simple, ++ create_simple_item, ++ prepare_simple, ++ create_simple_header); ++ g_object_set_data_full (G_OBJECT (widget), "the-items", items, g_object_unref); ++ ++ source = create_source_model (1, 50); ++ selection = gtk_no_selection_new (G_LIST_MODEL (source)); ++ gtk_list_item_manager_set_model (items, GTK_SELECTION_MODEL (selection)); ++ check_list_item_manager (items, NULL, 0); ++ gtk_list_item_manager_set_model (items, GTK_SELECTION_MODEL (selection)); ++ check_list_item_manager (items, NULL, 0); ++ ++ g_object_unref (selection); ++ gtk_window_destroy (GTK_WINDOW (widget)); ++} ++ ++#define N_TRACKERS 3 ++#define N_WIDGETS_PER_TRACKER 10 ++#define N_RUNS 500 ++ ++static void ++print_changes_cb (GListModel *model, ++ guint position, ++ guint removed, ++ guint added, ++ gpointer unused) ++{ ++ if (!g_test_verbose ()) ++ return; ++ ++ if (removed == 0) ++ g_test_message ("%u/%u: adding %u items", position, g_list_model_get_n_items (model), added); ++ else if (added == 0) ++ g_test_message ("%u/%u: removing %u items", position, g_list_model_get_n_items (model), removed); ++ else ++ g_test_message ("%u/%u: removing %u and adding %u items", position, g_list_model_get_n_items (model), removed, added); ++} ++ ++static void ++test_exhaustive (void) ++{ ++ GtkListItemTracker *trackers[N_TRACKERS]; ++ GListStore *store; ++ GtkFlattenListModel *flatten; ++ GtkNoSelection *selection; ++ GtkListItemManager *items; ++ GtkWidget *widget; ++ gsize i; ++ ++ widget = gtk_window_new (); ++ items = gtk_list_item_manager_new (widget, ++ split_simple, ++ create_simple_item, ++ prepare_simple, ++ create_simple_header); ++ for (i = 0; i < N_TRACKERS; i++) ++ trackers[i] = gtk_list_item_tracker_new (items); ++ ++ g_object_set_data_full (G_OBJECT (widget), "the-items", items, g_object_unref); ++ ++ store = g_list_store_new (G_TYPE_OBJECT); ++ flatten = gtk_flatten_list_model_new (G_LIST_MODEL (store)); ++ selection = gtk_no_selection_new (G_LIST_MODEL (flatten)); ++ g_signal_connect (selection, "items-changed", G_CALLBACK (print_changes_cb), NULL); ++ gtk_list_item_manager_set_model (items, GTK_SELECTION_MODEL (selection)); ++ ++ for (i = 0; i < N_RUNS; i++) ++ { ++ gboolean add = FALSE, remove = FALSE; ++ guint position, n_items; ++ ++ if (g_test_verbose ()) ++ print_list_item_manager_tiles (items); ++ ++ switch (g_test_rand_int_range (0, 6)) ++ { ++ case 0: ++ if (g_test_verbose ()) ++ g_test_message ("GC and checking"); ++ check_list_item_manager (items, trackers, N_TRACKERS); ++ break; ++ ++ case 1: ++ /* remove a model */ ++ remove = TRUE; ++ break; ++ ++ case 2: ++ /* add a model */ ++ add = TRUE; ++ break; ++ ++ case 3: ++ /* replace a model */ ++ remove = TRUE; ++ add = TRUE; ++ break; ++ ++ case 4: ++ n_items = g_list_model_get_n_items (G_LIST_MODEL (selection)); ++ if (n_items > 0) ++ { ++ guint tracker_id = g_test_rand_int_range (0, N_TRACKERS); ++ guint pos = g_test_rand_int_range (0, n_items); ++ guint n_before = g_test_rand_int_range (0, N_WIDGETS_PER_TRACKER / 2); ++ guint n_after = g_test_rand_int_range (0, N_WIDGETS_PER_TRACKER / 2); ++ if (g_test_verbose ()) ++ g_test_message ("setting tracker %u to %u -%u + %u", tracker_id, pos, n_before, n_after); ++ gtk_list_item_tracker_set_position (items, ++ trackers [tracker_id], ++ pos, ++ n_before, n_after); ++ } ++ break; ++ ++ case 5: ++ { ++ gboolean has_sections = g_test_rand_bit (); ++ if (g_test_verbose ()) ++ g_test_message ("Setting has_sections to %s", has_sections ? "true" : "false"); ++ gtk_list_item_manager_set_has_sections (items, has_sections); ++ } ++ break; ++ ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ ++ position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store)) + 1); ++ if (g_list_model_get_n_items (G_LIST_MODEL (store)) == position) ++ remove = FALSE; ++ ++ if (add) ++ { ++ /* We want at least one element, otherwise the filters will see no changes */ ++ GListModel *source = create_source_model (1, 50); ++ g_list_store_splice (store, ++ position, ++ remove ? 1 : 0, ++ (gpointer *) &source, 1); ++ g_object_unref (source); ++ } ++ else if (remove) ++ { ++ g_list_store_remove (store, position); ++ } ++ } ++ ++ check_list_item_manager (items, trackers, N_TRACKERS); ++ ++ for (i = 0; i < N_TRACKERS; i++) ++ gtk_list_item_tracker_free (items, trackers[i]); ++ g_object_unref (selection); ++ gtk_window_destroy (GTK_WINDOW (widget)); ++} ++ ++int ++main (int argc, char *argv[]) ++{ ++ gtk_test_init (&argc, &argv); ++ ++ g_test_add_func ("/listitemmanager/create", test_create); ++ g_test_add_func ("/listitemmanager/create_with_items", test_create_with_items); ++ g_test_add_func ("/listitemmanager/exhaustive", test_exhaustive); ++ ++ return g_test_run (); ++} +diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build +index 11560a95f479df15686d7afa12c702bc375b6b42..a3f2be717213e4954fb342e58984bc636f38c500 100644 +--- a/testsuite/gtk/meson.build ++++ b/testsuite/gtk/meson.build +@@ -126,6 +126,7 @@ internal_tests = [ + { 'name': 'texthistory' }, + { 'name': 'fnmatch' }, + { 'name': 'a11y' }, ++ { 'name': 'listitemmanager' }, + ] + + is_debug = get_option('buildtype').startswith('debug') +diff --git a/testsuite/gtk/sortlistmodel-exhaustive.c b/testsuite/gtk/sortlistmodel-exhaustive.c +index 0ca2c68f5eab02176288541e1ced0c699d136053..e06bed8195eec9a34da7b5a670e47617c8d8734f 100644 +--- a/testsuite/gtk/sortlistmodel-exhaustive.c ++++ b/testsuite/gtk/sortlistmodel-exhaustive.c +@@ -36,6 +36,8 @@ + if (o1 != o2) \ + { \ + char *_s = g_strdup_printf ("Objects differ at index %u out of %u", _i, _n); \ ++ g_print ("%s\n", model_to_string (model1)); \ ++ g_print ("%s\n", model_to_string (model2)); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, _s); \ + g_free (_s); \ + } \ +@@ -45,6 +47,19 @@ + } \ + }G_STMT_END + ++#define assert_model_sections(model) G_STMT_START{ \ ++ guint _i, _start, _end; \ ++ _start = 0; \ ++ _end = 0; \ ++ for (_i = 0; _i < G_MAXUINT; _i = _end) \ ++ { \ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (model), _i, &_start, &_end); \ ++\ ++ g_assert_cmpint (_start, ==, _i); \ ++ g_assert_cmpint (_end, >, _i); \ ++ } \ ++}G_STMT_END ++ + G_GNUC_UNUSED static char * + model_to_string (GListModel *model) + { +@@ -287,7 +302,7 @@ create_sorter (gsize id) + /* match all As, Bs and nothing */ + sorter = GTK_SORTER (gtk_string_sorter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string"))); + if (id == 1) +- gtk_string_sorter_set_ignore_case (GTK_STRING_SORTER (sorter), TRUE); ++ gtk_string_sorter_set_ignore_case (GTK_STRING_SORTER (sorter), FALSE); + return sorter; + + default: +@@ -463,6 +478,258 @@ test_stability (gconstpointer model_id) + g_object_unref (flatten); + } + ++static gboolean ++string_is_lowercase (GtkStringObject *o) ++{ ++ return g_ascii_islower (*gtk_string_object_get_string (o)); ++} ++ ++/* Run: ++ * source => section-sorter ++ * source => sorter ++ * and set a section sorter on the section sorter that is a subsort of ++ * the real sorter. ++ * ++ * And then randomly add/remove sources and change the sorters and ++ * see if the two sorters stay identical ++ */ ++static void ++test_section_sorters (gconstpointer model_id) ++{ ++ GListStore *store; ++ GtkFlattenListModel *flatten; ++ GtkSortListModel *sort1, *sort2; ++ GtkSorter *sorter; ++ gsize i; ++ ++ store = g_list_store_new (G_TYPE_OBJECT); ++ flatten = gtk_flatten_list_model_new (G_LIST_MODEL (store)); ++ sort1 = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (flatten), NULL); ++ sort2 = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (flatten), NULL); ++ ++ for (i = 0; i < 500; i++) ++ { ++ gboolean add = FALSE, remove = FALSE; ++ guint position; ++ ++ switch (g_test_rand_int_range (0, 4)) ++ { ++ case 0: ++ /* set the same sorter, once as section sorter, once as sorter */ ++ sorter = create_random_sorter (TRUE); ++ gtk_sort_list_model_set_section_sorter (sort1, sorter); ++ gtk_sort_list_model_set_sorter (sort1, NULL); ++ gtk_sort_list_model_set_sorter (sort2, sorter); ++ g_clear_object (&sorter); ++ break; ++ ++ case 1: ++ /* use a section sorter that is a more generic version of the sorter */ ++ sorter = GTK_SORTER (gtk_string_sorter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string"))); ++ gtk_string_sorter_set_ignore_case (GTK_STRING_SORTER (sorter), FALSE); ++ gtk_sort_list_model_set_sorter (sort1, sorter); ++ gtk_sort_list_model_set_sorter (sort2, sorter); ++ g_clear_object (&sorter); ++ sorter = GTK_SORTER (gtk_numeric_sorter_new (gtk_cclosure_expression_new (G_TYPE_BOOLEAN, NULL, 0, NULL, G_CALLBACK (string_is_lowercase), NULL, NULL))); ++ gtk_sort_list_model_set_section_sorter (sort1, sorter); ++ g_clear_object (&sorter); ++ break; ++ ++ case 2: ++ /* remove a model */ ++ remove = TRUE; ++ break; ++ ++ case 3: ++ /* add a model */ ++ add = TRUE; ++ break; ++ ++ case 4: ++ /* replace a model */ ++ remove = TRUE; ++ add = TRUE; ++ break; ++ ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ ++ position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store)) + 1); ++ if (g_list_model_get_n_items (G_LIST_MODEL (store)) == position) ++ remove = FALSE; ++ ++ if (add) ++ { ++ /* We want at least one element, otherwise the sorters will see no changes */ ++ GListModel *source = create_source_model (1, 50); ++ g_list_store_splice (store, ++ position, ++ remove ? 1 : 0, ++ (gpointer *) &source, 1); ++ g_object_unref (source); ++ } ++ else if (remove) ++ { ++ g_list_store_remove (store, position); ++ } ++ ++ if (g_test_rand_bit ()) ++ { ++ ensure_updated (); ++ assert_model_equal (G_LIST_MODEL (sort1), G_LIST_MODEL (sort2)); ++ } ++ ++ if (g_test_rand_bit ()) ++ assert_model_sections (G_LIST_MODEL (sort1)); ++ } ++ ++ g_object_unref (sort2); ++ g_object_unref (sort1); ++ g_object_unref (flatten); ++} ++ ++/* Run: ++ * source => sorter ++ * And then randomly add/remove sources and change the sorters and ++ * see if the invariants for sections keep correct. ++ */ ++static void ++test_sections (gconstpointer model_id) ++{ ++ GListStore *store; ++ GtkFlattenListModel *flatten; ++ GtkSortListModel *sort; ++ GtkSorter *sorter; ++ gsize i; ++ ++ store = g_list_store_new (G_TYPE_OBJECT); ++ flatten = gtk_flatten_list_model_new (G_LIST_MODEL (store)); ++ sort = create_sort_list_model (model_id, FALSE, G_LIST_MODEL (flatten), NULL); ++ ++ for (i = 0; i < 500; i++) ++ { ++ gboolean add = FALSE, remove = FALSE; ++ guint position; ++ ++ switch (g_test_rand_int_range (0, 4)) ++ { ++ case 0: ++ /* set the same sorter, once as section sorter, once as sorter */ ++ sorter = create_random_sorter (TRUE); ++ gtk_sort_list_model_set_sorter (sort, sorter); ++ g_clear_object (&sorter); ++ break; ++ ++ case 1: ++ /* set the same sorter, once as section sorter, once as sorter */ ++ sorter = create_random_sorter (TRUE); ++ gtk_sort_list_model_set_section_sorter (sort, sorter); ++ g_clear_object (&sorter); ++ break; ++ ++ case 2: ++ /* remove a model */ ++ remove = TRUE; ++ break; ++ ++ case 3: ++ /* add a model */ ++ add = TRUE; ++ break; ++ ++ case 4: ++ /* replace a model */ ++ remove = TRUE; ++ add = TRUE; ++ break; ++ ++ default: ++ g_assert_not_reached (); ++ break; ++ } ++ ++ position = g_test_rand_int_range (0, g_list_model_get_n_items (G_LIST_MODEL (store)) + 1); ++ if (g_list_model_get_n_items (G_LIST_MODEL (store)) == position) ++ remove = FALSE; ++ ++ if (add) ++ { ++ /* We want at least one element, otherwise the sorters will see no changes */ ++ GListModel *source = create_source_model (1, 50); ++ g_list_store_splice (store, ++ position, ++ remove ? 1 : 0, ++ (gpointer *) &source, 1); ++ g_object_unref (source); ++ } ++ else if (remove) ++ { ++ g_list_store_remove (store, position); ++ } ++ ++ if (g_test_rand_bit ()) ++ ensure_updated (); ++ ++ if (g_test_rand_bit ()) ++ { ++ guint start, end, pos, n, sec_start, sec_end; ++ gpointer prev_item, item; ++ ++ n = g_list_model_get_n_items (G_LIST_MODEL (sort)); ++ sorter = gtk_sort_list_model_get_section_sorter (sort); ++ start = end = 0; ++ prev_item = item = NULL; ++ ++ for (pos = 0; pos < n; pos++) ++ { ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sort), pos, &sec_start, &sec_end); ++ prev_item = item; ++ item = g_list_model_get_item (G_LIST_MODEL (sort), pos); ++ if (end <= pos) ++ { ++ g_assert_cmpint (pos, ==, end); ++ /* there should be a new section */ ++ g_assert_cmpint (sec_start, ==, end); ++ g_assert_cmpint (sec_end, >, sec_start); ++ g_assert_cmpint (sec_end, <=, n); ++ start = sec_start; ++ end = sec_end; ++ if (prev_item) ++ { ++ g_assert_nonnull (sorter); ++ g_assert_cmpint (gtk_sorter_compare (sorter, prev_item, item), !=, GTK_ORDERING_EQUAL); ++ } ++ } ++ else ++ { ++ /* the old section keeps on going */ ++ g_assert_cmpint (sec_start, ==, start); ++ g_assert_cmpint (sec_end, ==, end); ++ if (prev_item && sorter) ++ g_assert_cmpint (gtk_sorter_compare (sorter, prev_item, item), ==, GTK_ORDERING_EQUAL); ++ } ++ g_clear_object (&prev_item); ++ } ++ ++ g_clear_object (&item); ++ ++ /* for good measure, check the error condition */ ++ if (n < G_MAXINT32) ++ { ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sort), g_test_rand_int_range (n, G_MAXINT32), &sec_start, &sec_end); ++ g_assert_cmpint (sec_start, ==, n); ++ g_assert_cmpint (sec_end, ==, G_MAXUINT); ++ } ++ sorter = NULL; ++ } ++ } ++ ++ g_object_unref (sort); ++ g_object_unref (flatten); ++} ++ + static void + add_test_for_all_models (const char *name, + GTestDataFunc test_func) +@@ -488,6 +755,8 @@ main (int argc, char *argv[]) + + add_test_for_all_models ("two-sorters", test_two_sorters); + add_test_for_all_models ("stability", test_stability); ++ add_test_for_all_models ("section-sorters", test_section_sorters); ++ add_test_for_all_models ("sections", test_sections); + + return g_test_run (); + } +diff --git a/testsuite/gtk/sortlistmodel.c b/testsuite/gtk/sortlistmodel.c +index 6a1753373c845357c978caeac180ef25f8447275..d2cd69926901b32ac6483711d325faef3d785635 100644 +--- a/testsuite/gtk/sortlistmodel.c ++++ b/testsuite/gtk/sortlistmodel.c +@@ -258,7 +258,7 @@ test_create (void) + { + GtkSortListModel *sort; + GListStore *store; +- ++ + store = new_store ((guint[]) { 4, 8, 2, 6, 10, 0 }); + sort = new_model (store); + assert_model (sort, "2 4 6 8 10"); +@@ -280,7 +280,7 @@ test_set_model (void) + { + GtkSortListModel *sort; + GListStore *store; +- ++ + sort = new_model (NULL); + assert_model (sort, ""); + assert_changes (sort, ""); +@@ -319,7 +319,7 @@ test_set_sorter (void) + GtkSortListModel *sort; + GtkSorter *sorter; + GListStore *store; +- ++ + store = new_store ((guint[]) { 4, 8, 2, 6, 10, 0 }); + sort = new_model (store); + assert_model (sort, "2 4 6 8 10"); +@@ -350,7 +350,7 @@ test_add_items (void) + { + GtkSortListModel *sort; + GListStore *store; +- ++ + /* add beginning */ + store = new_store ((guint[]) { 51, 99, 100, 49, 50, 0 }); + sort = new_model (store); +@@ -390,7 +390,7 @@ test_remove_items (void) + { + GtkSortListModel *sort; + GListStore *store; +- ++ + /* remove beginning */ + store = new_store ((guint[]) { 51, 99, 100, 49, 1, 2, 50, 0 }); + sort = new_model (store); +@@ -570,6 +570,58 @@ test_add_remove_item (void) + g_object_unref (sort); + } + ++static int ++sort_func (gconstpointer p1, ++ gconstpointer p2, ++ gpointer data) ++{ ++ const char *s1 = gtk_string_object_get_string ((GtkStringObject *)p1); ++ const char *s2 = gtk_string_object_get_string ((GtkStringObject *)p2); ++ ++ /* compare just the first byte */ ++ return (int)(s1[0]) - (int)(s2[0]); ++} ++ ++static void ++test_sections (void) ++{ ++ GtkStringList *list; ++ const char *strings[] = { ++ "aaa", ++ "aab", ++ "abc", ++ "bbb", ++ "bq1", ++ "bq2", ++ "cc", ++ "cx", ++ NULL ++ }; ++ GtkSorter *sorter; ++ GtkSortListModel *sorted; ++ GtkSorter *section_sorter; ++ guint s, e; ++ ++ list = gtk_string_list_new (strings); ++ sorter = GTK_SORTER (gtk_string_sorter_new (gtk_property_expression_new (GTK_TYPE_STRING_OBJECT, NULL, "string"))); ++ sorted = gtk_sort_list_model_new (G_LIST_MODEL (list), sorter); ++ section_sorter = GTK_SORTER (gtk_custom_sorter_new (sort_func, NULL, NULL)); ++ gtk_sort_list_model_set_section_sorter (GTK_SORT_LIST_MODEL (sorted), section_sorter); ++ g_object_unref (section_sorter); ++ ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sorted), 0, &s, &e); ++ g_assert_cmpint (s, ==, 0); ++ g_assert_cmpint (e, ==, 3); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sorted), 3, &s, &e); ++ g_assert_cmpint (s, ==, 3); ++ g_assert_cmpint (e, ==, 6); ++ gtk_section_model_get_section (GTK_SECTION_MODEL (sorted), 6, &s, &e); ++ g_assert_cmpint (s, ==, 6); ++ g_assert_cmpint (e, ==, 8); ++ ++ g_object_unref (sorted); ++} ++ + int + main (int argc, char *argv[]) + { +@@ -589,6 +641,7 @@ main (int argc, char *argv[]) + g_test_add_func ("/sortlistmodel/incremental/remove", test_incremental_remove); + g_test_add_func ("/sortlistmodel/oob-access", test_out_of_bounds_access); + g_test_add_func ("/sortlistmodel/add-remove-item", test_add_remove_item); ++ g_test_add_func ("/sortlistmodel/sections", test_sections); + + return g_test_run (); + } diff --git a/gui-libs/gtk/gtk-4.11.2-r1.ebuild b/gui-libs/gtk/gtk-4.11.2-r1.ebuild index 47625a5..a4b6bf4 100644 --- a/gui-libs/gtk/gtk-4.11.2-r1.ebuild +++ b/gui-libs/gtk/gtk-4.11.2-r1.ebuild @@ -97,6 +97,7 @@ BDEPEND=" " PATCHES=( + "${FILESDIR}"/5818.patch "${FILESDIR}"/4447.patch )