/* gtd-list-model-sort.c * * Copyright 2018 Georges Basile Stavracas Neto * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "GtdListModelSort" #include "gtd-debug.h" #include "gtd-list-model-sort.h" #include "gtd-task.h" #include "gtd-task-list.h" struct _GtdListModelSort { GObject parent; GListModel *child_model; GSequence *child_seq; GSequence *sort_seq; GtdListModelCompareFunc compare_func; gpointer compare_func_data; GDestroyNotify compare_func_data_destroy; gboolean supress_items_changed : 1; /* cache */ gint64 length; gint64 last_position; GSequenceIter *last_iter; }; static void list_model_iface_init (GListModelInterface *iface); G_DEFINE_TYPE_WITH_CODE (GtdListModelSort, gtd_list_model_sort, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) enum { PROP_0, PROP_CHILD_MODEL, N_PROPS }; static GParamSpec *properties [N_PROPS]; static void gtd_list_model_sort_item_free (gpointer data) { GSequenceIter *iter = data; g_clear_pointer (&iter, g_sequence_remove); } static gint seq_compare_func (gconstpointer a, gconstpointer b, gpointer user_data) { GtdListModelSort *self = (GtdListModelSort*) user_data; return self->compare_func (G_OBJECT (a), G_OBJECT (b), self->compare_func_data); } static gint default_compare_func (GObject *a, GObject *b, gpointer user_data) { return 0; } static void invalidate_cache (GtdListModelSort *self) { GTD_TRACE_MSG ("Invalidating cache"); self->last_iter = NULL; self->last_position = -1; } static void emit_items_changed (GtdListModelSort *self, guint position, guint n_removed, guint n_added) { if (position <= self->last_position) invalidate_cache (self); self->length -= n_removed; self->length += n_added; GTD_TRACE_MSG ("Emitting items-changed(%u, %u, %u)", position, n_removed, n_added); g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added); } static void child_model_items_changed (GtdListModelSort *self, guint position, guint n_removed, guint n_added, GListModel *child_model) { guint i; GTD_ENTRY; g_assert (GTD_IS_LIST_MODEL_SORT (self)); g_assert (G_IS_LIST_MODEL (child_model)); g_assert (self->child_model == child_model); g_assert (position <= (guint)g_sequence_get_length (self->child_seq)); g_assert (g_sequence_get_length (self->child_seq) - n_removed + n_added == g_list_model_get_n_items (child_model)); GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added); if (n_removed > 0) { GSequenceIter *iter; gint64 previous_sort_position = -1; gint64 first_position = -1; gint64 counter = 0; /* Small shortcut when all items are removed */ if (n_removed == (guint)g_sequence_get_length (self->child_seq)) { g_sequence_remove_range (g_sequence_get_begin_iter (self->child_seq), g_sequence_get_end_iter (self->child_seq)); g_assert (g_sequence_is_empty (self->child_seq)); g_assert (g_sequence_is_empty (self->sort_seq)); emit_items_changed (self, 0, n_removed, 0); goto add_new_items; } iter = g_sequence_get_iter_at_pos (self->child_seq, position); g_assert (!g_sequence_iter_is_end (iter)); for (i = 0; i < n_removed; i++) { GSequenceIter *sort_iter = g_sequence_get (iter); GSequenceIter *to_remove = iter; guint sort_position; g_assert (g_sequence_iter_get_sequence (sort_iter) == self->sort_seq); sort_position = g_sequence_iter_get_position (sort_iter); /* Fetch the next while the iter is still valid */ iter = g_sequence_iter_next (iter); /* Cascades into also removing from sort_seq. */ g_sequence_remove (to_remove); if (first_position == -1) first_position = sort_position; /* * This happens when the position in the sorted sequence is different * from the position in the child sequence. We try to accumulate as many * items-changed() signals as possible, but we can't do that when the * order doesn't match. */ if (previous_sort_position != -1 && sort_position != previous_sort_position + 1) { emit_items_changed (self, first_position, counter, 0); first_position = sort_position; counter = 0; } previous_sort_position = sort_position; counter++; } /* * Last items-changed() - if the child model is already sorted, * only this one will be fired. */ if (counter > 0) emit_items_changed (self, first_position, counter, 0); } add_new_items: if (n_added > 0) { GSequenceIter *iter = g_sequence_get_iter_at_pos (self->child_seq, position); gint64 previous_sort_position = -1; gint64 first_position = -1; gint64 counter = 0; for (i = 0; i < n_added; i++) { g_autoptr (GObject) instance = NULL; GSequenceIter *sort_iter; guint new_sort_position; instance = g_list_model_get_item (child_model, position + i); g_assert (G_IS_OBJECT (instance)); sort_iter = g_sequence_insert_sorted (self->sort_seq, g_steal_pointer (&instance), seq_compare_func, self); new_sort_position = g_sequence_iter_get_position (sort_iter); /* Insert next item before this */ iter = g_sequence_insert_before (iter, sort_iter); iter = g_sequence_iter_next (iter); if (first_position == -1) first_position = new_sort_position; /* * This happens when the position in the sorted sequence is different * from the position in the child sequence. We try to accumulate as many * items-changed() signals as possible, but we can't do that when the * order doesn't match. */ if (previous_sort_position != -1 && new_sort_position != previous_sort_position + 1) { emit_items_changed (self, first_position, 0, counter); first_position = new_sort_position; counter = 0; } previous_sort_position = new_sort_position; counter++; } /* * Last items-changed() - if the child model is already sorted, * only this one will be fired. */ if (counter > 0) emit_items_changed (self, first_position, 0, counter); } g_assert ((guint)g_sequence_get_length (self->child_seq) == g_list_model_get_n_items (child_model)); g_assert ((guint)g_sequence_get_length (self->sort_seq) == g_list_model_get_n_items (child_model)); GTD_EXIT; } /* * GListModel iface */ static GType gtd_list_model_sort_get_item_type (GListModel *model) { GtdListModelSort *self = (GtdListModelSort*) model; g_assert (GTD_IS_LIST_MODEL_SORT (self)); return g_list_model_get_item_type (self->child_model); } static guint gtd_list_model_sort_get_n_items (GListModel *model) { GtdListModelSort *self = (GtdListModelSort*) model; g_assert (GTD_IS_LIST_MODEL_SORT (self)); g_assert (self->sort_seq != NULL); return self->length; } static gpointer gtd_list_model_sort_get_item (GListModel *model, guint position) { GtdListModelSort *self; GSequenceIter *iter; gpointer item; g_assert (GTD_IS_LIST_MODEL_SORT (model)); self = (GtdListModelSort*) model; iter = NULL; if (self->last_position != -1) { if (self->last_position == position + 1) iter = g_sequence_iter_prev (self->last_iter); else if (self->last_position == position - 1) iter = g_sequence_iter_next (self->last_iter); else if (self->last_position == position) iter = self->last_iter; } if (!iter) iter = g_sequence_get_iter_at_pos (self->sort_seq, position); if (g_sequence_iter_is_end (iter)) return NULL; self->last_iter = iter; self->last_position = position; item = g_sequence_get (iter); g_assert (item != NULL); return g_object_ref (G_OBJECT (item)); } static void list_model_iface_init (GListModelInterface *iface) { iface->get_item_type = gtd_list_model_sort_get_item_type; iface->get_n_items = gtd_list_model_sort_get_n_items; iface->get_item = gtd_list_model_sort_get_item; } /* * GObject overrides */ static void gtd_list_model_sort_finalize (GObject *object) { GtdListModelSort *self = (GtdListModelSort*) object; g_clear_pointer (&self->child_seq, g_sequence_free); g_clear_pointer (&self->sort_seq, g_sequence_free); if (self->compare_func_data_destroy) { g_clear_pointer (&self->compare_func_data, self->compare_func_data_destroy); self->compare_func_data_destroy = NULL; } g_clear_object (&self->child_model); G_OBJECT_CLASS (gtd_list_model_sort_parent_class)->finalize (object); } static void gtd_list_model_sort_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtdListModelSort *self = GTD_LIST_MODEL_SORT (object); switch (prop_id) { case PROP_CHILD_MODEL: g_value_set_object (value, gtd_list_model_sort_get_child_model (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtd_list_model_sort_class_init (GtdListModelSortClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gtd_list_model_sort_finalize; object_class->get_property = gtd_list_model_sort_get_property; properties [PROP_CHILD_MODEL] = g_param_spec_object ("child-model", "Child Model", "The child model being sorted.", G_TYPE_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, N_PROPS, properties); } static void gtd_list_model_sort_init (GtdListModelSort *self) { self->compare_func = default_compare_func; self->child_seq = g_sequence_new (gtd_list_model_sort_item_free); self->sort_seq = g_sequence_new (g_object_unref); self->last_position = -1; } GtdListModelSort * gtd_list_model_sort_new (GListModel *child_model) { GtdListModelSort *self; g_return_val_if_fail (G_IS_LIST_MODEL (child_model), NULL); self = g_object_new (GTD_TYPE_LIST_MODEL_SORT, NULL); self->child_model = g_object_ref (child_model); g_signal_connect_object (child_model, "items-changed", G_CALLBACK (child_model_items_changed), self, G_CONNECT_SWAPPED); child_model_items_changed (self, 0, 0, g_list_model_get_n_items (child_model), child_model); return self; } /** * gtd_list_model_sort_get_child_model: * @self: A #GtdListModelSort * * Gets the child model that is being sorted. * * Returns: (transfer none): A #GListModel. */ GListModel * gtd_list_model_sort_get_child_model (GtdListModelSort *self) { g_return_val_if_fail (GTD_IS_LIST_MODEL_SORT (self), NULL); return self->child_model; } void gtd_list_model_sort_invalidate (GtdListModelSort *self) { guint n_items; g_return_if_fail (GTD_IS_LIST_MODEL_SORT (self)); /* First determine how many items we need to synthesize as a removal */ n_items = g_sequence_get_length (self->child_seq); g_assert (G_IS_LIST_MODEL (self->child_model)); invalidate_cache (self); g_sequence_sort (self->sort_seq, seq_compare_func, self); g_assert ((guint)g_sequence_get_length (self->child_seq) == n_items); g_assert ((guint)g_sequence_get_length (self->sort_seq) == n_items); if (n_items > 0) emit_items_changed (self, 0, n_items, n_items); } void gtd_list_model_sort_set_sort_func (GtdListModelSort *self, GtdListModelCompareFunc compare_func, gpointer compare_func_data, GDestroyNotify compare_func_data_destroy) { g_return_if_fail (GTD_IS_LIST_MODEL_SORT (self)); g_return_if_fail (compare_func || (!compare_func_data && !compare_func_data_destroy)); GTD_ENTRY; if (self->compare_func_data_destroy != NULL) g_clear_pointer (&self->compare_func_data, self->compare_func_data_destroy); if (compare_func != NULL) { self->compare_func = compare_func; self->compare_func_data = compare_func_data; self->compare_func_data_destroy = compare_func_data_destroy; } else { self->compare_func = default_compare_func; self->compare_func_data = NULL; self->compare_func_data_destroy = NULL; } gtd_list_model_sort_invalidate (self); GTD_EXIT; }