From 9d29a7a34ee8497036783037460e383aafe2caf1 Mon Sep 17 00:00:00 2001 From: John Cardullo Date: Mon, 5 Oct 2026 13:00:17 -0700 Subject: [PATCH] core: Add "expand-recurrences" property to GcalCalendarMonitor and GcalTimeline Add an "expand-recurrences" boolean property (defaulting to TRUE) to both GcalCalendarMonitor and GcalTimeline. When set to FALSE: - Recurring master components are kept as single events without expanding each recurrence instance. - Detached recurrence instances are ignored. - Pruning outside range retains recurring master events so they are not prematurely removed. - Subscriber filtering permits recurring master events within the subscriber model. This allows consumers like search providers to avoid expanding all recurrence instances when searching, improving performance and avoiding duplicate hits. Part-of: --- src/core/gcal-calendar-monitor.c | 66 ++++++++++++++++++++++++++++---- src/core/gcal-calendar-monitor.h | 2 + src/core/gcal-timeline.c | 41 +++++++++++++++++++- src/core/gcal-timeline.h | 2 + 4 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/core/gcal-calendar-monitor.c b/src/core/gcal-calendar-monitor.c index 6690cd4d..0a6dac79 100644 --- a/src/core/gcal-calendar-monitor.c +++ b/src/core/gcal-calendar-monitor.c @@ -81,6 +81,7 @@ struct _GcalCalendarMonitor GHashTable *events; /* gchar* -> GcalEvent* */ GcalRange *range; gchar *filter; + gboolean expand_recurrences; } shared; }; @@ -98,6 +99,7 @@ enum PROP_0, PROP_CALENDAR, PROP_COMPLETE, + PROP_EXPAND_RECURRENCES, N_PROPS }; @@ -383,14 +385,26 @@ on_client_view_objects_added_cb (ECalClientView *view, 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)) + if (self->shared.expand_recurrences) { - 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); + /* 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; + } + } + else if (e_cal_util_component_is_instance (icomponent)) + { + /* + * When recurrence expansion is disabled, ignore detached recurrence + * instances (components with a RECURRENCE-ID) returned by EDS so + * only the master recurring component is processed. + */ continue; } @@ -528,12 +542,17 @@ on_client_view_objects_modified_cb (ECalClientView *view, if (!icomponent || !i_cal_component_get_uid (icomponent)) continue; + /* Ignore modified detached recurrence instances when expansion is disabled */ + if (!self->shared.expand_recurrences && e_cal_util_component_is_instance (icomponent)) + continue; + ecomponent = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomponent)); if (!ecomponent) continue; - recurrence_main = e_cal_util_component_has_recurrences (icomponent) && + recurrence_main = self->shared.expand_recurrences && + e_cal_util_component_has_recurrences (icomponent) && !e_cal_util_component_is_instance (icomponent); /* @@ -1047,6 +1066,9 @@ remove_events_outside_range (GcalCalendarMonitor *self, { GcalRange *event_range = gcal_event_get_range (event); + if (!self->shared.expand_recurrences && gcal_event_has_recurrence (event)) + continue; + if (gcal_range_calculate_overlap (range, event_range, NULL) != GCAL_RANGE_NO_OVERLAP) continue; @@ -1326,6 +1348,10 @@ gcal_calendar_monitor_get_property (GObject *object, g_value_set_boolean (value, self->complete); break; + case PROP_EXPAND_RECURRENCES: + g_value_set_boolean (value, gcal_calendar_monitor_get_expand_recurrences (self)); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -1350,6 +1376,10 @@ gcal_calendar_monitor_set_property (GObject *object, self); break; + case PROP_EXPAND_RECURRENCES: + self->shared.expand_recurrences = g_value_get_boolean (value); + break; + case PROP_COMPLETE: default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -1374,12 +1404,24 @@ gcal_calendar_monitor_class_init (GcalCalendarMonitorClass *klass) FALSE, G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * GcalCalendarMonitor:expand-recurrences: + * + * Whether to expand recurrences into multiple instances. + */ + properties[PROP_EXPAND_RECURRENCES] = g_param_spec_boolean ("expand-recurrences", + NULL, + NULL, + TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, N_PROPS, properties); } static void gcal_calendar_monitor_init (GcalCalendarMonitor *self) { + self->shared.expand_recurrences = TRUE; self->shared.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 (); @@ -1502,3 +1544,11 @@ gcal_calendar_monitor_is_complete (GcalCalendarMonitor *self) return self->complete; } +gboolean +gcal_calendar_monitor_get_expand_recurrences (GcalCalendarMonitor *self) +{ + g_return_val_if_fail (GCAL_IS_CALENDAR_MONITOR (self), TRUE); + + return self->shared.expand_recurrences; +} + diff --git a/src/core/gcal-calendar-monitor.h b/src/core/gcal-calendar-monitor.h index fb8577c9..74231a68 100644 --- a/src/core/gcal-calendar-monitor.h +++ b/src/core/gcal-calendar-monitor.h @@ -43,4 +43,6 @@ void gcal_calendar_monitor_set_filter (GcalCalendarMo gboolean gcal_calendar_monitor_is_complete (GcalCalendarMonitor *self); +gboolean gcal_calendar_monitor_get_expand_recurrences (GcalCalendarMonitor *self); + G_END_DECLS diff --git a/src/core/gcal-timeline.c b/src/core/gcal-timeline.c index 13d02771..404e92df 100644 --- a/src/core/gcal-timeline.c +++ b/src/core/gcal-timeline.c @@ -46,6 +46,7 @@ struct _GcalTimeline GHashTable *calendars; /* GcalCalendar* -> GcalCalendarMonitor* */ gboolean complete; + gboolean expand_recurrences; GListStore *calendar_monitors; GListModel *events_model; @@ -61,6 +62,7 @@ enum { PROP_0, PROP_COMPLETE, + PROP_EXPAND_RECURRENCES, PROP_FILTER, N_PROPS }; @@ -74,6 +76,7 @@ static GParamSpec *properties [N_PROPS] = { NULL, }; typedef struct { + GcalTimeline *timeline; GcalRange *range; GtkFilterListModel *events; GtkSortListModel *sorted_events; @@ -103,6 +106,9 @@ event_in_subscriber_range_func (gpointer item, if (!data->range) return FALSE; + if (!data->timeline->expand_recurrences && gcal_event_has_recurrence (item)) + return TRUE; + return gcal_event_overlaps (item, data->range); } @@ -159,6 +165,7 @@ subscriber_data_new (GcalTimeline *self, g_autoptr (SubscriberData) data = NULL; data = g_new0 (SubscriberData, 1); + data->timeline = self; data->range = gcal_timeline_subscriber_get_range (subscriber); filter = gtk_custom_filter_new (event_in_subscriber_range_func, data, NULL); @@ -502,6 +509,10 @@ gcal_timeline_get_property (GObject *object, g_value_set_boolean (value, self->complete); break; + case PROP_EXPAND_RECURRENCES: + g_value_set_boolean (value, self->expand_recurrences); + break; + case PROP_FILTER: g_value_set_string (value, self->filter); break; @@ -521,6 +532,10 @@ gcal_timeline_set_property (GObject *object, switch (prop_id) { + case PROP_EXPAND_RECURRENCES: + self->expand_recurrences = g_value_get_boolean (value); + break; + case PROP_FILTER: gcal_timeline_set_filter (self, g_value_get_string (value)); break; @@ -550,6 +565,17 @@ gcal_timeline_class_init (GcalTimelineClass *klass) FALSE, G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * GcalTimeline:expand-recurrences: + * + * Whether to expand recurrences into multiple instances. + */ + properties[PROP_EXPAND_RECURRENCES] = g_param_spec_boolean ("expand-recurrences", + "Expand recurrences", + "Expand recurrences", + TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** * GcalTimeline::filter: * @@ -568,6 +594,7 @@ static void gcal_timeline_init (GcalTimeline *self) { self->augmentation_factor = 1.0; + self->expand_recurrences = TRUE; self->cancellable = g_cancellable_new (); self->calendars = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref); @@ -635,7 +662,10 @@ gcal_timeline_add_calendar (GcalTimeline *self, GCAL_TRACE_MSG ("Adding calendar '%s' to timeline %p", gcal_calendar_get_name (calendar), self); - monitor = gcal_calendar_monitor_new (calendar); + monitor = g_object_new (GCAL_TYPE_CALENDAR_MONITOR, + "calendar", calendar, + "expand-recurrences", self->expand_recurrences, + NULL); g_signal_connect (monitor, "notify::complete", G_CALLBACK (on_calendar_monitor_completed_cb), self); g_hash_table_insert (self->calendars, calendar, g_object_ref (monitor)); g_list_store_append (self->calendar_monitors, monitor); @@ -768,3 +798,12 @@ gcal_timeline_is_complete (GcalTimeline *self) return self->complete; } + +gboolean +gcal_timeline_get_expand_recurrences (GcalTimeline *self) +{ + g_return_val_if_fail (GCAL_IS_TIMELINE (self), TRUE); + + return self->expand_recurrences; +} + diff --git a/src/core/gcal-timeline.h b/src/core/gcal-timeline.h index 6dbd581f..a07d5e13 100644 --- a/src/core/gcal-timeline.h +++ b/src/core/gcal-timeline.h @@ -56,4 +56,6 @@ void gcal_timeline_set_filter (GcalTimeline gboolean gcal_timeline_is_complete (GcalTimeline *self); +gboolean gcal_timeline_get_expand_recurrences (GcalTimeline *self); + G_END_DECLS