gjdwebserver-overlay/gui-libs/gtk/files/5818.patch

6140 lines
207 KiB
Diff

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 @@
<gresource prefix="/listview_settings">
<file>listview_settings.ui</file>
</gresource>
+ <gresource prefix="/listview_settings2">
+ <file>listview_settings2.ui</file>
+ </gresource>
<gresource prefix="/listview_ucd_data/">
<file>ucdnames.data</file>
</gresource>
@@ -312,6 +315,7 @@
<file>listview_minesweeper.c</file>
<file>listview_selections.c</file>
<file>listview_settings.c</file>
+ <file>listview_settings2.c</file>
<file>listview_ucd.c</file>
<file>listview_weather.c</file>
<file>listview_words.c</file>
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 <gtk/gtk.h>
-#include <stdlib.h>
-
-/* 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 <gtk/gtk.h>
+
+#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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <object class="GtkStringFilter" id="filter">
+ <property name="expression">
+ <closure type="gchararray" function="settings_key_get_search_string" />
+ </property>
+ <property name="search" bind-source="entry" bind-property="text" />
+ </object>
+ <object class="GtkWindow" id="window">
+ <property name="title" translatable="yes">Settings</property>
+ <property name="default-width">640</property>
+ <property name="default-height">480</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <child type="end">
+ <object class="GtkToggleButton" id="search_button">
+ <property name="icon-name">system-search-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkSearchBar">
+ <property name="search-mode-enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional"/>
+ <signal name="notify::search-mode-enabled" handler="search_enabled" object="entry"/>
+ <child>
+ <object class="GtkSearchEntry" id="entry">
+ <signal name="stop-search" handler="stop_search"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <child>
+ <object class="GtkListView" id="listview">
+ <property name="vexpand">1</property>
+ <style>
+ <class name="rich-list"/>
+ </style>
+ <property name="factory">
+ <object class="GtkBuilderListItemFactory">
+ <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtkListItem">
+ <property name="child">
+ <object class="GtkBox">
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="xalign">0</property>
+ <binding name="label">
+ <lookup name="name" type="SettingsKey">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <property name="xalign">0</property>
+ <property name="ellipsize">end</property>
+ <binding name="label">
+ <lookup name="summary" type="SettingsKey">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry">
+ <property name="hexpand">1</property>
+ <property name="halign">end</property>
+ <binding name="text">
+ <lookup name="value" type="SettingsKey">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ <signal name="notify::label" handler="item_value_changed"/>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
+ ]]></property>
+ </object>
+ </property>
+ <property name="header-factory">
+ <object class="GtkBuilderListItemFactory">
+ <property name="bytes"><![CDATA[
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtkListHeader">
+ <property name="child">
+ <object class="GtkLabel">
+ <property name="xalign">0</property>
+ <binding name="label">
+ <lookup name="schema" type="GSettings">
+ <lookup name="settings" type="SettingsKey">
+ <lookup name="item">GtkListHeader</lookup>
+ </lookup>
+ </lookup>
+ </binding>
+ </object>
+ </property>
+ </template>
+</interface>
+ ]]></property>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
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 <gtk/gtk.h>
+
+#include <stdlib.h>
+
+/* 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 <gtk/gtklistbase.h>
#include <gtk/gtklinkbutton.h>
#include <gtk/gtklistbox.h>
+#include <gtk/gtklistheader.h>
#include <gtk/gtklistitem.h>
#include <gtk/gtklistitemfactory.h>
#include <gtk/deprecated/gtkliststore.h>
@@ -225,6 +226,7 @@
#include <gtk/gtkscrolledwindow.h>
#include <gtk/gtksearchbar.h>
#include <gtk/gtksearchentry.h>
+#include <gtk/gtksectionmodel.h>
#include <gtk/gtkselectionfiltermodel.h>
#include <gtk/gtkselectionmodel.h>
#include <gtk/gtkseparator.h>
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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#pragma once
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#include <gtk/gtktypes.h>
+
+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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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, &section->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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#pragma once
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#include <gtk/gtktypes.h>
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <locale.h>
+
+#include <gtk/gtk.h>
+#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 ();
}