/* * 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 "gtkrbtreeprivate.h" #include "gtkintl.h" #include "gtkprivate.h" /** * GtkMapListModel: * * A `GtkMapListModel` maps the items in a list model to different items. * * `GtkMapListModel` uses a [callback@Gtk.MapListModelMapFunc]. * * Example: Create a list of `GtkEventControllers` * ```c * static gpointer * map_to_controllers (gpointer widget, * gpointer data) * { * gpointer result = gtk_widget_observe_controllers (widget); * g_object_unref (widget); * return result; * } * * widgets = gtk_widget_observe_children (widget); * * controllers = gtk_map_list_model_new (G_TYPE_LIST_MODEL, * widgets, * map_to_controllers, * NULL, NULL); * * model = gtk_flatten_list_model_new (GTK_TYPE_EVENT_CONTROLLER, * controllers); * ``` * * `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_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; GListModel *model; GtkMapListModelMapFunc map_func; gpointer user_data; GDestroyNotify user_destroy; GtkRbTree *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 (GtkRbTree *tree, guint position, guint *out_start_pos) { MapNode *node, *tmp; guint start_pos = position; node = gtk_rb_tree_get_root (tree); while (node) { tmp = gtk_rb_tree_node_get_left (node); if (tmp) { MapAugment *aug = gtk_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_rb_tree_node_get_right (node); } if (out_start_pos) *out_start_pos = start_pos; return node; } static GType gtk_map_list_model_get_item_type (GListModel *list) { return G_TYPE_OBJECT; } 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_rb_tree_insert_before (self->items, node); before->n_items = position - offset; node->n_items -= before->n_items; gtk_rb_tree_node_mark_dirty (node); } if (node->n_items > 1) { MapNode *after = gtk_rb_tree_insert_after (self->items, node); after->n_items = node->n_items - 1; node->n_items = 1; gtk_rb_tree_node_mark_dirty (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_rb_tree_node_get_next (node); removed -= node->n_items; gtk_rb_tree_remove (self->items, node); node = next; } else { if (end >= position + removed) { node->n_items -= removed; removed = 0; gtk_rb_tree_node_mark_dirty (node); } else if (start < position) { guint overlap = node->n_items - (position - start); node->n_items -= overlap; gtk_rb_tree_node_mark_dirty (node); removed -= overlap; start = position; node = gtk_rb_tree_node_get_next (node); } } } if (added) { if (node == NULL) node = gtk_rb_tree_insert_before (self->items, NULL); else if (node->item) node = gtk_rb_tree_insert_after (self->items, node); node->n_items += added; gtk_rb_tree_node_mark_dirty (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_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_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_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: (attributes org.gtk.Property.get=gtk_map_list_model_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:model: (attributes org.gtk.Property.get=gtk_map_list_model_get_model org.gtk.Property.set=gtk_map_list_model_set_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 (GtkRbTree *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_rb_tree_get_augment (map, left); aug->n_items += left_aug->n_items; } if (right) { MapAugment *right_aug = gtk_rb_tree_get_augment (map, right); aug->n_items += right_aug->n_items; } } /** * gtk_map_list_model_new: * @model: (transfer full) (allow-none): The model to map or %NULL for none * @map_func: (allow-none): map function or %NULL to not map items * @user_data: (closure): user data passed to @map_func * @user_destroy: destroy notifier for @user_data * * Creates a new `GtkMapListModel` for the given arguments. * * Returns: a new `GtkMapListModel` */ GtkMapListModel * gtk_map_list_model_new (GListModel *model, GtkMapListModelMapFunc map_func, gpointer user_data, GDestroyNotify user_destroy) { GtkMapListModel *result; g_return_val_if_fail (model == NULL || G_IS_LIST_MODEL (model), NULL); result = g_object_new (GTK_TYPE_MAP_LIST_MODEL, "model", model, NULL); /* consume the reference */ g_clear_object (&model); if (map_func) gtk_map_list_model_set_map_func (result, map_func, user_data, user_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_rb_tree_remove_all (self->items); } else { self->items = gtk_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_rb_tree_insert_before (self->items, NULL); node->n_items = g_list_model_get_n_items (self->model); gtk_rb_tree_node_mark_dirty (node); } } else { g_clear_pointer (&self->items, gtk_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: (closure): user data passed to @map_func * @user_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: (attributes org.gtk.Method.set_property=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: (attributes org.gtk.Method.get_property=model) * @self: a `GtkMapListModel` * * Gets the model that is currently 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: (attributes org.gtk.Method.get_property=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; }