summaryrefslogtreecommitdiff
path: root/src/models
diff options
context:
space:
mode:
authorMatthew Fennell <matthew@fennell.dev>2025-12-27 12:40:20 +0000
committerMatthew Fennell <matthew@fennell.dev>2025-12-27 12:40:20 +0000
commit5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 (patch)
treeed28aefed8add0da1c55c08fdf80b23c4346e0dc /src/models
Import Upstream version 43.0upstream/latest
Diffstat (limited to 'src/models')
-rw-r--r--src/models/gtd-list-model-filter.c569
-rw-r--r--src/models/gtd-list-model-filter.h44
-rw-r--r--src/models/gtd-list-model-sort.c500
-rw-r--r--src/models/gtd-list-model-sort.h46
-rw-r--r--src/models/gtd-list-store.c567
-rw-r--r--src/models/gtd-list-store.h66
-rw-r--r--src/models/gtd-task-list-view-model.c216
-rw-r--r--src/models/gtd-task-list-view-model.h43
-rw-r--r--src/models/gtd-task-model-private.h29
-rw-r--r--src/models/gtd-task-model.c216
-rw-r--r--src/models/gtd-task-model.h33
11 files changed, 2329 insertions, 0 deletions
diff --git a/src/models/gtd-list-model-filter.c b/src/models/gtd-list-model-filter.c
new file mode 100644
index 0000000..632c227
--- /dev/null
+++ b/src/models/gtd-list-model-filter.c
@@ -0,0 +1,569 @@
+/* gtd-list-model-filter.c
+ *
+ * Copyright (C) 2016 Christian Hergert <christian@hergert.me>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "GtdListModelFilter"
+
+#include "gtd-debug.h"
+#include "gtd-list-model-filter.h"
+#include "gtd-task-list.h"
+
+typedef struct
+{
+ GSequenceIter *child_iter;
+ GSequenceIter *filter_iter;
+} GtdListModelFilterItem;
+
+typedef struct
+{
+ /* The list we are filtering */
+ GListModel *child_model;
+
+ /*
+ * Both sequences point to the same GtdListModelFilterItem which
+ * contains cross-referencing stable GSequenceIter pointers.
+ * The child_seq is considered the "owner" and used to release
+ * allocated resources.
+ */
+ GSequence *child_seq;
+ GSequence *filter_seq;
+
+ /*
+ * Typical set of callback/closure/free function pointers and data.
+ * Called for child items to determine visibility state.
+ */
+ GtdListModelFilterFunc filter_func;
+ gpointer filter_func_data;
+ GDestroyNotify filter_func_data_destroy;
+
+ /* cache */
+ gint64 length;
+ gint64 last_position;
+ GSequenceIter *last_iter;
+
+ /*
+ * If set, we will not emit items-changed. This is useful during
+ * invalidation so that we can do a single emission for all items
+ * that have changed.
+ */
+ gboolean supress_items_changed : 1;
+} GtdListModelFilterPrivate;
+
+struct _GtdListModelFilter
+{
+ GObject parent_instance;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GtdListModelFilter, gtd_list_model_filter, G_TYPE_OBJECT, 0,
+ G_ADD_PRIVATE (GtdListModelFilter)
+ 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 guint signal_id;
+
+static void
+gtd_list_model_filter_item_free (gpointer data)
+{
+ GtdListModelFilterItem *item = data;
+
+ g_clear_pointer (&item->filter_iter, g_sequence_remove);
+ item->child_iter = NULL;
+ g_slice_free (GtdListModelFilterItem, item);
+}
+
+static gboolean
+gtd_list_model_filter_default_filter_func (GObject *item,
+ gpointer user_data)
+{
+ return TRUE;
+}
+
+/*
+ * Locates the next item in the filter sequence starting from
+ * the cross-reference found at @iter. If none are found, the
+ * end_iter for the filter sequence is returned.
+ *
+ * This returns an iter in the filter_sequence, not the child_seq.
+ *
+ * Returns: a #GSequenceIter from the filter sequence.
+ */
+static GSequenceIter *
+find_next_visible_filter_iter (GtdListModelFilter *self,
+ GSequenceIter *iter)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+ g_assert (iter != NULL);
+
+ for (; !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter))
+ {
+ GtdListModelFilterItem *item = g_sequence_get (iter);
+
+ g_assert (item->child_iter == iter);
+ g_assert (item->filter_iter == NULL ||
+ g_sequence_iter_get_sequence (item->filter_iter) == priv->filter_seq);
+
+ if (item->filter_iter != NULL)
+ return item->filter_iter;
+ }
+
+ return g_sequence_get_end_iter (priv->filter_seq);
+}
+
+static void
+invalidate_cache (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ GTD_TRACE_MSG ("Invalidating cache");
+
+ priv->last_iter = NULL;
+ priv->last_position = -1u;
+}
+
+static void
+emit_items_changed (GtdListModelFilter *self,
+ guint position,
+ guint n_removed,
+ guint n_added)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ if (position <= priv->last_position)
+ invalidate_cache (self);
+
+ priv->length -= n_removed;
+ priv->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 (GtdListModelFilter *self,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GListModel *child_model)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+ gboolean unblocked;
+ guint i;
+
+ GTD_ENTRY;
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+ g_assert (G_IS_LIST_MODEL (child_model));
+ g_assert (priv->child_model == child_model);
+ g_assert (position <= (guint)g_sequence_get_length (priv->child_seq));
+ g_assert ((g_sequence_get_length (priv->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);
+
+ unblocked = !priv->supress_items_changed;
+
+ if (n_removed > 0)
+ {
+ GSequenceIter *iter = g_sequence_get_iter_at_pos (priv->child_seq, position);
+ gint64 first_position = -1;
+ guint count = 0;
+
+ g_assert (!g_sequence_iter_is_end (iter));
+
+ /* Small shortcut when all items are removed */
+ if (n_removed == (guint)g_sequence_get_length (priv->child_seq))
+ {
+ g_sequence_remove_range (g_sequence_get_begin_iter (priv->child_seq),
+ g_sequence_get_end_iter (priv->child_seq));
+ g_assert (g_sequence_is_empty (priv->child_seq));
+ g_assert (g_sequence_is_empty (priv->filter_seq));
+
+ if (unblocked)
+ emit_items_changed (self, 0, priv->length, 0);
+
+ GTD_TRACE_MSG ("Removed all items");
+
+ GTD_GOTO (add_new_items);
+ }
+
+ for (i = 0; i < n_removed; i++)
+ {
+ GSequenceIter *to_remove = iter;
+ GtdListModelFilterItem *item = g_sequence_get (iter);
+
+ g_assert (item != NULL);
+ g_assert (item->child_iter == iter);
+ g_assert (item->filter_iter == NULL ||
+ g_sequence_iter_get_sequence (item->filter_iter) == priv->filter_seq);
+
+ /* If this is visible, we need to notify about removal */
+ if (unblocked && item->filter_iter != NULL)
+ {
+ if (first_position < 0)
+ first_position = g_sequence_iter_get_position (item->filter_iter);
+
+ count++;
+ }
+
+ /* Fetch the next while the iter is still valid */
+ iter = g_sequence_iter_next (iter);
+
+ /* Cascades into also removing from filter_seq. */
+ g_sequence_remove (to_remove);
+ }
+
+ GTD_TRACE_MSG ("Removed %u items", count);
+
+ if (unblocked && first_position >= 0)
+ emit_items_changed (self, first_position, count, 0);
+ }
+
+add_new_items:
+
+ if (n_added > 0)
+ {
+ GSequenceIter *iter = g_sequence_get_iter_at_pos (priv->child_seq, position);
+ GSequenceIter *filter_iter = find_next_visible_filter_iter (self, iter);
+ guint filter_position = g_sequence_iter_get_position (filter_iter);
+ guint count = 0;
+
+ /* Walk backwards to insert items into the filter list so that
+ * we can use the same filter_position for each items-changed
+ * signal emission.
+ */
+ for (i = position + n_added; i > position; i--)
+ {
+ GtdListModelFilterItem *item;
+ g_autoptr (GObject) instance = NULL;
+
+ item = g_slice_new0 (GtdListModelFilterItem);
+ item->filter_iter = NULL;
+ item->child_iter = g_sequence_insert_before (iter, item);
+
+ instance = g_list_model_get_item (child_model, i - 1);
+ g_assert (G_IS_OBJECT (instance));
+
+ /* Check if this item is visible */
+ if (priv->filter_func (instance, priv->filter_func_data))
+ {
+ item->filter_iter = g_sequence_insert_before (filter_iter, item);
+ count++;
+
+ /* Use this in the future for relative positioning */
+ filter_iter = item->filter_iter;
+ }
+
+ /* Insert next item before this */
+ iter = item->child_iter;
+ }
+
+ GTD_TRACE_MSG ("Added %u items (%u were filtered)", count, n_added - count);
+
+ if (unblocked && count)
+ emit_items_changed (self, filter_position, 0, count);
+ }
+
+ g_assert ((guint)g_sequence_get_length (priv->child_seq) == g_list_model_get_n_items (child_model));
+
+ GTD_EXIT;
+}
+
+static void
+gtd_list_model_filter_finalize (GObject *object)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)object;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_clear_pointer (&priv->child_seq, g_sequence_free);
+ g_clear_pointer (&priv->filter_seq, g_sequence_free);
+
+ if (priv->filter_func_data_destroy)
+ {
+ g_clear_pointer (&priv->filter_func_data, priv->filter_func_data_destroy);
+ priv->filter_func_data_destroy = NULL;
+ }
+
+ g_clear_object (&priv->child_model);
+
+ G_OBJECT_CLASS (gtd_list_model_filter_parent_class)->finalize (object);
+}
+
+static void
+gtd_list_model_filter_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListModelFilter *self = GTD_LIST_MODEL_FILTER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CHILD_MODEL:
+ g_value_set_object (value, gtd_list_model_filter_get_child_model (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_list_model_filter_class_init (GtdListModelFilterClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_list_model_filter_finalize;
+ object_class->get_property = gtd_list_model_filter_get_property;
+
+ properties [PROP_CHILD_MODEL] =
+ g_param_spec_object ("child-model",
+ "Child Model",
+ "The child model being filtered.",
+ G_TYPE_LIST_MODEL,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ signal_id = g_signal_lookup ("items-changed", GTD_TYPE_LIST_MODEL_FILTER);
+}
+
+static void
+gtd_list_model_filter_init (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ priv->filter_func = gtd_list_model_filter_default_filter_func;
+ priv->child_seq = g_sequence_new (gtd_list_model_filter_item_free);
+ priv->filter_seq = g_sequence_new (NULL);
+ priv->last_position = -1;
+}
+
+static GType
+gtd_list_model_filter_get_item_type (GListModel *model)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)model;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+
+ return g_list_model_get_item_type (priv->child_model);
+}
+
+static guint
+gtd_list_model_filter_get_n_items (GListModel *model)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)model;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+ g_assert (priv->filter_seq != NULL);
+
+ return priv->length;
+}
+
+static gpointer
+gtd_list_model_filter_get_item (GListModel *model,
+ guint position)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)model;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+ GtdListModelFilterItem *item;
+ GSequenceIter *iter;
+ guint child_position;
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+
+ iter = NULL;
+
+ if (priv->last_position != -1)
+ {
+ if (priv->last_position == position + 1)
+ iter = g_sequence_iter_prev (priv->last_iter);
+ else if (priv->last_position == position - 1)
+ iter = g_sequence_iter_next (priv->last_iter);
+ else if (priv->last_position == position)
+ iter = priv->last_iter;
+ }
+
+ if (!iter)
+ iter = g_sequence_get_iter_at_pos (priv->filter_seq, position);
+
+ if (g_sequence_iter_is_end (iter))
+ return NULL;
+
+ item = g_sequence_get (iter);
+ g_assert (item != NULL);
+ g_assert (item->filter_iter == iter);
+ g_assert (item->child_iter != NULL);
+ g_assert (g_sequence_iter_get_sequence (item->child_iter) == priv->child_seq);
+
+ child_position = g_sequence_iter_get_position (item->child_iter);
+
+ return g_list_model_get_item (priv->child_model, child_position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtd_list_model_filter_get_item_type;
+ iface->get_n_items = gtd_list_model_filter_get_n_items;
+ iface->get_item = gtd_list_model_filter_get_item;
+}
+
+GtdListModelFilter *
+gtd_list_model_filter_new (GListModel *child_model)
+{
+ GtdListModelFilter *ret;
+ GtdListModelFilterPrivate *priv;
+
+ g_return_val_if_fail (G_IS_LIST_MODEL (child_model), NULL);
+
+ ret = g_object_new (GTD_TYPE_LIST_MODEL_FILTER, NULL);
+ priv = gtd_list_model_filter_get_instance_private (ret);
+ priv->child_model = g_object_ref (child_model);
+
+ g_signal_connect_object (child_model,
+ "items-changed",
+ G_CALLBACK (child_model_items_changed),
+ ret,
+ G_CONNECT_SWAPPED);
+
+ gtd_list_model_filter_invalidate (ret);
+
+ return ret;
+}
+
+/**
+ * gtd_list_model_filter_get_child_model:
+ * @self: A #GtdListModelFilter
+ *
+ * Gets the child model that is being filtered.
+ *
+ * Returns: (transfer none): A #GListModel.
+ */
+GListModel *
+gtd_list_model_filter_get_child_model (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_LIST_MODEL_FILTER (self), NULL);
+
+ return priv->child_model;
+}
+
+void
+gtd_list_model_filter_invalidate (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+ guint n_items;
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_LIST_MODEL_FILTER (self));
+
+ /* We block emission while in invalidate so that we can use
+ * a single larger items-changed rather lots of small emissions.
+ */
+ priv->supress_items_changed = TRUE;
+
+ /* First determine how many items we need to synthesize as a removal */
+ n_items = g_sequence_get_length (priv->filter_seq);
+
+ /*
+ * If we have a child store, we want to rebuild our list of items
+ * from scratch, so just remove everything.
+ */
+ if (!g_sequence_is_empty (priv->child_seq))
+ g_sequence_remove_range (g_sequence_get_begin_iter (priv->child_seq),
+ g_sequence_get_end_iter (priv->child_seq));
+
+ g_assert (g_sequence_is_empty (priv->child_seq));
+ g_assert (g_sequence_is_empty (priv->filter_seq));
+ g_assert (!priv->child_model || G_IS_LIST_MODEL (priv->child_model));
+
+ /*
+ * Now add the new items by synthesizing the addition of all the
+ * items in the list.
+ */
+ if (priv->child_model != NULL)
+ {
+ guint child_n_items;
+
+ /*
+ * Now add all the items as one shot to our list so that
+ * we get populate our sequence and filter sequence.
+ */
+ child_n_items = g_list_model_get_n_items (priv->child_model);
+ child_model_items_changed (self, 0, 0, child_n_items, priv->child_model);
+
+ g_assert ((guint)g_sequence_get_length (priv->child_seq) == child_n_items);
+ g_assert ((guint)g_sequence_get_length (priv->filter_seq) <= child_n_items);
+ }
+
+ priv->supress_items_changed = FALSE;
+
+ /* Now that we've updated our sequences, notify of all the changes
+ * as a single series of updates to the consumers.
+ */
+ if (n_items > 0 || !g_sequence_is_empty (priv->filter_seq))
+ emit_items_changed (self, 0, n_items, g_sequence_get_length (priv->filter_seq));
+
+ GTD_EXIT;
+}
+
+void
+gtd_list_model_filter_set_filter_func (GtdListModelFilter *self,
+ GtdListModelFilterFunc filter_func,
+ gpointer filter_func_data,
+ GDestroyNotify filter_func_data_destroy)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_LIST_MODEL_FILTER (self));
+ g_return_if_fail (filter_func || (!filter_func_data && !filter_func_data_destroy));
+
+ if (priv->filter_func_data_destroy != NULL)
+ g_clear_pointer (&priv->filter_func_data, priv->filter_func_data_destroy);
+
+ if (filter_func != NULL)
+ {
+ priv->filter_func = filter_func;
+ priv->filter_func_data = filter_func_data;
+ priv->filter_func_data_destroy = filter_func_data_destroy;
+ }
+ else
+ {
+ priv->filter_func = gtd_list_model_filter_default_filter_func;
+ priv->filter_func_data = NULL;
+ priv->filter_func_data_destroy = NULL;
+ }
+
+ gtd_list_model_filter_invalidate (self);
+}
diff --git a/src/models/gtd-list-model-filter.h b/src/models/gtd-list-model-filter.h
new file mode 100644
index 0000000..f5c2a42
--- /dev/null
+++ b/src/models/gtd-list-model-filter.h
@@ -0,0 +1,44 @@
+/* gtd-list-model-filter.h
+ *
+ * Copyright © 2016 Christian Hergert <christian@hergert.me>
+ * 2018 Georges Basile Stavracas Neto <gbsneto@gnome.org>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_LIST_MODEL_FILTER (gtd_list_model_filter_get_type())
+
+typedef gboolean (*GtdListModelFilterFunc) (GObject *object,
+ gpointer user_data);
+
+G_DECLARE_FINAL_TYPE (GtdListModelFilter, gtd_list_model_filter, GTD, LIST_MODEL_FILTER, GObject)
+
+GtdListModelFilter* gtd_list_model_filter_new (GListModel *child_model);
+
+GListModel* gtd_list_model_filter_get_child_model (GtdListModelFilter *self);
+
+void gtd_list_model_filter_invalidate (GtdListModelFilter *self);
+
+void gtd_list_model_filter_set_filter_func (GtdListModelFilter *self,
+ GtdListModelFilterFunc filter_func,
+ gpointer filter_func_data,
+ GDestroyNotify filter_func_data_destroy);
+
+G_END_DECLS
diff --git a/src/models/gtd-list-model-sort.c b/src/models/gtd-list-model-sort.c
new file mode 100644
index 0000000..dedbb6d
--- /dev/null
+++ b/src/models/gtd-list-model-sort.c
@@ -0,0 +1,500 @@
+/* gtd-list-model-sort.c
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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;
+}
diff --git a/src/models/gtd-list-model-sort.h b/src/models/gtd-list-model-sort.h
new file mode 100644
index 0000000..c95db29
--- /dev/null
+++ b/src/models/gtd-list-model-sort.h
@@ -0,0 +1,46 @@
+/* gtd-list-model-sort.h
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_LIST_MODEL_SORT (gtd_list_model_sort_get_type())
+
+typedef gboolean (*GtdListModelCompareFunc) (GObject *a,
+ GObject *b,
+ gpointer user_data);
+
+G_DECLARE_FINAL_TYPE (GtdListModelSort, gtd_list_model_sort, GTD, LIST_MODEL_SORT, GObject)
+
+GtdListModelSort* gtd_list_model_sort_new (GListModel *child_model);
+
+GListModel* gtd_list_model_sort_get_child_model (GtdListModelSort *self);
+
+void gtd_list_model_sort_invalidate (GtdListModelSort *self);
+
+void gtd_list_model_sort_set_sort_func (GtdListModelSort *self,
+ GtdListModelCompareFunc compare_func,
+ gpointer compare_func_data,
+ GDestroyNotify compare_func_data_destroy);
+
+G_END_DECLS
diff --git a/src/models/gtd-list-store.c b/src/models/gtd-list-store.c
new file mode 100644
index 0000000..bd30bcc
--- /dev/null
+++ b/src/models/gtd-list-store.c
@@ -0,0 +1,567 @@
+/* gtd-list-store.c
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GtdListStore"
+
+#include "gtd-list-store.h"
+
+#include <gio/gio.h>
+
+/**
+ * SECTION:gtdliststore
+ * @title: GtdListStore
+ * @short_description: A simple implementation of #GListModel
+ * @include: gio/gio.h
+ *
+ * #GtdListStore is a simple implementation of #GListModel that stores all
+ * items in memory.
+ *
+ * It provides insertions, deletions, and lookups in logarithmic time
+ * with a fast path for the common case of iterating the list linearly.
+ */
+
+/**
+ * GtdListStore:
+ *
+ * #GtdListStore is an opaque data structure and can only be accessed
+ * using the following functions.
+ **/
+
+struct _GtdListStore
+{
+ GObject parent;
+
+ GType item_type;
+ GSequence *items;
+
+ GHashTable *item_to_iter;
+
+ /* cache */
+ gint64 length;
+ gint64 last_position;
+ GSequenceIter *last_iter;
+};
+
+enum
+{
+ PROP_0,
+ PROP_ITEM_TYPE,
+ N_PROPERTIES
+};
+
+static void gtd_list_store_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdListStore, gtd_list_store, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtd_list_store_iface_init));
+
+static void
+remove_item_from_sequence_cb (gpointer item,
+ gpointer user_data)
+{
+ GSequenceIter *it;
+ GtdListStore *store;
+
+ store = (GtdListStore *)user_data;
+ it = g_hash_table_lookup (store->item_to_iter, item);
+
+ g_hash_table_remove (store->item_to_iter, item);
+ g_sequence_remove (it);
+}
+
+static void
+gtd_list_store_items_changed (GtdListStore *store,
+ guint position,
+ guint removed,
+ guint added)
+{
+ /* check if the iter cache may have been invalidated */
+ if (position <= store->last_position)
+ {
+ store->last_iter = NULL;
+ store->last_position = -1;
+ }
+
+ store->length -= removed;
+ store->length += added;
+
+ g_list_model_items_changed (G_LIST_MODEL (store), position, removed, added);
+}
+
+static void
+gtd_list_store_dispose (GObject *object)
+{
+ GtdListStore *store = GTD_LIST_STORE (object);
+
+ g_clear_pointer (&store->item_to_iter, g_hash_table_destroy);
+ g_clear_pointer (&store->items, g_sequence_free);
+
+ G_OBJECT_CLASS (gtd_list_store_parent_class)->dispose (object);
+}
+
+static void
+gtd_list_store_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListStore *store = GTD_LIST_STORE (object);
+
+ switch (property_id)
+ {
+ case PROP_ITEM_TYPE:
+ g_value_set_gtype (value, store->item_type);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtd_list_store_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListStore *store = GTD_LIST_STORE (object);
+
+ switch (property_id)
+ {
+ case PROP_ITEM_TYPE: /* construct-only */
+ store->item_type = g_value_get_gtype (value);
+ if (!g_type_is_a (store->item_type, G_TYPE_OBJECT))
+ g_critical ("GtdListStore cannot store items of type '%s'. Items must be GObjects.",
+ g_type_name (store->item_type));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtd_list_store_class_init (GtdListStoreClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gtd_list_store_dispose;
+ object_class->get_property = gtd_list_store_get_property;
+ object_class->set_property = gtd_list_store_set_property;
+
+ /**
+ * GtdListStore:item-type:
+ *
+ * The type of items contained in this list store. Items must be
+ * subclasses of #GObject.
+ **/
+ g_object_class_install_property (object_class, PROP_ITEM_TYPE,
+ g_param_spec_gtype ("item-type", "", "", G_TYPE_OBJECT,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static GType
+gtd_list_store_get_item_type (GListModel *list)
+{
+ GtdListStore *store = GTD_LIST_STORE (list);
+
+ return store->item_type;
+}
+
+static guint
+gtd_list_store_get_n_items (GListModel *list)
+{
+ GtdListStore *store = GTD_LIST_STORE (list);
+
+ return store->length;
+}
+
+static gpointer
+gtd_list_store_get_item (GListModel *list,
+ guint position)
+{
+ GtdListStore *store = GTD_LIST_STORE (list);
+ GSequenceIter *it = NULL;
+
+ if (store->last_position != -1)
+ {
+ if (store->last_position == position + 1)
+ it = g_sequence_iter_prev (store->last_iter);
+ else if (store->last_position == position - 1)
+ it = g_sequence_iter_next (store->last_iter);
+ else if (store->last_position == position)
+ it = store->last_iter;
+ }
+
+ if (it == NULL)
+ it = g_sequence_get_iter_at_pos (store->items, position);
+
+ store->last_iter = it;
+ store->last_position = position;
+
+ if (g_sequence_iter_is_end (it))
+ return NULL;
+ else
+ return g_object_ref (g_sequence_get (it));
+}
+
+static void
+gtd_list_store_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtd_list_store_get_item_type;
+ iface->get_n_items = gtd_list_store_get_n_items;
+ iface->get_item = gtd_list_store_get_item;
+}
+
+static void
+gtd_list_store_init (GtdListStore *store)
+{
+ store->item_to_iter = g_hash_table_new (g_direct_hash, g_direct_equal);
+ store->items = g_sequence_new (g_object_unref);
+ store->last_position = -1;
+}
+
+/**
+ * gtd_list_store_new:
+ * @item_type: the #GType of items in the list
+ *
+ * Creates a new #GtdListStore with items of type @item_type. @item_type
+ * must be a subclass of #GObject.
+ *
+ * Returns: a new #GtdListStore
+ */
+GtdListStore *
+gtd_list_store_new (GType item_type)
+{
+ /* We only allow GObjects as item types right now. This might change
+ * in the future.
+ */
+ g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL);
+
+ return g_object_new (GTD_TYPE_LIST_STORE,
+ "item-type", item_type,
+ NULL);
+}
+
+/**
+ * gtd_list_store_insert:
+ * @store: a #GtdListStore
+ * @position: the position at which to insert the new item
+ * @item: (type GObject): the new item
+ *
+ * Inserts @item into @store at @position. @item must be of type
+ * #GtdListStore:item-type or derived from it. @position must be smaller
+ * than the length of the list, or equal to it to append.
+ *
+ * This function takes a ref on @item.
+ *
+ * Use gtd_list_store_splice() to insert multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_insert (GtdListStore *store,
+ guint position,
+ gpointer item)
+{
+ GSequenceIter *new_it;
+ GSequenceIter *it;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type));
+ g_return_if_fail (position <= (guint) g_sequence_get_length (store->items));
+
+ it = g_sequence_get_iter_at_pos (store->items, position);
+ new_it = g_sequence_insert_before (it, g_object_ref (item));
+
+ g_hash_table_insert (store->item_to_iter, item, new_it);
+
+ gtd_list_store_items_changed (store, position, 0, 1);
+}
+
+/**
+ * gtd_list_store_insert_sorted:
+ * @store: a #GtdListStore
+ * @item: (type GObject): the new item
+ * @compare_func: (scope call): pairwise comparison function for sorting
+ * @user_data: (closure): user data for @compare_func
+ *
+ * Inserts @item into @store at a position to be determined by the
+ * @compare_func.
+ *
+ * The list must already be sorted before calling this function or the
+ * result is undefined. Usually you would approach this by only ever
+ * inserting items by way of this function.
+ *
+ * This function takes a ref on @item.
+ *
+ * Returns: the position at which @item was inserted
+ */
+guint
+gtd_list_store_insert_sorted (GtdListStore *store,
+ gpointer item,
+ GCompareDataFunc compare_func,
+ gpointer user_data)
+{
+ GSequenceIter *it;
+ guint position;
+
+ g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0);
+ g_return_val_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type), 0);
+ g_return_val_if_fail (compare_func != NULL, 0);
+
+ it = g_sequence_insert_sorted (store->items, g_object_ref (item), compare_func, user_data);
+ position = g_sequence_iter_get_position (it);
+
+ g_hash_table_insert (store->item_to_iter, item, it);
+
+ gtd_list_store_items_changed (store, position, 0, 1);
+
+ return position;
+}
+
+/**
+ * gtd_list_store_sort:
+ * @store: a #GtdListStore
+ * @compare_func: (scope call): pairwise comparison function for sorting
+ * @user_data: (closure): user data for @compare_func
+ *
+ * Sort the items in @store according to @compare_func.
+ */
+void
+gtd_list_store_sort (GtdListStore *store,
+ GCompareDataFunc compare_func,
+ gpointer user_data)
+{
+ gint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (compare_func != NULL);
+
+ g_sequence_sort (store->items, compare_func, user_data);
+
+ n_items = g_sequence_get_length (store->items);
+ gtd_list_store_items_changed (store, 0, n_items, n_items);
+}
+
+/**
+ * gtd_list_store_append:
+ * @store: a #GtdListStore
+ * @item: (type GObject): the new item
+ *
+ * Appends @item to @store. @item must be of type #GtdListStore:item-type.
+ *
+ * This function takes a ref on @item.
+ *
+ * Use gtd_list_store_splice() to append multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_append (GtdListStore *store,
+ gpointer item)
+{
+ GSequenceIter *it;
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type));
+
+ n_items = g_sequence_get_length (store->items);
+ it = g_sequence_append (store->items, g_object_ref (item));
+
+ g_hash_table_insert (store->item_to_iter, item, it);
+
+ gtd_list_store_items_changed (store, n_items, 0, 1);
+}
+
+/**
+ * gtd_list_store_remove:
+ * @store: a #GtdListStore
+ * @item: the item that is to be removed
+ *
+ * Removes the item from @store.
+ *
+ * Use gtd_list_store_splice() to remove multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_remove (GtdListStore *store,
+ gpointer item)
+{
+ GSequenceIter *it;
+ guint position;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+
+ it = g_hash_table_lookup (store->item_to_iter, item);
+ if (!it)
+ return;
+
+ g_return_if_fail (!g_sequence_iter_is_end (it));
+
+ position = g_sequence_iter_get_position (it);
+
+ g_hash_table_remove (store->item_to_iter, item);
+ g_sequence_remove (it);
+ gtd_list_store_items_changed (store, position, 1, 0);
+}
+
+/**
+ * gtd_list_store_remove_at_position:
+ * @store: a #GtdListStore
+ * @position: the position of the item that is to be removed
+ *
+ * Removes the item from @store that is at @position. @position must be
+ * smaller than the current length of the list.
+ *
+ * Use gtd_list_store_splice() to remove multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_remove_at_position (GtdListStore *store,
+ guint position)
+{
+ GSequenceIter *it;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+
+ it = g_sequence_get_iter_at_pos (store->items, position);
+ g_return_if_fail (!g_sequence_iter_is_end (it));
+
+ g_hash_table_remove (store->item_to_iter, g_sequence_get (it));
+ g_sequence_remove (it);
+ gtd_list_store_items_changed (store, position, 1, 0);
+}
+
+/**
+ * gtd_list_store_remove_all:
+ * @store: a #GtdListStore
+ *
+ * Removes all items from @store.
+ */
+void
+gtd_list_store_remove_all (GtdListStore *store)
+{
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+
+ n_items = g_sequence_get_length (store->items);
+ g_sequence_remove_range (g_sequence_get_begin_iter (store->items),
+ g_sequence_get_end_iter (store->items));
+ g_hash_table_remove_all (store->item_to_iter);
+
+ gtd_list_store_items_changed (store, 0, n_items, 0);
+}
+
+/**
+ * gtd_list_store_splice:
+ * @store: a #GtdListStore
+ * @position: the position at which to make the change
+ * @n_removals: the number of items to remove
+ * @additions: (array length=n_additions) (element-type GObject): the items to add
+ * @n_additions: the number of items to add
+ *
+ * Changes @store by removing @n_removals items and adding @n_additions
+ * items to it. @additions must contain @n_additions items of type
+ * #GtdListStore:item-type. %NULL is not permitted.
+ *
+ * This function is more efficient than gtd_list_store_insert() and
+ * gtd_list_store_remove(), because it only emits
+ * #GListModel::items-changed once for the change.
+ *
+ * This function takes a ref on each item in @additions.
+ *
+ * The parameters @position and @n_removals must be correct (ie:
+ * @position + @n_removals must be less than or equal to the length of
+ * the list at the time this function is called).
+ */
+void
+gtd_list_store_splice (GtdListStore *store,
+ guint position,
+ guint n_removals,
+ gpointer *additions,
+ guint n_additions)
+{
+ GSequenceIter *it;
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (position + n_removals >= position); /* overflow */
+
+ n_items = g_sequence_get_length (store->items);
+ g_return_if_fail (position + n_removals <= n_items);
+
+ it = g_sequence_get_iter_at_pos (store->items, position);
+
+ if (n_removals)
+ {
+ GSequenceIter *end;
+
+ end = g_sequence_iter_move (it, n_removals);
+ g_sequence_foreach_range (it, end, remove_item_from_sequence_cb, store);
+
+ it = end;
+ }
+
+ if (n_additions)
+ {
+ guint i;
+
+ for (i = 0; i < n_additions; i++)
+ {
+ if G_UNLIKELY (!g_type_is_a (G_OBJECT_TYPE (additions[i]), store->item_type))
+ {
+ g_critical ("%s: item %d is a %s instead of a %s. GtdListStore is now in an undefined state.",
+ G_STRFUNC, i, G_OBJECT_TYPE_NAME (additions[i]), g_type_name (store->item_type));
+ return;
+ }
+
+ it = g_sequence_insert_before (it, g_object_ref (additions[i]));
+ g_hash_table_insert (store->item_to_iter, additions[i], it);
+
+ it = g_sequence_iter_next (it);
+ }
+ }
+
+ gtd_list_store_items_changed (store, position, n_removals, n_additions);
+}
+
+/**
+ * gtd_list_store_get_item_position:
+ * @store: a #GtdListStore
+ * @item: the item to retrieve the position
+ *
+ * Retrieves the position of @items inside @store. It is a programming
+ * error to pass an @item that is not contained in @store.
+ *
+ * Returns: the position of @item in @store.
+ */
+guint
+gtd_list_store_get_item_position (GtdListStore *store,
+ gpointer item)
+{
+ GSequenceIter *iter;
+
+ g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0);
+
+ iter = g_hash_table_lookup (store->item_to_iter, item);
+ g_assert (iter != NULL);
+
+ return g_sequence_iter_get_position (iter);
+}
diff --git a/src/models/gtd-list-store.h b/src/models/gtd-list-store.h
new file mode 100644
index 0000000..84e8882
--- /dev/null
+++ b/src/models/gtd-list-store.h
@@ -0,0 +1,66 @@
+/* gtd-list-store.h
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_LIST_STORE (gtd_list_store_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdListStore, gtd_list_store, GTD, LIST_STORE, GObject)
+
+GtdListStore* gtd_list_store_new (GType item_type);
+
+void gtd_list_store_insert (GtdListStore *store,
+ guint position,
+ gpointer item);
+
+guint gtd_list_store_insert_sorted (GtdListStore *store,
+ gpointer item,
+ GCompareDataFunc compare_func,
+ gpointer user_data);
+
+void gtd_list_store_sort (GtdListStore *store,
+ GCompareDataFunc compare_func,
+ gpointer user_data);
+
+void gtd_list_store_append (GtdListStore *store,
+ gpointer item);
+
+void gtd_list_store_remove (GtdListStore *store,
+ gpointer item);
+
+void gtd_list_store_remove_at_position (GtdListStore *store,
+ guint position);
+
+void gtd_list_store_remove_all (GtdListStore *store);
+
+void gtd_list_store_splice (GtdListStore *store,
+ guint position,
+ guint n_removals,
+ gpointer *additions,
+ guint n_additions);
+
+guint gtd_list_store_get_item_position (GtdListStore *store,
+ gpointer item);
+
+G_END_DECLS
diff --git a/src/models/gtd-task-list-view-model.c b/src/models/gtd-task-list-view-model.c
new file mode 100644
index 0000000..d569d29
--- /dev/null
+++ b/src/models/gtd-task-list-view-model.c
@@ -0,0 +1,216 @@
+/* gtd-task-list-view-model.c
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "gtd-task-list.h"
+#include "gtd-task-list-view-model.h"
+
+
+/*
+ * Sentinel
+ */
+
+struct _GtdSentinel
+{
+ GObject parent_instance;
+};
+
+G_DEFINE_TYPE (GtdSentinel, gtd_sentinel, G_TYPE_OBJECT);
+
+static void
+gtd_sentinel_init (GtdSentinel *self)
+{
+}
+
+static void
+gtd_sentinel_class_init (GtdSentinelClass *klass)
+{
+}
+
+
+
+struct _GtdTaskListViewModel
+{
+ GObject parent_instance;
+
+ GtdSentinel *sentinel;
+
+ GListModel *model;
+ guint n_items;
+};
+
+static void g_list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdTaskListViewModel, gtd_task_list_view_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init))
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+update_n_items (GtdTaskListViewModel *self,
+ guint position,
+ guint removed,
+ guint added)
+{
+ self->n_items = self->n_items - removed + added;
+ g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_model_items_changed_cb (GListModel *model,
+ guint position,
+ guint removed,
+ guint added,
+ GtdTaskListViewModel *self)
+{
+ update_n_items (self, position, removed, added);
+}
+
+
+/*
+ * GListModel interface
+ */
+
+static guint
+gtd_task_list_view_model_get_n_items (GListModel *model)
+{
+ GtdTaskListViewModel *self = (GtdTaskListViewModel *)model;
+
+ return self->n_items + 1;
+}
+
+static GType
+gtd_task_list_view_model_get_item_type (GListModel *model)
+{
+ return G_TYPE_OBJECT;
+}
+
+static gpointer
+gtd_task_list_view_model_get_item (GListModel *model,
+ guint position)
+{
+ GtdTaskListViewModel *self = (GtdTaskListViewModel *)model;
+
+ if (gtd_task_list_view_model_is_sentinel (self, position))
+ return g_object_ref (self->sentinel);
+
+ return g_list_model_get_item (self->model, position);
+}
+
+static void
+g_list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_n_items = gtd_task_list_view_model_get_n_items;
+ iface->get_item_type = gtd_task_list_view_model_get_item_type;
+ iface->get_item = gtd_task_list_view_model_get_item;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_list_view_model_finalize (GObject *object)
+{
+ GtdTaskListViewModel *self = (GtdTaskListViewModel *)object;
+
+ g_clear_object (&self->model);
+ g_clear_object (&self->sentinel);
+
+ G_OBJECT_CLASS (gtd_task_list_view_model_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_list_view_model_class_init (GtdTaskListViewModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_task_list_view_model_finalize;
+}
+
+static void
+gtd_task_list_view_model_init (GtdTaskListViewModel *self)
+{
+ self->sentinel = g_object_new (GTD_TYPE_SENTINEL, NULL);
+}
+
+GtdTaskListViewModel *
+gtd_task_list_view_model_new (void)
+{
+ return g_object_new (GTD_TYPE_TASK_LIST_VIEW_MODEL, NULL);
+}
+
+GListModel *
+gtd_task_list_view_model_get_model (GtdTaskListViewModel *self)
+{
+ return self->model;
+}
+
+void
+gtd_task_list_view_model_set_model (GtdTaskListViewModel *self,
+ GListModel *model)
+{
+ guint old_n_items = 0;
+ guint new_n_items = 0;
+
+ if (G_UNLIKELY (self->model == model))
+ return;
+
+ old_n_items = self->n_items;
+
+ if (self->model)
+ {
+ g_signal_handlers_disconnect_by_func (self->model,
+ on_model_items_changed_cb,
+ self);
+ }
+
+ g_clear_object (&self->model);
+
+ if (model)
+ {
+ self->model = g_object_ref (model);
+
+ g_signal_connect_object (model,
+ "items-changed",
+ G_CALLBACK (on_model_items_changed_cb),
+ self,
+ 0);
+
+ new_n_items = g_list_model_get_n_items (model);
+ }
+
+ update_n_items (self, 0, old_n_items, new_n_items);
+}
+
+gboolean
+gtd_task_list_view_model_is_sentinel (GtdTaskListViewModel *self,
+ guint position)
+{
+ return position == self->n_items;
+}
diff --git a/src/models/gtd-task-list-view-model.h b/src/models/gtd-task-list-view-model.h
new file mode 100644
index 0000000..7323f3a
--- /dev/null
+++ b/src/models/gtd-task-list-view-model.h
@@ -0,0 +1,43 @@
+/* gtd-task-list-view-model.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_SENTINEL (gtd_sentinel_get_type ())
+G_DECLARE_FINAL_TYPE (GtdSentinel, gtd_sentinel, GTD, SENTINEL, GObject)
+
+#define GTD_TYPE_TASK_LIST_VIEW_MODEL (gtd_task_list_view_model_get_type())
+G_DECLARE_FINAL_TYPE (GtdTaskListViewModel, gtd_task_list_view_model, GTD, TASK_LIST_VIEW_MODEL, GObject)
+
+GtdTaskListViewModel* gtd_task_list_view_model_new (void);
+
+GListModel* gtd_task_list_view_model_get_model (GtdTaskListViewModel *self);
+
+void gtd_task_list_view_model_set_model (GtdTaskListViewModel *self,
+ GListModel *model);
+
+gboolean gtd_task_list_view_model_is_sentinel (GtdTaskListViewModel *self,
+ guint position);
+
+G_END_DECLS
diff --git a/src/models/gtd-task-model-private.h b/src/models/gtd-task-model-private.h
new file mode 100644
index 0000000..7bc1c7d
--- /dev/null
+++ b/src/models/gtd-task-model-private.h
@@ -0,0 +1,29 @@
+/* gtd-task-model-private.h
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "gtd-task-model.h"
+
+G_BEGIN_DECLS
+
+GtdTaskModel* _gtd_task_model_new (GtdManager *manager);
+
+G_END_DECLS
diff --git a/src/models/gtd-task-model.c b/src/models/gtd-task-model.c
new file mode 100644
index 0000000..62984e7
--- /dev/null
+++ b/src/models/gtd-task-model.c
@@ -0,0 +1,216 @@
+/* gtd-task-model.c
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "GtdTaskModel"
+
+#include "gtd-debug.h"
+#include "gtd-list-store.h"
+#include "gtd-manager.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-task-model.h"
+#include "gtd-task-model-private.h"
+
+struct _GtdTaskModel
+{
+ GObject parent;
+
+ GtkFlattenListModel *model;
+
+ guint number_of_tasks;
+
+ GtdManager *manager;
+};
+
+static void g_list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdTaskModel, gtd_task_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_MANAGER,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_model_items_changed_cb (GListModel *model,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GtdTaskModel *self)
+{
+ GTD_TRACE_MSG ("Child model changed with position=%u, n_removed=%u, n_added=%u", position, n_removed, n_added);
+
+ g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added);
+}
+
+
+/*
+ * GListModel iface
+ */
+
+static gpointer
+gtd_task_model_get_item (GListModel *model,
+ guint position)
+{
+ GtdTaskModel *self = (GtdTaskModel*) model;
+
+ return g_list_model_get_item (G_LIST_MODEL (self->model), position);
+}
+
+static guint
+gtd_task_model_get_n_items (GListModel *model)
+{
+ GtdTaskModel *self = (GtdTaskModel*) model;
+
+ return g_list_model_get_n_items (G_LIST_MODEL (self->model));
+}
+
+static GType
+gtd_task_model_get_item_type (GListModel *model)
+{
+ return GTD_TYPE_TASK;
+}
+
+static void
+g_list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item = gtd_task_model_get_item;
+ iface->get_n_items = gtd_task_model_get_n_items;
+ iface->get_item_type = gtd_task_model_get_item_type;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_model_finalize (GObject *object)
+{
+ GtdTaskModel *self = (GtdTaskModel *)object;
+
+ g_clear_object (&self->manager);
+ g_clear_object (&self->model);
+
+ G_OBJECT_CLASS (gtd_task_model_parent_class)->finalize (object);
+}
+
+
+static void
+gtd_task_model_constructed (GObject *object)
+{
+ GtdTaskModel *self = (GtdTaskModel *)object;
+ GListModel *model;
+
+ g_assert (self->manager != NULL);
+
+ model = gtd_manager_get_task_lists_model (self->manager);
+
+ g_signal_connect_object (self->model,
+ "items-changed",
+ G_CALLBACK (on_model_items_changed_cb),
+ self,
+ 0);
+ gtk_flatten_list_model_set_model (self->model, model);
+
+ G_OBJECT_CLASS (gtd_task_model_parent_class)->constructed (object);
+}
+
+static void
+gtd_task_model_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskModel *self = GTD_TASK_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_value_set_object (value, self->manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_model_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskModel *self = GTD_TASK_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_assert (self->manager == NULL);
+ self->manager = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_model_class_init (GtdTaskModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_task_model_finalize;
+ object_class->constructed = gtd_task_model_constructed;
+ object_class->get_property = gtd_task_model_get_property;
+ object_class->set_property = gtd_task_model_set_property;
+
+ properties[PROP_MANAGER] = g_param_spec_object ("manager",
+ "Manager",
+ "Manager",
+ GTD_TYPE_MANAGER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtd_task_model_init (GtdTaskModel *self)
+{
+ self->model = gtk_flatten_list_model_new (NULL);
+}
+
+GtdTaskModel*
+_gtd_task_model_new (GtdManager *manager)
+{
+ return g_object_new (GTD_TYPE_TASK_MODEL,
+ "manager", manager,
+ NULL);
+}
diff --git a/src/models/gtd-task-model.h b/src/models/gtd-task-model.h
new file mode 100644
index 0000000..2dc7aee
--- /dev/null
+++ b/src/models/gtd-task-model.h
@@ -0,0 +1,33 @@
+/* gtd-task-model.h
+ *
+ * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TASK_MODEL (gtd_task_model_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdTaskModel, gtd_task_model, GTD, TASK_MODEL, GObject)
+
+G_END_DECLS