diff --git a/src/core/gcal-calendar-monitor.c b/src/core/gcal-calendar-monitor.c new file mode 100644 index 00000000..0bf66e2a --- /dev/null +++ b/src/core/gcal-calendar-monitor.c @@ -0,0 +1,1131 @@ +/* gcal-calendar-monitor.c + * + * Copyright 2020 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 "GcalCalendarMonitor" + +#include "gcal-calendar-monitor.h" +#include "gcal-date-time-utils.h" +#include "gcal-debug.h" +#include "gcal-event.h" + +#include +#include + +typedef struct +{ + GcalCalendarMonitor *monitor; + GcalEvent *event; + gchar *event_id; +} IdleData; + +typedef enum +{ + INVALID_EVENT, + CREATE_VIEW, + REMOVE_VIEW, + RANGE_UPDATED, + QUIT, +} MonitorThreadEvent; + +struct _GcalCalendarMonitor +{ + GObject parent; + + GThread *thread; + GCancellable *cancellable; + GMainContext *thread_context; + GMainContext *main_context; + + GHashTable *events; /* gchar* -> GcalEvent* */ + + GAsyncQueue *messages; + GcalCalendar *calendar; + + /* + * These fields are only accessed on the monitor thread, and + * never on the main thread. + */ + struct { + gboolean populated; + GHashTable *events_to_add; + ECalClientView *view; + } monitor_thread; + + /* + * Accessing any of the fields below must be happen inside + * a locked muxed. + */ + struct { + GMutex mutex; + GDateTime *range_start; + GDateTime *range_end; + } shared; +}; + +static gboolean add_event_to_timeline_in_idle_cb (gpointer user_data); +static gboolean update_event_in_idle_cb (gpointer user_data); +static gboolean remove_event_from_timeline_in_idle_cb (gpointer user_data); + +G_DEFINE_TYPE (GcalCalendarMonitor, gcal_calendar_monitor, G_TYPE_OBJECT) + +enum +{ + EVENT_ADDED, + EVENT_UPDATED, + EVENT_REMOVED, + N_SIGNALS, +}; + +enum +{ + PROP_0, + PROP_CALENDAR, + N_PROPS +}; + +static gulong signals[N_SIGNALS] = { 0, }; +static GParamSpec *properties [N_PROPS] = { NULL, }; + +/* + * Threads + * + * These methods must *never* be executed in the main thread. + */ + +static gchar* +build_subscriber_filter (GDateTime *range_start, + GDateTime *range_end) +{ + g_autoptr (GDateTime) utc_range_start = NULL; + g_autoptr (GDateTime) utc_range_end = NULL; + g_autofree gchar *start_str = NULL; + g_autofree gchar *end_str = NULL; + g_autofree gchar *filter = NULL; + + /* + * XXX: E-D-S ISO8601 parser is incomplete and doesn't accept the output of + * g_date_time_format_iso8601(). + */ + + utc_range_start = g_date_time_to_utc (range_start); + start_str = g_date_time_format (utc_range_start, "%Y%m%dT%H%M%SZ"); + + utc_range_end = g_date_time_to_utc (range_end); + end_str = g_date_time_format (utc_range_end, "%Y%m%dT%H%M%SZ"); + + filter = g_strdup_printf ("(occur-in-time-range? (make-time \"%s\") (make-time \"%s\"))", + start_str, + end_str); + + return g_steal_pointer (&filter); +} + +static void +maybe_init_event_arrays (GcalCalendarMonitor *self) +{ + if (self->monitor_thread.populated) + return; + + if (!self->monitor_thread.events_to_add) + { + self->monitor_thread.events_to_add = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); + } +} + +static void +get_monitor_ranges (GcalCalendarMonitor *self, + GDateTime **out_range_start, + GDateTime **out_range_end) +{ + g_mutex_lock (&self->shared.mutex); + + if (out_range_start) + *out_range_start = g_date_time_ref (self->shared.range_start); + + if (out_range_end) + *out_range_end = g_date_time_ref (self->shared.range_end); + + g_mutex_unlock (&self->shared.mutex); +} + +static void +idle_data_free (IdleData *data) +{ + g_clear_object (&data->monitor); + g_clear_object (&data->event); + g_clear_pointer (&data->event_id, g_free); + g_free (data); +} + +static void +add_event_in_idle (GcalCalendarMonitor *self, + GcalEvent *event) +{ + IdleData *idle_data; + + idle_data = g_new0 (IdleData, 1); + idle_data->monitor = g_object_ref (self); + idle_data->event = g_object_ref (event); + + g_main_context_invoke_full (self->main_context, + G_PRIORITY_DEFAULT_IDLE, + add_event_to_timeline_in_idle_cb, + idle_data, + (GDestroyNotify) idle_data_free); +} + +static void +update_event_in_idle (GcalCalendarMonitor *self, + GcalEvent *event) +{ + IdleData *idle_data; + + idle_data = g_new0 (IdleData, 1); + idle_data->monitor = g_object_ref (self); + idle_data->event = g_object_ref (event); + + g_main_context_invoke_full (self->main_context, + G_PRIORITY_DEFAULT_IDLE, + update_event_in_idle_cb, + idle_data, + (GDestroyNotify) idle_data_free); +} + +static void +remove_event_in_idle (GcalCalendarMonitor *self, + const gchar *event_id) +{ + IdleData *idle_data; + + idle_data = g_new0 (IdleData, 1); + idle_data->monitor = g_object_ref (self); + idle_data->event_id = g_strdup (event_id); + + g_main_context_invoke_full (self->main_context, + G_PRIORITY_DEFAULT_IDLE, + remove_event_from_timeline_in_idle_cb, + idle_data, + (GDestroyNotify) idle_data_free); +} + +typedef struct +{ + GcalCalendarMonitor *monitor; + GPtrArray *expanded_events; +} GenerateRecurrencesData; + +static gboolean +client_instance_generated_cb (ICalComponent *icomponent, + ICalTime *instance_start, + ICalTime *instance_end, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (GcalEvent) event = NULL; + g_autoptr (GError) local_error = NULL; + GenerateRecurrencesData *data; + GcalCalendarMonitor *self; + ECalComponent *ecomponent; + + data = user_data; + self = data->monitor; + + if (g_cancellable_set_error_if_cancelled (cancellable, error)) + return FALSE; + + ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent)); + if (!ecomponent) + return TRUE; + + event = gcal_event_new (self->calendar, ecomponent, &local_error); + if (local_error) + { + g_propagate_error (error, local_error); + return TRUE; + } + + g_ptr_array_add (data->expanded_events, g_steal_pointer (&event)); + + return TRUE; +} + +static void +on_client_view_objects_added_cb (ECalClientView *view, + const GSList *objects, + GcalCalendarMonitor *self) +{ + g_autoptr (GPtrArray) components_to_expand = NULL; + g_autoptr (GDateTime) range_start = NULL; + g_autoptr (GDateTime) range_end = NULL; + const GSList *l; + gint i; + + GCAL_ENTRY; + + get_monitor_ranges (self, &range_start, &range_end); + + maybe_init_event_arrays (self); + components_to_expand = g_ptr_array_new (); + + for (l = objects; l; l = l->next) + { + g_autoptr (GcalEvent) event = NULL; + g_autoptr (GError) error = NULL; + g_autofree gchar *event_id = NULL; + ICalComponent *icomponent; + ECalComponent *ecomponent; + + icomponent = l->data; + + if (g_cancellable_is_cancelled (self->cancellable)) + return; + + if (!icomponent || !i_cal_component_get_uid (icomponent)) + continue; + + /* Recurrent events will be processed later */ + if (e_cal_util_component_has_recurrences (icomponent) && + !e_cal_util_component_is_instance (icomponent)) + { + GCAL_TRACE_MSG ("Component %s (%s) needs to be expanded", + i_cal_component_get_summary (icomponent), + i_cal_component_get_uid (icomponent)); + g_ptr_array_add (components_to_expand, icomponent); + continue; + } + + ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent)); + + if (!ecomponent) + continue; + + event = gcal_event_new (self->calendar, ecomponent, &error); + + if (error) + { + g_warning ("Error creating event: %s", error->message); + continue; + } + + if (!gcal_event_is_within_range (event, range_start, range_end)) + continue; + + event_id = g_strdup (gcal_event_get_uid (event)); + + if (!self->monitor_thread.populated) + { + g_hash_table_insert (self->monitor_thread.events_to_add, + g_steal_pointer (&event_id), + g_object_ref (event)); + } + else + { + add_event_in_idle (self, event); + } + } + + /* Recurrent events */ + if (components_to_expand->len > 0) + { + g_autoptr (GPtrArray) expanded_events = NULL; + ECalClient *client; + time_t range_start_time; + time_t range_end_time; + + GCAL_TRACE_MSG ("Expanding recurrencies of %d events", components_to_expand->len); + + client = gcal_calendar_get_client (self->calendar); + + range_start_time = g_date_time_to_unix (range_start); + range_end_time = g_date_time_to_unix (range_end); + + expanded_events = g_ptr_array_new_with_free_func (g_object_unref); + + /* Generate the instances */ + for (i = 0; i < components_to_expand->len; i++) + { + GenerateRecurrencesData recurrences_data; + ICalComponent *icomponent; + gint old_size; + + if (g_cancellable_is_cancelled (self->cancellable)) + return; + + icomponent = g_ptr_array_index (components_to_expand, i); + + recurrences_data.monitor = self; + recurrences_data.expanded_events = expanded_events; + + old_size = expanded_events->len; + e_cal_client_generate_instances_for_object_sync (client, + icomponent, + range_start_time, + range_end_time, + self->cancellable, + client_instance_generated_cb, + &recurrences_data); + +#if GCAL_ENABLE_TRACE + { + g_autofree gchar *range_start_str = g_date_time_format_iso8601 (range_start); + g_autofree gchar *range_end_str = g_date_time_format_iso8601 (range_end); + + GCAL_TRACE_MSG ("Component %s (%s) added %d instance(s) between %s and %s", + i_cal_component_get_summary (icomponent), + i_cal_component_get_uid (icomponent), + expanded_events->len - old_size, + range_start_str, + range_end_str); + } +#endif + } + + /* Process all these instances */ + for (i = 0; i < expanded_events->len; i++) + { + g_autofree gchar *event_id = NULL; + GcalEvent *event = g_ptr_array_index (expanded_events, i); + + if (g_cancellable_is_cancelled (self->cancellable)) + return; + + if (!gcal_event_is_within_range (event, range_start, range_end)) + continue; + + event_id = g_strdup (gcal_event_get_uid (event)); + + if (!self->monitor_thread.populated) + { + g_hash_table_insert (self->monitor_thread.events_to_add, + g_steal_pointer (&event_id), + g_object_ref (event)); + } + else + { + add_event_in_idle (self, event); + } + } + } + + GCAL_EXIT; +} + +static void +on_client_view_objects_modified_cb (ECalClientView *view, + const GSList *objects, + GcalCalendarMonitor *self) +{ + g_autoptr (GDateTime) range_start = NULL; + g_autoptr (GDateTime) range_end = NULL; + const GSList *l; + + GCAL_ENTRY; + + if (!self->monitor_thread.populated && self->monitor_thread.events_to_add) + { + g_clear_pointer (&self->monitor_thread.events_to_add, g_hash_table_destroy); + return; + } + + get_monitor_ranges (self, &range_start, &range_end); + + for (l = objects; l; l = l->next) + { + g_autoptr (GcalEvent) event = NULL; + g_autoptr (GError) error = NULL; + ICalComponent *icomponent; + ECalComponent *ecomponent; + + icomponent = l->data; + + if (!icomponent || !i_cal_component_get_uid (icomponent)) + continue; + + ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent)); + + if (!ecomponent) + continue; + + event = gcal_event_new (self->calendar, ecomponent, &error); + + if (error) + { + g_warning ("Error creating event: %s", error->message); + continue; + } + + update_event_in_idle (self, event); + } + + GCAL_EXIT; +} + +static void +on_client_view_objects_removed_cb (ECalClientView *view, + const GSList *objects, + GcalCalendarMonitor *self) +{ + const GSList *l; + + GCAL_ENTRY; + + for (l = objects; l; l = l->next) + { + g_autofree gchar *event_id = NULL; + ECalComponentId *component_id; + + if (g_cancellable_is_cancelled (self->cancellable)) + return; + + component_id = l->data; + + if (e_cal_component_id_get_rid (component_id)) + { + event_id = g_strdup_printf ("%s:%s:%s", + gcal_calendar_get_id (self->calendar), + e_cal_component_id_get_uid (component_id), + e_cal_component_id_get_rid (component_id)); + } + else + { + event_id = g_strdup_printf ("%s:%s", + gcal_calendar_get_id (self->calendar), + e_cal_component_id_get_uid (component_id)); + } + + remove_event_in_idle (self, event_id); + } + + GCAL_EXIT; +} + +static void +on_client_view_complete_cb (ECalClientView *view, + const GError *error, + GcalCalendarMonitor *self) +{ + g_autoptr (GHashTable) events_to_add = NULL; + + GCAL_ENTRY; + + g_assert (!self->monitor_thread.populated); + + events_to_add = g_steal_pointer (&self->monitor_thread.events_to_add); + + /* Process all these instances */ + if (events_to_add) + { + GHashTableIter iter; + GcalEvent *event; + + g_hash_table_iter_init (&iter, events_to_add); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event)) + { + if (g_cancellable_is_cancelled (self->cancellable)) + return; + + GCAL_TRACE_MSG ("Sending event '%s' (%s) to main thead's idle", + gcal_event_get_summary (event), + gcal_event_get_uid (event)); + + add_event_in_idle (self, event); + } + } + + self->monitor_thread.populated = TRUE; + + g_debug ("Finished initial loading of calendar '%s'", gcal_calendar_get_name (self->calendar)); + + GCAL_EXIT; +} + +static void +create_view (GcalCalendarMonitor *self) +{ + g_autofree gchar *filter = NULL; + g_autoptr (GError) error = NULL; + ECalClientView *view; + ECalClient *client; + + GCAL_ENTRY; + + g_mutex_lock (&self->shared.mutex); + + g_assert (self->cancellable == NULL); + self->cancellable = g_cancellable_new (); + + g_assert (self->shared.range_start != NULL); + g_assert (self->shared.range_end != NULL); + + filter = build_subscriber_filter (self->shared.range_start, self->shared.range_end); + + g_mutex_unlock (&self->shared.mutex); + + if (!gcal_calendar_get_visible (self->calendar)) + GCAL_RETURN (); + + client = gcal_calendar_get_client (self->calendar); + e_cal_client_get_view_sync (client, filter, &view, self->cancellable, &error); + + if (error) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error creating view: %s", error->message); + GCAL_RETURN (); + } + + GCAL_TRACE_MSG ("Initialized ECalClientView with query \"%s\"", filter); + + g_signal_connect (view, "objects-added", G_CALLBACK (on_client_view_objects_added_cb), self); + g_signal_connect (view, "objects-modified", G_CALLBACK (on_client_view_objects_modified_cb), self); + g_signal_connect (view, "objects-removed", G_CALLBACK (on_client_view_objects_removed_cb), self); + g_signal_connect (view, "complete", G_CALLBACK (on_client_view_complete_cb), self); + + if (g_cancellable_is_cancelled (self->cancellable)) + GCAL_RETURN (); + + g_debug ("Starting ECalClientView for calendar '%s'", gcal_calendar_get_name (self->calendar)); + + e_cal_client_view_start (view, &error); + + if (error) + { + g_warning ("Error starting up view: %s", error->message); + g_clear_object (&view); + GCAL_RETURN (); + } + + self->monitor_thread.view = g_steal_pointer (&view); + + GCAL_EXIT; +} + +static void +remove_view (GcalCalendarMonitor *self) +{ + g_autoptr (GError) error = NULL; + + GCAL_ENTRY; + + g_clear_object (&self->cancellable); + + if (!self->monitor_thread.view) + GCAL_RETURN (); + + g_debug ("Tearing down view for calendar %s", gcal_calendar_get_id (self->calendar)); + + e_cal_client_view_stop (self->monitor_thread.view, &error); + + if (error) + { + g_warning ("Error stopping view: %s", error->message); + GCAL_RETURN (); + } + + g_clear_object (&self->monitor_thread.view); + self->monitor_thread.populated = FALSE; + + GCAL_EXIT; +} + +typedef struct +{ + GSource parent; + GcalCalendarMonitor *monitor; + GMainLoop *mainloop; +} MessageQueueSource; + +static gboolean +message_queue_source_prepare (GSource *source, + gint *timeout) +{ + GcalCalendarMonitor *self; + MessageQueueSource *queue_source; + + queue_source = (MessageQueueSource*) source; + self = queue_source->monitor; + + return g_async_queue_length (self->messages) > 0; +} + +static gboolean +message_queue_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + GcalCalendarMonitor *self; + MessageQueueSource *queue_source; + MonitorThreadEvent event; + + GCAL_ENTRY; + + queue_source = (MessageQueueSource*) source; + self = queue_source->monitor; + event = GPOINTER_TO_INT (g_async_queue_pop (self->messages)); + + switch (event) + { + case RANGE_UPDATED: + remove_view (self); + create_view (self); + break; + + case CREATE_VIEW: + create_view (self); + break; + + case REMOVE_VIEW: + remove_view (self); + break; + + case INVALID_EVENT: + case QUIT: + remove_view (self); + g_main_loop_quit (queue_source->mainloop); + GCAL_RETURN (G_SOURCE_REMOVE); + } + + GCAL_RETURN (G_SOURCE_CONTINUE); +} + +static GSourceFuncs monitor_queue_source_funcs = +{ + message_queue_source_prepare, + NULL, + message_queue_source_dispatch, + NULL, +}; + +static MessageQueueSource* +views_monitor_source_new (GcalCalendarMonitor *self, + GMainLoop *mainloop) +{ + MessageQueueSource *queue_source; + GSource *source; + + /* Prepare the message queue source */ + source = g_source_new (&monitor_queue_source_funcs, sizeof (MessageQueueSource)); + queue_source = (MessageQueueSource*) source; + queue_source->monitor = self; + queue_source->mainloop = mainloop; + g_source_set_name (source, "Message Queue Source"); + + return queue_source; +} + +static gpointer +calendar_view_thread_func (gpointer data) +{ + g_autoptr (GMainLoop) mainloop = NULL; + MessageQueueSource *queue_source; + GcalCalendarMonitor *self; + + GCAL_ENTRY; + + self = data; + mainloop = g_main_loop_new (self->thread_context, FALSE); + g_main_context_push_thread_default (self->thread_context); + + /* Events source */ + queue_source = views_monitor_source_new (self, mainloop); + g_source_attach ((GSource*) queue_source, self->thread_context); + + g_main_loop_run (mainloop); + g_main_loop_quit (mainloop); + + g_main_context_pop_thread_default (self->thread_context); + + GCAL_RETURN (NULL); +} + +/* + * Auxiliary methods + */ + +static void +notify_view_thread (GcalCalendarMonitor *self, + MonitorThreadEvent event) +{ + g_async_queue_push (self->messages, GINT_TO_POINTER (event)); + g_main_context_wakeup (self->thread_context); +} + +static void +maybe_spawn_view_thread (GcalCalendarMonitor *self) +{ + g_autofree gchar *thread_name = NULL; + + if (self->thread || !self->shared.range_start || !self->shared.range_end) + return; + + thread_name = g_strdup_printf ("GcalCalendarMonitor (%s)", gcal_calendar_get_id (self->calendar)); + self->thread = g_thread_new (thread_name, calendar_view_thread_func, self); + + g_debug ("Spawning thread %s", thread_name); +} + +static void +remove_events_outside_range (GcalCalendarMonitor *self, + GDateTime *range_start, + GDateTime *range_end) +{ + GHashTableIter iter; + GcalEvent *event; + + GCAL_TRACE_MSG ("Removing events outside range from monitor"); + + g_hash_table_iter_init (&iter, self->events); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event)) + { + if (gcal_event_is_within_range (event, range_start, range_end)) + continue; + + g_object_ref (event); + + g_hash_table_iter_remove (&iter); + g_signal_emit (self, signals[EVENT_REMOVED], 0, event); + + g_object_unref (event); + } +} + +static void +remove_all_events (GcalCalendarMonitor *self) +{ + GHashTableIter iter; + GcalEvent *event; + + GCAL_TRACE_MSG ("Removing all events from view"); + + g_hash_table_iter_init (&iter, self->events); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &event)) + { + g_hash_table_iter_remove (&iter); + g_signal_emit (self, signals[EVENT_REMOVED], 0, event); + } +} + + +/* + * Callbacks + */ + +static gboolean +add_event_to_timeline_in_idle_cb (gpointer user_data) +{ + GcalCalendarMonitor *self; + GcalEvent *event; + IdleData *idle_data; + const gchar *event_id; + + GCAL_ENTRY; + + idle_data = user_data; + self = idle_data->monitor; + event = idle_data->event; + g_assert (idle_data->event_id == NULL); + + event_id = gcal_event_get_uid (event); + if (g_hash_table_insert (self->events, g_strdup (event_id), g_object_ref (event))) + g_signal_emit (self, signals[EVENT_ADDED], 0, event); + + GCAL_RETURN (G_SOURCE_REMOVE); +} + +static gboolean +update_event_in_idle_cb (gpointer user_data) +{ + GcalCalendarMonitor *self; + GcalEvent *old_event; + GcalEvent *event; + IdleData *idle_data; + const gchar *event_id; + + GCAL_ENTRY; + + idle_data = user_data; + self = idle_data->monitor; + event = idle_data->event; + g_assert (idle_data->event_id == NULL); + + event_id = gcal_event_get_uid (event); + old_event = g_hash_table_lookup (self->events, event_id); + if (old_event) + { + g_hash_table_insert (self->events, g_strdup (event_id), g_object_ref (event)); + g_signal_emit (self, signals[EVENT_UPDATED], 0, old_event, event); + } + + GCAL_RETURN (G_SOURCE_REMOVE); +} + +static gboolean +remove_event_from_timeline_in_idle_cb (gpointer user_data) +{ + g_autoptr (GcalEvent) event = NULL; + GcalCalendarMonitor *self; + IdleData *idle_data; + gchar *event_id; + + GCAL_ENTRY; + + idle_data = user_data; + self = idle_data->monitor; + g_assert (idle_data->event == NULL); + event_id = idle_data->event_id; + + event = g_hash_table_lookup (self->events, event_id); + + if (event) + { + /* Keep the event alive until the signal is emitted */ + g_object_ref (event); + + g_hash_table_remove (self->events, event_id); + g_signal_emit (self, signals[EVENT_REMOVED], 0, event); + } + + GCAL_RETURN (G_SOURCE_REMOVE); +} + +static void +on_calendar_visible_changed_cb (GcalCalendar *calendar, + GParamSpec *pspec, + GcalCalendarMonitor *self) +{ + if (gcal_calendar_get_visible (calendar)) + { + notify_view_thread (self, CREATE_VIEW); + } + else + { + remove_all_events (self); + notify_view_thread (self, REMOVE_VIEW); + } +} + + +/* + * GObject overrides + */ + +static void +gcal_calendar_monitor_finalize (GObject *object) +{ + GcalCalendarMonitor *self = (GcalCalendarMonitor *)object; + + g_cancellable_cancel (self->cancellable); + notify_view_thread (self, QUIT); + g_thread_join (self->thread); + + remove_all_events (self); + + g_clear_object (&self->calendar); + g_clear_pointer (&self->events, g_hash_table_destroy); + g_clear_pointer (&self->thread_context, g_main_context_unref); + g_clear_pointer (&self->main_context, g_main_context_unref); + g_clear_pointer (&self->messages, g_async_queue_unref); + gcal_clear_date_time (&self->shared.range_start); + gcal_clear_date_time (&self->shared.range_end); + + G_OBJECT_CLASS (gcal_calendar_monitor_parent_class)->finalize (object); +} + +static void +gcal_calendar_monitor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object); + + switch (prop_id) + { + case PROP_CALENDAR: + g_value_set_object (value, self->calendar); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_calendar_monitor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalCalendarMonitor *self = GCAL_CALENDAR_MONITOR (object); + + switch (prop_id) + { + case PROP_CALENDAR: + g_assert (self->calendar == NULL); + self->calendar = g_value_dup_object (value); + g_signal_connect (self->calendar, + "notify::visible", + G_CALLBACK (on_calendar_visible_changed_cb), + self); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_calendar_monitor_class_init (GcalCalendarMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gcal_calendar_monitor_finalize; + object_class->get_property = gcal_calendar_monitor_get_property; + object_class->set_property = gcal_calendar_monitor_set_property; + + signals[EVENT_ADDED] = g_signal_new ("event-added", + GCAL_TYPE_CALENDAR_MONITOR, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GCAL_TYPE_EVENT); + + signals[EVENT_UPDATED] = g_signal_new ("event-updated", + GCAL_TYPE_CALENDAR_MONITOR, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 2, + GCAL_TYPE_EVENT, + GCAL_TYPE_EVENT); + + signals[EVENT_REMOVED] = g_signal_new ("event-removed", + GCAL_TYPE_CALENDAR_MONITOR, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GCAL_TYPE_EVENT); + + properties[PROP_CALENDAR] = g_param_spec_object ("calendar", + "Calendar", + "Calendar to be monitores", + GCAL_TYPE_CALENDAR, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gcal_calendar_monitor_init (GcalCalendarMonitor *self) +{ + self->events = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + self->thread_context = g_main_context_new (); + self->main_context = g_main_context_ref_thread_default (); + self->messages = g_async_queue_new (); +} + +GcalCalendarMonitor* +gcal_calendar_monitor_new (GcalCalendar *calendar) +{ + return g_object_new (GCAL_TYPE_CALENDAR_MONITOR, + "calendar", calendar, + NULL); +} + +/** + * gcal_calendar_monitor_set_range: + * @self: a #GcalCalendarMonitor + * @range_start: new range start + * @range_end: new range end + * + * Updates the range of @self. This usually will result in + * @self creating a new view, and gathering the events from + * the events server. + */ +void +gcal_calendar_monitor_set_range (GcalCalendarMonitor *self, + GDateTime *range_start, + GDateTime *range_end) +{ + gboolean range_changed; + + g_return_if_fail (GCAL_IS_CALENDAR_MONITOR (self)); + g_return_if_fail (range_start != NULL); + g_return_if_fail (range_end != NULL); + g_return_if_fail (g_date_time_compare (range_start, range_end) < 0); + + GCAL_ENTRY; + + g_mutex_lock (&self->shared.mutex); + + range_changed = + !self->shared.range_start || + !self->shared.range_end || + (self->shared.range_start && range_start && g_date_time_compare (self->shared.range_start, range_start) != 0) || + (self->shared.range_end && range_end && g_date_time_compare (self->shared.range_end, range_end) != 0); + + if (!range_changed) + { + g_mutex_unlock (&self->shared.mutex); + GCAL_RETURN (); + } + + gcal_set_date_time (&self->shared.range_start, range_start); + gcal_set_date_time (&self->shared.range_end, range_end); + + g_mutex_unlock (&self->shared.mutex); + + maybe_spawn_view_thread (self); + remove_events_outside_range (self, range_start, range_end); + + g_cancellable_cancel (self->cancellable); + + if (gcal_calendar_get_visible (self->calendar)) + notify_view_thread (self, RANGE_UPDATED); + + GCAL_EXIT; +} + +/** + * gcal_calendar_monitor_get_cached_event: + * @self: a #GcalCalendarMonitor + * @event_id: the id of the #GcalEvent + * + * Returns: (transfer full)(nullable): a #GcalEvent, or %NULL + */ +GcalEvent* +gcal_calendar_monitor_get_cached_event (GcalCalendarMonitor *self, + const gchar *event_id) +{ + GcalEvent *event; + + g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), NULL); + g_return_val_if_fail (event_id, NULL); + + event = g_hash_table_lookup (self->events, event_id); + + return event ? g_object_ref (event) : NULL; +} diff --git a/src/core/gcal-calendar-monitor.h b/src/core/gcal-calendar-monitor.h new file mode 100644 index 00000000..a6423f16 --- /dev/null +++ b/src/core/gcal-calendar-monitor.h @@ -0,0 +1,41 @@ +/* gcal-calendar-monitor.h + * + * Copyright 2020 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 + +#include "gcal-types.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_CALENDAR_MONITOR (gcal_calendar_monitor_get_type()) +G_DECLARE_FINAL_TYPE (GcalCalendarMonitor, gcal_calendar_monitor, GCAL, CALENDAR_MONITOR, GObject) + +GcalCalendarMonitor* gcal_calendar_monitor_new (GcalCalendar *calendar); + +void gcal_calendar_monitor_set_range (GcalCalendarMonitor *self, + GDateTime *range_start, + GDateTime *range_end); + +GcalEvent* gcal_calendar_monitor_get_cached_event (GcalCalendarMonitor *self, + const gchar *event_id); + +G_END_DECLS diff --git a/src/core/gcal-event.c b/src/core/gcal-event.c index 831ea644..98047c50 100644 --- a/src/core/gcal-event.c +++ b/src/core/gcal-event.c @@ -1829,3 +1829,24 @@ gcal_event_format_date (GcalEvent *self) return g_steal_pointer (&formatted_string); } + +gboolean +gcal_event_is_within_range (GcalEvent *self, + GDateTime *range_start, + GDateTime *range_end) +{ + GDateTime *event_start; + GDateTime *event_end; + + event_start = gcal_event_get_date_start (self); + event_end = gcal_event_get_date_end (self); + + if (gcal_event_get_all_day (self)) + { + return gcal_date_time_compare_date (range_end, event_start) < 0 && + gcal_date_time_compare_date (range_start, event_end) > 0; + } + + return g_date_time_compare (range_end, event_start) > 0 && + g_date_time_compare (event_end, range_start) > 0; +} diff --git a/src/core/gcal-event.h b/src/core/gcal-event.h index 7ec8a4af..be6e6c0c 100644 --- a/src/core/gcal-event.h +++ b/src/core/gcal-event.h @@ -131,6 +131,10 @@ void gcal_event_get_original_timezones (GcalEvent gchar* gcal_event_format_date (GcalEvent *self); +gboolean gcal_event_is_within_range (GcalEvent *self, + GDateTime *range_start, + GDateTime *range_end); + G_END_DECLS #endif /* GCAL_EVENT_H */ diff --git a/src/core/gcal-timeline-subscriber.c b/src/core/gcal-timeline-subscriber.c new file mode 100644 index 00000000..5a907ab4 --- /dev/null +++ b/src/core/gcal-timeline-subscriber.c @@ -0,0 +1,96 @@ +/* gcal-timeline-subscriber.c + * + * Copyright 2020 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 "gcal-timeline-subscriber.h" + +G_DEFINE_INTERFACE (GcalTimelineSubscriber, gcal_timeline_subscriber, G_TYPE_OBJECT) + +enum +{ + RANGE_CHANGED, + N_SIGNALS, +}; + +static guint signals[N_SIGNALS] = { 0, }; + +static void +gcal_timeline_subscriber_default_init (GcalTimelineSubscriberInterface *iface) +{ + /** + * GcalTimelineSubscriber::range-changed: + * + * Emitted when the subscriber range is changed. + */ + signals[RANGE_CHANGED] = g_signal_new ("range-changed", + GCAL_TYPE_TIMELINE_SUBSCRIBER, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +/** + * gcal_timeline_subscriber_get_range_start: + * @self: a #GcalTimelineSubscriber + * + * Retrieves the start of the range of @self. + * + * Returns: (transfer full): a #GDateTime + */ +GDateTime* +gcal_timeline_subscriber_get_range_start (GcalTimelineSubscriber *self) +{ + g_return_val_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (self), NULL); + + return GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (self)->get_range_start (self); +} + +/** + * gcal_timeline_subscriber_get_range_end: + * @self: a #GcalTimelineSubscriber + * + * Retrieves the end of the range of @self. + * + * Returns: (transfer full): a #GDateTime + */ +GDateTime* +gcal_timeline_subscriber_get_range_end (GcalTimelineSubscriber *self) +{ + g_return_val_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (self), NULL); + + return GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (self)->get_range_end (self); +} + +/** + * gcal_timeline_subscriber_range_changed: + * @self: a #GcalTimelineSubscriber + * + * Emits the "range-changed" signal for @self. Only + * implementations of #GcalTimelineSubscriber are + * expected to use this method. + */ +void +gcal_timeline_subscriber_range_changed (GcalTimelineSubscriber *self) +{ + g_return_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (self)); + + g_signal_emit (self, signals[RANGE_CHANGED], 0); +} diff --git a/src/core/gcal-timeline-subscriber.h b/src/core/gcal-timeline-subscriber.h new file mode 100644 index 00000000..f916953e --- /dev/null +++ b/src/core/gcal-timeline-subscriber.h @@ -0,0 +1,59 @@ +/* gcal-timeline-subscriber.h + * + * Copyright 2020 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 + +#define LIBICAL_GLIB_UNSTABLE_API +#include + +#include "gcal-types.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_TIMELINE_SUBSCRIBER (gcal_timeline_subscriber_get_type ()) + +G_DECLARE_INTERFACE (GcalTimelineSubscriber, gcal_timeline_subscriber, GCAL, TIMELINE_SUBSCRIBER, GObject) + +struct _GcalTimelineSubscriberInterface +{ + GTypeInterface parent; + + GDateTime* (*get_range_start) (GcalTimelineSubscriber *self); + GDateTime* (*get_range_end) (GcalTimelineSubscriber *self); + + void (*add_event) (GcalTimelineSubscriber *self, + GcalEvent *event); + + void (*update_event) (GcalTimelineSubscriber *self, + GcalEvent *event); + + void (*remove_event) (GcalTimelineSubscriber *self, + GcalEvent *event); +}; + +GDateTime* gcal_timeline_subscriber_get_range_start (GcalTimelineSubscriber *self); + +GDateTime* gcal_timeline_subscriber_get_range_end (GcalTimelineSubscriber *self); + +void gcal_timeline_subscriber_range_changed (GcalTimelineSubscriber *self); + +G_END_DECLS diff --git a/src/core/gcal-timeline.c b/src/core/gcal-timeline.c new file mode 100644 index 00000000..4ad57102 --- /dev/null +++ b/src/core/gcal-timeline.c @@ -0,0 +1,713 @@ +/* gcal-timeline.c + * + * Copyright 2020 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 "GcalTimeline" + +#include "gcal-calendar.h" +#include "gcal-calendar-monitor.h" +#include "gcal-context.h" +#include "gcal-date-time-utils.h" +#include "gcal-debug.h" +#include "gcal-event.h" +#include "gcal-range-tree.h" +#include "gcal-timeline.h" +#include "gcal-timeline-subscriber.h" + +#include + +typedef struct +{ + GDateTime *range_start; + GDateTime *range_end; +} SubscriberData; + +struct _GcalTimeline +{ + GObject parent_instance; + + gulong update_range_idle_id; + GDateTime *range_start; + GDateTime *range_end; + + GcalRangeTree *events; + + GHashTable *calendars; /* GcalCalendar* -> GcalCalendarMonitor* */ + GHashTable *subscribers; /* GcalTimelineSubscriber* -> SubscriberData* */ + GcalRangeTree *subscriber_ranges; + + GCancellable *cancellable; + + GcalContext *context; +}; + +G_DEFINE_TYPE (GcalTimeline, gcal_timeline, G_TYPE_OBJECT) + +enum +{ + PROP_0, + PROP_CONTEXT, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + + +/* + * Auxiliary methods + */ + +static SubscriberData* +subscriber_data_new (GcalTimelineSubscriber *subscriber) +{ + SubscriberData *subscriber_data; + + subscriber_data = g_new0 (SubscriberData, 1); + subscriber_data->range_start = gcal_timeline_subscriber_get_range_start (subscriber); + subscriber_data->range_end = gcal_timeline_subscriber_get_range_end (subscriber); + + return g_steal_pointer (&subscriber_data); +} + +static void +subscriber_data_free (SubscriberData *subscriber_data) +{ + gcal_clear_date_time (&subscriber_data->range_start); + gcal_clear_date_time (&subscriber_data->range_end); + g_free (subscriber_data); +} + +static void +add_event_to_subscriber (GcalTimelineSubscriber *subscriber, + GcalEvent *event) +{ + GCAL_TRACE_MSG ("Adding event %s to subscriber %s", + gcal_event_get_uid (event), + G_OBJECT_TYPE_NAME (subscriber)); + + GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (subscriber)->add_event (subscriber, event); +} + +static void +update_subscriber_event (GcalTimelineSubscriber *subscriber, + GcalEvent *event) +{ + GCAL_TRACE_MSG ("Updating event '%s' (%s) at subscriber %s", + gcal_event_get_summary (event), + gcal_event_get_uid (event), + G_OBJECT_TYPE_NAME (subscriber)); + + GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (subscriber)->update_event (subscriber, event); +} +static void +remove_event_from_subscriber (GcalTimelineSubscriber *subscriber, + GcalEvent *event) +{ + GCAL_TRACE_MSG ("Removing event '%s' (%s) from subscriber %s", + gcal_event_get_summary (event), + gcal_event_get_uid (event), + G_OBJECT_TYPE_NAME (subscriber)); + + GCAL_TIMELINE_SUBSCRIBER_GET_IFACE (subscriber)->remove_event (subscriber, event); +} + +static gboolean +event_is_really_within_range (GcalEvent *event, + GDateTime *range_start, + GDateTime *range_end) +{ + GDateTime *event_start; + GDateTime *event_end; + + /* Assume that ECalClientView did its job correctly */ + if (!gcal_event_get_all_day (event)) + return TRUE; + + event_start = gcal_event_get_date_start (event); + event_end = gcal_event_get_date_end (event); + + return gcal_date_time_compare_date (range_end, event_start) < 0 && + gcal_date_time_compare_date (range_start, event_end) > 0; +} + +static gboolean +subscriber_contains_event (GcalTimelineSubscriber *subscriber, + GcalEvent *event) +{ + g_autoptr (GDateTime) subscriber_range_start = NULL; + g_autoptr (GDateTime) subscriber_range_end = NULL; + + subscriber_range_start = gcal_timeline_subscriber_get_range_start (subscriber); + subscriber_range_end = gcal_timeline_subscriber_get_range_end (subscriber); + + return event_is_really_within_range (event, subscriber_range_start, subscriber_range_end); +} + +static void +update_range (GcalTimeline *self) +{ + g_autoptr (GDateTime) new_range_start = NULL; + g_autoptr (GDateTime) new_range_end = NULL; + GcalTimelineSubscriber *subscriber; + GHashTableIter iter; + gboolean has_subscribers; + gboolean range_changed; + + GCAL_ENTRY; + + range_changed = FALSE; + has_subscribers = g_hash_table_size (self->subscribers) > 0; + + if (has_subscribers) + { + g_hash_table_iter_init (&iter, self->subscribers); + while (g_hash_table_iter_next (&iter, (gpointer*) &subscriber, NULL)) + { + g_autoptr (GDateTime) subscriber_start = NULL; + g_autoptr (GDateTime) subscriber_end = NULL; + + subscriber_start = gcal_timeline_subscriber_get_range_start (subscriber); + subscriber_end = gcal_timeline_subscriber_get_range_end (subscriber); + + if (!new_range_start || g_date_time_compare (subscriber_start, new_range_start) < 0) + { + gcal_clear_date_time (&new_range_start); + new_range_start = g_steal_pointer (&subscriber_start); + } + + if (!new_range_end || g_date_time_compare (subscriber_end, new_range_end) > 0) + { + gcal_clear_date_time (&new_range_end); + new_range_end = g_steal_pointer (&subscriber_end); + } + } + + if (!self->range_start || g_date_time_compare (self->range_start, new_range_start) != 0) + { + gcal_clear_date_time (&self->range_start); + self->range_start = g_steal_pointer (&new_range_start); + range_changed = TRUE; + } + + if (!self->range_end || g_date_time_compare (self->range_end, new_range_end) != 0) + { + gcal_clear_date_time (&self->range_end); + self->range_end = g_steal_pointer (&new_range_end); + range_changed = TRUE; + } + + } + else if (self->range_start || self->range_end) + { + gcal_clear_date_time (&self->range_start); + gcal_clear_date_time (&self->range_end); + range_changed = TRUE; + } + + if (range_changed) + { + GcalCalendarMonitor *monitor; + + g_hash_table_iter_init (&iter, self->calendars); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &monitor)) + gcal_calendar_monitor_set_range (monitor, self->range_start, self->range_end); + } + + GCAL_EXIT; +} + +static void +calculate_changed_events (GcalTimeline *self, + GcalTimelineSubscriber *subscriber, + GDateTime *old_range_start, + GDateTime *old_range_end, + GDateTime *new_range_start, + GDateTime *new_range_end) +{ + g_autoptr (GPtrArray) events_to_remove = NULL; + g_autoptr (GPtrArray) events_to_add = NULL; + gint range_diff; + gint i; + + events_to_add = g_ptr_array_new (); + events_to_remove = g_ptr_array_new (); + + /* Start ranges diff */ + range_diff = g_date_time_compare (old_range_start, new_range_start); + if (range_diff != 0) + { + g_autoptr (GPtrArray) events = NULL; + + if (range_diff < 0) + { + /* Removed */ + events = gcal_range_tree_get_data_at_range (self->events, old_range_start, new_range_start); + if (events) + g_ptr_array_extend (events_to_remove, events, NULL, NULL); + } + else if (range_diff > 0) + { + /* Added */ + events = gcal_range_tree_get_data_at_range (self->events, new_range_start, old_range_start); + if (events) + g_ptr_array_extend (events_to_add, events, NULL, NULL); + } + } + + /* End ranges diff */ + range_diff = g_date_time_compare (old_range_end, new_range_end); + if (range_diff != 0) + { + g_autoptr (GPtrArray) events = NULL; + + if (range_diff < 0) + { + events = gcal_range_tree_get_data_at_range (self->events, old_range_end, new_range_end); + if (events) + g_ptr_array_extend (events_to_add, events, NULL, NULL); + } + else if (range_diff > 0) + { + events = gcal_range_tree_get_data_at_range (self->events, new_range_end, old_range_end); + if (events) + g_ptr_array_extend (events_to_remove, events, NULL, NULL); + } + } + + for (i = 0; i < events_to_remove->len; i++) + { + GcalEvent *event = g_ptr_array_index (events_to_remove, i); + + if (!event_is_really_within_range (event, old_range_start, old_range_end)) + continue; + + remove_event_from_subscriber (subscriber, event); + } + + for (i = 0; i < events_to_add->len; i++) + { + GcalEvent *event = g_ptr_array_index (events_to_add, i); + + if (!event_is_really_within_range (event, new_range_start, new_range_end)) + continue; + + add_event_to_subscriber (subscriber, event); + } +} + +static void +update_subscriber_range (GcalTimeline *self, + GcalTimelineSubscriber *subscriber) +{ + g_autoptr (GDateTime) new_range_start = NULL; + g_autoptr (GDateTime) new_range_end = NULL; + SubscriberData *subscriber_data; + + + GCAL_ENTRY; + + subscriber_data = g_hash_table_lookup (self->subscribers, subscriber); + g_assert (subscriber_data != NULL); + + new_range_start = gcal_timeline_subscriber_get_range_start (subscriber); + new_range_end = gcal_timeline_subscriber_get_range_end (subscriber); + + /* Diff new and old event ranges */ + calculate_changed_events (self, + subscriber, + subscriber_data->range_start, + subscriber_data->range_end, + new_range_start, + new_range_end); + + /* Update the subscriber range */ + gcal_range_tree_remove_data (self->subscriber_ranges, subscriber); + gcal_range_tree_add_range (self->subscriber_ranges, + new_range_start, + new_range_end, + subscriber); + + gcal_set_date_time (&subscriber_data->range_start, new_range_start); + gcal_set_date_time (&subscriber_data->range_end, new_range_end); + + GCAL_EXIT; +} + + +/* + * Callbacks + */ + +static void +on_calendar_monitor_event_added_cb (GcalCalendarMonitor *monitor, + GcalEvent *event, + GcalTimeline *self) +{ + g_autoptr (GPtrArray) subscribers_at_range = NULL; + GDateTime *event_start; + GDateTime *event_end; + gint i; + + GCAL_ENTRY; + + event_start = gcal_event_get_date_start (event); + event_end = gcal_event_get_date_end (event); + + gcal_range_tree_add_range (self->events, event_start, event_end, g_object_ref (event)); + + /* Add to all subscribers within the event range */ + subscribers_at_range = gcal_range_tree_get_data_at_range (self->subscriber_ranges, event_start, event_end); + for (i = 0; subscribers_at_range && i < subscribers_at_range->len; i++) + { + GcalTimelineSubscriber *subscriber = g_ptr_array_index (subscribers_at_range, i); + + if (!subscriber_contains_event (subscriber, event)) + continue; + + add_event_to_subscriber (subscriber, event); + } + + GCAL_EXIT; +} + +static void +on_calendar_monitor_event_updated_cb (GcalCalendarMonitor *monitor, + GcalEvent *old_event, + GcalEvent *event, + GcalTimeline *self) +{ + g_autoptr (GPtrArray) subscribers_at_range = NULL; + GDateTime *old_event_start; + GDateTime *old_event_end; + GDateTime *event_start; + GDateTime *event_end; + gint i; + + GCAL_ENTRY; + + /* Remove the old event */ + old_event_start = gcal_event_get_date_start (old_event); + old_event_end = gcal_event_get_date_end (old_event); + + gcal_range_tree_remove_range (self->events, old_event_start, old_event_end, old_event); + + /* Add the updated event to the (potentially new) range */ + event_start = gcal_event_get_date_start (event); + event_end = gcal_event_get_date_end (event); + + gcal_range_tree_add_range (self->events, event_start, event_end, g_object_ref (event)); + + /* Add to all subscribers within the event range */ + subscribers_at_range = gcal_range_tree_get_data_at_range (self->subscriber_ranges, event_start, event_end); + for (i = 0; subscribers_at_range && i < subscribers_at_range->len; i++) + { + GcalTimelineSubscriber *subscriber = g_ptr_array_index (subscribers_at_range, i); + + if (!subscriber_contains_event (subscriber, event)) + continue; + + update_subscriber_event (subscriber, event); + } + + GCAL_EXIT; +} + +static void +on_calendar_monitor_event_removed_cb (GcalCalendarMonitor *monitor, + GcalEvent *event, + GcalTimeline *self) +{ + g_autoptr (GPtrArray) subscribers_at_range = NULL; + GDateTime *event_start; + GDateTime *event_end; + gint i; + + GCAL_ENTRY; + + event_start = gcal_event_get_date_start (event); + event_end = gcal_event_get_date_end (event); + + /* Add to all subscribers within the event range */ + subscribers_at_range = gcal_range_tree_get_data_at_range (self->subscriber_ranges, event_start, event_end); + for (i = 0; subscribers_at_range && i < subscribers_at_range->len; i++) + { + GcalTimelineSubscriber *subscriber = g_ptr_array_index (subscribers_at_range, i); + + if (!subscriber_contains_event (subscriber, event)) + continue; + + remove_event_from_subscriber (subscriber, event); + } + + gcal_range_tree_remove_range (self->events, event_start, event_end, event); + + GCAL_EXIT; +} + +static gboolean +update_timeline_range_in_idle_cb (gpointer user_data) +{ + GcalTimeline *self; + + GCAL_ENTRY; + + self = GCAL_TIMELINE (user_data); + + update_range (self); + + self->update_range_idle_id = 0; + + GCAL_RETURN (G_SOURCE_REMOVE); +} + +static void +on_subscriber_range_changed_cb (GcalTimelineSubscriber *subscriber, + GcalTimeline *self) +{ + update_subscriber_range (self, subscriber); + + if (self->update_range_idle_id) + return; + + self->update_range_idle_id = g_idle_add (update_timeline_range_in_idle_cb, self); +} + + +/* + * GObject overrides + */ + +static void +gcal_timeline_finalize (GObject *object) +{ + GcalTimeline *self = (GcalTimeline *)object; + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + if (self->update_range_idle_id > 0) + { + g_source_remove (self->update_range_idle_id); + self->update_range_idle_id = 0; + } + + g_clear_pointer (&self->events, gcal_range_tree_unref); + g_clear_pointer (&self->calendars, g_hash_table_destroy); + g_clear_pointer (&self->subscribers, g_hash_table_destroy); + g_clear_pointer (&self->subscriber_ranges, gcal_range_tree_unref); + + G_OBJECT_CLASS (gcal_timeline_parent_class)->finalize (object); +} + +static void +gcal_timeline_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalTimeline *self = GCAL_TIMELINE (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_value_set_object (value, self->context); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_timeline_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalTimeline *self = GCAL_TIMELINE (object); + + switch (prop_id) + { + case PROP_CONTEXT: + g_assert (self->context == NULL); + self->context = g_value_get_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_timeline_class_init (GcalTimelineClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gcal_timeline_finalize; + object_class->get_property = gcal_timeline_get_property; + object_class->set_property = gcal_timeline_set_property; + + /** + * GcalSearchEngine::context: + * + * The #GcalContext of the application. + */ + properties[PROP_CONTEXT] = g_param_spec_object ("context", + "Data context", + "Data context", + GCAL_TYPE_CONTEXT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gcal_timeline_init (GcalTimeline *self) +{ + self->cancellable = g_cancellable_new (); + self->events = gcal_range_tree_new_with_free_func (g_object_unref); + self->calendars = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); + self->subscribers = g_hash_table_new_full (NULL, NULL, g_object_unref, (GDestroyNotify) subscriber_data_free); + self->subscriber_ranges = gcal_range_tree_new (); +} + +/** + * gcal_timeline_new: + * @context: a #GcalContext + * + * Creates a new #GcalTimeline. + * + * Returns: (transfer full): a #GcalTimeline + */ +GcalTimeline* +gcal_timeline_new (GcalContext *context) +{ + return g_object_new (GCAL_TYPE_TIMELINE, + "context", context, + NULL); +} + +/** + * gcal_timeline_add_calendar: + * @self: a #GcalTimeline + * @calendar: a #GcalCalendar + * + * Adds @calendar to @self. The events in @calendar will be added + * respectively to + * + */ +void +gcal_timeline_add_calendar (GcalTimeline *self, + GcalCalendar *calendar) +{ + g_autoptr (GcalCalendarMonitor) monitor = NULL; + + g_return_if_fail (GCAL_IS_TIMELINE (self)); + g_return_if_fail (GCAL_IS_CALENDAR (calendar)); + + GCAL_ENTRY; + + if (g_hash_table_contains (self->calendars, calendar)) + return; + + GCAL_TRACE_MSG ("Adding calendar '%s' to timeline %p", gcal_calendar_get_name (calendar), self); + + monitor = gcal_calendar_monitor_new (calendar); + g_signal_connect (monitor, "event-added", G_CALLBACK (on_calendar_monitor_event_added_cb), self); + g_signal_connect (monitor, "event-updated", G_CALLBACK (on_calendar_monitor_event_updated_cb), self); + g_signal_connect (monitor, "event-removed", G_CALLBACK (on_calendar_monitor_event_removed_cb), self); + g_hash_table_insert (self->calendars, calendar, g_object_ref (monitor)); + + if (self->range_start && self->range_end) + gcal_calendar_monitor_set_range (monitor, self->range_start, self->range_end); + + GCAL_EXIT; +} + +void +gcal_timeline_remove_calendar (GcalTimeline *self, + GcalCalendar *calendar) +{ + g_return_if_fail (GCAL_IS_TIMELINE (self)); + g_return_if_fail (GCAL_IS_CALENDAR (calendar)); + + GCAL_ENTRY; + + g_object_ref (calendar); + + if (g_hash_table_remove (self->calendars, calendar)) + GCAL_TRACE_MSG ("Removing calendar '%s' from timeline %p", gcal_calendar_get_name (calendar), self); + + g_object_unref (calendar); + + GCAL_EXIT; +} + +void +gcal_timeline_add_subscriber (GcalTimeline *self, + GcalTimelineSubscriber *subscriber) +{ + SubscriberData *subscriber_data; + + g_return_if_fail (GCAL_IS_TIMELINE (self)); + g_return_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (subscriber)); + + GCAL_ENTRY; + + if (g_hash_table_contains (self->subscribers, subscriber)) + GCAL_RETURN (); + + g_debug ("Adding subscriber %s to timeline %p", G_OBJECT_TYPE_NAME (subscriber), self); + + subscriber_data = subscriber_data_new (subscriber); + + g_hash_table_insert (self->subscribers, g_object_ref (subscriber), g_steal_pointer (&subscriber_data)); + g_signal_connect_object (subscriber, + "range-changed", + G_CALLBACK (on_subscriber_range_changed_cb), + self, + 0); + + update_subscriber_range (self, subscriber); + update_range (self); + + GCAL_EXIT; +} + +void +gcal_timeline_remove_subscriber (GcalTimeline *self, + GcalTimelineSubscriber *subscriber) +{ + g_return_if_fail (GCAL_IS_TIMELINE (self)); + g_return_if_fail (GCAL_IS_TIMELINE_SUBSCRIBER (subscriber)); + + GCAL_ENTRY; + + if (!g_hash_table_contains (self->subscribers, subscriber)) + GCAL_RETURN (); + + g_debug ("Removing subscriber %s from timeline %p", G_OBJECT_TYPE_NAME (subscriber), self); + + g_signal_handlers_disconnect_by_func (subscriber, on_subscriber_range_changed_cb, self); + g_hash_table_remove (self->subscribers, subscriber); + + update_range (self); + + GCAL_EXIT; +} diff --git a/src/core/gcal-timeline.h b/src/core/gcal-timeline.h new file mode 100644 index 00000000..ccaa2e73 --- /dev/null +++ b/src/core/gcal-timeline.h @@ -0,0 +1,46 @@ +/* gcal-timeline.h + * + * Copyright 2020 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_TIMELINE (gcal_timeline_get_type()) +G_DECLARE_FINAL_TYPE (GcalTimeline, gcal_timeline, GCAL, TIMELINE, GObject) + +GcalTimeline* gcal_timeline_new (GcalContext *context); + +void gcal_timeline_add_calendar (GcalTimeline *self, + GcalCalendar *calendar); + +void gcal_timeline_remove_calendar (GcalTimeline *self, + GcalCalendar *calendar); + +void gcal_timeline_add_subscriber (GcalTimeline *self, + GcalTimelineSubscriber *subscriber); + +void gcal_timeline_remove_subscriber (GcalTimeline *self, + GcalTimelineSubscriber *subscriber); + +G_END_DECLS diff --git a/src/gcal-types.h b/src/gcal-types.h index 3746f05a..fc3a2941 100644 --- a/src/gcal-types.h +++ b/src/gcal-types.h @@ -24,6 +24,10 @@ G_BEGIN_DECLS +typedef struct _GcalCalendar GcalCalendar; typedef struct _GcalContext GcalContext; +typedef struct _GcalEvent GcalEvent; +typedef struct _GcalTimeline GcalTimeline; +typedef struct _GcalTimelineSubscriber GcalTimelineSubscriber; G_END_DECLS diff --git a/src/meson.build b/src/meson.build index 2244b076..43187dcf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -93,6 +93,7 @@ calendar_incs += contrib_incs sources = files( 'core/gcal-calendar.c', + 'core/gcal-calendar-monitor.c', 'core/gcal-clock.c', 'core/gcal-context.c', 'core/gcal-event.c', @@ -101,6 +102,8 @@ sources = files( 'core/gcal-night-light-monitor.c', 'core/gcal-recurrence.c', 'core/gcal-shell-search-provider.c', + 'core/gcal-timeline.c', + 'core/gcal-timeline-subscriber.c', 'core/gcal-timer.c', 'core/gcal-time-zone-monitor.c', 'gui/calendar-management/gcal-calendar-management-dialog.c',