diff options
author | Matthias Clasen <mclasen@redhat.com> | 2023-05-09 16:36:55 +0000 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2023-05-09 16:36:55 +0000 |
commit | 526ddfa8666cf31e26763f2bcd0c83b066714134 (patch) | |
tree | dde096f76dc75d8588e316786df78b6081d303b9 | |
parent | a959fba18afe9d161205d13523205c91c5a23a15 (diff) | |
parent | c88ac79437f1a88b8ba56ff48e6d5fcab95c04ca (diff) | |
download | gtk+-526ddfa8666cf31e26763f2bcd0c83b066714134.tar.gz |
Merge branch 'wip/otte/sections' into 'main'
Add GtkSectionModel
See merge request GNOME/gtk!5818
42 files changed, 4286 insertions, 512 deletions
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml index 615d782bfe..ba87e860ce 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 4e8e69bfbf..6a7779ba12 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 0000000000..4f3e87fb68 --- /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 0000000000..0600f63582 --- /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 c03beb3940..976c3ec997 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 0000000000..13c0a5d559 --- /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 0000000000..5260a84516 --- /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); @@ -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 1804b7d12a..7e53e52c11 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 04cf04c6fd..f80e80ff6d 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 ead43a8c8b..274f34c3c9 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 0a7dbb0b02..00ac9c571d 100644 --- a/gtk/gtklistbase.c +++ b/gtk/gtklistbase.c @@ -1959,12 +1959,26 @@ 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 7aa596ffde..fad3872cdc 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 0000000000..62cfd5b4c9 --- /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 0000000000..87862bab59 --- /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 0000000000..a2b5b72e1e --- /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 0000000000..fa3fa45f22 --- /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 0000000000..81aa850c8a --- /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 0000000000..5a1a2559ce --- /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 0000000000..54f3c666d8 --- /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 67bbf95912..cdd8fe9703 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 8ef33b5def..2ca7f9eaf8 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,27 +58,105 @@ 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 @@ -462,6 +712,18 @@ gtk_list_tile_set_area_size (GtkListItemManager *self, } static void +gtk_list_tile_set_type (GtkListTile *tile, + GtkListTileType type) +{ + if (tile->type == type) + return; + + g_assert (tile->widget == NULL); + tile->type = type; + gtk_rb_tree_node_mark_dirty (tile); +} + +static void gtk_list_item_tracker_unset_position (GtkListItemManager *self, GtkListItemTracker *tracker) { @@ -569,11 +831,11 @@ gtk_list_item_manager_ensure_split (GtkListItemManager *self, static void gtk_list_item_manager_remove_items (GtkListItemManager *self, - GHashTable *change, + GtkListItemChange *change, guint position, guint n_items) { - GtkListTile *tile, *next; + GtkListTile *tile, *header; guint offset; if (n_items == 0) @@ -582,24 +844,63 @@ gtk_list_item_manager_remove_items (GtkListItemManager *self, tile = gtk_list_item_manager_get_nth (self, position, &offset); if (offset) tile = gtk_list_item_manager_ensure_split (self, tile, offset); + header = gtk_list_tile_get_previous_skip (tile); + if (header->type != GTK_LIST_TILE_HEADER && header->type != GTK_LIST_TILE_UNMATCHED_HEADER) + header = NULL; while (n_items > 0) { - if (tile->n_items > n_items) + switch (tile->type) { - gtk_list_item_manager_ensure_split (self, tile, n_items); - g_assert (tile->n_items <= n_items); + case GTK_LIST_TILE_HEADER: + case GTK_LIST_TILE_UNMATCHED_HEADER: + g_assert (header == NULL); + header = tile; + break; + + case GTK_LIST_TILE_FOOTER: + case GTK_LIST_TILE_UNMATCHED_FOOTER: + if (header) + { + gtk_list_item_change_clear_header (change, &header->widget); + gtk_list_tile_set_type (header, GTK_LIST_TILE_REMOVED); + gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); + header = NULL; + } + break; + + case GTK_LIST_TILE_ITEM: + if (tile->n_items > n_items) + { + gtk_list_item_manager_ensure_split (self, tile, n_items); + g_assert (tile->n_items <= n_items); + } + if (tile->widget) + gtk_list_item_change_release (change, GTK_LIST_ITEM_BASE (tile->widget)); + tile->widget = NULL; + n_items -= tile->n_items; + tile->n_items = 0; + gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); + break; + + case GTK_LIST_TILE_FILLER: + case GTK_LIST_TILE_REMOVED: + default: + g_assert_not_reached (); + break; } - next = gtk_rb_tree_node_get_next (tile); - if (tile->widget) - gtk_list_item_manager_release_list_item (self, change, tile->widget); - tile->widget = NULL; - n_items -= tile->n_items; - tile->n_items = 0; - gtk_rb_tree_node_mark_dirty (tile); + tile = gtk_list_tile_get_next_skip (tile); + } - tile = next; + if (header) + { + if (tile->type == GTK_LIST_TILE_FOOTER || tile->type == GTK_LIST_TILE_UNMATCHED_FOOTER) + { + gtk_list_item_change_clear_header (change, &header->widget); + gtk_list_tile_set_type (header, GTK_LIST_TILE_REMOVED); + gtk_list_tile_set_type (tile, GTK_LIST_TILE_REMOVED); + } } gtk_widget_queue_resize (GTK_WIDGET (self->widget)); @@ -607,23 +908,70 @@ gtk_list_item_manager_remove_items (GtkListItemManager *self, static void gtk_list_item_manager_add_items (GtkListItemManager *self, + GtkListItemChange *change, guint position, guint n_items) { GtkListTile *tile; guint offset; + gboolean has_sections; if (n_items == 0) return; + has_sections = gtk_list_item_manager_has_sections (self); + tile = gtk_list_item_manager_get_nth (self, position, &offset); + if (tile == NULL) + { + /* at end of list, pick the footer */ + for (tile = gtk_rb_tree_get_last (self->items); + tile && (tile->type == GTK_LIST_TILE_REMOVED || tile->type == GTK_LIST_TILE_FILLER); + tile = gtk_rb_tree_node_get_previous (tile)) + { } + + if (tile == NULL) + { + /* empty list, there isn't even a footer yet */ + tile = gtk_rb_tree_insert_after (self->items, NULL); + tile->type = GTK_LIST_TILE_UNMATCHED_HEADER; + + tile = gtk_rb_tree_insert_after (self->items, tile); + tile->type = GTK_LIST_TILE_UNMATCHED_FOOTER; + } + else if (has_sections && tile->type == GTK_LIST_TILE_FOOTER) + { + GtkListTile *header; + + gtk_list_tile_set_type (tile, GTK_LIST_TILE_UNMATCHED_FOOTER); + + header = gtk_list_tile_get_header (self, tile); + gtk_list_item_change_clear_header (change, &header->widget); + gtk_list_tile_set_type (header, GTK_LIST_TILE_UNMATCHED_HEADER); + } + } if (offset) tile = gtk_list_item_manager_ensure_split (self, tile, offset); - + tile = gtk_rb_tree_insert_before (self->items, tile); + tile->type = GTK_LIST_TILE_ITEM; tile->n_items = n_items; gtk_rb_tree_node_mark_dirty (tile); + if (has_sections) + { + GtkListTile *section = gtk_list_tile_get_previous_skip (tile); + + if (section->type == GTK_LIST_TILE_HEADER) + { + gtk_list_item_change_clear_header (change, §ion->widget); + gtk_list_tile_set_type (section, + GTK_LIST_TILE_UNMATCHED_HEADER); + gtk_list_tile_set_type (gtk_list_tile_get_footer (self, section), + GTK_LIST_TILE_UNMATCHED_FOOTER); + } + } + gtk_widget_queue_resize (GTK_WIDGET (self->widget)); } @@ -632,7 +980,8 @@ gtk_list_item_manager_merge_list_items (GtkListItemManager *self, GtkListTile *first, GtkListTile *second) { - if (first->widget || second->widget) + if (first->widget || second->widget || + first->type != GTK_LIST_TILE_ITEM || second->type != GTK_LIST_TILE_ITEM) return FALSE; first->n_items += second->n_items; @@ -653,7 +1002,7 @@ gtk_list_item_manager_merge_list_items (GtkListItemManager *self, * items will be given to the new tile, which will be * nserted after the tile. * - * It is valid for either tile to have 0 items after + * It is not valid for either tile to have 0 items after * the split. * * Returns: The new tile @@ -665,9 +1014,12 @@ gtk_list_tile_split (GtkListItemManager *self, { GtkListTile *result; - g_assert (n_items <= tile->n_items); + g_assert (n_items > 0); + g_assert (n_items < tile->n_items); + g_assert (tile->type == GTK_LIST_TILE_ITEM); result = gtk_rb_tree_insert_after (self->items, tile); + result->type = GTK_LIST_TILE_ITEM; result->n_items = tile->n_items - n_items; tile->n_items = n_items; gtk_rb_tree_node_mark_dirty (tile); @@ -676,9 +1028,37 @@ gtk_list_tile_split (GtkListItemManager *self, } /* + * 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 44a9ac82ea..cc92ef74a9 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 f6709be181..ef274d93a4 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; @@ -850,6 +884,18 @@ gtk_list_view_class_init (GtkListViewClass *klass) 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) * * Model for the items displayed. @@ -1054,23 +1100,81 @@ 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` * @show_separators: %TRUE to show separators @@ -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 b3e3afb60a..df4901e319 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 002376fc1e..7dd02ce3f8 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 055472d3d4..f549541baa 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 c0848f7c0e..20bff24df2 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 0000000000..a5084ab340 --- /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 0000000000..a242b42c20 --- /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 0000000000..e6cfb0cb1b --- /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 e4e148ec83..07f0c7eaee 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 c2feb9e250..105dfd4358 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); }; @@ -847,6 +1054,18 @@ gtk_sort_list_model_class_init (GtkSortListModelClass *class) 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) * * The sorter for this model. @@ -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]); } @@ -1002,6 +1222,55 @@ gtk_sort_list_model_get_sorter (GtkSortListModel *self) } /** + * 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` * @incremental: %TRUE to sort incrementally diff --git a/gtk/gtksortlistmodel.h b/gtk/gtksortlistmodel.h index 957f5b0019..64bb0d019c 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 b3975fb3fc..ec214f0d74 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 33b7172914..231600ecf8 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 084f1d301d..2af32072f5 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 b3c8303daa..6f1465b3d1 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 0000000000..ba5fbd54ba --- /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 11560a95f4..a3f2be7172 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 0ca2c68f5e..e06bed8195 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 6a1753373c..d2cd699269 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 (); } |