gtk2/gtk/gtkmaplistmodel.c
Benjamin Otte 5080730728 listmodels: Stop respecting item-type
Simplify all view model APIs and always return G_TYPE_OBJECT as the
item-type for every model.

It turns out nobody uses item-type anyway.

So instead of adding lots of APIs, forcing people to think about it and
trying to figure out how to handle filter or map models that modify item
types, just having an easy life is a better approach.

All the models need to be able to deal with any type of object going
through anyway.
2020-07-05 02:59:21 +02:00

619 lines
16 KiB
C

/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include "config.h"
#include "gtkmaplistmodel.h"
#include "gtkrbtreeprivate.h"
#include "gtkintl.h"
#include "gtkprivate.h"
/**
* SECTION:gtkmaplistmodel
* @title: GtkMapListModel
* @short_description: A list model that transforms its items
* @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.
*
* Example: Create a list of #GtkEventControllers
* |[
* 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:
*
* 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:
*
* 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: (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);
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:
* @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 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:
* @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;
}