From 6da43e93530ae47b84474b74f105a0b2303fbd47 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 31 Mar 2026 22:07:22 -0300 Subject: [PATCH] Introduce GcalEventList This is a GListModel implementation that is specific to GcalEvents. Events are stored in a GSequence in order of date. Add a couple of tests exercising additions and removals. WIP: add more tests Part-of: --- src/core/gcal-event-list.c | 326 +++++++++++++++++++++++++++++++++++++ src/core/gcal-event-list.h | 44 +++++ src/core/meson.build | 1 + tests/meson.build | 1 + tests/test-event-list.c | 251 ++++++++++++++++++++++++++++ 5 files changed, 623 insertions(+) create mode 100644 src/core/gcal-event-list.c create mode 100644 src/core/gcal-event-list.h create mode 100644 tests/test-event-list.c diff --git a/src/core/gcal-event-list.c b/src/core/gcal-event-list.c new file mode 100644 index 00000000..021408fc --- /dev/null +++ b/src/core/gcal-event-list.c @@ -0,0 +1,326 @@ +/* + * gcal-event-list.c + * + * Copyright 2025 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 "GcalEventList" + +#include "gcal-event.h" +#include "gcal-event-list.h" + +#include + +struct _GcalEventList +{ + GObject parent_instance; + + GSequence *events; + GHashTable *event_to_iter; + GHashTable *uid_to_iter; + guint n_events; + + /* cache */ + guint last_position; + GSequenceIter *last_iter; + gboolean last_position_valid; +}; + +static void g_list_model_interface_init (GListModelInterface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (GcalEventList, gcal_event_list, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_interface_init)) + + +/* + * Auxiliary functions + */ + +static void +gcal_event_list_items_changed (GcalEventList *self, + guint position, + guint removed, + guint added) +{ + /* check if the iter cache may have been invalidated */ + if (position <= self->last_position) + { + self->last_iter = NULL; + self->last_position = 0; + self->last_position_valid = FALSE; + } + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); +} + + +/* + * GListModel interface + */ + +static GType +gcal_event_list_get_item_type (GListModel *model G_GNUC_UNUSED) +{ + return GCAL_TYPE_EVENT; +} + +static guint +gcal_event_list_get_n_items (GListModel *model) +{ + GcalEventList *self = (GcalEventList *) model; + + g_assert (GCAL_IS_EVENT_LIST (self)); + + return g_hash_table_size (self->event_to_iter); +} + +static gpointer +gcal_event_list_get_item (GListModel *model, + guint position) +{ + GcalEventList *self = (GcalEventList *) model; + GSequenceIter *it = NULL; + + g_assert (GCAL_IS_EVENT_LIST (self)); + + if (self->last_position_valid) + { + if (position < G_MAXUINT && self->last_position == position + 1) + it = g_sequence_iter_prev (self->last_iter); + else if (position > 0 && self->last_position == position - 1) + it = g_sequence_iter_next (self->last_iter); + else if (self->last_position == position) + it = self->last_iter; + } + + if (it == NULL) + it = g_sequence_get_iter_at_pos (self->events, position); + + self->last_iter = it; + self->last_position = position; + self->last_position_valid = TRUE; + + if (g_sequence_iter_is_end (it)) + return NULL; + else + return g_object_ref (g_sequence_get (it)); +} + +static void +g_list_model_interface_init (GListModelInterface *iface) +{ + iface->get_item_type = gcal_event_list_get_item_type; + iface->get_n_items = gcal_event_list_get_n_items; + iface->get_item = gcal_event_list_get_item; +} + + +/* + * GObject overrides + */ + +static void +gcal_event_list_finalize (GObject *object) +{ + GcalEventList *self = (GcalEventList *)object; + + g_clear_pointer (&self->event_to_iter, g_hash_table_destroy); + g_clear_pointer (&self->events, g_sequence_free); + + G_OBJECT_CLASS (gcal_event_list_parent_class)->finalize (object); +} + +static void +gcal_event_list_class_init (GcalEventListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gcal_event_list_finalize; +} + +static void +gcal_event_list_init (GcalEventList *self) +{ + self->events = g_sequence_new (g_object_unref); + self->event_to_iter = g_hash_table_new (g_direct_hash, g_direct_equal); +} + +/** + * gcal_event_list_new: + * + */ +GcalEventList * +gcal_event_list_new (void) +{ + return g_object_new (GCAL_TYPE_EVENT_LIST, NULL); +} + +/** + * gcal_event_list_add_event: + * + * Adds @event to the list. A reference is taken. + * If @event is already part of the list, nothing + * happens. + */ +void +gcal_event_list_add_event (GcalEventList *self, + GcalEvent *event) +{ + GcalEvent *events[2] = { }; + + g_assert (GCAL_IS_EVENT_LIST (self)); + g_assert (GCAL_IS_EVENT (event)); + + events[0] = event; + events[1] = NULL; + + gcal_event_list_add_events (self, events); +} + +/** + * gcal_event_list_add_events: + * @events: a NULL-terminated list of events + * + * Adds @events to the list. A reference is taken. + * Events that are already part of the list are + * ignored. + */ +void +gcal_event_list_add_events (GcalEventList *self, + GcalEvent **events) +{ + unsigned int position = 0; + unsigned int n_added = 0; + + g_assert (GCAL_IS_EVENT_LIST (self)); + g_assert (events != NULL); + + position = g_hash_table_size (self->event_to_iter); + + for (gsize i = 0; events[i]; i++) + { + GSequenceIter *iter; + + if (g_hash_table_contains (self->event_to_iter, events[i])) + continue; + + iter = g_sequence_append (self->events, g_object_ref (events[i])); + + g_hash_table_insert (self->event_to_iter, events[i], iter); + n_added++; + } + + if (n_added > 0) + gcal_event_list_items_changed (self, position, 0, n_added); +} + +/** + * gcal_event_list_remove_event: + * + * Removes @event from the list. If the event is not part of + * the list, nothing happens. + */ +void +gcal_event_list_remove_event (GcalEventList *self, + GcalEvent *event) +{ + GcalEvent *events[2] = { }; + + g_assert (GCAL_IS_EVENT_LIST (self)); + g_assert (GCAL_IS_EVENT (event)); + + events[0] = event; + events[1] = NULL; + + gcal_event_list_remove_events (self, events); +} + +/** + * gcal_event_list_remove_events: + * @events: a NULL-terminated list of events + * + * Removes @events from the list. Events that are not part of + * the list are ignored. + */ +void +gcal_event_list_remove_events (GcalEventList *self, + GcalEvent **events) +{ + g_autoptr (GtkBitset) bitset = NULL; + g_autoptr (GPtrArray) iters = NULL; + + g_assert (GCAL_IS_EVENT_LIST (self)); + g_assert (events != NULL); + + bitset = gtk_bitset_new_empty (); + iters = g_ptr_array_new (); + + for (gsize i = 0; events[i]; i++) + { + GSequenceIter *iter = g_hash_table_lookup (self->event_to_iter, events[i]); + + if (!iter) + continue; + + g_ptr_array_add (iters, iter); + gtk_bitset_add (bitset, g_sequence_iter_get_position (iter)); + + g_hash_table_remove (self->event_to_iter, events[i]); + } + + for (gsize i = 0; i < iters->len; i++) + { + GSequenceIter *iter = g_ptr_array_index (iters, i); + + g_assert (iter != NULL); + + g_sequence_remove (iter); + } + + if (!gtk_bitset_is_empty (bitset)) + { + guint first = gtk_bitset_get_minimum (bitset); + guint last = gtk_bitset_get_maximum (bitset); + guint range_size = last - first + 1; + guint n_removed = gtk_bitset_get_size_in_range (bitset, first, last); + + gcal_event_list_items_changed (self, first, range_size, range_size - n_removed); + } +} + +/** + * gcal_event_list_remove_all_events: + * + * Removes all events from the list. + */ +void +gcal_event_list_remove_all_events (GcalEventList *self) +{ + unsigned int n_removed = 0; + + g_assert (GCAL_IS_EVENT_LIST (self)); + + n_removed = g_hash_table_size (self->event_to_iter); + + g_hash_table_remove_all (self->event_to_iter); + g_sequence_remove_range (g_sequence_get_begin_iter (self->events), + g_sequence_get_end_iter (self->events)); + + if (n_removed > 0) + gcal_event_list_items_changed (self, 0, n_removed, 0); +} diff --git a/src/core/gcal-event-list.h b/src/core/gcal-event-list.h new file mode 100644 index 00000000..1ce8e284 --- /dev/null +++ b/src/core/gcal-event-list.h @@ -0,0 +1,44 @@ +/* + * gcal-event-list.h + * + * Copyright 2026 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 + */ + +#pragma once + +#include "gcal-types.h" + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_EVENT_LIST (gcal_event_list_get_type ()) +G_DECLARE_FINAL_TYPE (GcalEventList, gcal_event_list, GCAL, EVENT_LIST, GObject) + +GcalEventList *gcal_event_list_new (void); +void gcal_event_list_add_event (GcalEventList *self, + GcalEvent *event); +void gcal_event_list_add_events (GcalEventList *self, + GcalEvent **events); +void gcal_event_list_remove_event (GcalEventList *self, + GcalEvent *event); +void gcal_event_list_remove_events (GcalEventList *self, + GcalEvent **events); +void gcal_event_list_remove_all_events (GcalEventList *self); + +G_END_DECLS diff --git a/src/core/meson.build b/src/core/meson.build index 4b4f5671..0d9af7f4 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -6,6 +6,7 @@ sources += files( 'gcal-clock.c', 'gcal-context.c', 'gcal-event.c', + 'gcal-event-list.c', 'gcal-global.c', 'gcal-log.c', 'gcal-manager.c', diff --git a/tests/meson.build b/tests/meson.build index e4a6c182..05ac17f0 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -35,6 +35,7 @@ tests = [ 'daylight-saving', 'discoverer', 'event', + 'event-list', 'internals', 'range', 'range-tree', diff --git a/tests/test-event-list.c b/tests/test-event-list.c new file mode 100644 index 00000000..af688cc2 --- /dev/null +++ b/tests/test-event-list.c @@ -0,0 +1,251 @@ +/* + * test-event-list.c + * + * Copyright 2026 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 + */ + +#include + +#include "gcal-event.h" +#include "gcal-event-list.h" +#include "gcal-stub-calendar.h" +#include "gcal-utils.h" + +#define EVENT_STRING_FOR_DATE(dtstart,dtend) \ + "BEGIN:VEVENT\n" \ + "SUMMARY:Stub event\n" \ + "UID:example@uid\n" \ + "DTSTAMP:19970114T170000Z\n" \ + "DTSTART"dtstart"\n" \ + "DTEND"dtend"\n" \ + "END:VEVENT\n" + +#define EVENT_STRING_FOR_DATE_START(dtstart) \ + EVENT_STRING_FOR_DATE(dtstart, ":20260715T035959Z") + +#define EVENT_STRING_FOR_DATE_END(dtend) \ + EVENT_STRING_FOR_DATE(":19970101T170000Z", dtend) + +/* + * Auxiliary methods + */ + +static GcalEvent* +create_event_for_string (const gchar *string, + GError **error) +{ + g_autoptr (ECalComponent) component = NULL; + g_autoptr (GcalCalendar) calendar = NULL; + + component = e_cal_component_new_from_string (string); + calendar = gcal_stub_calendar_new (NULL, error); + + return component ? gcal_event_new (calendar, component, error) : NULL; +} + +/*********************************************************************************************************************/ + +static void +event_list_new (void) +{ + g_autoptr (GcalEventList) event_list = NULL; + + event_list = gcal_event_list_new (); + g_assert_true (GCAL_IS_EVENT_LIST (event_list)); +} + +/*********************************************************************************************************************/ + +typedef struct { + guint n_items_changed; + guint position; + guint added; + guint removed; +} AddEventsHelper; + +static void +items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + gpointer user_data) +{ + AddEventsHelper *helper = user_data; + + helper->n_items_changed++; + helper->position = position; + helper->removed = removed; + helper->added = added; +} + +static void +event_list_add_events (void) +{ + g_autoptr (GcalEventList) event_list = NULL; + g_autoptr (GPtrArray) events = NULL; + AddEventsHelper helper = { }; + + const gchar * const event_strings[] = { + EVENT_STRING_FOR_DATE (":20260331T000000Z", ":20260331T030000Z"), + EVENT_STRING_FOR_DATE (":20260401T000000Z", ":20260401T030000Z"), + EVENT_STRING_FOR_DATE (":20260402T000000Z", ":20260403T030000Z"), + }; + + event_list = gcal_event_list_new (); + g_assert_true (GCAL_IS_EVENT_LIST (event_list)); + + events = g_ptr_array_new_null_terminated (G_N_ELEMENTS (event_strings), g_object_unref, TRUE); + for (gsize i = 0; i < G_N_ELEMENTS (event_strings); i++) + { + g_autoptr (GcalEvent) event = NULL; + g_autoptr (GError) error = NULL; + + event = create_event_for_string (event_strings[i], &error); + + g_assert_no_error (error); + + g_ptr_array_add (events, g_steal_pointer (&event)); + } + + g_signal_connect (event_list, "items-changed", G_CALLBACK (items_changed_cb), &helper); + gcal_event_list_add_events (event_list, (GcalEvent **) events->pdata); + + g_assert_cmpuint (helper.n_items_changed, ==, 1); + g_assert_cmpuint (helper.position, ==, 0); + g_assert_cmpuint (helper.removed, ==, 0); + g_assert_cmpuint (helper.added, ==, 3); + g_assert_cmpuint (g_list_model_get_n_items (G_LIST_MODEL (event_list)), ==, 3); + + for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (event_list)); i++) + { + g_autoptr (GcalEvent) event = g_list_model_get_item (G_LIST_MODEL (event_list), i); + + g_assert_true (event == g_ptr_array_index (events, i)); + } +} + +/*********************************************************************************************************************/ + +typedef struct { + guint n_items_changed; + guint position; + guint added; + guint removed; +} RemoveEventsHelper; + +static void +event_list_remove_events_items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + gpointer user_data) +{ + RemoveEventsHelper *helper = user_data; + + helper->n_items_changed++; + helper->position = position; + helper->removed = removed; + helper->added = added; +} + +static void +event_list_remove_events (void) +{ + g_autoptr (GcalEventList) event_list = NULL; + g_autoptr (GPtrArray) events = NULL; + RemoveEventsHelper helper = { }; + + const gchar * const event_strings[] = { + EVENT_STRING_FOR_DATE (":20260331T000000Z", ":20260331T030000Z"), + EVENT_STRING_FOR_DATE (":20260401T000000Z", ":20260401T030000Z"), + EVENT_STRING_FOR_DATE (":20260402T000000Z", ":20260403T030000Z"), + }; + + event_list = gcal_event_list_new (); + g_assert_true (GCAL_IS_EVENT_LIST (event_list)); + + events = g_ptr_array_new_null_terminated (G_N_ELEMENTS (event_strings), g_object_unref, TRUE); + for (gsize i = 0; i < G_N_ELEMENTS (event_strings); i++) + { + g_autoptr (GcalEvent) event = NULL; + g_autoptr (GError) error = NULL; + + event = create_event_for_string (event_strings[i], &error); + + g_assert_no_error (error); + + g_ptr_array_add (events, g_steal_pointer (&event)); + } + + g_signal_connect (event_list, + "items-changed", + G_CALLBACK (event_list_remove_events_items_changed_cb), + &helper); + + gcal_event_list_add_events (event_list, (GcalEvent **) events->pdata); + + g_assert_cmpuint (helper.n_items_changed, ==, 1); + g_assert_cmpuint (helper.position, ==, 0); + g_assert_cmpuint (helper.removed, ==, 0); + g_assert_cmpuint (helper.added, ==, 3); + g_assert_cmpuint (g_list_model_get_n_items (G_LIST_MODEL (event_list)), ==, 3); + + for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (event_list)); i++) + { + g_autoptr (GcalEvent) event = g_list_model_get_item (G_LIST_MODEL (event_list), i); + + g_assert_true (event == g_ptr_array_index (events, i)); + } + + helper = (RemoveEventsHelper) { }; + + gcal_event_list_remove_events (event_list, (GcalEvent **) events->pdata); + g_assert_cmpuint (helper.n_items_changed, ==, 1); + g_assert_cmpuint (helper.position, ==, 0); + g_assert_cmpuint (helper.removed, ==, 3); + g_assert_cmpuint (helper.added, ==, 0); + g_assert_cmpuint (g_list_model_get_n_items (G_LIST_MODEL (event_list)), ==, 0); + + + /* Remove again. These events are not in the list anymore. Nothing should happen */ + helper = (RemoveEventsHelper) { }; + + gcal_event_list_remove_events (event_list, (GcalEvent **) events->pdata); + g_assert_cmpuint (helper.n_items_changed, ==, 0); + g_assert_cmpuint (helper.position, ==, 0); + g_assert_cmpuint (helper.removed, ==, 0); + g_assert_cmpuint (helper.added, ==, 0); + g_assert_cmpuint (g_list_model_get_n_items (G_LIST_MODEL (event_list)), ==, 0); +} + +/*********************************************************************************************************************/ + +gint +main (gint argc, + gchar *argv[]) +{ + g_setenv ("TZ", "UTC", TRUE); + + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/event-list/new", event_list_new); + g_test_add_func ("/event-list/add-events", event_list_add_events); + g_test_add_func ("/event-list/remove-events", event_list_remove_events); + + return g_test_run (); +}