diff --git a/docs/reference/gtk/gtk4-docs.xml b/docs/reference/gtk/gtk4-docs.xml index 98f0c897e0..b2e7de2c5f 100644 --- a/docs/reference/gtk/gtk4-docs.xml +++ b/docs/reference/gtk/gtk4-docs.xml @@ -45,6 +45,7 @@ GListModel support + diff --git a/docs/reference/gtk/gtk4-sections.txt b/docs/reference/gtk/gtk4-sections.txt index 23293e2693..5410ec67a4 100644 --- a/docs/reference/gtk/gtk4-sections.txt +++ b/docs/reference/gtk/gtk4-sections.txt @@ -1744,6 +1744,27 @@ GtkLinkButtonPrivate gtk_link_button_get_type +
+gtkmaplistmodel +GtkMapListModel +GtkMapListModel +GtkMapListModelMapFunc +gtk_map_list_model_new +gtk_map_list_model_set_map_func +gtk_map_list_model_set_model +gtk_map_list_model_get_model +gtk_map_list_model_has_map + +GTK_MAP_LIST_MODEL +GTK_IS_MAP_LIST_MODEL +GTK_TYPE_MAP_LIST_MODEL +GTK_MAP_LIST_MODEL_CLASS +GTK_IS_MAP_LIST_MODEL_CLASS +GTK_MAP_LIST_MODEL_GET_CLASS + +gtk_map_list_model_get_type +
+
gtkmenu GtkMenu diff --git a/gtk/gtk.h b/gtk/gtk.h index 6a5f9b25dd..cde9b4a46b 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -141,6 +141,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtkmaplistmodel.c b/gtk/gtkmaplistmodel.c new file mode 100644 index 0000000000..028bc060e4 --- /dev/null +++ b/gtk/gtkmaplistmodel.c @@ -0,0 +1,627 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "gtkmaplistmodel.h" + +#include "gtkcssrbtreeprivate.h" +#include "gtkintl.h" +#include "gtkprivate.h" + +/** + * SECTION:gtkmaplistmodel + * @title: GtkMapListModel + * @short_description: a #GListModel that maps items from a child list model to + * a different item + * @see_also: #GListModel + * + * #GtkMapListModel is a list model that takes a list model and maps the items + * in that model to different items according to a #GtkMapListModelMapFunc. + * + * FIXME: Add useful examples here, like turning #GFile into #GFileInfo or #GdkPixmap. + * + * #GtkMapListModel will attempt to discard the mapped objects as soon as they are no + * longer needed and recreate them if necessary. + */ + +enum { + PROP_0, + PROP_HAS_MAP, + PROP_ITEM_TYPE, + PROP_MODEL, + NUM_PROPERTIES +}; + +typedef struct _MapNode MapNode; +typedef struct _MapAugment MapAugment; + +struct _MapNode +{ + guint n_items; + gpointer item; /* can only be set when n_items == 1 */ +}; + +struct _MapAugment +{ + guint n_items; +}; + +struct _GtkMapListModel +{ + GObject parent_instance; + + GType item_type; + GListModel *model; + GtkMapListModelMapFunc map_func; + gpointer user_data; + GDestroyNotify user_destroy; + + GtkCssRbTree *items; /* NULL if map_func == NULL */ +}; + +struct _GtkMapListModelClass +{ + GObjectClass parent_class; +}; + +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +static MapNode * +gtk_map_list_model_get_nth (GtkCssRbTree *tree, + guint position, + guint *out_start_pos) +{ + MapNode *node, *tmp; + guint start_pos = position; + + node = gtk_css_rb_tree_get_root (tree); + + while (node) + { + tmp = gtk_css_rb_tree_get_left (tree, node); + if (tmp) + { + MapAugment *aug = gtk_css_rb_tree_get_augment (tree, tmp); + if (position < aug->n_items) + { + node = tmp; + continue; + } + position -= aug->n_items; + } + + if (position < node->n_items) + { + start_pos -= position; + break; + } + position -= node->n_items; + + node = gtk_css_rb_tree_get_right (tree, node); + } + + if (out_start_pos) + *out_start_pos = start_pos; + + return node; +} + +static GType +gtk_map_list_model_get_item_type (GListModel *list) +{ + GtkMapListModel *self = GTK_MAP_LIST_MODEL (list); + + return self->item_type; +} + +static guint +gtk_map_list_model_get_n_items (GListModel *list) +{ + GtkMapListModel *self = GTK_MAP_LIST_MODEL (list); + + if (self->model == NULL) + return 0; + + return g_list_model_get_n_items (self->model); +} + +static gpointer +gtk_map_list_model_get_item (GListModel *list, + guint position) +{ + GtkMapListModel *self = GTK_MAP_LIST_MODEL (list); + MapNode *node; + guint offset; + + if (self->model == NULL) + return NULL; + + if (self->items == NULL) + return g_list_model_get_item (self->model, position); + + node = gtk_map_list_model_get_nth (self->items, position, &offset); + if (node == NULL) + return NULL; + + if (node->item) + return g_object_ref (node->item); + + if (offset != position) + { + MapNode *before = gtk_css_rb_tree_insert_before (self->items, node); + before->n_items = position - offset; + node->n_items -= before->n_items; + gtk_css_rb_tree_mark_dirty (self->items, node); + } + + if (node->n_items > 1) + { + MapNode *after = gtk_css_rb_tree_insert_after (self->items, node); + after->n_items = node->n_items - 1; + node->n_items = 1; + gtk_css_rb_tree_mark_dirty (self->items, node); + } + + node->item = self->map_func (g_list_model_get_item (self->model, position), self->user_data); + g_object_add_weak_pointer (node->item, &node->item); + + return node->item; +} + +static void +gtk_map_list_model_model_init (GListModelInterface *iface) +{ + iface->get_item_type = gtk_map_list_model_get_item_type; + iface->get_n_items = gtk_map_list_model_get_n_items; + iface->get_item = gtk_map_list_model_get_item; +} + +G_DEFINE_TYPE_WITH_CODE (GtkMapListModel, gtk_map_list_model, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_map_list_model_model_init)) + +static void +gtk_map_list_model_items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtkMapListModel *self) +{ + MapNode *node; + guint start, end; + + if (self->items == NULL) + { + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + return; + } + + node = gtk_map_list_model_get_nth (self->items, position, &start); + g_assert (start <= position); + + while (removed > 0) + { + end = start + node->n_items; + if (start == position && end <= position + removed) + { + MapNode *next = gtk_css_rb_tree_get_next (self->items, node); + removed -= node->n_items; + gtk_css_rb_tree_remove (self->items, node); + node = next; + } + else + { + if (end >= position + removed) + { + node->n_items -= removed; + removed = 0; + gtk_css_rb_tree_mark_dirty (self->items, node); + } + else if (start < position) + { + guint overlap = node->n_items - (position - start); + node->n_items -= overlap; + gtk_css_rb_tree_mark_dirty (self->items, node); + removed -= overlap; + start = position; + node = gtk_css_rb_tree_get_next (self->items, node); + } + } + } + + if (added) + { + if (node == NULL) + node = gtk_css_rb_tree_insert_before (self->items, NULL); + else if (node->item) + node = gtk_css_rb_tree_insert_after (self->items, node); + + node->n_items += added; + gtk_css_rb_tree_mark_dirty (self->items, node); + } + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); +} + +static void +gtk_map_list_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkMapListModel *self = GTK_MAP_LIST_MODEL (object); + + switch (prop_id) + { + case PROP_ITEM_TYPE: + self->item_type = g_value_get_gtype (value); + break; + + case PROP_MODEL: + gtk_map_list_model_set_model (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_map_list_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkMapListModel *self = GTK_MAP_LIST_MODEL (object); + + switch (prop_id) + { + case PROP_HAS_MAP: + g_value_set_boolean (value, self->items != NULL); + break; + + case PROP_ITEM_TYPE: + g_value_set_gtype (value, self->item_type); + break; + + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_map_list_model_clear_model (GtkMapListModel *self) +{ + if (self->model == NULL) + return; + + g_signal_handlers_disconnect_by_func (self->model, gtk_map_list_model_items_changed_cb, self); + g_clear_object (&self->model); +} + +static void +gtk_map_list_model_dispose (GObject *object) +{ + GtkMapListModel *self = GTK_MAP_LIST_MODEL (object); + + gtk_map_list_model_clear_model (self); + if (self->user_destroy) + self->user_destroy (self->user_data); + self->map_func = NULL; + self->user_data = NULL; + self->user_destroy = NULL; + g_clear_pointer (&self->items, gtk_css_rb_tree_unref); + + G_OBJECT_CLASS (gtk_map_list_model_parent_class)->dispose (object); +}; + +static void +gtk_map_list_model_class_init (GtkMapListModelClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (class); + + gobject_class->set_property = gtk_map_list_model_set_property; + gobject_class->get_property = gtk_map_list_model_get_property; + gobject_class->dispose = gtk_map_list_model_dispose; + + /** + * GtkMapListModel:has-map: + * + * If a map is set for this model + */ + properties[PROP_HAS_MAP] = + g_param_spec_boolean ("has-map", + P_("has map"), + P_("If a map is set for this model"), + FALSE, + GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkMapListModel:item-type: + * + * The #GType for elements of this object + */ + properties[PROP_ITEM_TYPE] = + g_param_spec_gtype ("item-type", + P_("Item type"), + P_("The type of elements of this object"), + G_TYPE_OBJECT, + GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkMapListModel:model: + * + * The model being mapped + */ + properties[PROP_MODEL] = + g_param_spec_object ("model", + P_("Model"), + P_("The model being mapped"), + G_TYPE_LIST_MODEL, + GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties); +} + +static void +gtk_map_list_model_init (GtkMapListModel *self) +{ +} + + +static void +gtk_map_list_model_augment (GtkCssRbTree *map, + gpointer _aug, + gpointer _node, + gpointer left, + gpointer right) +{ + MapNode *node= _node; + MapAugment *aug = _aug; + + aug->n_items = node->n_items; + + if (left) + { + MapAugment *left_aug = gtk_css_rb_tree_get_augment (map, left); + aug->n_items += left_aug->n_items; + } + if (right) + { + MapAugment *right_aug = gtk_css_rb_tree_get_augment (map, right); + aug->n_items += right_aug->n_items; + } +} + +/** + * gtk_map_list_model_new: + * @item_type: the #GType to use as the model's item type + * @model: (allow-none): The model to map or %NULL for none + * @map_func: (allow-none): map function or %NULL to not map items + * @user_data: user data passed to @map_func + * @destroy: destroy notifier for @user_data + * + * Creates a new #GtkMapListModel for the given arguments. + * + * Returns: a new #GtkMapListModel + **/ +GtkMapListModel * +gtk_map_list_model_new (GType item_type, + GListModel *model, + GtkMapListModelMapFunc map_func, + gpointer data, + GDestroyNotify data_destroy) +{ + GtkMapListModel *result; + + g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL); + g_return_val_if_fail (model == NULL || G_IS_LIST_MODEL (model), NULL); + + result = g_object_new (GTK_TYPE_MAP_LIST_MODEL, + "item-type", item_type, + "model", model, + NULL); + + if (map_func) + gtk_map_list_model_set_map_func (result, map_func, data, data_destroy); + + return result; +} + +static void +gtk_map_list_model_clear_node (gpointer _node) +{ + MapNode *node = _node; + + if (node->item) + g_object_remove_weak_pointer (node->item, &node->item); +} + +static void +gtk_map_list_model_init_items (GtkMapListModel *self) +{ + if (self->map_func && self->model) + { + guint n_items; + + if (self->items) + { + gtk_css_rb_tree_remove_all (self->items); + } + else + { + self->items = gtk_css_rb_tree_new (MapNode, + MapAugment, + gtk_map_list_model_augment, + gtk_map_list_model_clear_node, + NULL); + } + + n_items = g_list_model_get_n_items (self->model); + if (n_items) + { + MapNode *node = gtk_css_rb_tree_insert_before (self->items, NULL); + node->n_items = g_list_model_get_n_items (self->model); + gtk_css_rb_tree_mark_dirty (self->items, node); + } + } + else + { + g_clear_pointer (&self->items, gtk_css_rb_tree_unref); + } +} + +/** + * gtk_map_list_model_set_map_func: + * @self: a #GtkMapListModel + * @map_func: (allow-none): map function or %NULL to not map items + * @user_data: user data passed to @map_func + * @destroy: destroy notifier for @user_data + * + * Sets the function used to map items. The function will be called whenever + * an item needs to be mapped and must return the item to use for the given + * input item. + * + * Note that #GtkMapListModel may call this function multiple times on the + * same item, because it may delete items it doesn't need anymore. + * + * GTK makes no effort to ensure that @map_func conforms to the item type + * of @self. It assumes that the caller knows what they are doing and the map + * function returns items of the appropriate type. + **/ +void +gtk_map_list_model_set_map_func (GtkMapListModel *self, + GtkMapListModelMapFunc map_func, + gpointer user_data, + GDestroyNotify user_destroy) +{ + gboolean was_maped, will_be_maped; + guint n_items; + + g_return_if_fail (GTK_IS_MAP_LIST_MODEL (self)); + g_return_if_fail (map_func != NULL || (user_data == NULL && !user_destroy)); + + was_maped = self->map_func != NULL; + will_be_maped = map_func != NULL; + + if (!was_maped && !will_be_maped) + return; + + if (self->user_destroy) + self->user_destroy (self->user_data); + + self->map_func = map_func; + self->user_data = user_data; + self->user_destroy = user_destroy; + + gtk_map_list_model_init_items (self); + + if (self->model) + n_items = g_list_model_get_n_items (self->model); + else + n_items = 0; + if (n_items) + g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items, n_items); + + if (was_maped != will_be_maped) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_MAP]); +} + +/** + * gtk_map_list_model_set_model: + * @self: a #GtkMapListModel + * @model: (allow-none): The model to be mapped + * + * Sets the model to be mapped. + * + * GTK makes no effort to ensure that @model conforms to the item type + * expected by the map function. It assumes that the caller knows what + * they are doing and have set up an appropriate map function. + **/ +void +gtk_map_list_model_set_model (GtkMapListModel *self, + GListModel *model) +{ + guint removed, added; + + g_return_if_fail (GTK_IS_MAP_LIST_MODEL (self)); + g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); + + if (self->model == model) + return; + + removed = g_list_model_get_n_items (G_LIST_MODEL (self)); + gtk_map_list_model_clear_model (self); + + if (model) + { + self->model = g_object_ref (model); + g_signal_connect (model, "items-changed", G_CALLBACK (gtk_map_list_model_items_changed_cb), self); + added = g_list_model_get_n_items (model); + } + else + { + added = 0; + } + + gtk_map_list_model_init_items (self); + + if (removed > 0 || added > 0) + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]); +} + +/** + * gtk_map_list_model_get_model: + * @self: a #GtkMapListModel + * + * Gets the model that is curently being mapped or %NULL if none. + * + * Returns: (nullable) (transfer none): The model that gets mapped + **/ +GListModel * +gtk_map_list_model_get_model (GtkMapListModel *self) +{ + g_return_val_if_fail (GTK_IS_MAP_LIST_MODEL (self), NULL); + + return self->model; +} + +/** + * gtk_map_list_model_has_map: + * @self: a #GtkMapListModel + * + * Checks if a map function is currently set on @self + * + * Returns: %TRUE if a map function is set + **/ +gboolean +gtk_map_list_model_has_map (GtkMapListModel *self) +{ + g_return_val_if_fail (GTK_IS_MAP_LIST_MODEL (self), FALSE); + + return self->map_func != NULL; +} diff --git a/gtk/gtkmaplistmodel.h b/gtk/gtkmaplistmodel.h new file mode 100644 index 0000000000..185c8ed0da --- /dev/null +++ b/gtk/gtkmaplistmodel.h @@ -0,0 +1,72 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_MAP_LIST_MODEL_H__ +#define __GTK_MAP_LIST_MODEL_H__ + + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + + +G_BEGIN_DECLS + +#define GTK_TYPE_MAP_LIST_MODEL (gtk_map_list_model_get_type ()) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkMapListModel, gtk_map_list_model, GTK, MAP_LIST_MODEL, GObject) + +/** + * GtkMapListModelMapFunc: + * @item: (transfer full): The item to map + * @data: user data + * + * User function that is called to map an @item of the original model. + * + * Returns: (transfer full): The item to map to. This function may not return %NULL + */ +typedef gpointer (* GtkMapListModelMapFunc) (gpointer item, gpointer data); + +GDK_AVAILABLE_IN_ALL +GtkMapListModel * gtk_map_list_model_new (GType item_type, + GListModel *model, + GtkMapListModelMapFunc map_func, + gpointer user_data, + GDestroyNotify user_destroy); + +GDK_AVAILABLE_IN_ALL +void gtk_map_list_model_set_map_func (GtkMapListModel *self, + GtkMapListModelMapFunc map_func, + gpointer user_data, + GDestroyNotify user_destroy); +GDK_AVAILABLE_IN_ALL +void gtk_map_list_model_set_model (GtkMapListModel *self, + GListModel *model); +GDK_AVAILABLE_IN_ALL +GListModel * gtk_map_list_model_get_model (GtkMapListModel *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_map_list_model_has_map (GtkMapListModel *self); + +G_END_DECLS + +#endif /* __GTK_MAP_LIST_MODEL_H__ */ diff --git a/gtk/meson.build b/gtk/meson.build index 9d0f517b49..386d72dbed 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -271,6 +271,7 @@ gtk_public_sources = files([ 'gtkliststore.c', 'gtklockbutton.c', 'gtkmain.c', + 'gtkmaplistmodel.c', 'gtkmediacontrols.c', 'gtkmediafile.c', 'gtkmediastream.c', @@ -514,6 +515,7 @@ gtk_public_headers = files([ 'gtkliststore.h', 'gtklockbutton.h', 'gtkmain.h', + 'gtkmaplistmodel.h', 'gtkmediacontrols.h', 'gtkmediafile.h', 'gtkmediastream.h', diff --git a/testsuite/gtk/maplistmodel.c b/testsuite/gtk/maplistmodel.c new file mode 100644 index 0000000000..b32d95b128 --- /dev/null +++ b/testsuite/gtk/maplistmodel.c @@ -0,0 +1,300 @@ +/* GtkRBTree tests. + * + * Copyright (C) 2011, Red Hat, Inc. + * Authors: Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#include + +#include + +static GQuark number_quark; +static GQuark changes_quark; + +static guint +get (GListModel *model, + guint position) +{ + GObject *object = g_list_model_get_item (model, position); + g_assert (object != NULL); + return GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark)); +} + +static char * +model_to_string (GListModel *model) +{ + GString *string = g_string_new (NULL); + guint i; + + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + if (i > 0) + g_string_append (string, " "); + g_string_append_printf (string, "%u", get (model, i)); + } + + return g_string_free (string, FALSE); +} + +static GListStore * +new_store (guint start, + guint end, + guint step); + +#if 0 +static void +splice (GListStore *store, + guint pos, + guint removed, + guint *numbers, + guint added) +{ + GObject *objects[added]; + guint i; + + for (i = 0; i < added; i++) + { + /* 0 cannot be differentiated from NULL, so don't use it */ + g_assert (numbers[i] != 0); + objects[i] = g_object_new (G_TYPE_OBJECT, NULL); + g_object_set_qdata (objects[i], number_quark, GUINT_TO_POINTER (numbers[i])); + } + + g_list_store_splice (store, pos, removed, (gpointer *) objects, added); + + for (i = 0; i < added; i++) + g_object_unref (objects[i]); +} +#endif + +static void +add (GListStore *store, + guint number) +{ + GObject *object; + + /* 0 cannot be differentiated from NULL, so don't use it */ + g_assert (number != 0); + + object = g_object_new (G_TYPE_OBJECT, NULL); + g_object_set_qdata (object, number_quark, GUINT_TO_POINTER (number)); + g_list_store_append (store, object); + g_object_unref (object); +} + +#define assert_model(model, expected) G_STMT_START{ \ + char *s = model_to_string (G_LIST_MODEL (model)); \ + if (!g_str_equal (s, expected)) \ + g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + #model " == " #expected, s, "==", expected); \ + g_free (s); \ +}G_STMT_END + +#define assert_changes(model, expected) G_STMT_START{ \ + GString *changes = g_object_get_qdata (G_OBJECT (model), changes_quark); \ + if (!g_str_equal (changes->str, expected)) \ + g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + #model " == " #expected, changes->str, "==", expected); \ + g_string_set_size (changes, 0); \ +}G_STMT_END + +static GListStore * +new_empty_store (void) +{ + return g_list_store_new (G_TYPE_OBJECT); +} + +static GListStore * +new_store (guint start, + guint end, + guint step) +{ + GListStore *store = new_empty_store (); + guint i; + + for (i = start; i <= end; i += step) + add (store, i); + + return store; +} + +static void +items_changed (GListModel *model, + guint position, + guint removed, + guint added, + GString *changes) +{ + g_assert (removed != 0 || added != 0); + + if (changes->len) + g_string_append (changes, ", "); + + if (removed == 1 && added == 0) + { + g_string_append_printf (changes, "-%u", position); + } + else if (removed == 0 && added == 1) + { + g_string_append_printf (changes, "+%u", position); + } + else + { + g_string_append_printf (changes, "%u", position); + if (removed > 0) + g_string_append_printf (changes, "-%u", removed); + if (added > 0) + g_string_append_printf (changes, "+%u", added); + } +} + +static void +free_changes (gpointer data) +{ + GString *changes = data; + + /* all changes must have been checked via assert_changes() before */ + g_assert_cmpstr (changes->str, ==, ""); + + g_string_free (changes, TRUE); +} + +static gpointer +map_multiply (gpointer item, + gpointer factor) +{ + GObject *object; + guint num; + + object = g_object_new (G_TYPE_OBJECT, NULL); + num = GPOINTER_TO_UINT (g_object_get_qdata (item, number_quark)); + g_object_set_qdata (object, number_quark, GUINT_TO_POINTER (GPOINTER_TO_UINT (factor) * num)); + g_object_unref (item); + + return object; +} + +static GtkMapListModel * +new_model (GListStore *store) +{ + GtkMapListModel *result; + GString *changes; + + result = gtk_map_list_model_new (G_TYPE_OBJECT, G_LIST_MODEL (store), map_multiply, GUINT_TO_POINTER (2), NULL); + changes = g_string_new (""); + g_object_set_qdata_full (G_OBJECT(result), changes_quark, changes, free_changes); + g_signal_connect (result, "items-changed", G_CALLBACK (items_changed), changes); + + return result; +} + +static void +test_create_empty (void) +{ + GtkMapListModel *map; + + map = new_model (NULL); + assert_model (map, ""); + assert_changes (map, ""); + + g_object_unref (map); +} + +static void +test_create (void) +{ + GtkMapListModel *map; + GListStore *store; + + store = new_store (1, 5, 1); + map = new_model (store); + assert_model (map, "2 4 6 8 10"); + assert_changes (map, ""); + + g_object_unref (store); + assert_model (map, "2 4 6 8 10"); + assert_changes (map, ""); + + g_object_unref (map); +} + +static void +test_set_model (void) +{ + GtkMapListModel *map; + GListStore *store; + + map = new_model (NULL); + assert_model (map, ""); + assert_changes (map, ""); + + store = new_store (1, 5, 1); + gtk_map_list_model_set_model (map, G_LIST_MODEL (store)); + assert_model (map, "2 4 6 8 10"); + assert_changes (map, "0+5"); + + gtk_map_list_model_set_model (map, NULL); + assert_model (map, ""); + assert_changes (map, "0-5"); + + g_object_unref (store); + g_object_unref (map); +} + +static void +test_set_map_func (void) +{ + GtkMapListModel *map; + GListStore *store; + + store = new_store (1, 5, 1); + map = new_model (store); + assert_model (map, "2 4 6 8 10"); + assert_changes (map, ""); + + gtk_map_list_model_set_map_func (map, map_multiply, GUINT_TO_POINTER (3), NULL); + assert_model (map, "3 6 9 12 15"); + assert_changes (map, "0-5+5"); + + gtk_map_list_model_set_map_func (map, NULL, NULL, NULL); + assert_model (map, "1 2 3 4 5"); + assert_changes (map, "0-5+5"); + + gtk_map_list_model_set_map_func (map, map_multiply, GUINT_TO_POINTER (2), NULL); + assert_model (map, "2 4 6 8 10"); + assert_changes (map, "0-5+5"); + + g_object_unref (store); + g_object_unref (map); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + setlocale (LC_ALL, "C"); + g_test_bug_base ("http://bugzilla.gnome.org/show_bug.cgi?id=%s"); + + number_quark = g_quark_from_static_string ("Hell and fire was spawned to be released."); + changes_quark = g_quark_from_static_string ("What did I see? Can I believe what I saw?"); + + g_test_add_func ("/maplistmodel/create_empty", test_create_empty); + g_test_add_func ("/maplistmodel/create", test_create); + g_test_add_func ("/maplistmodel/set-model", test_set_model); + g_test_add_func ("/maplistmodel/set-map-func", test_set_map_func); + + return g_test_run (); +} diff --git a/testsuite/gtk/meson.build b/testsuite/gtk/meson.build index e8171ea571..2323a158f6 100644 --- a/testsuite/gtk/meson.build +++ b/testsuite/gtk/meson.build @@ -31,6 +31,7 @@ tests = [ ['keyhash', ['../../gtk/gtkkeyhash.c', gtkresources, '../../gtk/gtkprivate.c'], gtk_cargs], ['listbox'], ['main'], + ['maplistmodel'], ['notify'], ['no-gtk-init'], ['object'],