From 05e2f29f3c7322f838c75a468832ba300bf26600 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 22:19:01 +0200 Subject: [PATCH 01/19] clock: refactor calendar sources to support multiple providers Replace the monolithic EDS-only calendar-sources.c with an extensible provider architecture: - Add CalendarProvider abstract base class with virtual vtable and signals (appointments-changed, tasks-changed) - Add CalendarEDSProvider: absorbs all EDS/calendar-sources logic, guarded by HAVE_EDS - Add CalendarVdirProvider: reads vdir collections (vdirsyncer format), expands recurrences via libical-glib, watches directories with GFileMonitor, guarded by HAVE_LIBICAL - Rewrite CalendarClient as a thin aggregator over a list of providers - Auto-discover vdirsyncer default path (~/.local/share/vdirsyncer/) and allow extra paths via new GSettings key vdir-calendar-paths - Add HAVE_LIBICAL autoconf check for libical-glib >= 3.0 (independent of EDS; EDS implies it) - Update calendar-window.c guards from HAVE_EDS to HAVE_EDS || HAVE_LIBICAL so vdir events show without EDS Remove calendar-sources.c/h (functionality absorbed into calendar-eds-provider.c). Signed-off-by: Oz Tiram --- applets/clock/Makefile.am | 24 +- applets/clock/calendar-client.c | 2368 ++++------------- applets/clock/calendar-client.h | 4 +- applets/clock/calendar-eds-provider.c | 1152 ++++++++ applets/clock/calendar-eds-provider.h | 57 + applets/clock/calendar-provider.c | 200 ++ applets/clock/calendar-provider.h | 114 + applets/clock/calendar-sources.c | 503 ---- applets/clock/calendar-sources.h | 64 - applets/clock/calendar-vdir-provider.c | 895 +++++++ applets/clock/calendar-vdir-provider.h | 63 + applets/clock/calendar-window.c | 38 +- ...org.mate.panel.applet.clock.gschema.xml.in | 7 +- configure.ac | 30 + 14 files changed, 3061 insertions(+), 2458 deletions(-) create mode 100644 applets/clock/calendar-eds-provider.c create mode 100644 applets/clock/calendar-eds-provider.h create mode 100644 applets/clock/calendar-provider.c create mode 100644 applets/clock/calendar-provider.h delete mode 100644 applets/clock/calendar-sources.c delete mode 100644 applets/clock/calendar-sources.h create mode 100644 applets/clock/calendar-vdir-provider.c create mode 100644 applets/clock/calendar-vdir-provider.h diff --git a/applets/clock/Makefile.am b/applets/clock/Makefile.am index 5a932422e..a9dfdab16 100644 --- a/applets/clock/Makefile.am +++ b/applets/clock/Makefile.am @@ -35,13 +35,23 @@ CLOCK_SOURCES = \ set-timezone.h \ $(BUILT_SOURCES) -if HAVE_EDS CLOCK_SOURCES += \ calendar-client.c \ calendar-client.h \ - calendar-sources.c \ - calendar-sources.h \ + calendar-provider.c \ + calendar-provider.h \ calendar-debug.h + +if HAVE_LIBICAL +CLOCK_SOURCES += \ + calendar-vdir-provider.c \ + calendar-vdir-provider.h +endif + +if HAVE_EDS +CLOCK_SOURCES += \ + calendar-eds-provider.c \ + calendar-eds-provider.h endif CLOCK_CPPFLAGS = \ @@ -53,6 +63,10 @@ CLOCK_CPPFLAGS = \ -DMATELOCALEDIR=\""$(datadir)/locale"\" \ -DMATEWEATHER_I_KNOW_THIS_IS_UNSTABLE +if HAVE_LIBICAL +CLOCK_CPPFLAGS += $(LIBICAL_CFLAGS) -DLIBICAL_GLIB_UNSTABLE_API +endif + if HAVE_EDS CLOCK_CPPFLAGS += $(EDS_CFLAGS) endif @@ -64,6 +78,10 @@ CLOCK_LDADD = \ libsystem-timezone.la \ -lm +if HAVE_LIBICAL +CLOCK_LDADD += $(LIBICAL_LIBS) +endif + if HAVE_EDS CLOCK_LDADD += $(EDS_LIBS) endif diff --git a/applets/clock/calendar-client.c b/applets/clock/calendar-client.c index 562234fd2..b9726f260 100644 --- a/applets/clock/calendar-client.c +++ b/applets/clock/calendar-client.c @@ -1,4 +1,6 @@ /* + * calendar-client.c: aggregator that merges events from multiple providers + * * Copyright (C) 2004 Free Software Foundation, Inc. * * This program is free software; you can redistribute it and/or @@ -23,98 +25,50 @@ #include -#include "calendar-client.h" - -#include #include -#define HANDLE_LIBICAL_MEMORY - -#ifdef HAVE_EDS - -#include -#include "calendar-sources.h" -#include "system-timezone.h" -#endif +#include -#undef CALENDAR_ENABLE_DEBUG -#include "calendar-debug.h" +#include +#include -#ifndef _ -#define _(x) gettext(x) -#endif +#include "calendar-client.h" +#include "calendar-provider.h" -#ifndef N_ -#define N_(x) x +#ifdef HAVE_LIBICAL +# include "calendar-vdir-provider.h" #endif #ifdef HAVE_EDS +# include "calendar-eds-provider.h" +#endif -typedef struct _CalendarClientQuery CalendarClientQuery; -typedef struct _CalendarClientSource CalendarClientSource; - -struct _CalendarClientQuery -{ - ECalClientView *view; - GHashTable *events; -}; - -struct _CalendarClientSource -{ - CalendarClient *client; - ECalClient *source; - - CalendarClientQuery completed_query; - CalendarClientQuery in_progress_query; +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" - guint changed_signal_id; +/* GSettings key for extra vdir calendar paths */ +#define KEY_VDIR_CALENDAR_PATHS "vdir-calendar-paths" - guint query_completed : 1; - guint query_in_progress : 1; -}; +/* Default vdirsyncer storage base directory (relative to XDG data home) */ +#define VDIRSYNCER_DEFAULT_SUBDIR "vdirsyncer" struct _CalendarClientPrivate { - CalendarSources *calendar_sources; - - GSList *appointment_sources; - GSList *task_sources; - - ICalTimezone *zone; - - guint zone_listener; - GSettings *calendar_settings; + GSList *providers; /* list of CalendarProvider * (owned, refcounted) */ - guint day; - guint month; - guint year; + guint day; + guint month; + guint year; }; -static void calendar_client_finalize (GObject *object); -static void calendar_client_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec); -static void calendar_client_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec); - -static GSList *calendar_client_update_sources_list (CalendarClient *client, - GSList *sources, - GList *esources, - guint changed_signal_id); -static void calendar_client_appointment_sources_changed (CalendarClient *client); -static void calendar_client_task_sources_changed (CalendarClient *client); - -static void calendar_client_stop_query (CalendarClient *client, - CalendarClientSource *source, - CalendarClientQuery *query); -static void calendar_client_start_query (CalendarClient *client, - CalendarClientSource *source, - const char *query); - -static void calendar_client_source_finalize (CalendarClientSource *source); -static void calendar_client_query_finalize (CalendarClientQuery *query); +static void calendar_client_finalize (GObject *object); +static void calendar_client_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void calendar_client_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); enum { @@ -131,1960 +85,640 @@ enum LAST_SIGNAL }; -static guint signals [LAST_SIGNAL] = { 0, }; +static guint signals[LAST_SIGNAL] = { 0, }; G_DEFINE_TYPE_WITH_PRIVATE (CalendarClient, calendar_client, G_TYPE_OBJECT) +/* ========================================================================= + * CalendarEvent copy / free (the structs live in calendar-client.h, but the + * copy/free helpers are here since they need access to the private type layout) + * ========================================================================= */ + static void -calendar_client_class_init (CalendarClientClass *klass) +calendar_appointment_copy (CalendarAppointment *src, CalendarAppointment *dst) { - GObjectClass *gobject_class = (GObjectClass *) klass; + dst->occurrences = g_slist_copy (src->occurrences); + for (GSList *l = dst->occurrences; l != NULL; l = l->next) + { + CalendarOccurrence *o = l->data; + CalendarOccurrence *copy = g_new0 (CalendarOccurrence, 1); + copy->start_time = o->start_time; + copy->end_time = o->end_time; + l->data = copy; + } - gobject_class->finalize = calendar_client_finalize; - gobject_class->set_property = calendar_client_set_property; - gobject_class->get_property = calendar_client_get_property; + dst->uid = g_strdup (src->uid); + dst->rid = g_strdup (src->rid); + dst->backend_name = g_strdup (src->backend_name); + dst->summary = g_strdup (src->summary); + dst->description = g_strdup (src->description); + dst->color_string = g_strdup (src->color_string); + dst->start_time = src->start_time; + dst->end_time = src->end_time; + dst->is_all_day = src->is_all_day; +} - g_object_class_install_property (gobject_class, - PROP_DAY, - g_param_spec_uint ("day", - "Day", - "The currently monitored day between 1 and 31 (0 denotes unset)", - 0, G_MAXUINT, 0, - G_PARAM_READWRITE)); - - g_object_class_install_property (gobject_class, - PROP_MONTH, - g_param_spec_uint ("month", - "Month", - "The currently monitored month between 0 and 11", - 0, G_MAXUINT, 0, - G_PARAM_READWRITE)); - - g_object_class_install_property (gobject_class, - PROP_YEAR, - g_param_spec_uint ("year", - "Year", - "The currently monitored year", - 0, G_MAXUINT, 0, - G_PARAM_READWRITE)); - - signals [APPOINTMENTS_CHANGED] = - g_signal_new ("appointments-changed", - G_TYPE_FROM_CLASS (gobject_class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (CalendarClientClass, tasks_changed), - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0); - - signals [TASKS_CHANGED] = - g_signal_new ("tasks-changed", - G_TYPE_FROM_CLASS (gobject_class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (CalendarClientClass, tasks_changed), - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0); +static void +calendar_appointment_finalize (CalendarAppointment *appt) +{ + for (GSList *l = appt->occurrences; l != NULL; l = l->next) + g_free (l->data); + g_slist_free (appt->occurrences); + appt->occurrences = NULL; + + g_free (appt->uid); + g_free (appt->rid); + g_free (appt->backend_name); + g_free (appt->summary); + g_free (appt->description); + g_free (appt->color_string); +} + +static void +calendar_task_copy (CalendarTask *src, CalendarTask *dst) +{ + dst->uid = g_strdup (src->uid); + dst->summary = g_strdup (src->summary); + dst->description = g_strdup (src->description); + dst->color_string = g_strdup (src->color_string); + dst->url = g_strdup (src->url); + dst->start_time = src->start_time; + dst->due_time = src->due_time; + dst->percent_complete = src->percent_complete; + dst->completed_time = src->completed_time; + dst->priority = src->priority; } -/* Timezone code adapted from evolution/calendar/gui/calendar-config.c */ -/* The current timezone, e.g. "Europe/London". It may be NULL, in which case - you should assume UTC. */ -static gchar * -calendar_client_config_get_timezone (GSettings *calendar_settings) +static void +calendar_task_finalize (CalendarTask *task) { - gchar *timezone = NULL; + g_free (task->uid); + g_free (task->summary); + g_free (task->description); + g_free (task->color_string); + g_free (task->url); +} - if (calendar_settings == NULL) +CalendarEvent * +calendar_event_copy (CalendarEvent *event) +{ + if (!event) return NULL; - /* Check if we can list the keys to see if timezone exists */ - gchar **keys = g_settings_list_keys (calendar_settings); - gboolean has_timezone = FALSE; + CalendarEvent *copy = g_new0 (CalendarEvent, 1); + copy->type = event->type; - for (gint i = 0; keys[i] != NULL; i++) { - if (g_strcmp0 (keys[i], "timezone") == 0) { - has_timezone = TRUE; + switch (event->type) + { + case CALENDAR_EVENT_APPOINTMENT: + calendar_appointment_copy (CALENDAR_APPOINTMENT (event), + CALENDAR_APPOINTMENT (copy)); break; + case CALENDAR_EVENT_TASK: + calendar_task_copy (CALENDAR_TASK (event), CALENDAR_TASK (copy)); + break; + default: + g_assert_not_reached (); } - } - g_strfreev (keys); - - if (has_timezone) { - timezone = g_settings_get_string (calendar_settings, "timezone"); - } - return timezone; + return copy; } -static ICalTimezone * -calendar_client_config_get_icaltimezone (CalendarClient *client) +gboolean +calendar_event_equal (CalendarEvent *a, CalendarEvent *b) { - gchar *location = NULL; - ICalTimezone *zone = NULL; + if (!a && !b) return TRUE; + if (!a || !b) return FALSE; + if (a->type != b->type) return FALSE; - if (client->priv->calendar_settings != NULL) - location = calendar_client_config_get_timezone (client->priv->calendar_settings); - - if (!location) { - /* MATE panel doesn't store timezone in GSettings - * Since libical timezone lookup often fails, just use UTC - * The display code will handle local time conversion */ - return i_cal_timezone_get_utc_timezone (); - } + if (a->type == CALENDAR_EVENT_APPOINTMENT) + { + CalendarAppointment *aa = CALENDAR_APPOINTMENT (a); + CalendarAppointment *ab = CALENDAR_APPOINTMENT (b); - zone = i_cal_timezone_get_builtin_timezone (location); - g_free (location); + if (g_slist_length (aa->occurrences) != g_slist_length (ab->occurrences)) + return FALSE; + for (GSList *la = aa->occurrences, *lb = ab->occurrences; + la && lb; la = la->next, lb = lb->next) + { + CalendarOccurrence *oa = la->data; + CalendarOccurrence *ob = lb->data; + if (oa->start_time != ob->start_time || oa->end_time != ob->end_time) + return FALSE; + } + return g_strcmp0 (aa->uid, ab->uid) == 0 && + g_strcmp0 (aa->backend_name, ab->backend_name) == 0 && + g_strcmp0 (aa->summary, ab->summary) == 0 && + g_strcmp0 (aa->description, ab->description) == 0 && + g_strcmp0 (aa->color_string, ab->color_string) == 0 && + aa->start_time == ab->start_time && + aa->end_time == ab->end_time && + aa->is_all_day == ab->is_all_day; + } + + if (a->type == CALENDAR_EVENT_TASK) + { + CalendarTask *ta = CALENDAR_TASK (a); + CalendarTask *tb = CALENDAR_TASK (b); + return g_strcmp0 (ta->uid, tb->uid) == 0 && + g_strcmp0 (ta->summary, tb->summary) == 0 && + g_strcmp0 (ta->description, tb->description) == 0 && + g_strcmp0 (ta->color_string, tb->color_string) == 0 && + ta->start_time == tb->start_time && + ta->due_time == tb->due_time && + ta->percent_complete == tb->percent_complete && + ta->completed_time == tb->completed_time && + ta->priority == tb->priority; + } - return zone; + g_assert_not_reached (); + return FALSE; } -static void -calendar_client_set_timezone (CalendarClient *client) +void +calendar_event_free (CalendarEvent *event) { - GList *list, *link; - - client->priv->zone = calendar_client_config_get_icaltimezone (client); + if (!event) return; - list = calendar_sources_get_appointment_clients (client->priv->calendar_sources); - for (link = list; link != NULL; link = g_list_next (link)) + switch (event->type) { - ECalClient *cal = E_CAL_CLIENT (link->data); - - e_cal_client_set_default_timezone (cal, client->priv->zone); + case CALENDAR_EVENT_APPOINTMENT: + calendar_appointment_finalize (CALENDAR_APPOINTMENT (event)); + break; + case CALENDAR_EVENT_TASK: + calendar_task_finalize (CALENDAR_TASK (event)); + break; + default: + g_assert_not_reached (); } - g_list_free (list); -} -static void -calendar_client_timezone_changed_cb (GSettings *calendar_settings, - const gchar *key, - CalendarClient *client) -{ - calendar_client_set_timezone (client); + g_free (event); } +/* ========================================================================= + * GObject boilerplate + * ========================================================================= */ + static void -load_calendars (CalendarClient *client, - CalendarEventType type) +calendar_client_class_init (CalendarClientClass *klass) { - GSList *l, *clients; + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - switch (type) - { - case CALENDAR_EVENT_APPOINTMENT: - clients = client->priv->appointment_sources; - break; - case CALENDAR_EVENT_TASK: - clients = client->priv->task_sources; - break; - case CALENDAR_EVENT_ALL: - default: - g_assert_not_reached (); - } + gobject_class->finalize = calendar_client_finalize; + gobject_class->set_property = calendar_client_set_property; + gobject_class->get_property = calendar_client_get_property; - for (l = clients; l != NULL; l = l->next) - { - if (type == CALENDAR_EVENT_APPOINTMENT) - calendar_client_update_appointments (client); - else if (type == CALENDAR_EVENT_TASK) - calendar_client_update_tasks (client); - } + g_object_class_install_property (gobject_class, PROP_DAY, + g_param_spec_uint ("day", "Day", + "Currently monitored day (1–31; 0 = unset)", + 0, G_MAXUINT, 0, G_PARAM_READWRITE)); + + g_object_class_install_property (gobject_class, PROP_MONTH, + g_param_spec_uint ("month", "Month", + "Currently monitored month (0–11)", + 0, G_MAXUINT, 0, G_PARAM_READWRITE)); + + g_object_class_install_property (gobject_class, PROP_YEAR, + g_param_spec_uint ("year", "Year", + "Currently monitored year", + 0, G_MAXUINT, 0, G_PARAM_READWRITE)); + + signals[APPOINTMENTS_CHANGED] = + g_signal_new ("appointments-changed", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarClientClass, appointments_changed), + NULL, NULL, NULL, G_TYPE_NONE, 0); + + signals[TASKS_CHANGED] = + g_signal_new ("tasks-changed", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarClientClass, tasks_changed), + NULL, NULL, NULL, G_TYPE_NONE, 0); } static void calendar_client_init (CalendarClient *client) { - GList *list; - client->priv = calendar_client_get_instance_private (client); - - client->priv->calendar_sources = calendar_sources_get (); - - list = calendar_sources_get_appointment_clients (client->priv->calendar_sources); - client->priv->appointment_sources = - calendar_client_update_sources_list (client, NULL, list, signals [APPOINTMENTS_CHANGED]); - g_list_free (list); - - list = calendar_sources_get_task_clients (client->priv->calendar_sources); - client->priv->task_sources = calendar_client_update_sources_list (client, NULL, list, signals [TASKS_CHANGED]); - g_list_free (list); - - /* set the timezone before loading the clients */ - calendar_client_set_timezone (client); - load_calendars (client, CALENDAR_EVENT_APPOINTMENT); - load_calendars (client, CALENDAR_EVENT_TASK); - - g_signal_connect_swapped (client->priv->calendar_sources, - "appointment-sources-changed", - G_CALLBACK (calendar_client_appointment_sources_changed), - client); - g_signal_connect_swapped (client->priv->calendar_sources, - "task-sources-changed", - G_CALLBACK (calendar_client_task_sources_changed), - client); - - if (client->priv->calendar_settings != NULL) - client->priv->zone_listener = g_signal_connect (client->priv->calendar_settings, - "changed::timezone", - G_CALLBACK (calendar_client_timezone_changed_cb), - client); - - client->priv->day = G_MAXUINT; + client->priv->day = G_MAXUINT; client->priv->month = G_MAXUINT; - client->priv->year = G_MAXUINT; + client->priv->year = G_MAXUINT; } static void calendar_client_finalize (GObject *object) { CalendarClient *client = CALENDAR_CLIENT (object); - GSList *l; - - if (client->priv->zone_listener) - { - g_signal_handler_disconnect (client->priv->calendar_settings, - client->priv->zone_listener); - client->priv->zone_listener = 0; - } - - if (client->priv->calendar_settings) - g_object_unref (client->priv->calendar_settings); - client->priv->calendar_settings = NULL; - - for (l = client->priv->appointment_sources; l; l = l->next) - { - calendar_client_source_finalize (l->data); - g_free (l->data); - } - g_slist_free (client->priv->appointment_sources); - client->priv->appointment_sources = NULL; - - for (l = client->priv->task_sources; l; l = l->next) - { - calendar_client_source_finalize (l->data); - g_free (l->data); - } - g_slist_free (client->priv->task_sources); - client->priv->task_sources = NULL; - if (client->priv->calendar_sources) - g_object_unref (client->priv->calendar_sources); - client->priv->calendar_sources = NULL; + for (GSList *l = client->priv->providers; l != NULL; l = l->next) + g_object_unref (l->data); + g_slist_free (client->priv->providers); + client->priv->providers = NULL; G_OBJECT_CLASS (calendar_client_parent_class)->finalize (object); } static void calendar_client_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) + guint prop_id, + const GValue *value, + GParamSpec *pspec) { CalendarClient *client = CALENDAR_CLIENT (object); - switch (prop_id) { case PROP_DAY: calendar_client_select_day (client, g_value_get_uint (value)); break; case PROP_MONTH: - calendar_client_select_month (client, - g_value_get_uint (value), - client->priv->year); + calendar_client_select_month (client, g_value_get_uint (value), + client->priv->year); break; case PROP_YEAR: - calendar_client_select_month (client, - client->priv->month, - g_value_get_uint (value)); + calendar_client_select_month (client, client->priv->month, + g_value_get_uint (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; } } static void calendar_client_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) + guint prop_id, + GValue *value, + GParamSpec *pspec) { CalendarClient *client = CALENDAR_CLIENT (object); - switch (prop_id) { - case PROP_DAY: - g_value_set_uint (value, client->priv->day); - break; - case PROP_MONTH: - g_value_set_uint (value, client->priv->month); - break; - case PROP_YEAR: - g_value_set_uint (value, client->priv->year); - break; + case PROP_DAY: g_value_set_uint (value, client->priv->day); break; + case PROP_MONTH: g_value_set_uint (value, client->priv->month); break; + case PROP_YEAR: g_value_set_uint (value, client->priv->year); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; } } -CalendarClient * -calendar_client_new (GSettings *settings) -{ - CalendarClient *client = g_object_new (CALENDAR_TYPE_CLIENT, NULL); - - /* Use the provided MATE panel settings instead of Evolution settings */ - if (settings) { - client->priv->calendar_settings = g_object_ref (settings); - } else { - /* Fallback to Evolution calendar settings if available */ - GSettingsSchemaSource *schema_source = g_settings_schema_source_get_default(); - const gchar *evolution_calendar_schema = "org.gnome.evolution.calendar"; - GSettingsSchema *schema = g_settings_schema_source_lookup (schema_source, evolution_calendar_schema, FALSE); - if (schema) { - client->priv->calendar_settings = g_settings_new (evolution_calendar_schema); - g_settings_schema_unref (schema); - } else { - /* No Evolution settings available, calendar_settings will remain NULL */ - client->priv->calendar_settings = NULL; - } - } +/* ========================================================================= + * Provider management (internal) + * ========================================================================= */ - return client; +static void +on_provider_appointments_changed (CalendarProvider *provider, + CalendarClient *client) +{ + (void) provider; + g_signal_emit (client, signals[APPOINTMENTS_CHANGED], 0); } -/* @day and @month can happily be out of range as - * mktime() will normalize them correctly. From mktime(3): - * - * "If structure members are outside their legal interval, - * they will be normalized (so that, e.g., 40 October is - * changed into 9 November)." - * - * "What?", you say, "Something useful in libc?" - */ -static inline time_t -make_time_for_day_begin (int day, - int month, - int year) +static void +on_provider_tasks_changed (CalendarProvider *provider, + CalendarClient *client) { - struct tm localtime_tm = { 0, }; - - localtime_tm.tm_mday = day; - localtime_tm.tm_mon = month; - localtime_tm.tm_year = year - 1900; - localtime_tm.tm_isdst = -1; - - return mktime (&localtime_tm); + (void) provider; + g_signal_emit (client, signals[TASKS_CHANGED], 0); } -static inline char * -make_isodate_for_day_begin (int day, - int month, - int year) +static void +calendar_client_add_provider (CalendarClient *client, + CalendarProvider *provider) { - time_t utctime; + g_return_if_fail (CALENDAR_IS_PROVIDER (provider)); - utctime = make_time_for_day_begin (day, month, year); + client->priv->providers = + g_slist_prepend (client->priv->providers, g_object_ref (provider)); - return utctime != -1 ? isodate_from_time_t (utctime) : NULL; + g_signal_connect (provider, "appointments-changed", + G_CALLBACK (on_provider_appointments_changed), client); + g_signal_connect (provider, "tasks-changed", + G_CALLBACK (on_provider_tasks_changed), client); } -static time_t -get_time_from_property (ICalComponent *icomp, - ICalPropertyKind prop_kind, - ICalTime * (* get_prop_func) (ICalProperty *prop), - ICalTimezone *default_zone) -{ - ICalProperty *prop; - ICalTime *ical_time; - ICalParameter *param; - ICalTimezone *timezone; - time_t retval; - - prop = i_cal_component_get_first_property (icomp, prop_kind); - if (!prop) - return 0; - - param = i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER); - ical_time = get_prop_func (prop); - g_object_unref (prop); +/* ========================================================================= + * Auto-discovery helpers + * ========================================================================= */ - if (param) +#ifdef HAVE_LIBICAL +static void +add_vdir_providers_from_path (CalendarClient *client, const char *path) +{ + GSList *providers = calendar_vdir_discover (path); + for (GSList *l = providers; l != NULL; l = l->next) { - const char *tzid; + calendar_client_add_provider (client, CALENDAR_PROVIDER (l->data)); + g_object_unref (l->data); + } + g_slist_free (providers); +} +#endif /* HAVE_LIBICAL */ - tzid = i_cal_parameter_get_tzid (param); - timezone = i_cal_timezone_get_builtin_timezone_from_tzid (tzid); - g_object_unref (param); +/* ========================================================================= + * Public constructor + * ========================================================================= */ - /* If timezone lookup failed, fall back to default zone */ - if (!timezone) { - timezone = default_zone; - } - } - else if (i_cal_time_is_utc (ical_time)) - { - timezone = i_cal_timezone_get_utc_timezone (); - } - else - { - timezone = default_zone; - } +CalendarClient * +calendar_client_new (GSettings *settings) +{ + CalendarClient *client = g_object_new (CALENDAR_TYPE_CLIENT, NULL); - retval = i_cal_time_as_timet_with_zone (ical_time, timezone); +#ifdef HAVE_EDS + { + CalendarProvider *eds = calendar_eds_provider_new (settings); + if (eds != NULL) + { + calendar_client_add_provider (client, eds); + g_object_unref (eds); + } + } +#endif /* HAVE_EDS */ - g_object_unref (ical_time); +#ifdef HAVE_LIBICAL + { + /* Auto-discover vdirsyncer's default storage path */ + const char *data_home = g_get_user_data_dir (); + char *vdir_base = g_build_filename (data_home, VDIRSYNCER_DEFAULT_SUBDIR, NULL); + add_vdir_providers_from_path (client, vdir_base); + g_free (vdir_base); - return retval; + /* Additional paths from GSettings (if the key exists in @settings) */ + if (settings != NULL) + { + /* Check whether this settings instance has the vdir key before + * calling get_strv (avoids a GLib critical if key is absent). */ + gchar **all_keys = g_settings_list_keys (settings); + gboolean has_key = FALSE; + for (int ki = 0; all_keys[ki] != NULL; ki++) + if (g_strcmp0 (all_keys[ki], KEY_VDIR_CALENDAR_PATHS) == 0) + { has_key = TRUE; break; } + g_strfreev (all_keys); + + if (has_key) + { + gchar **paths = g_settings_get_strv (settings, KEY_VDIR_CALENDAR_PATHS); + for (gint i = 0; paths[i] != NULL; i++) + { + CalendarProvider *p = calendar_vdir_provider_new (paths[i]); + if (p != NULL) + { + calendar_client_add_provider (client, p); + g_object_unref (p); + } + } + g_strfreev (paths); + } /* if (has_key) */ + } /* if (settings != NULL) */ + } /* HAVE_LIBICAL block */ +#endif /* HAVE_LIBICAL */ + + (void) settings; /* suppress warning when both EDS and libical are absent */ + return client; } -static char * -get_component_uid (ICalComponent *component) +/* ========================================================================= + * Date selection + * ========================================================================= */ + +void +calendar_client_get_date (CalendarClient *client, + guint *year, + guint *month, + guint *day) { - return g_strdup (i_cal_component_get_uid (component)); + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + + if (year) *year = client->priv->year; + if (month) *month = client->priv->month; + if (day) *day = client->priv->day; } -static char * -get_component_rid (ICalComponent *component) +void +calendar_client_select_month (CalendarClient *client, + guint month, + guint year) { - ICalProperty *prop; - ICalTime *time; - char *rid; - - prop = i_cal_component_get_first_property (component, I_CAL_RECURRENCEID_PROPERTY); - if (!prop) - return NULL; + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (month <= 11); - time = i_cal_property_get_recurrenceid (prop); - g_object_unref (prop); + if (client->priv->month == month && client->priv->year == year) + return; - if (!i_cal_time_is_valid_time (time) || i_cal_time_is_null_time (time)) - { - g_object_unref (time); - return NULL; - } + client->priv->month = month; + client->priv->year = year; - rid = g_strdup (i_cal_time_as_ical_string (time)); - g_object_unref (time); + for (GSList *l = client->priv->providers; l != NULL; l = l->next) + calendar_provider_select_month (CALENDAR_PROVIDER (l->data), month, year); - return rid; + g_object_freeze_notify (G_OBJECT (client)); + g_object_notify (G_OBJECT (client), "month"); + g_object_notify (G_OBJECT (client), "year"); + g_object_thaw_notify (G_OBJECT (client)); } -static char * -get_component_summary (ICalComponent *component) +void +calendar_client_select_day (CalendarClient *client, guint day) { - ICalProperty *prop; - char *summary; + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (day <= 31); - prop = i_cal_component_get_first_property (component, I_CAL_SUMMARY_PROPERTY); - if (!prop) - return NULL; + if (client->priv->day == day) + return; - summary = g_strdup (i_cal_property_get_summary (prop)); - g_object_unref (prop); + client->priv->day = day; - return summary; + for (GSList *l = client->priv->providers; l != NULL; l = l->next) + calendar_provider_select_day (CALENDAR_PROVIDER (l->data), day); + + g_object_notify (G_OBJECT (client), "day"); } -static char * -get_component_description (ICalComponent *component) +/* ========================================================================= + * Time range helpers + * ========================================================================= */ + +static inline time_t +make_time_for_day_begin (int day, int month, int year) { - ICalProperty *prop; - char *description; + struct tm tm = { 0, }; + tm.tm_mday = day; + tm.tm_mon = month; + tm.tm_year = year - 1900; + tm.tm_isdst = -1; + return mktime (&tm); +} - prop = i_cal_component_get_first_property (component, I_CAL_DESCRIPTION_PROPERTY); - if (!prop) - return NULL; +/* ========================================================================= + * Event retrieval + * ========================================================================= */ - description = g_strdup (i_cal_property_get_description (prop)); - g_object_unref (prop); +/* Explicit update triggers (kept for backward compatibility with + * calendar-window.c which calls them after certain state changes) */ +void +calendar_client_update_appointments (CalendarClient *client) +{ + g_return_if_fail (CALENDAR_IS_CLIENT (client)); - return description; -} + if (client->priv->month == G_MAXUINT || client->priv->year == G_MAXUINT) + return; -static inline time_t -get_component_start_time (ICalComponent *component, - ICalTimezone *default_zone) -{ - return get_time_from_property (component, - I_CAL_DTSTART_PROPERTY, - i_cal_property_get_dtstart, - default_zone); + /* Re-issue select_month so providers re-run their queries */ + guint month = client->priv->month; + guint year = client->priv->year; + client->priv->month = G_MAXUINT; /* force change detection */ + calendar_client_select_month (client, month, year); } -static inline time_t -get_component_end_time (ICalComponent *component, - ICalTimezone *default_zone) +void +calendar_client_update_tasks (CalendarClient *client) { - return get_time_from_property (component, - I_CAL_DTEND_PROPERTY, - i_cal_property_get_dtend, - default_zone); + calendar_client_update_appointments (client); /* providers refresh both */ } -static gboolean -get_component_is_all_day (ICalComponent *component, - time_t start_time, - ICalTimezone *default_zone) +GSList * +calendar_client_get_events (CalendarClient *client, + CalendarEventType event_mask) { - ICalTime *dtstart; - struct tm *start_tm; - time_t end_time; - ICalProperty *prop; - ICalDuration *duration; - gboolean is_all_day; + g_return_val_if_fail (CALENDAR_IS_CLIENT (client), NULL); + g_return_val_if_fail (client->priv->day != G_MAXUINT, NULL); + g_return_val_if_fail (client->priv->month != G_MAXUINT, NULL); + g_return_val_if_fail (client->priv->year != G_MAXUINT, NULL); - dtstart = i_cal_component_get_dtstart (component); + time_t day_begin = make_time_for_day_begin ((int) client->priv->day, + (int) client->priv->month, + (int) client->priv->year); + time_t day_end = make_time_for_day_begin ((int) client->priv->day + 1, + (int) client->priv->month, + (int) client->priv->year); - if (dtstart && i_cal_time_is_date (dtstart)) + GSList *result = NULL; + for (GSList *l = client->priv->providers; l != NULL; l = l->next) { - g_object_unref (dtstart); - return TRUE; + GSList *events = + calendar_provider_get_events (CALENDAR_PROVIDER (l->data), + event_mask, day_begin, day_end); + result = g_slist_concat (result, events); } - g_object_unref (dtstart); - - start_tm = gmtime (&start_time); - if (start_tm->tm_sec != 0 || - start_tm->tm_min != 0 || - start_tm->tm_hour != 0) - return FALSE; - - if ((end_time = get_component_end_time (component, default_zone))) - return (end_time - start_time) % 86400 == 0; - - prop = i_cal_component_get_first_property (component, I_CAL_DURATION_PROPERTY); - if (!prop) - return FALSE; - - duration = i_cal_property_get_duration (prop); - g_object_unref (prop); - - is_all_day = i_cal_duration_as_int (duration) % 86400 == 0; - g_object_unref (duration); - - return is_all_day; + return result; } -static inline time_t -get_component_due_time (ICalComponent *component, - ICalTimezone *default_zone) +static inline int +day_from_time_t (time_t t) { - return get_time_from_property (component, - I_CAL_DUE_PROPERTY, - i_cal_property_get_due, - default_zone); + struct tm *tm = localtime (&t); + return (tm && tm->tm_mday >= 1 && tm->tm_mday <= 31) ? tm->tm_mday : 0; } -static guint -get_component_percent_complete (ICalComponent *component) +void +calendar_client_foreach_appointment_day (CalendarClient *client, + CalendarDayIter iter_func, + gpointer user_data) { - ICalPropertyStatus status; - ICalProperty *prop; - int percent_complete; - - status = i_cal_component_get_status (component); - if (status == I_CAL_STATUS_COMPLETED) - return 100; + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (iter_func != NULL); + g_return_if_fail (client->priv->month != G_MAXUINT); + g_return_if_fail (client->priv->year != G_MAXUINT); - prop = i_cal_component_get_first_property (component, I_CAL_COMPLETED_PROPERTY); + time_t month_begin = make_time_for_day_begin (1, + (int) client->priv->month, + (int) client->priv->year); + time_t month_end = make_time_for_day_begin (1, + (int) client->priv->month + 1, + (int) client->priv->year); - if (prop) + /* Collect appointments for the entire month from all providers */ + GSList *all_appts = NULL; + for (GSList *l = client->priv->providers; l != NULL; l = l->next) { - g_object_unref (prop); - return 100; + GSList *events = + calendar_provider_get_events (CALENDAR_PROVIDER (l->data), + CALENDAR_EVENT_APPOINTMENT, + month_begin, month_end); + all_appts = g_slist_concat (all_appts, events); } - prop = i_cal_component_get_first_property (component, I_CAL_PERCENTCOMPLETE_PROPERTY); - if (!prop) - return 0; - - percent_complete = i_cal_property_get_percentcomplete (prop); - g_object_unref (prop); + /* Mark days */ + gboolean marked_days[32] = { FALSE, }; - return CLAMP (percent_complete, 0, 100); -} + for (GSList *l = all_appts; l != NULL; l = l->next) + { + CalendarAppointment *appt = CALENDAR_APPOINTMENT (l->data); -static inline time_t -get_component_completed_time (ICalComponent *component, - ICalTimezone *default_zone) -{ - return get_time_from_property (component, - I_CAL_COMPLETED_PROPERTY, - i_cal_property_get_completed, - default_zone); -} + if (!appt->start_time) + continue; -static int -get_component_priority (ICalComponent *component) -{ - ICalProperty *prop; - int priority; + time_t day_time = appt->start_time; + if (day_time >= month_begin) + marked_days[day_from_time_t (day_time)] = TRUE; - prop = i_cal_component_get_first_property (component, I_CAL_PRIORITY_PROPERTY); - if (!prop) - return -1; + if (appt->end_time) + { + int duration = (int)(appt->end_time - appt->start_time); + for (int offset = 1; + offset <= duration / 86400 && duration != offset * 86400; + offset++) + { + time_t day_tm = appt->start_time + offset * 86400; + if (day_tm > month_end) + break; + if (day_tm >= month_begin) + marked_days[day_from_time_t (day_tm)] = TRUE; + } + } - priority = i_cal_property_get_priority (prop); - g_object_unref (prop); + calendar_event_free (CALENDAR_EVENT (l->data)); + } + g_slist_free (all_appts); - return priority; + for (int i = 1; i < 32; i++) + if (marked_days[i]) + iter_func (client, (guint) i, user_data); } -static char * -get_source_color (ECalClient *esource) -{ - ESource *source; - ECalClientSourceType source_type; - ESourceSelectable *extension; - const gchar *extension_name; - - g_return_val_if_fail (E_IS_CAL_CLIENT (esource), NULL); +/* ========================================================================= + * Task mutation (delegated to the first provider that supports it) + * ========================================================================= */ - source = e_client_get_source (E_CLIENT (esource)); - source_type = e_cal_client_get_source_type (esource); +void +calendar_client_set_task_completed (CalendarClient *client, + char *task_uid, + gboolean task_completed, + guint percent_complete) +{ + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (task_uid != NULL); - switch (source_type) + for (GSList *l = client->priv->providers; l != NULL; l = l->next) { - case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: - extension_name = E_SOURCE_EXTENSION_CALENDAR; - break; - case E_CAL_CLIENT_SOURCE_TYPE_TASKS: - extension_name = E_SOURCE_EXTENSION_TASK_LIST; - break; - case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: - case E_CAL_CLIENT_SOURCE_TYPE_LAST: - default: - g_return_val_if_reached (NULL); + CalendarProviderClass *klass = + CALENDAR_PROVIDER_GET_CLASS (CALENDAR_PROVIDER (l->data)); + if (klass->set_task_completed != NULL) + { + calendar_provider_set_task_completed (CALENDAR_PROVIDER (l->data), + task_uid, + task_completed, + percent_complete); + return; + } } - - extension = e_source_get_extension (source, extension_name); - - return e_source_selectable_dup_color (extension); } -static gchar * -get_source_backend_name (ECalClient *esource) +gboolean +calendar_client_create_task (CalendarClient *client, + const char *summary) { - ESource *source; - ECalClientSourceType source_type; - ESourceBackend *extension; - const gchar *extension_name; - - g_return_val_if_fail (E_IS_CAL_CLIENT (esource), NULL); - - source = e_client_get_source (E_CLIENT (esource)); - source_type = e_cal_client_get_source_type (esource); + g_return_val_if_fail (CALENDAR_IS_CLIENT (client), FALSE); + g_return_val_if_fail (summary != NULL && *summary != '\0', FALSE); - switch (source_type) + for (GSList *l = client->priv->providers; l != NULL; l = l->next) { - case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: - extension_name = E_SOURCE_EXTENSION_CALENDAR; - break; - case E_CAL_CLIENT_SOURCE_TYPE_TASKS: - extension_name = E_SOURCE_EXTENSION_TASK_LIST; - break; - case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: - case E_CAL_CLIENT_SOURCE_TYPE_LAST: - default: - g_return_val_if_reached (NULL); + CalendarProviderClass *klass = + CALENDAR_PROVIDER_GET_CLASS (CALENDAR_PROVIDER (l->data)); + if (klass->create_task != NULL) + return calendar_provider_create_task (CALENDAR_PROVIDER (l->data), summary); } - extension = e_source_get_extension (source, extension_name); - - return e_source_backend_dup_backend_name (extension); + return FALSE; } - -static inline gboolean -calendar_appointment_equal (CalendarAppointment *a, - CalendarAppointment *b) -{ - GSList *la, *lb; - - if (g_slist_length (a->occurrences) != g_slist_length (b->occurrences)) - return FALSE; - - for (la = a->occurrences, lb = b->occurrences; la && lb; la = la->next, lb = lb->next) - { - CalendarOccurrence *oa = la->data; - CalendarOccurrence *ob = lb->data; - - if (oa->start_time != ob->start_time || - oa->end_time != ob->end_time) - return FALSE; - } - - return - g_strcmp0 (a->uid, b->uid) == 0 && - g_strcmp0 (a->backend_name, b->backend_name) == 0 && - g_strcmp0 (a->summary, b->summary) == 0 && - g_strcmp0 (a->description, b->description) == 0 && - g_strcmp0 (a->color_string, b->color_string) == 0 && - a->start_time == b->start_time && - a->end_time == b->end_time && - a->is_all_day == b->is_all_day; -} - -static void -calendar_appointment_copy (CalendarAppointment *appointment, - CalendarAppointment *appointment_copy) -{ - GSList *l; - - g_assert (appointment != NULL); - g_assert (appointment_copy != NULL); - - appointment_copy->occurrences = g_slist_copy (appointment->occurrences); - for (l = appointment_copy->occurrences; l; l = l->next) - { - CalendarOccurrence *occurrence = l->data; - CalendarOccurrence *occurrence_copy; - - occurrence_copy = g_new0 (CalendarOccurrence, 1); - occurrence_copy->start_time = occurrence->start_time; - occurrence_copy->end_time = occurrence->end_time; - - l->data = occurrence_copy; - } - - appointment_copy->uid = g_strdup (appointment->uid); - appointment_copy->backend_name = g_strdup (appointment->backend_name); - appointment_copy->summary = g_strdup (appointment->summary); - appointment_copy->description = g_strdup (appointment->description); - appointment_copy->color_string = g_strdup (appointment->color_string); - appointment_copy->start_time = appointment->start_time; - appointment_copy->end_time = appointment->end_time; - appointment_copy->is_all_day = appointment->is_all_day; -} - -static void -calendar_appointment_finalize (CalendarAppointment *appointment) -{ - GSList *l; - - for (l = appointment->occurrences; l; l = l->next) - g_free (l->data); - g_slist_free (appointment->occurrences); - appointment->occurrences = NULL; - - g_free (appointment->uid); - appointment->uid = NULL; - - g_free (appointment->rid); - appointment->rid = NULL; - - g_free (appointment->backend_name); - appointment->backend_name = NULL; - - g_free (appointment->summary); - appointment->summary = NULL; - - g_free (appointment->description); - appointment->description = NULL; - - g_free (appointment->color_string); - appointment->color_string = NULL; - - appointment->start_time = 0; - appointment->is_all_day = FALSE; -} - -static void -calendar_appointment_init (CalendarAppointment *appointment, - ICalComponent *component, - CalendarClientSource *source, - ICalTimezone *default_zone) -{ - appointment->uid = get_component_uid (component); - appointment->rid = get_component_rid (component); - appointment->backend_name = get_source_backend_name (source->source); - appointment->summary = get_component_summary (component); - appointment->description = get_component_description (component); - appointment->color_string = get_source_color (source->source); - appointment->start_time = get_component_start_time (component, default_zone); - appointment->end_time = get_component_end_time (component, default_zone); - appointment->is_all_day = get_component_is_all_day (component, - appointment->start_time, - default_zone); -} - -static inline gboolean -calendar_task_equal (CalendarTask *a, - CalendarTask *b) -{ - return - g_strcmp0 (a->uid, b->uid) == 0 && - g_strcmp0 (a->summary, b->summary) == 0 && - g_strcmp0 (a->description, b->description) == 0 && - g_strcmp0 (a->color_string, b->color_string) == 0 && - a->start_time == b->start_time && - a->due_time == b->due_time && - a->percent_complete == b->percent_complete && - a->completed_time == b->completed_time && - a->priority == b->priority; -} - -static void -calendar_task_copy (CalendarTask *task, - CalendarTask *task_copy) -{ - g_assert (task != NULL); - g_assert (task_copy != NULL); - - task_copy->uid = g_strdup (task->uid); - task_copy->summary = g_strdup (task->summary); - task_copy->description = g_strdup (task->description); - task_copy->color_string = g_strdup (task->color_string); - task_copy->start_time = task->start_time; - task_copy->due_time = task->due_time; - task_copy->percent_complete = task->percent_complete; - task_copy->completed_time = task->completed_time; - task_copy->priority = task->priority; -} - -static void -calendar_task_finalize (CalendarTask *task) -{ - g_free (task->uid); - task->uid = NULL; - - g_free (task->summary); - task->summary = NULL; - - g_free (task->description); - task->description = NULL; - - g_free (task->color_string); - task->color_string = NULL; - - task->percent_complete = 0; -} - -static void -calendar_task_init (CalendarTask *task, - ICalComponent *component, - CalendarClientSource *source, - ICalTimezone *default_zone) -{ - task->uid = get_component_uid (component); - task->summary = get_component_summary (component); - task->description = get_component_description (component); - task->color_string = get_source_color (source->source); - task->start_time = get_component_start_time (component, default_zone); - task->due_time = get_component_due_time (component, default_zone); - task->percent_complete = get_component_percent_complete (component); - task->completed_time = get_component_completed_time (component, default_zone); - task->priority = get_component_priority (component); -} - -void -calendar_event_free (CalendarEvent *event) -{ - switch (event->type) - { - case CALENDAR_EVENT_APPOINTMENT: - calendar_appointment_finalize (CALENDAR_APPOINTMENT (event)); - break; - case CALENDAR_EVENT_TASK: - calendar_task_finalize (CALENDAR_TASK (event)); - break; - case CALENDAR_EVENT_ALL: - default: - g_assert_not_reached (); - break; - } - - g_free (event); -} - -static CalendarEvent * -calendar_event_new (ICalComponent *component, - CalendarClientSource *source, - ICalTimezone *default_zone) -{ - CalendarEvent *event; - ICalComponentKind component_kind; - - event = g_new0 (CalendarEvent, 1); - component_kind = i_cal_component_isa (component); - - if (component_kind == I_CAL_VEVENT_COMPONENT) - { - event->type = CALENDAR_EVENT_APPOINTMENT; - calendar_appointment_init (CALENDAR_APPOINTMENT (event), - component, source, default_zone); - } - else if (component_kind == I_CAL_VTODO_COMPONENT) - { - event->type = CALENDAR_EVENT_TASK; - calendar_task_init (CALENDAR_TASK (event), - component, source, default_zone); - } - else - { - g_warning ("Unknown calendar component type: %d\n", component_kind); - g_free (event); - - return NULL; - } - - return event; -} - -static CalendarEvent * -calendar_event_copy (CalendarEvent *event) -{ - CalendarEvent *retval; - - if (!event) - return NULL; - - retval = g_new0 (CalendarEvent, 1); - - retval->type = event->type; - - switch (event->type) - { - case CALENDAR_EVENT_APPOINTMENT: - calendar_appointment_copy (CALENDAR_APPOINTMENT (event), - CALENDAR_APPOINTMENT (retval)); - break; - case CALENDAR_EVENT_TASK: - calendar_task_copy (CALENDAR_TASK (event), - CALENDAR_TASK (retval)); - break; - case CALENDAR_EVENT_ALL: - default: - g_assert_not_reached (); - break; - } - - return retval; -} - -static char * -calendar_event_get_uid (CalendarEvent *event) -{ - switch (event->type) - { - case CALENDAR_EVENT_APPOINTMENT: - return g_strdup_printf ("%s%s", CALENDAR_APPOINTMENT (event)->uid, CALENDAR_APPOINTMENT (event)->rid ? CALENDAR_APPOINTMENT (event)->rid : ""); - break; - case CALENDAR_EVENT_TASK: - return g_strdup (CALENDAR_TASK (event)->uid); - break; - case CALENDAR_EVENT_ALL: - default: - g_assert_not_reached (); - break; - } - - return NULL; -} - -static gboolean -calendar_event_equal (CalendarEvent *a, - CalendarEvent *b) -{ - if (!a && !b) - return TRUE; - - if ((a && !b) || (!a && b)) - return FALSE; - - if (a->type != b->type) - return FALSE; - - switch (a->type) - { - case CALENDAR_EVENT_APPOINTMENT: - return calendar_appointment_equal (CALENDAR_APPOINTMENT (a), - CALENDAR_APPOINTMENT (b)); - case CALENDAR_EVENT_TASK: - return calendar_task_equal (CALENDAR_TASK (a), - CALENDAR_TASK (b)); - case CALENDAR_EVENT_ALL: - default: - break; - } - - g_assert_not_reached (); - - return FALSE; -} - -static inline void -calendar_event_debug_dump (CalendarEvent *event) -{ -#ifdef CALENDAR_ENABLE_DEBUG - switch (event->type) - { - case CALENDAR_EVENT_APPOINTMENT: - { - char *start_str; - char *end_str; - GSList *l; - - start_str = CALENDAR_APPOINTMENT (event)->start_time ? - isodate_from_time_t (CALENDAR_APPOINTMENT (event)->start_time) : - g_strdup ("(undefined)"); - end_str = CALENDAR_APPOINTMENT (event)->end_time ? - isodate_from_time_t (CALENDAR_APPOINTMENT (event)->end_time) : - g_strdup ("(undefined)"); - - g_free (start_str); - g_free (end_str); - - for (l = CALENDAR_APPOINTMENT (event)->occurrences; l; l = l->next) - { - CalendarOccurrence *occurrence = l->data; - - start_str = occurrence->start_time ? - isodate_from_time_t (occurrence->start_time) : - g_strdup ("(undefined)"); - - end_str = occurrence->end_time ? - isodate_from_time_t (occurrence->end_time) : - g_strdup ("(undefined)"); - - g_free (start_str); - g_free (end_str); - } - } - break; - case CALENDAR_EVENT_TASK: - { - char *start_str; - char *due_str; - char *completed_str; - - start_str = CALENDAR_TASK (event)->start_time ? - isodate_from_time_t (CALENDAR_TASK (event)->start_time) : - g_strdup ("(undefined)"); - due_str = CALENDAR_TASK (event)->due_time ? - isodate_from_time_t (CALENDAR_TASK (event)->due_time) : - g_strdup ("(undefined)"); - completed_str = CALENDAR_TASK (event)->completed_time ? - isodate_from_time_t (CALENDAR_TASK (event)->completed_time) : - g_strdup ("(undefined)"); - - g_free (completed_str); - } - break; - default: - g_assert_not_reached (); - break; - } -#endif -} - -static inline CalendarClientQuery * -goddamn_this_is_crack (CalendarClientSource *source, - ECalClientView *view, - gboolean *emit_signal) -{ - g_assert (view != NULL); - - if (source->completed_query.view == view) - { - if (emit_signal) - *emit_signal = TRUE; - return &source->completed_query; - } - else if (source->in_progress_query.view == view) - { - if (emit_signal) - *emit_signal = FALSE; - return &source->in_progress_query; - } - - g_assert_not_reached (); - - return NULL; -} - -static void -calendar_client_query_finalize (CalendarClientQuery *query) -{ - if (query->view) - g_object_unref (query->view); - query->view = NULL; - - if (query->events) - g_hash_table_destroy (query->events); - query->events = NULL; -} - -static void -calendar_client_stop_query (CalendarClient *client, - CalendarClientSource *source, - CalendarClientQuery *query) -{ - if (query == &source->in_progress_query) - { - g_assert (source->query_in_progress != FALSE); - - source->query_in_progress = FALSE; - } - else if (query == &source->completed_query) - { - g_assert (source->query_completed != FALSE); - - source->query_completed = FALSE; - } - else - g_assert_not_reached (); - - calendar_client_query_finalize (query); -} - -typedef struct { - CalendarClient *client; - CalendarClientSource *source; - time_t start_time; - time_t end_time; - gboolean events_changed; - ICalTimezone *system_timezone; -} InstanceGenerationData; - -static gboolean -calendar_client_instance_cb (ICalComponent *icomp, - ICalTime *instance_start, - ICalTime *instance_end, - gpointer user_data, - GCancellable *cancellable, - GError **error) -{ - InstanceGenerationData *data = user_data; - CalendarEvent *event; - CalendarEvent *old_event; - char *uid; - time_t start_time_t, end_time_t; - - /* Convert instance times from their timezone to local time */ - ICalTimezone *event_tz = i_cal_time_get_timezone(instance_start); - - ICalTime *local_start = i_cal_time_clone(instance_start); - ICalTime *local_end = i_cal_time_clone(instance_end); - - /* Convert to local timezone */ - if (event_tz && data->system_timezone && event_tz != data->system_timezone) { - i_cal_time_convert_timezone(local_start, event_tz, data->system_timezone); - i_cal_time_convert_timezone(local_end, event_tz, data->system_timezone); - } - - start_time_t = i_cal_time_as_timet (local_start); - end_time_t = i_cal_time_as_timet (local_end); - - g_object_unref(local_start); - g_object_unref(local_end); - - /* Create event from the component */ - event = calendar_event_new (icomp, data->source, data->client->priv->zone); - if (!event) - return TRUE; - - /* Override the times with the instance times (already converted to local timezone) */ - if (event->type == CALENDAR_EVENT_APPOINTMENT) { - CALENDAR_APPOINTMENT (event)->start_time = start_time_t; - CALENDAR_APPOINTMENT (event)->end_time = end_time_t; - - /* Create a single occurrence for this instance */ - CalendarOccurrence *occurrence = g_new0 (CalendarOccurrence, 1); - occurrence->start_time = start_time_t; - occurrence->end_time = end_time_t; - CALENDAR_APPOINTMENT (event)->occurrences = g_slist_prepend (NULL, occurrence); - } - - uid = calendar_event_get_uid (event); - old_event = g_hash_table_lookup (data->source->in_progress_query.events, uid); - - if (!calendar_event_equal (event, old_event)) { - calendar_event_debug_dump (event); - g_hash_table_replace (data->source->in_progress_query.events, uid, event); - data->events_changed = TRUE; - } else { - calendar_event_free (event); - g_free (uid); - } - - return TRUE; -} - -static void -calendar_client_start_query (CalendarClient *client, - CalendarClientSource *source, - const char *query) -{ - time_t month_begin, month_end; - GSList *objects = NULL; - GError *error = NULL; - InstanceGenerationData instance_data; - - /* Validate that client is properly initialized */ - if (client->priv->month == G_MAXUINT || client->priv->year == G_MAXUINT) { - return; - } - - /* Calculate time range */ - month_begin = make_time_for_day_begin (1, client->priv->month, client->priv->year); - - /* Handle year rollover when month is December (11) */ - if (client->priv->month == 11) { /* December */ - month_end = make_time_for_day_begin (1, 0, client->priv->year + 1); /* January next year */ - } else { - month_end = make_time_for_day_begin (1, client->priv->month + 1, client->priv->year); - } - - /* Validate time range */ - if (month_begin == -1 || month_end == -1) { - g_warning ("Invalid time range: month_begin=%ld, month_end=%ld", (long)month_begin, (long)month_end); - calendar_client_stop_query (client, source, &source->in_progress_query); - return; - } - - if (source->query_in_progress) - calendar_client_stop_query (client, source, &source->in_progress_query); - - source->query_in_progress = TRUE; - source->in_progress_query.view = NULL; /* No view needed for instance generation */ - source->in_progress_query.events = g_hash_table_new_full (g_str_hash, - g_str_equal, - g_free, - (GDestroyNotify) calendar_event_free); - - /* Get all objects for the month using query */ - if (!e_cal_client_get_object_list_sync (source->source, query, &objects, NULL, &error)) { - g_warning ("Error getting calendar objects: %s", error->message); - g_error_free (error); - calendar_client_stop_query (client, source, &source->in_progress_query); - return; - } - - /* Get system timezone once for all instances */ - SystemTimezone *systz = system_timezone_new(); - const char *system_tz_name = system_timezone_get(systz); - ICalTimezone *system_timezone = i_cal_timezone_get_builtin_timezone(system_tz_name); - g_object_unref(systz); - - /* Set up instance generation data */ - instance_data.client = client; - instance_data.source = source; - instance_data.start_time = month_begin; - instance_data.end_time = month_end; - instance_data.events_changed = FALSE; - instance_data.system_timezone = system_timezone; - - /* Generate instances for each object with automatic timezone conversion */ - for (GSList *l = objects; l; l = l->next) { - ICalComponent *component = l->data; - - /* Some instances of recurring events may yield negative months - I think these are safe to skip */ - if (month_begin < 0 || month_end < 0) { - continue; - } - - e_cal_client_generate_instances_for_object_sync (source->source, - component, - month_begin, - month_end, - NULL, /* cancellable */ - calendar_client_instance_cb, - &instance_data); - } - - g_slist_free_full (objects, g_object_unref); - - /* Query is now completed */ - calendar_client_query_finalize (&source->completed_query); - source->completed_query = source->in_progress_query; - source->query_completed = TRUE; - - source->query_in_progress = FALSE; - source->in_progress_query.view = NULL; - source->in_progress_query.events = NULL; - - /* Emit signal to capture changed events */ - if (instance_data.events_changed) { - g_signal_emit (source->client, source->changed_signal_id, 0); - } -} - -void -calendar_client_update_appointments (CalendarClient *client) -{ - GSList *l; - char *query; - char *month_begin; - char *month_end; - - if (client->priv->month == G_MAXUINT || client->priv->year == G_MAXUINT) - return; - - month_begin = make_isodate_for_day_begin (1, - client->priv->month, - client->priv->year); - - month_end = make_isodate_for_day_begin (1, - client->priv->month + 1, - client->priv->year); - - query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") " - "(make-time \"%s\")", - month_begin, month_end); - - for (l = client->priv->appointment_sources; l; l = l->next) - { - CalendarClientSource *cs = l->data; - - calendar_client_start_query (client, cs, query); - } - - g_free (month_begin); - g_free (month_end); - g_free (query); -} - -/* FIXME: - * perhaps we should use evo's "hide_completed_tasks" pref? - */ -void -calendar_client_update_tasks (CalendarClient *client) -{ - GSList *l; - char *query; - -#ifdef FIX_BROKEN_TASKS_QUERY - /* FIXME: this doesn't work for tasks without a start or - * due date - * Look at filter_task() to see the behaviour we - * want. - */ - - char *day_begin; - char *day_end; - - if (client->priv->day == G_MAXUINT || - client->priv->month == G_MAXUINT || - client->priv->year == G_MAXUINT) - return; - - day_begin = make_isodate_for_day_begin (client->priv->day, - client->priv->month, - client->priv->year); - - day_end = make_isodate_for_day_begin (client->priv->day + 1, - client->priv->month, - client->priv->year); - if (!day_begin || !day_end) - { - g_warning ("Cannot run query with invalid date: %dd %dy %dm\n", - client->priv->day, - client->priv->month, - client->priv->year); - g_free (day_begin); - g_free (day_end); - return; - } - - query = g_strdup_printf ("(and (occur-in-time-range? (make-time \"%s\") " - "(make-time \"%s\")) " - "(or (not is-completed?) " - "(and (is-completed?) " - "(not (completed-before? (make-time \"%s\"))))))", - day_begin, day_end, day_begin); -#else - query = g_strdup ("#t"); -#endif /* FIX_BROKEN_TASKS_QUERY */ - - for (l = client->priv->task_sources; l; l = l->next) - { - CalendarClientSource *cs = l->data; - - calendar_client_start_query (client, cs, query); - } - -#ifdef FIX_BROKEN_TASKS_QUERY - g_free (day_begin); - g_free (day_end); -#endif - g_free (query); -} - -static void -calendar_client_source_finalize (CalendarClientSource *source) -{ - source->client = NULL; - - if (source->source) { - g_object_unref (source->source); - } - source->source = NULL; - - calendar_client_query_finalize (&source->completed_query); - calendar_client_query_finalize (&source->in_progress_query); - - source->query_completed = FALSE; - source->query_in_progress = FALSE; -} - -static int -compare_calendar_sources (CalendarClientSource *s1, - CalendarClientSource *s2) -{ - return (s1->source == s2->source) ? 0 : 1; -} - -static GSList * -calendar_client_update_sources_list (CalendarClient *client, - GSList *sources, - GList *esources, - guint changed_signal_id) -{ - GList *link; - GSList *retval, *l; - - retval = NULL; - - for (link = esources; link != NULL; link = g_list_next (link)) - { - CalendarClientSource dummy_source; - CalendarClientSource *new_source; - GSList *s; - ECalClient *esource = link->data; - - dummy_source.source = esource; - - if ((s = g_slist_find_custom (sources, - &dummy_source, - (GCompareFunc) compare_calendar_sources))) - { - new_source = s->data; - sources = g_slist_delete_link (sources, s); - } - else - { - new_source = g_new0 (CalendarClientSource, 1); - new_source->client = client; - new_source->source = g_object_ref (esource); - new_source->changed_signal_id = changed_signal_id; - } - - retval = g_slist_prepend (retval, new_source); - } - - for (l = sources; l; l = l->next) - { - CalendarClientSource *source = l->data; - - calendar_client_source_finalize (source); - g_free (source); - } - g_slist_free (sources); - - return retval; -} - -static void -calendar_client_appointment_sources_changed (CalendarClient *client) -{ - GList *list; - - list = calendar_sources_get_appointment_clients (client->priv->calendar_sources); - - client->priv->appointment_sources = calendar_client_update_sources_list (client, - client->priv->appointment_sources, - list, - signals [APPOINTMENTS_CHANGED]); - - load_calendars (client, CALENDAR_EVENT_APPOINTMENT); - calendar_client_update_appointments (client); - - g_list_free (list); -} - -static void -calendar_client_task_sources_changed (CalendarClient *client) -{ - GList *list; - - list = calendar_sources_get_task_clients (client->priv->calendar_sources); - - client->priv->task_sources = calendar_client_update_sources_list (client, - client->priv->task_sources, - list, - signals [TASKS_CHANGED]); - - load_calendars (client, CALENDAR_EVENT_TASK); - calendar_client_update_tasks (client); - - g_list_free (list); -} - -void -calendar_client_get_date (CalendarClient *client, - guint *year, - guint *month, - guint *day) -{ - g_return_if_fail (CALENDAR_IS_CLIENT (client)); - - if (year) - *year = client->priv->year; - - if (month) - *month = client->priv->month; - - if (day) - *day = client->priv->day; -} - -void -calendar_client_select_month (CalendarClient *client, - guint month, - guint year) -{ - g_return_if_fail (CALENDAR_IS_CLIENT (client)); - g_return_if_fail (month <= 11); - - if (client->priv->year != year || client->priv->month != month) - { - client->priv->month = month; - client->priv->year = year; - - calendar_client_update_appointments (client); - calendar_client_update_tasks (client); - - g_object_freeze_notify (G_OBJECT (client)); - g_object_notify (G_OBJECT (client), "month"); - g_object_notify (G_OBJECT (client), "year"); - g_object_thaw_notify (G_OBJECT (client)); - } -} - -void -calendar_client_select_day (CalendarClient *client, - guint day) -{ - g_return_if_fail (CALENDAR_IS_CLIENT (client)); - g_return_if_fail (day <= 31); - - if (client->priv->day != day) - { - client->priv->day = day; - - /* don't need to update appointments unless - * the selected month changes - */ -#ifdef FIX_BROKEN_TASKS_QUERY - calendar_client_update_tasks (client); -#endif - - g_object_notify (G_OBJECT (client), "day"); - } -} - -typedef struct -{ - CalendarClient *client; - GSList *events; - time_t start_time; - time_t end_time; -} FilterData; - -typedef void (* CalendarEventFilterFunc) (const char *uid, - CalendarEvent *event, - FilterData *filter_data); - -static void -filter_appointment (const char *uid, - CalendarEvent *event, - FilterData *filter_data) -{ - GSList *occurrences, *l; - - if (event->type != CALENDAR_EVENT_APPOINTMENT) - return; - - occurrences = CALENDAR_APPOINTMENT (event)->occurrences; - CALENDAR_APPOINTMENT (event)->occurrences = NULL; - - for (l = occurrences; l; l = l->next) - { - CalendarOccurrence *occurrence = l->data; - time_t start_time = occurrence->start_time; - time_t end_time = occurrence->end_time; - - if ((start_time >= filter_data->start_time && - start_time < filter_data->end_time) || - (start_time <= filter_data->start_time && - (end_time - 1) > filter_data->start_time)) - { - CalendarEvent *new_event; - - new_event = calendar_event_copy (event); - - CALENDAR_APPOINTMENT (new_event)->start_time = occurrence->start_time; - CALENDAR_APPOINTMENT (new_event)->end_time = occurrence->end_time; - - filter_data->events = g_slist_prepend (filter_data->events, new_event); - } - } - - CALENDAR_APPOINTMENT (event)->occurrences = occurrences; -} - -static void -filter_task (const char *uid, - CalendarEvent *event, - FilterData *filter_data) -{ -#ifdef FIX_BROKEN_TASKS_QUERY - CalendarTask *task; -#endif - - if (event->type != CALENDAR_EVENT_TASK) - return; - -#ifdef FIX_BROKEN_TASKS_QUERY - task = CALENDAR_TASK (event); - - if (task->start_time && task->start_time > filter_data->start_time) - return; - - if (task->completed_time && - (task->completed_time < filter_data->start_time || - task->completed_time > filter_data->end_time)) - return; -#endif /* FIX_BROKEN_TASKS_QUERY */ - - filter_data->events = g_slist_prepend (filter_data->events, - calendar_event_copy (event)); -} - -static GSList * -calendar_client_filter_events (CalendarClient *client, - GSList *sources, - CalendarEventFilterFunc filter_func, - time_t start_time, - time_t end_time) -{ - FilterData filter_data; - GSList *l; - GSList *retval; - - if (!sources) - return NULL; - - filter_data.client = client; - filter_data.events = NULL; - filter_data.start_time = start_time; - filter_data.end_time = end_time; - - retval = NULL; - for (l = sources; l; l = l->next) - { - CalendarClientSource *source = l->data; - - if (source->query_completed) - { - filter_data.events = NULL; - g_hash_table_foreach (source->completed_query.events, - (GHFunc) filter_func, - &filter_data); - - filter_data.events = g_slist_reverse (filter_data.events); - - retval = g_slist_concat (retval, filter_data.events); - } - } - - return retval; -} - -GSList * -calendar_client_get_events (CalendarClient *client, - CalendarEventType event_mask) -{ - GSList *appointments; - GSList *tasks; - time_t day_begin; - time_t day_end; - - g_return_val_if_fail (CALENDAR_IS_CLIENT (client), NULL); - g_return_val_if_fail (client->priv->day != G_MAXUINT, NULL); - g_return_val_if_fail (client->priv->month != G_MAXUINT, NULL); - g_return_val_if_fail (client->priv->year != G_MAXUINT, NULL); - - day_begin = make_time_for_day_begin (client->priv->day, - client->priv->month, - client->priv->year); - day_end = make_time_for_day_begin (client->priv->day + 1, - client->priv->month, - client->priv->year); - - appointments = NULL; - if (event_mask & CALENDAR_EVENT_APPOINTMENT) - { - appointments = calendar_client_filter_events (client, - client->priv->appointment_sources, - filter_appointment, - day_begin, - day_end); - } - - tasks = NULL; - if (event_mask & CALENDAR_EVENT_TASK) - { - tasks = calendar_client_filter_events (client, - client->priv->task_sources, - filter_task, - day_begin, - day_end); - } - - return g_slist_concat (appointments, tasks); -} - -static inline int -day_from_time_t (time_t t) -{ - struct tm *tm = localtime (&t); - - g_assert (tm == NULL || (tm->tm_mday >=1 && tm->tm_mday <= 31)); - - return tm ? tm->tm_mday : 0; -} - -void -calendar_client_foreach_appointment_day (CalendarClient *client, - CalendarDayIter iter_func, - gpointer user_data) -{ - GSList *appointments, *l; - gboolean marked_days [32] = { FALSE, }; - time_t month_begin; - time_t month_end; - int i; - - g_return_if_fail (CALENDAR_IS_CLIENT (client)); - g_return_if_fail (iter_func != NULL); - g_return_if_fail (client->priv->month != G_MAXUINT); - g_return_if_fail (client->priv->year != G_MAXUINT); - - month_begin = make_time_for_day_begin (1, - client->priv->month, - client->priv->year); - month_end = make_time_for_day_begin (1, - client->priv->month + 1, - client->priv->year); - - appointments = calendar_client_filter_events (client, - client->priv->appointment_sources, - filter_appointment, - month_begin, - month_end); - for (l = appointments; l; l = l->next) - { - CalendarAppointment *appointment = l->data; - - if (appointment->start_time) - { - time_t day_time = appointment->start_time; - - if (day_time >= month_begin) - marked_days [day_from_time_t (day_time)] = TRUE; - - if (appointment->end_time) - { - int day_offset; - int duration = appointment->end_time - appointment->start_time; - /* mark the days for the appointment, no need to add an extra one when duration is a multiple of 86400 */ - for (day_offset = 1; day_offset <= duration / 86400 && duration != day_offset * 86400; day_offset++) - { - time_t day_tm = appointment->start_time + day_offset * 86400; - - if (day_tm > month_end) - break; - if (day_tm >= month_begin) - marked_days [day_from_time_t (day_tm)] = TRUE; - } - } - } - calendar_event_free (CALENDAR_EVENT (appointment)); - } - - g_slist_free (appointments); - - for (i = 1; i < 32; i++) - { - if (marked_days [i]) - iter_func (client, i, user_data); - } -} - -void -calendar_client_set_task_completed (CalendarClient *client, - char *task_uid, - gboolean task_completed, - guint percent_complete) -{ - GSList *l; - ECalClient *esource; - ICalComponent *component; - ICalProperty *prop; - ICalPropertyStatus status; - - g_return_if_fail (CALENDAR_IS_CLIENT (client)); - g_return_if_fail (task_uid != NULL); - g_return_if_fail (task_completed == FALSE || percent_complete == 100); - - component = NULL; - esource = NULL; - for (l = client->priv->task_sources; l; l = l->next) - { - CalendarClientSource *source = l->data; - - esource = source->source; - e_cal_client_get_object_sync (esource, task_uid, NULL, &component, NULL, NULL); - if (component) - break; - } - - if (!component) - { - g_warning ("Cannot locate task with uid = '%s'\n", task_uid); - return; - } - - g_assert (esource != NULL); - - /* Completed time */ - prop = i_cal_component_get_first_property (component, I_CAL_COMPLETED_PROPERTY); - if (task_completed) - { - ICalTime *completed_time; - - completed_time = i_cal_time_new_current_with_zone (client->priv->zone); - if (!prop) - { - i_cal_component_take_property (component, - i_cal_property_new_completed (completed_time)); - } - else - { - i_cal_property_set_completed (prop, completed_time); - } - } - else if (prop) - { - i_cal_component_remove_property (component, prop); - } - g_clear_object (&prop); - - /* Percent complete */ - prop = i_cal_component_get_first_property (component, I_CAL_PERCENTCOMPLETE_PROPERTY); - if (!prop) - { - i_cal_component_take_property (component, - i_cal_property_new_percentcomplete (percent_complete)); - } - else - { - i_cal_property_set_percentcomplete (prop, percent_complete); - } - g_clear_object (&prop); - - /* Status */ - status = task_completed ? I_CAL_STATUS_COMPLETED : I_CAL_STATUS_NEEDSACTION; - prop = i_cal_component_get_first_property (component, I_CAL_STATUS_PROPERTY); - if (prop) - { - i_cal_property_set_status (prop, status); - } - else - { - i_cal_component_take_property (component, i_cal_property_new_status (status)); - } - g_clear_object (&prop); - - e_cal_client_modify_object_sync (esource, - component, - E_CAL_OBJ_MOD_ALL, - 0, - NULL, - NULL); -} - -gboolean -calendar_client_create_task (CalendarClient *client, - const char *summary) -{ - GSList *l; - ECalClient *task_client = NULL; - ICalComponent *vtodo_component; - gchar *uid; - GError *error = NULL; - gboolean success = FALSE; - - g_return_val_if_fail (CALENDAR_IS_CLIENT (client), FALSE); - g_return_val_if_fail (summary != NULL && *summary != '\0', FALSE); - - /* Use the first available task source (like the existing code does) */ - for (l = client->priv->task_sources; l; l = l->next) - { - CalendarClientSource *source = l->data; - task_client = source->source; - if (task_client) - break; - } - - if (!task_client) - { - g_warning ("No task client available for task creation"); - return FALSE; - } - - /* Create a simple VTODO component */ - vtodo_component = i_cal_component_new (I_CAL_VTODO_COMPONENT); - - /* Generate UID */ - uid = e_util_generate_uid (); - i_cal_component_set_uid (vtodo_component, uid); - g_free (uid); - - /* Set summary */ - i_cal_component_set_summary (vtodo_component, summary); - - /* Set created time */ - ICalTime *now = i_cal_time_new_current_with_zone (client->priv->zone); - i_cal_component_set_dtstamp (vtodo_component, now); - g_object_unref (now); - - /* Create the task */ - success = e_cal_client_create_object_sync (task_client, - vtodo_component, - E_CAL_OPERATION_FLAG_NONE, - NULL, /* out uid */ - NULL, /* cancellable */ - &error); - - if (error) - { - g_warning ("Failed to create task: %s", error->message); - g_error_free (error); - success = FALSE; - } - - /* Cleanup */ - g_object_unref (vtodo_component); - - return success; -} - -#endif /* HAVE_EDS */ diff --git a/applets/clock/calendar-client.h b/applets/clock/calendar-client.h index f74ac2c3a..38898be1e 100644 --- a/applets/clock/calendar-client.h +++ b/applets/clock/calendar-client.h @@ -147,7 +147,9 @@ gboolean calendar_client_create_task (CalendarClient *cl void calendar_client_update_appointments (CalendarClient *client); void calendar_client_update_tasks (CalendarClient *client); -void calendar_event_free (CalendarEvent *event); +void calendar_event_free (CalendarEvent *event); +CalendarEvent *calendar_event_copy (CalendarEvent *event); +gboolean calendar_event_equal (CalendarEvent *a, CalendarEvent *b); G_END_DECLS diff --git a/applets/clock/calendar-eds-provider.c b/applets/clock/calendar-eds-provider.c new file mode 100644 index 000000000..795d65cc0 --- /dev/null +++ b/applets/clock/calendar-eds-provider.c @@ -0,0 +1,1152 @@ +/* + * calendar-eds-provider.c: Evolution Data Server calendar source + * + * Absorbs the functionality previously split between calendar-sources.c + * (ESourceRegistry management) and the EDS-specific internals of + * calendar-client.c (query dispatch, instance generation, filtering). + * + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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 2 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 . + */ + +#include + +#include +#include +#define HANDLE_LIBICAL_MEMORY + +#include + +#include "calendar-eds-provider.h" +#include "system-timezone.h" + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +#ifndef _ +#define _(x) gettext(x) +#endif + +/* ========================================================================= + * Internal types + * ========================================================================= + * + * One CalendarEDSQuery holds the event hash-table for a single query pass. + * One CalendarEDSSource wraps a single ECalClient and owns two queries + * (completed = current results, in_progress = next results being built). + * Both queries are used only while calendar_eds_source_run_query() executes + * — after it returns, in_progress is swapped into completed synchronously. + */ + +typedef struct +{ + GHashTable *events; /* uid_string → CalendarEvent *; owned */ +} CalendarEDSQuery; + +typedef struct +{ + CalendarEDSProvider *provider; + ECalClient *client; + gulong backend_died_id; + + CalendarEDSQuery completed_query; + CalendarEDSQuery in_progress_query; + + guint query_completed : 1; + guint query_in_progress: 1; + + /* Which provider signal to emit when events change */ + gboolean is_appointments; /* TRUE=appointments, FALSE=tasks */ +} CalendarEDSSource; + +struct _CalendarEDSProviderPrivate +{ + ESourceRegistry *registry; + gulong source_added_id; + gulong source_changed_id; + gulong source_removed_id; + + /* ESource* → CalendarEDSSource* */ + GHashTable *appointment_clients; + GHashTable *task_clients; + + ICalTimezone *zone; + GSettings *calendar_settings; + gulong zone_listener; + + guint month; /* 0–11; G_MAXUINT = unset */ + guint year; /* e.g. 2025; G_MAXUINT = unset */ +}; + +G_DEFINE_TYPE_WITH_PRIVATE (CalendarEDSProvider, + calendar_eds_provider, + CALENDAR_TYPE_PROVIDER) + +/* Forward declarations */ +static void eds_load_source_list (CalendarEDSProvider *self, gboolean is_appointments); +static void eds_run_query (CalendarEDSProvider *self, gboolean is_appointments); +static void backend_died_cb (EClient *client, CalendarEDSSource *src); + +/* ========================================================================= + * CalendarEDSQuery helpers + * ========================================================================= */ + +static void +eds_query_init (CalendarEDSQuery *q) +{ + q->events = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) calendar_event_free); +} + +static void +eds_query_clear (CalendarEDSQuery *q) +{ + if (q->events) + { + g_hash_table_destroy (q->events); + q->events = NULL; + } +} + +/* ========================================================================= + * CalendarEDSSource helpers + * ========================================================================= */ + +static CalendarEDSSource * +eds_source_new (CalendarEDSProvider *provider, + ECalClient *client, + gboolean is_appointments) +{ + CalendarEDSSource *src = g_new0 (CalendarEDSSource, 1); + src->provider = provider; + src->client = g_object_ref (client); + src->is_appointments = is_appointments; + src->backend_died_id = + g_signal_connect (client, "backend-died", + G_CALLBACK (backend_died_cb), src); + return src; +} + +static void +eds_source_free (CalendarEDSSource *src) +{ + if (src->backend_died_id) + g_signal_handler_disconnect (src->client, src->backend_died_id); + + eds_query_clear (&src->completed_query); + eds_query_clear (&src->in_progress_query); + + g_object_unref (src->client); + g_free (src); +} + +/* ========================================================================= + * ICalComponent → CalendarEvent (private helpers) + * ========================================================================= */ + +static time_t +get_time_from_property (ICalComponent *icomp, + ICalPropertyKind prop_kind, + ICalTime *(*get_prop_func)(ICalProperty *), + ICalTimezone *default_zone) +{ + ICalProperty *prop = + i_cal_component_get_first_property (icomp, prop_kind); + if (!prop) + return 0; + + ICalParameter *param = + i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER); + ICalTime *ical_time = get_prop_func (prop); + g_object_unref (prop); + + ICalTimezone *timezone = NULL; + if (param) + { + const char *tzid = i_cal_parameter_get_tzid (param); + timezone = i_cal_timezone_get_builtin_timezone_from_tzid (tzid); + g_object_unref (param); + if (!timezone) + timezone = default_zone; + } + else if (i_cal_time_is_utc (ical_time)) + { + timezone = i_cal_timezone_get_utc_timezone (); + } + else + { + timezone = default_zone; + } + + time_t retval = i_cal_time_as_timet_with_zone (ical_time, timezone); + g_object_unref (ical_time); + return retval; +} + +static char * +get_source_color (ECalClient *ecal) +{ + ESource *source = e_client_get_source (E_CLIENT (ecal)); + ECalClientSourceType stype = e_cal_client_get_source_type (ecal); + const char *ext_name = + (stype == E_CAL_CLIENT_SOURCE_TYPE_EVENTS) + ? E_SOURCE_EXTENSION_CALENDAR + : E_SOURCE_EXTENSION_TASK_LIST; + ESourceSelectable *ext = e_source_get_extension (source, ext_name); + return e_source_selectable_dup_color (ext); +} + +static char * +get_source_backend_name (ECalClient *ecal) +{ + ESource *source = e_client_get_source (E_CLIENT (ecal)); + ECalClientSourceType stype = e_cal_client_get_source_type (ecal); + const char *ext_name = + (stype == E_CAL_CLIENT_SOURCE_TYPE_EVENTS) + ? E_SOURCE_EXTENSION_CALENDAR + : E_SOURCE_EXTENSION_TASK_LIST; + ESourceBackend *ext = e_source_get_extension (source, ext_name); + return e_source_backend_dup_backend_name (ext); +} + +static gboolean +get_component_is_all_day (ICalComponent *comp, + time_t start_time, + ICalTimezone *default_zone) +{ + ICalTime *dtstart = i_cal_component_get_dtstart (comp); + if (dtstart && i_cal_time_is_date (dtstart)) + { + g_object_unref (dtstart); + return TRUE; + } + if (dtstart) g_object_unref (dtstart); + + struct tm *start_tm = gmtime (&start_time); + if (start_tm->tm_sec != 0 || start_tm->tm_min != 0 || start_tm->tm_hour != 0) + return FALSE; + + time_t end_time = + get_time_from_property (comp, I_CAL_DTEND_PROPERTY, + i_cal_property_get_dtend, default_zone); + if (end_time) + return (end_time - start_time) % 86400 == 0; + + ICalProperty *prop = + i_cal_component_get_first_property (comp, I_CAL_DURATION_PROPERTY); + if (!prop) return FALSE; + + ICalDuration *dur = i_cal_property_get_duration (prop); + gboolean all_d = i_cal_duration_as_int (dur) % 86400 == 0; + g_object_unref (dur); + g_object_unref (prop); + return all_d; +} + +static void +appointment_init (CalendarAppointment *appt, + ICalComponent *comp, + CalendarEDSSource *src, + ICalTimezone *zone) +{ + ICalProperty *prop; + + prop = i_cal_component_get_first_property (comp, I_CAL_UID_PROPERTY); + appt->uid = prop ? g_strdup (i_cal_property_get_uid (prop)) : NULL; + if (prop) g_object_unref (prop); + + prop = i_cal_component_get_first_property (comp, I_CAL_RECURRENCEID_PROPERTY); + if (prop) + { + ICalTime *rid_time = i_cal_property_get_recurrenceid (prop); + g_object_unref (prop); + if (rid_time && i_cal_time_is_valid_time (rid_time) && + !i_cal_time_is_null_time (rid_time)) + appt->rid = g_strdup (i_cal_time_as_ical_string (rid_time)); + if (rid_time) g_object_unref (rid_time); + } + + appt->backend_name = get_source_backend_name (src->client); + appt->color_string = get_source_color (src->client); + + prop = i_cal_component_get_first_property (comp, I_CAL_SUMMARY_PROPERTY); + appt->summary = prop ? g_strdup (i_cal_property_get_summary (prop)) : NULL; + if (prop) g_object_unref (prop); + + prop = i_cal_component_get_first_property (comp, I_CAL_DESCRIPTION_PROPERTY); + appt->description = prop ? g_strdup (i_cal_property_get_description (prop)) : NULL; + if (prop) g_object_unref (prop); + + appt->start_time = + get_time_from_property (comp, I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, zone); + appt->end_time = + get_time_from_property (comp, I_CAL_DTEND_PROPERTY, + i_cal_property_get_dtend, zone); + appt->is_all_day = get_component_is_all_day (comp, appt->start_time, zone); +} + +static void +task_init (CalendarTask *task, + ICalComponent *comp, + CalendarEDSSource *src, + ICalTimezone *zone) +{ + ICalProperty *prop; + + prop = i_cal_component_get_first_property (comp, I_CAL_UID_PROPERTY); + task->uid = prop ? g_strdup (i_cal_property_get_uid (prop)) : NULL; + if (prop) g_object_unref (prop); + + prop = i_cal_component_get_first_property (comp, I_CAL_SUMMARY_PROPERTY); + task->summary = prop ? g_strdup (i_cal_property_get_summary (prop)) : NULL; + if (prop) g_object_unref (prop); + + prop = i_cal_component_get_first_property (comp, I_CAL_DESCRIPTION_PROPERTY); + task->description = prop ? g_strdup (i_cal_property_get_description (prop)) : NULL; + if (prop) g_object_unref (prop); + + prop = i_cal_component_get_first_property (comp, I_CAL_URL_PROPERTY); + task->url = prop ? g_strdup (i_cal_property_get_url (prop)) : NULL; + if (prop) g_object_unref (prop); + + task->color_string = get_source_color (src->client); + task->start_time = + get_time_from_property (comp, I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, zone); + task->due_time = + get_time_from_property (comp, I_CAL_DUE_PROPERTY, + i_cal_property_get_due, zone); + task->completed_time = + get_time_from_property (comp, I_CAL_COMPLETED_PROPERTY, + i_cal_property_get_completed, zone); + + ICalPropertyStatus status = i_cal_component_get_status (comp); + if (status == I_CAL_STATUS_COMPLETED) + { + task->percent_complete = 100; + } + else + { + prop = + i_cal_component_get_first_property (comp, I_CAL_PERCENTCOMPLETE_PROPERTY); + if (prop) + { + task->percent_complete = + CLAMP (i_cal_property_get_percentcomplete (prop), 0, 100); + g_object_unref (prop); + } + } + + prop = i_cal_component_get_first_property (comp, I_CAL_PRIORITY_PROPERTY); + task->priority = prop ? i_cal_property_get_priority (prop) : -1; + if (prop) g_object_unref (prop); +} + +/* ========================================================================= + * Instance generation callback (called by e_cal_client_generate_instances_*) + * ========================================================================= */ + +typedef struct +{ + CalendarEDSSource *source; + ICalTimezone *system_timezone; + gboolean events_changed; +} InstanceCbData; + +static gboolean +instance_generated_cb (ICalComponent *icomp, + ICalTime *instance_start, + ICalTime *instance_end, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + InstanceCbData *data = user_data; + CalendarEDSSource *src = data->source; + CalendarEDSProvider *provider = src->provider; + CalendarEDSProviderPrivate *priv = provider->priv; + + /* Convert instance times to local timezone */ + ICalTimezone *event_tz = i_cal_time_get_timezone (instance_start); + ICalTime *local_start = i_cal_time_clone (instance_start); + ICalTime *local_end = i_cal_time_clone (instance_end); + + if (event_tz && data->system_timezone && event_tz != data->system_timezone) + { + i_cal_time_convert_timezone (local_start, event_tz, data->system_timezone); + i_cal_time_convert_timezone (local_end, event_tz, data->system_timezone); + } + + time_t start_t = i_cal_time_as_timet (local_start); + time_t end_t = i_cal_time_as_timet (local_end); + + g_object_unref (local_start); + g_object_unref (local_end); + + /* Build a CalendarEvent from the component */ + ICalComponentKind kind = i_cal_component_isa (icomp); + CalendarEvent *event = g_new0 (CalendarEvent, 1); + + if (kind == I_CAL_VEVENT_COMPONENT) + { + event->type = CALENDAR_EVENT_APPOINTMENT; + appointment_init (CALENDAR_APPOINTMENT (event), icomp, src, priv->zone); + CALENDAR_APPOINTMENT (event)->start_time = start_t; + CALENDAR_APPOINTMENT (event)->end_time = end_t; + + CalendarOccurrence *occ = g_new0 (CalendarOccurrence, 1); + occ->start_time = start_t; + occ->end_time = end_t; + CALENDAR_APPOINTMENT (event)->occurrences = g_slist_prepend (NULL, occ); + } + else if (kind == I_CAL_VTODO_COMPONENT) + { + event->type = CALENDAR_EVENT_TASK; + task_init (CALENDAR_TASK (event), icomp, src, priv->zone); + } + else + { + g_free (event); + return TRUE; + } + + /* Build a uid key: uid + optional rid */ + char *uid_key; + if (event->type == CALENDAR_EVENT_APPOINTMENT) + { + const char *uid = CALENDAR_APPOINTMENT (event)->uid; + const char *rid = CALENDAR_APPOINTMENT (event)->rid; + uid_key = (rid != NULL) + ? g_strdup_printf ("%s%s", uid ? uid : "", rid) + : g_strdup (uid ? uid : ""); + } + else + { + uid_key = g_strdup (CALENDAR_TASK (event)->uid ? CALENDAR_TASK (event)->uid : ""); + } + + CalendarEvent *old = + g_hash_table_lookup (src->in_progress_query.events, uid_key); + + if (!calendar_event_equal (event, old)) + { + g_hash_table_replace (src->in_progress_query.events, uid_key, event); + data->events_changed = TRUE; + } + else + { + calendar_event_free (event); + g_free (uid_key); + } + + return TRUE; +} + +/* ========================================================================= + * Query runner + * ========================================================================= */ + +static inline time_t +make_time_for_day_begin (int day, int month, int year) +{ + struct tm tm = { 0, }; + tm.tm_mday = day; + tm.tm_mon = month; + tm.tm_year = year - 1900; + tm.tm_isdst = -1; + return mktime (&tm); +} + +static inline char * +make_isodate_for_day_begin (int day, int month, int year) +{ + time_t t = make_time_for_day_begin (day, month, year); + return (t != -1) ? isodate_from_time_t (t) : NULL; +} + +static void +eds_run_query_on_source (CalendarEDSProvider *self, + CalendarEDSSource *src, + const char *query) +{ + CalendarEDSProviderPrivate *priv = self->priv; + + if (src->query_in_progress) + { + eds_query_clear (&src->in_progress_query); + src->query_in_progress = FALSE; + } + + src->query_in_progress = TRUE; + eds_query_init (&src->in_progress_query); + + GSList *objects = NULL; + GError *error = NULL; + + if (!e_cal_client_get_object_list_sync (src->client, query, + &objects, NULL, &error)) + { + g_warning ("CalendarEDSProvider: query failed: %s", error->message); + g_error_free (error); + eds_query_clear (&src->in_progress_query); + src->query_in_progress = FALSE; + return; + } + + /* Determine the system timezone once for all instances */ + SystemTimezone *systz = system_timezone_new (); + const char *tz_name = system_timezone_get (systz); + ICalTimezone *sys_zone = i_cal_timezone_get_builtin_timezone (tz_name); + g_object_unref (systz); + + time_t month_begin = + make_time_for_day_begin (1, (int) priv->month, (int) priv->year); + time_t month_end; + if (priv->month == 11) + month_end = make_time_for_day_begin (1, 0, (int) priv->year + 1); + else + month_end = make_time_for_day_begin (1, (int) priv->month + 1, (int) priv->year); + + InstanceCbData cb_data = { + .source = src, + .system_timezone = sys_zone, + .events_changed = FALSE, + }; + + for (GSList *l = objects; l != NULL; l = l->next) + { + ICalComponent *comp = l->data; + e_cal_client_generate_instances_for_object_sync (src->client, + comp, + month_begin, + month_end, + NULL, + instance_generated_cb, + &cb_data); + } + + g_slist_free_full (objects, g_object_unref); + + /* Swap in_progress → completed */ + eds_query_clear (&src->completed_query); + src->completed_query = src->in_progress_query; + src->query_completed = TRUE; + src->query_in_progress = FALSE; + src->in_progress_query.events = NULL; + + if (cb_data.events_changed) + { + if (src->is_appointments) + calendar_provider_emit_appointments_changed (CALENDAR_PROVIDER (self)); + else + calendar_provider_emit_tasks_changed (CALENDAR_PROVIDER (self)); + } +} + +static void +eds_run_query (CalendarEDSProvider *self, gboolean is_appointments) +{ + CalendarEDSProviderPrivate *priv = self->priv; + + if (priv->month == G_MAXUINT || priv->year == G_MAXUINT) + return; + + GHashTable *clients = + is_appointments ? priv->appointment_clients : priv->task_clients; + + if (is_appointments) + { + char *month_begin = + make_isodate_for_day_begin (1, (int) priv->month, (int) priv->year); + char *month_end = + make_isodate_for_day_begin (1, (int) priv->month + 1, (int) priv->year); + char *query = + g_strdup_printf ("occur-in-time-range? (make-time \"%s\") " + "(make-time \"%s\")", + month_begin, month_end); + + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + eds_run_query_on_source (self, (CalendarEDSSource *) value, query); + + g_free (month_begin); + g_free (month_end); + g_free (query); + } + else + { + /* Tasks: fetch all and let the UI filter */ + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + eds_run_query_on_source (self, (CalendarEDSSource *) value, "#t"); + } +} + +/* ========================================================================= + * Backend-died recovery + * ========================================================================= */ + +static gboolean +backend_restart_cb (gpointer user_data) +{ + CalendarEDSSource *src = user_data; + CalendarEDSProvider *self = src->provider; + + eds_load_source_list (self, src->is_appointments); + eds_run_query (self, src->is_appointments); + + return G_SOURCE_REMOVE; +} + +static void +backend_died_cb (EClient *client, CalendarEDSSource *src) +{ + ESource *source = e_client_get_source (client); + const char *display_name = e_source_get_display_name (source); + + g_warning ("Calendar backend for '%s' has crashed.", display_name); + + CalendarEDSProviderPrivate *priv = src->provider->priv; + GHashTable *clients = src->is_appointments + ? priv->appointment_clients + : priv->task_clients; + + g_hash_table_remove (clients, source); + g_timeout_add_seconds (2, backend_restart_cb, src); +} + +/* ========================================================================= + * ESourceRegistry management + * ========================================================================= */ + +static void +create_client_for_source (CalendarEDSProvider *self, + ESource *source, + ECalClientSourceType source_type, + GHashTable *clients, + gboolean is_appointments) +{ + if (g_hash_table_lookup (clients, source) != NULL) + return; + + GError *error = NULL; + EClient *client = + e_cal_client_connect_sync (source, source_type, -1, NULL, &error); + + if (client == NULL) + { + g_warning ("CalendarEDSProvider: cannot load source '%s': %s", + e_source_get_uid (source), error->message); + g_clear_error (&error); + return; + } + + CalendarEDSSource *src = + eds_source_new (self, E_CAL_CLIENT (client), is_appointments); + g_object_unref (client); + + g_hash_table_insert (clients, g_object_ref (source), src); +} + +static void +eds_load_source_list (CalendarEDSProvider *self, gboolean is_appointments) +{ + CalendarEDSProviderPrivate *priv = self->priv; + + ECalClientSourceType source_type; + const char *extension_name; + GHashTable *clients; + + if (is_appointments) + { + source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + extension_name = E_SOURCE_EXTENSION_CALENDAR; + clients = priv->appointment_clients; + } + else + { + source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; + extension_name = E_SOURCE_EXTENSION_TASK_LIST; + clients = priv->task_clients; + } + + GList *list = e_source_registry_list_sources (priv->registry, extension_name); + for (GList *l = list; l != NULL; l = l->next) + { + ESource *source = E_SOURCE (l->data); + ESourceSelectable *ext = e_source_get_extension (source, extension_name); + gboolean show = + e_source_get_enabled (source) && e_source_selectable_get_selected (ext); + + if (show) + create_client_for_source (self, source, source_type, + clients, is_appointments); + } + + g_list_free_full (list, g_object_unref); +} + +static void +registry_source_changed_cb (ESourceRegistry *registry, + ESource *source, + CalendarEDSProvider *self) +{ + CalendarEDSProviderPrivate *priv = self->priv; + (void) registry; + + if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + { + ESourceSelectable *ext = + e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); + gboolean have = g_hash_table_lookup (priv->appointment_clients, source) != NULL; + gboolean show = e_source_get_enabled (source) && + e_source_selectable_get_selected (ext); + + if (!show && have) + { + g_hash_table_remove (priv->appointment_clients, source); + calendar_provider_emit_appointments_changed (CALENDAR_PROVIDER (self)); + } + else if (show && !have) + { + create_client_for_source (self, source, + E_CAL_CLIENT_SOURCE_TYPE_EVENTS, + priv->appointment_clients, TRUE); + eds_run_query (self, TRUE); + calendar_provider_emit_appointments_changed (CALENDAR_PROVIDER (self)); + } + } + + if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + { + ESourceSelectable *ext = + e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST); + gboolean have = g_hash_table_lookup (priv->task_clients, source) != NULL; + gboolean show = e_source_get_enabled (source) && + e_source_selectable_get_selected (ext); + + if (!show && have) + { + g_hash_table_remove (priv->task_clients, source); + calendar_provider_emit_tasks_changed (CALENDAR_PROVIDER (self)); + } + else if (show && !have) + { + create_client_for_source (self, source, + E_CAL_CLIENT_SOURCE_TYPE_TASKS, + priv->task_clients, FALSE); + eds_run_query (self, FALSE); + calendar_provider_emit_tasks_changed (CALENDAR_PROVIDER (self)); + } + } +} + +static void +registry_source_removed_cb (ESourceRegistry *registry, + ESource *source, + CalendarEDSProvider *self) +{ + CalendarEDSProviderPrivate *priv = self->priv; + (void) registry; + + if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + { + g_hash_table_remove (priv->appointment_clients, source); + calendar_provider_emit_appointments_changed (CALENDAR_PROVIDER (self)); + } + if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + { + g_hash_table_remove (priv->task_clients, source); + calendar_provider_emit_tasks_changed (CALENDAR_PROVIDER (self)); + } +} + +/* ========================================================================= + * Timezone management + * ========================================================================= */ + +static void +eds_set_timezone (CalendarEDSProvider *self) +{ + CalendarEDSProviderPrivate *priv = self->priv; + + /* Try GSettings timezone key (Evolution or panel settings) */ + if (priv->calendar_settings != NULL) + { + gchar **keys = g_settings_list_keys (priv->calendar_settings); + for (gint i = 0; keys[i] != NULL; i++) + { + if (g_strcmp0 (keys[i], "timezone") == 0) + { + gchar *loc = g_settings_get_string (priv->calendar_settings, "timezone"); + if (loc && *loc) + { + ICalTimezone *tz = i_cal_timezone_get_builtin_timezone (loc); + if (tz) priv->zone = tz; + } + g_free (loc); + break; + } + } + g_strfreev (keys); + } + + if (priv->zone == NULL) + priv->zone = i_cal_timezone_get_utc_timezone (); + + /* Push timezone into all connected ECalClients */ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, priv->appointment_clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + CalendarEDSSource *src = value; + e_cal_client_set_default_timezone (src->client, priv->zone); + } +} + +static void +timezone_changed_cb (GSettings *settings, + const char *key, + CalendarEDSProvider *self) +{ + (void) settings; + (void) key; + eds_set_timezone (self); +} + +/* ========================================================================= + * CalendarProvider vtable + * ========================================================================= */ + +static void +eds_select_month (CalendarProvider *provider, guint month, guint year) +{ + CalendarEDSProvider *self = CALENDAR_EDS_PROVIDER (provider); + CalendarEDSProviderPrivate *priv = self->priv; + + if (priv->month == month && priv->year == year) + return; + + priv->month = month; + priv->year = year; + + eds_run_query (self, TRUE); + eds_run_query (self, FALSE); +} + +static void +eds_select_day (CalendarProvider *provider, guint day) +{ + (void) provider; + (void) day; +} + +static GSList * +eds_get_events (CalendarProvider *provider, + CalendarEventType event_mask, + time_t start_time, + time_t end_time) +{ + CalendarEDSProvider *self = CALENDAR_EDS_PROVIDER (provider); + CalendarEDSProviderPrivate *priv = self->priv; + GSList *result = NULL; + + if (event_mask & CALENDAR_EVENT_APPOINTMENT) + { + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, priv->appointment_clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + CalendarEDSSource *src = value; + if (!src->query_completed) continue; + + GSList *events = + calendar_provider_filter_events_by_range (src->completed_query.events, + CALENDAR_EVENT_APPOINTMENT, + start_time, end_time); + result = g_slist_concat (result, events); + } + } + + if (event_mask & CALENDAR_EVENT_TASK) + { + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, priv->task_clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + CalendarEDSSource *src = value; + if (!src->query_completed) continue; + + GSList *events = + calendar_provider_filter_events_by_range (src->completed_query.events, + CALENDAR_EVENT_TASK, + start_time, end_time); + result = g_slist_concat (result, events); + } + } + + return result; +} + +static void +eds_set_task_completed (CalendarProvider *provider, + const char *task_uid, + gboolean task_completed, + guint percent_complete) +{ + CalendarEDSProvider *self = CALENDAR_EDS_PROVIDER (provider); + CalendarEDSProviderPrivate *priv = self->priv; + + GHashTableIter iter; + gpointer key, value; + ECalClient *found_client = NULL; + ICalComponent *found_comp = NULL; + + g_hash_table_iter_init (&iter, priv->task_clients); + while (g_hash_table_iter_next (&iter, &key, &value) && found_comp == NULL) + { + CalendarEDSSource *src = value; + if (!src->query_completed) continue; + + CalendarEvent *ev = + g_hash_table_lookup (src->completed_query.events, task_uid); + if (ev == NULL) continue; + + GError *error = NULL; + if (e_cal_client_get_object_sync (src->client, task_uid, NULL, + &found_comp, NULL, &error)) + { + found_client = src->client; + } + else + { + g_clear_error (&error); + } + } + + if (found_comp == NULL || found_client == NULL) + return; + + /* Update status */ + ICalPropertyStatus status = task_completed + ? I_CAL_STATUS_COMPLETED + : I_CAL_STATUS_NEEDSACTION; + + ICalProperty *prop = + i_cal_component_get_first_property (found_comp, I_CAL_STATUS_PROPERTY); + if (prop) + { + i_cal_property_set_status (prop, status); + g_object_unref (prop); + } + else + { + i_cal_component_take_property (found_comp, + i_cal_property_new_status (status)); + } + + /* Update percent-complete */ + prop = i_cal_component_get_first_property (found_comp, + I_CAL_PERCENTCOMPLETE_PROPERTY); + if (prop) + { + i_cal_property_set_percentcomplete (prop, (int) percent_complete); + g_object_unref (prop); + } + else + { + i_cal_component_take_property ( + found_comp, i_cal_property_new_percentcomplete ((int) percent_complete)); + } + + e_cal_client_modify_object_sync (found_client, found_comp, + E_CAL_OBJ_MOD_ALL, 0, NULL, NULL); + g_object_unref (found_comp); +} + +static gboolean +eds_create_task (CalendarProvider *provider, const char *summary) +{ + CalendarEDSProvider *self = CALENDAR_EDS_PROVIDER (provider); + CalendarEDSProviderPrivate *priv = self->priv; + + /* Use the first available task client */ + ECalClient *task_client = NULL; + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init (&iter, priv->task_clients); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + task_client = ((CalendarEDSSource *) value)->client; + break; + } + + if (task_client == NULL) + return FALSE; + + ICalComponent *vtodo = i_cal_component_new_vtodo (); + i_cal_component_take_property (vtodo, i_cal_property_new_summary (summary)); + + char *uid = NULL; + GError *error = NULL; + gboolean ok = e_cal_client_create_object_sync (task_client, vtodo, + E_CAL_OPERATION_FLAG_NONE, + &uid, NULL, &error); + g_object_unref (vtodo); + g_free (uid); + if (error) g_error_free (error); + + return ok; +} + +/* ========================================================================= + * GObject lifecycle + * ========================================================================= */ + +static void +calendar_eds_provider_finalize (GObject *object) +{ + CalendarEDSProvider *self = CALENDAR_EDS_PROVIDER (object); + CalendarEDSProviderPrivate *priv = self->priv; + + if (priv->registry) + { + g_signal_handler_disconnect (priv->registry, priv->source_added_id); + g_signal_handler_disconnect (priv->registry, priv->source_changed_id); + g_signal_handler_disconnect (priv->registry, priv->source_removed_id); + g_clear_object (&priv->registry); + } + + if (priv->zone_listener && priv->calendar_settings) + g_signal_handler_disconnect (priv->calendar_settings, priv->zone_listener); + + g_clear_object (&priv->calendar_settings); + + g_hash_table_destroy (priv->appointment_clients); + g_hash_table_destroy (priv->task_clients); + + G_OBJECT_CLASS (calendar_eds_provider_parent_class)->finalize (object); +} + +static void +calendar_eds_provider_class_init (CalendarEDSProviderClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + CalendarProviderClass *provider_class = CALENDAR_PROVIDER_CLASS (klass); + + gobject_class->finalize = calendar_eds_provider_finalize; + + provider_class->select_month = eds_select_month; + provider_class->select_day = eds_select_day; + provider_class->get_events = eds_get_events; + provider_class->set_task_completed = eds_set_task_completed; + provider_class->create_task = eds_create_task; +} + +static void +calendar_eds_provider_init (CalendarEDSProvider *self) +{ + self->priv = calendar_eds_provider_get_instance_private (self); + CalendarEDSProviderPrivate *priv = self->priv; + + priv->month = G_MAXUINT; + priv->year = G_MAXUINT; + priv->zone = NULL; + + priv->appointment_clients = + g_hash_table_new_full ((GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) eds_source_free); + priv->task_clients = + g_hash_table_new_full ((GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) eds_source_free); +} + +/* ========================================================================= + * Public constructor + * ========================================================================= */ + +CalendarProvider * +calendar_eds_provider_new (GSettings *settings) +{ + CalendarEDSProvider *self = g_object_new (CALENDAR_TYPE_EDS_PROVIDER, NULL); + CalendarEDSProviderPrivate *priv = self->priv; + GError *error = NULL; + + /* GSettings: prefer the passed-in settings (MATE panel's own schema), then + * fall back to Evolution's schema if available. */ + if (settings != NULL) + { + priv->calendar_settings = g_object_ref (settings); + } + else + { + GSettingsSchemaSource *src = + g_settings_schema_source_get_default (); + GSettingsSchema *schema = + g_settings_schema_source_lookup (src, "org.gnome.evolution.calendar", FALSE); + if (schema) + { + priv->calendar_settings = g_settings_new ("org.gnome.evolution.calendar"); + g_settings_schema_unref (schema); + } + } + + eds_set_timezone (self); + + if (priv->calendar_settings) + priv->zone_listener = + g_signal_connect (priv->calendar_settings, "changed::timezone", + G_CALLBACK (timezone_changed_cb), self); + + priv->registry = e_source_registry_new_sync (NULL, &error); + if (error) + { + g_critical ("CalendarEDSProvider: cannot create ESourceRegistry: %s", + error->message); + g_error_free (error); + g_object_unref (self); + return NULL; + } + + priv->source_added_id = + g_signal_connect (priv->registry, "source-added", + G_CALLBACK (registry_source_changed_cb), self); + priv->source_changed_id = + g_signal_connect (priv->registry, "source-changed", + G_CALLBACK (registry_source_changed_cb), self); + priv->source_removed_id = + g_signal_connect (priv->registry, "source-removed", + G_CALLBACK (registry_source_removed_cb), self); + + /* Eagerly load the source lists so clients are ready before first query */ + eds_load_source_list (self, TRUE); + eds_load_source_list (self, FALSE); + + return CALENDAR_PROVIDER (self); +} diff --git a/applets/clock/calendar-eds-provider.h b/applets/clock/calendar-eds-provider.h new file mode 100644 index 000000000..dcbdf84e9 --- /dev/null +++ b/applets/clock/calendar-eds-provider.h @@ -0,0 +1,57 @@ +/* + * calendar-eds-provider.h: Evolution Data Server calendar source + * + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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 2 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 . + */ + +#ifndef __CALENDAR_EDS_PROVIDER_H__ +#define __CALENDAR_EDS_PROVIDER_H__ + +#include "calendar-provider.h" + +G_BEGIN_DECLS + +#define CALENDAR_TYPE_EDS_PROVIDER (calendar_eds_provider_get_type ()) +#define CALENDAR_EDS_PROVIDER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_EDS_PROVIDER, CalendarEDSProvider)) +#define CALENDAR_EDS_PROVIDER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_EDS_PROVIDER, CalendarEDSProviderClass)) +#define CALENDAR_IS_EDS_PROVIDER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_EDS_PROVIDER)) +#define CALENDAR_IS_EDS_PROVIDER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_EDS_PROVIDER)) +#define CALENDAR_EDS_PROVIDER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_EDS_PROVIDER, CalendarEDSProviderClass)) + +typedef struct _CalendarEDSProvider CalendarEDSProvider; +typedef struct _CalendarEDSProviderClass CalendarEDSProviderClass; +typedef struct _CalendarEDSProviderPrivate CalendarEDSProviderPrivate; + +struct _CalendarEDSProvider +{ + CalendarProvider parent; + CalendarEDSProviderPrivate *priv; +}; + +struct _CalendarEDSProviderClass +{ + CalendarProviderClass parent_class; +}; + +GType calendar_eds_provider_get_type (void) G_GNUC_CONST; + +/* @settings may be NULL; if so the provider checks for Evolution's own + * GSettings schema and falls back to UTC if unavailable. */ +CalendarProvider *calendar_eds_provider_new (GSettings *settings); + +G_END_DECLS + +#endif /* __CALENDAR_EDS_PROVIDER_H__ */ diff --git a/applets/clock/calendar-provider.c b/applets/clock/calendar-provider.c new file mode 100644 index 000000000..3b882edea --- /dev/null +++ b/applets/clock/calendar-provider.c @@ -0,0 +1,200 @@ +/* + * calendar-provider.c: abstract base class for calendar event sources + * + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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 2 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 . + */ + +#include +#include "calendar-provider.h" + +enum +{ + APPOINTMENTS_CHANGED, + TASKS_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_ABSTRACT_TYPE (CalendarProvider, calendar_provider, G_TYPE_OBJECT) + +static void +calendar_provider_class_init (CalendarProviderClass *klass) +{ + signals[APPOINTMENTS_CHANGED] = + g_signal_new ("appointments-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarProviderClass, appointments_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[TASKS_CHANGED] = + g_signal_new ("tasks-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarProviderClass, tasks_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +calendar_provider_init (CalendarProvider *provider) +{ + (void) provider; +} + +/* -------------------------------------------------------------------------- + * Dispatch helpers + * -------------------------------------------------------------------------- */ + +void +calendar_provider_select_month (CalendarProvider *provider, + guint month, + guint year) +{ + g_return_if_fail (CALENDAR_IS_PROVIDER (provider)); + + CalendarProviderClass *klass = CALENDAR_PROVIDER_GET_CLASS (provider); + if (klass->select_month) + klass->select_month (provider, month, year); +} + +void +calendar_provider_select_day (CalendarProvider *provider, + guint day) +{ + g_return_if_fail (CALENDAR_IS_PROVIDER (provider)); + + CalendarProviderClass *klass = CALENDAR_PROVIDER_GET_CLASS (provider); + if (klass->select_day) + klass->select_day (provider, day); +} + +GSList * +calendar_provider_get_events (CalendarProvider *provider, + CalendarEventType event_mask, + time_t start_time, + time_t end_time) +{ + g_return_val_if_fail (CALENDAR_IS_PROVIDER (provider), NULL); + + CalendarProviderClass *klass = CALENDAR_PROVIDER_GET_CLASS (provider); + if (klass->get_events) + return klass->get_events (provider, event_mask, start_time, end_time); + return NULL; +} + +void +calendar_provider_set_task_completed (CalendarProvider *provider, + const char *task_uid, + gboolean task_completed, + guint percent_complete) +{ + g_return_if_fail (CALENDAR_IS_PROVIDER (provider)); + + CalendarProviderClass *klass = CALENDAR_PROVIDER_GET_CLASS (provider); + if (klass->set_task_completed) + klass->set_task_completed (provider, task_uid, task_completed, percent_complete); +} + +gboolean +calendar_provider_create_task (CalendarProvider *provider, + const char *summary) +{ + g_return_val_if_fail (CALENDAR_IS_PROVIDER (provider), FALSE); + + CalendarProviderClass *klass = CALENDAR_PROVIDER_GET_CLASS (provider); + if (klass->create_task) + return klass->create_task (provider, summary); + return FALSE; +} + +void +calendar_provider_emit_appointments_changed (CalendarProvider *provider) +{ + g_signal_emit (provider, signals[APPOINTMENTS_CHANGED], 0); +} + +void +calendar_provider_emit_tasks_changed (CalendarProvider *provider) +{ + g_signal_emit (provider, signals[TASKS_CHANGED], 0); +} + +/* -------------------------------------------------------------------------- + * Shared filter utility + * -------------------------------------------------------------------------- + * + * Given a hash-table mapping uid strings to CalendarEvent* objects (cached + * for a whole month), return individually-copied events whose occurrences + * overlap the half-open interval [start_time, end_time). + * + * For CALENDAR_EVENT_APPOINTMENT the event is expanded: one copy is emitted + * per overlapping occurrence, with start_time/end_time set to that specific + * occurrence. For CALENDAR_EVENT_TASK the event is returned as-is (one copy). + */ +GSList * +calendar_provider_filter_events_by_range (GHashTable *events, + CalendarEventType event_mask, + time_t start_time, + time_t end_time) +{ + GSList *result = NULL; + GHashTableIter iter; + gpointer key, value; + + g_return_val_if_fail (events != NULL, NULL); + + g_hash_table_iter_init (&iter, events); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + CalendarEvent *event = (CalendarEvent *) value; + + if (event->type == CALENDAR_EVENT_APPOINTMENT && + (event_mask & CALENDAR_EVENT_APPOINTMENT)) + { + CalendarAppointment *appt = CALENDAR_APPOINTMENT (event); + /* Temporarily detach the occurrence list so calendar_event_copy() + * doesn't deep-copy it; we set per-occurrence times manually. */ + GSList *occurrences = appt->occurrences; + appt->occurrences = NULL; + + for (GSList *l = occurrences; l != NULL; l = l->next) + { + CalendarOccurrence *occ = l->data; + + if ((occ->start_time >= start_time && occ->start_time < end_time) || + (occ->start_time <= start_time && (occ->end_time - 1) > start_time)) + { + CalendarEvent *copy = calendar_event_copy (event); + CALENDAR_APPOINTMENT (copy)->start_time = occ->start_time; + CALENDAR_APPOINTMENT (copy)->end_time = occ->end_time; + result = g_slist_prepend (result, copy); + } + } + + appt->occurrences = occurrences; + } + else if (event->type == CALENDAR_EVENT_TASK && + (event_mask & CALENDAR_EVENT_TASK)) + { + result = g_slist_prepend (result, calendar_event_copy (event)); + } + } + + return result; +} diff --git a/applets/clock/calendar-provider.h b/applets/clock/calendar-provider.h new file mode 100644 index 000000000..993d4e4d0 --- /dev/null +++ b/applets/clock/calendar-provider.h @@ -0,0 +1,114 @@ +/* + * calendar-provider.h: abstract base class for calendar event sources + * + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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 2 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 . + */ + +#ifndef __CALENDAR_PROVIDER_H__ +#define __CALENDAR_PROVIDER_H__ + +#include +#include "calendar-client.h" + +G_BEGIN_DECLS + +#define CALENDAR_TYPE_PROVIDER (calendar_provider_get_type ()) +#define CALENDAR_PROVIDER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_PROVIDER, CalendarProvider)) +#define CALENDAR_PROVIDER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_PROVIDER, CalendarProviderClass)) +#define CALENDAR_IS_PROVIDER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_PROVIDER)) +#define CALENDAR_IS_PROVIDER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_PROVIDER)) +#define CALENDAR_PROVIDER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_PROVIDER, CalendarProviderClass)) + +typedef struct _CalendarProvider CalendarProvider; +typedef struct _CalendarProviderClass CalendarProviderClass; + +struct _CalendarProvider +{ + GObject parent; +}; + +struct _CalendarProviderClass +{ + GObjectClass parent_class; + + /* Called when the user navigates to a different month. + * Subclasses should refresh their event cache for this month/year + * and emit appointments-changed / tasks-changed when ready. */ + void (*select_month) (CalendarProvider *provider, + guint month, + guint year); + + /* Called when the user selects a specific day. Subclasses that + * cache by month (the common case) can ignore this. */ + void (*select_day) (CalendarProvider *provider, + guint day); + + /* Return a GSList of newly-allocated CalendarEvent* copies whose + * occurrences overlap the half-open interval [start_time, end_time). + * event_mask is a bitmask of CalendarEventType values. */ + GSList *(*get_events) (CalendarProvider *provider, + CalendarEventType event_mask, + time_t start_time, + time_t end_time); + + /* Optional write-back operations. Providers that do not support + * mutation should leave these NULL. */ + void (*set_task_completed) (CalendarProvider *provider, + const char *task_uid, + gboolean task_completed, + guint percent_complete); + gboolean (*create_task) (CalendarProvider *provider, + const char *summary); + + /* Signals */ + void (*appointments_changed) (CalendarProvider *provider); + void (*tasks_changed) (CalendarProvider *provider); +}; + +GType calendar_provider_get_type (void) G_GNUC_CONST; + +/* Dispatch helpers — call the corresponding virtual method */ +void calendar_provider_select_month (CalendarProvider *provider, + guint month, + guint year); +void calendar_provider_select_day (CalendarProvider *provider, + guint day); +GSList *calendar_provider_get_events (CalendarProvider *provider, + CalendarEventType event_mask, + time_t start_time, + time_t end_time); +void calendar_provider_set_task_completed (CalendarProvider *provider, + const char *task_uid, + gboolean task_completed, + guint percent_complete); +gboolean calendar_provider_create_task (CalendarProvider *provider, + const char *summary); + +/* For use by subclass implementations to fire signals */ +void calendar_provider_emit_appointments_changed (CalendarProvider *provider); +void calendar_provider_emit_tasks_changed (CalendarProvider *provider); + +/* Shared filter utility: given a hash table (uid → CalendarEvent*) containing + * events cached for a whole month, return copies of those that overlap + * [start_time, end_time). Callers own the returned list and its elements. */ +GSList *calendar_provider_filter_events_by_range (GHashTable *events, + CalendarEventType event_mask, + time_t start_time, + time_t end_time); + +G_END_DECLS + +#endif /* __CALENDAR_PROVIDER_H__ */ diff --git a/applets/clock/calendar-sources.c b/applets/clock/calendar-sources.c deleted file mode 100644 index 54c6dc8cd..000000000 --- a/applets/clock/calendar-sources.c +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright (C) 2004 Free Software Foundation, Inc. - * - * 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 2 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 . - * - * Authors: - * Mark McLoughlin - * William Jon McCann - * Martin Grimme - * Christian Kellner - */ - -#include - -#include "calendar-sources.h" - -#include -#include -#define HANDLE_LIBICAL_MEMORY - - -#include - -#undef CALENDAR_ENABLE_DEBUG -#include "calendar-debug.h" - -#ifndef _ -#define _(x) gettext(x) -#endif - -#ifndef N_ -#define N_(x) x -#endif - -typedef struct _ClientData ClientData; -typedef struct _CalendarSourceData CalendarSourceData; - -struct _ClientData -{ - ECalClient *client; - gulong backend_died_id; -}; - -struct _CalendarSourceData -{ - ECalClientSourceType source_type; - CalendarSources *sources; - guint changed_signal; - - /* ESource -> EClient */ - GHashTable *clients; - - guint timeout_id; - - guint loaded : 1; -}; - -struct _CalendarSourcesPrivate -{ - ESourceRegistry *registry; - gulong source_added_id; - gulong source_changed_id; - gulong source_removed_id; - - CalendarSourceData appointment_sources; - CalendarSourceData task_sources; -}; - -static void calendar_sources_finalize (GObject *object); - -static void backend_died_cb (EClient *client, CalendarSourceData *source_data); -static void calendar_sources_registry_source_changed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources); -static void calendar_sources_registry_source_removed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources); - -enum -{ - APPOINTMENT_SOURCES_CHANGED, - TASK_SOURCES_CHANGED, - LAST_SIGNAL -}; -static guint signals [LAST_SIGNAL] = { 0, }; - -static CalendarSources *calendar_sources_singleton = NULL; - -static void -client_data_free (ClientData *data) -{ - g_signal_handler_disconnect (data->client, data->backend_died_id); - g_object_unref (data->client); - g_free (data); -} - -G_DEFINE_TYPE_WITH_PRIVATE (CalendarSources, calendar_sources, G_TYPE_OBJECT) - -static void -calendar_sources_class_init (CalendarSourcesClass *klass) -{ - GObjectClass *gobject_class = (GObjectClass *) klass; - - gobject_class->finalize = calendar_sources_finalize; - - signals [APPOINTMENT_SOURCES_CHANGED] = - g_signal_new ("appointment-sources-changed", - G_TYPE_FROM_CLASS (gobject_class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (CalendarSourcesClass, - appointment_sources_changed), - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0); - - signals [TASK_SOURCES_CHANGED] = - g_signal_new ("task-sources-changed", - G_TYPE_FROM_CLASS (gobject_class), - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (CalendarSourcesClass, - task_sources_changed), - NULL, - NULL, - NULL, - G_TYPE_NONE, - 0); -} - -static void -calendar_sources_init (CalendarSources *sources) -{ - GError *error = NULL; - - sources->priv = calendar_sources_get_instance_private (sources); - - /* XXX Not sure what to do if this fails. - * Should this class implement GInitable or pass the - * registry in as a G_PARAM_CONSTRUCT_ONLY property? */ - sources->priv->registry = e_source_registry_new_sync (NULL, &error); - if (error != NULL) { - g_critical ("%s: %s", G_STRFUNC, error->message); - g_error_free (error); - } - - sources->priv->source_added_id = g_signal_connect (sources->priv->registry, - "source-added", - G_CALLBACK (calendar_sources_registry_source_changed_cb), - sources); - sources->priv->source_changed_id = g_signal_connect (sources->priv->registry, - "source-changed", - G_CALLBACK (calendar_sources_registry_source_changed_cb), - sources); - sources->priv->source_removed_id = g_signal_connect (sources->priv->registry, - "source-removed", - G_CALLBACK (calendar_sources_registry_source_removed_cb), - sources); - - sources->priv->appointment_sources.source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; - sources->priv->appointment_sources.sources = sources; - sources->priv->appointment_sources.changed_signal = signals [APPOINTMENT_SOURCES_CHANGED]; - sources->priv->appointment_sources.clients = g_hash_table_new_full ((GHashFunc) e_source_hash, - (GEqualFunc) e_source_equal, - (GDestroyNotify) g_object_unref, - (GDestroyNotify) client_data_free); - sources->priv->appointment_sources.timeout_id = 0; - - sources->priv->task_sources.source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; - sources->priv->task_sources.sources = sources; - sources->priv->task_sources.changed_signal = signals [TASK_SOURCES_CHANGED]; - sources->priv->task_sources.clients = g_hash_table_new_full ((GHashFunc) e_source_hash, - (GEqualFunc) e_source_equal, - (GDestroyNotify) g_object_unref, - (GDestroyNotify) client_data_free); - sources->priv->task_sources.timeout_id = 0; -} - -static void -calendar_sources_finalize_source_data (CalendarSources *sources, - CalendarSourceData *source_data) -{ - if (source_data->loaded) - { - g_hash_table_destroy (source_data->clients); - source_data->clients = NULL; - - if (source_data->timeout_id != 0) - { - g_source_remove (source_data->timeout_id); - source_data->timeout_id = 0; - } - - source_data->loaded = FALSE; - } -} - -static void -calendar_sources_finalize (GObject *object) -{ - CalendarSources *sources = CALENDAR_SOURCES (object); - - if (sources->priv->registry) - { - g_signal_handler_disconnect (sources->priv->registry, - sources->priv->source_added_id); - g_signal_handler_disconnect (sources->priv->registry, - sources->priv->source_changed_id); - g_signal_handler_disconnect (sources->priv->registry, - sources->priv->source_removed_id); - g_object_unref (sources->priv->registry); - } - sources->priv->registry = NULL; - - calendar_sources_finalize_source_data (sources, &sources->priv->appointment_sources); - calendar_sources_finalize_source_data (sources, &sources->priv->task_sources); - - G_OBJECT_CLASS (calendar_sources_parent_class)->finalize (object); -} - -CalendarSources * -calendar_sources_get (void) -{ - gpointer singleton_location = &calendar_sources_singleton; - - if (calendar_sources_singleton) - return g_object_ref (calendar_sources_singleton); - - calendar_sources_singleton = g_object_new (CALENDAR_TYPE_SOURCES, NULL); - g_object_add_weak_pointer (G_OBJECT (calendar_sources_singleton), - singleton_location); - - return calendar_sources_singleton; -} - -/* The clients are just created here but not loaded */ -static void -create_client_for_source (ESource *source, - ECalClientSourceType source_type, - CalendarSourceData *source_data) -{ - ClientData *data; - GError *error; - EClient *client; - - client = g_hash_table_lookup (source_data->clients, source); - g_return_if_fail (client == NULL); - - error = NULL; - client = e_cal_client_connect_sync (source, source_type, -1, NULL, &error); - - if (!client) - { - g_warning ("Could not load source '%s': %s", - e_source_get_uid (source), - error->message); - - g_clear_error (&error); - return; - } - - data = g_new0 (ClientData, 1); - data->client = E_CAL_CLIENT (client); /* takes ownership */ - data->backend_died_id = g_signal_connect (client, - "backend-died", - G_CALLBACK (backend_died_cb), - source_data); - - g_hash_table_insert (source_data->clients, g_object_ref (source), data); -} - -static inline void -debug_dump_ecal_list (GHashTable *clients) -{ -#ifdef CALENDAR_ENABLE_DEBUG - GList *list, *link; - - dprintf ("Loaded clients:\n"); - list = g_hash_table_get_keys (clients); - for (link = list; link != NULL; link = g_list_next (link)) - { - ESource *source = E_SOURCE (link->data); - - dprintf (" %s %s\n", - e_source_get_uid (source), - e_source_get_display_name (source)); - } -#endif -} - -static void -calendar_sources_load_esource_list (ESourceRegistry *registry, - CalendarSourceData *source_data); - -static gboolean -backend_restart (gpointer data) -{ - CalendarSourceData *source_data = data; - ESourceRegistry *registry; - - registry = source_data->sources->priv->registry; - calendar_sources_load_esource_list (registry, source_data); - g_signal_emit (source_data->sources, source_data->changed_signal, 0); - - source_data->timeout_id = 0; - - return FALSE; -} - -static void -backend_died_cb (EClient *client, CalendarSourceData *source_data) -{ - ESource *source; - const char *display_name; - - source = e_client_get_source (client); - display_name = e_source_get_display_name (source); - g_warning ("The calendar backend for '%s' has crashed.", display_name); - g_hash_table_remove (source_data->clients, source); - - if (source_data->timeout_id != 0) - { - g_source_remove (source_data->timeout_id); - source_data->timeout_id = 0; - } - - source_data->timeout_id = g_timeout_add_seconds (2, backend_restart, - source_data); -} - -static void -calendar_sources_load_esource_list (ESourceRegistry *registry, - CalendarSourceData *source_data) -{ - GList *list, *link; - const gchar *extension_name; - - switch (source_data->source_type) - { - case E_CAL_CLIENT_SOURCE_TYPE_EVENTS: - extension_name = E_SOURCE_EXTENSION_CALENDAR; - break; - case E_CAL_CLIENT_SOURCE_TYPE_TASKS: - extension_name = E_SOURCE_EXTENSION_TASK_LIST; - break; - case E_CAL_CLIENT_SOURCE_TYPE_MEMOS: - case E_CAL_CLIENT_SOURCE_TYPE_LAST: - default: - g_return_if_reached (); - } - - list = e_source_registry_list_sources (registry, extension_name); - - for (link = list; link != NULL; link = g_list_next (link)) - { - ESource *source = E_SOURCE (link->data); - ESourceSelectable *extension; - gboolean show_source; - - extension = e_source_get_extension (source, extension_name); - show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension); - - if (show_source) - create_client_for_source (source, source_data->source_type, source_data); - } - - debug_dump_ecal_list (source_data->clients); - - g_list_free_full (list, g_object_unref); -} - -static void -calendar_sources_registry_source_changed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources) -{ - if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) - { - CalendarSourceData *source_data; - ESourceSelectable *extension; - gboolean have_client; - gboolean show_source; - - source_data = &sources->priv->appointment_sources; - extension = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); - have_client = (g_hash_table_lookup (source_data->clients, source) != NULL); - show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension); - - if (!show_source && have_client) - { - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } - if (show_source && !have_client) - { - create_client_for_source (source, source_data->source_type, source_data); - g_signal_emit (sources, source_data->changed_signal, 0); - } - } - - if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) - { - CalendarSourceData *source_data; - ESourceSelectable *extension; - gboolean have_client; - gboolean show_source; - - source_data = &sources->priv->task_sources; - extension = e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST); - have_client = (g_hash_table_lookup (source_data->clients, source) != NULL); - show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension); - - if (!show_source && have_client) - { - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } - if (show_source && !have_client) - { - create_client_for_source (source, source_data->source_type, source_data); - g_signal_emit (sources, source_data->changed_signal, 0); - } - } -} - -static void -calendar_sources_registry_source_removed_cb (ESourceRegistry *registry, - ESource *source, - CalendarSources *sources) -{ - if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) - { - CalendarSourceData *source_data; - - source_data = &sources->priv->appointment_sources; - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } - - if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) - { - CalendarSourceData *source_data; - - source_data = &sources->priv->task_sources; - g_hash_table_remove (source_data->clients, source); - g_signal_emit (sources, source_data->changed_signal, 0); - } -} - -GList * -calendar_sources_get_appointment_clients (CalendarSources *sources) -{ - GList *list, *link; - - g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); - - if (!sources->priv->appointment_sources.loaded) - { - calendar_sources_load_esource_list (sources->priv->registry, - &sources->priv->appointment_sources); - sources->priv->appointment_sources.loaded = TRUE; - } - - list = g_hash_table_get_values (sources->priv->appointment_sources.clients); - - for (link = list; link != NULL; link = g_list_next (link)) - link->data = ((ClientData *) link->data)->client; - - return list; -} - -GList * -calendar_sources_get_task_clients (CalendarSources *sources) -{ - GList *list, *link; - - g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); - - if (!sources->priv->task_sources.loaded) - { - calendar_sources_load_esource_list (sources->priv->registry, - &sources->priv->task_sources); - sources->priv->task_sources.loaded = TRUE; - } - - list = g_hash_table_get_values (sources->priv->task_sources.clients); - - for (link = list; link != NULL; link = g_list_next (link)) - link->data = ((ClientData *) link->data)->client; - - return list; -} diff --git a/applets/clock/calendar-sources.h b/applets/clock/calendar-sources.h deleted file mode 100644 index 1dfc74450..000000000 --- a/applets/clock/calendar-sources.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2004 Free Software Foundation, Inc. - * - * 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 2 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 . - * - * Authors: - * Mark McLoughlin - * William Jon McCann - * Martin Grimme - * Christian Kellner - */ - -#ifndef __CALENDAR_SOURCES_H__ -#define __CALENDAR_SOURCES_H__ - -#include - -G_BEGIN_DECLS - -#define CALENDAR_TYPE_SOURCES (calendar_sources_get_type ()) -#define CALENDAR_SOURCES(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_SOURCES, CalendarSources)) -#define CALENDAR_SOURCES_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_SOURCES, CalendarSourcesClass)) -#define CALENDAR_IS_SOURCES(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_SOURCES)) -#define CALENDAR_IS_SOURCES_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_SOURCES)) -#define CALENDAR_SOURCES_GET_CLASS(o)(G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_SOURCES, CalendarSourcesClass)) - -typedef struct _CalendarSources CalendarSources; -typedef struct _CalendarSourcesClass CalendarSourcesClass; -typedef struct _CalendarSourcesPrivate CalendarSourcesPrivate; - -struct _CalendarSources -{ - GObject parent; - CalendarSourcesPrivate *priv; -}; - -struct _CalendarSourcesClass -{ - GObjectClass parent_class; - - void (* appointment_sources_changed) (CalendarSources *sources); - void (* task_sources_changed) (CalendarSources *sources); -}; - - -GType calendar_sources_get_type (void) G_GNUC_CONST; -CalendarSources *calendar_sources_get (void); -GList *calendar_sources_get_appointment_clients (CalendarSources *sources); -GList *calendar_sources_get_task_clients (CalendarSources *sources); - -G_END_DECLS - -#endif /* __CALENDAR_SOURCES_H__ */ diff --git a/applets/clock/calendar-vdir-provider.c b/applets/clock/calendar-vdir-provider.c new file mode 100644 index 000000000..2547305dd --- /dev/null +++ b/applets/clock/calendar-vdir-provider.c @@ -0,0 +1,895 @@ +/* + * calendar-vdir-provider.c: reads calendar events from a vdir collection + * + * A vdir collection is a directory where each .ics file contains exactly + * one VEVENT or VTODO component (as produced by vdirsyncer). Optional + * plain-text metadata files 'color' (#RRGGBB) and 'displayname' live in + * the same directory. + * + * Reference: https://vdirsyncer.readthedocs.io/en/stable/vdir.html + * + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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 2 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 . + */ + +#include + +#include +#include + +#include +#include +#include + +#include "calendar-vdir-provider.h" + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +struct _CalendarVdirProviderPrivate +{ + char *directory; + char *display_name; /* from 'displayname' file, or NULL */ + char *color_string; /* from 'color' file (#RRGGBB), or NULL */ + + ICalTimezone *zone; /* local timezone for time conversions */ + + /* filename (basename) → ICalComponent * (parsed VCALENDAR wrapper) */ + GHashTable *components; + + /* uid_string → CalendarEvent * (expanded for current month) */ + GHashTable *appointment_events; + GHashTable *task_events; + + guint month; /* 0-11; G_MAXUINT = unset */ + guint year; /* e.g. 2025; G_MAXUINT = unset */ + gboolean loaded; /* TRUE once the directory has been scanned */ + + GFileMonitor *dir_monitor; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (CalendarVdirProvider, + calendar_vdir_provider, + CALENDAR_TYPE_PROVIDER) + +/* ========================================================================= + * Small utilities + * ========================================================================= */ + +/* Read a plain-text metadata file from a vdir directory, strip trailing + * whitespace, and return it as a newly-allocated string. Returns NULL if + * the file doesn't exist or is empty. */ +static char * +read_metadata_file (const char *directory, const char *filename) +{ + char *path = g_build_filename (directory, filename, NULL); + char *contents = NULL; + + if (g_file_get_contents (path, &contents, NULL, NULL) && contents != NULL) + { + g_strchomp (contents); + if (*contents == '\0') + { + g_free (contents); + contents = NULL; + } + } + + g_free (path); + return contents; +} + +static inline time_t +make_month_begin (guint month, guint year) +{ + struct tm tm = { 0, }; + tm.tm_mday = 1; + tm.tm_mon = (int) month; + tm.tm_year = (int) year - 1900; + tm.tm_isdst = -1; + return mktime (&tm); +} + +static inline time_t +make_month_end (guint month, guint year) +{ + if (month == 11) + return make_month_begin (0, year + 1); + return make_month_begin (month + 1, year); +} + +/* ========================================================================= + * Recurrence expansion + * ========================================================================= + * + * Given an ICalComponent (VEVENT or VTODO) and a half-open time range + * [range_start, range_end), return a list of CalendarOccurrence* that fall + * within that range. Handles RRULE, RDATE, and EXDATE per RFC 5545. + */ +static GSList * +expand_recurrences (ICalComponent *comp, + ICalTimezone *zone, + time_t range_start, + time_t range_end) +{ + GSList *result = NULL; + ICalTime *dtstart = NULL; + ICalTime *dtend = NULL; + ICalProperty *prop = NULL; + time_t duration = 0; + + dtstart = i_cal_component_get_dtstart (comp); + if (!dtstart || i_cal_time_is_null_time (dtstart)) + { + if (dtstart) g_object_unref (dtstart); + return NULL; + } + + /* ------------------------------------------------------------------ + * Compute the duration of a single occurrence + * ------------------------------------------------------------------ */ + dtend = i_cal_component_get_dtend (comp); + if (dtend != NULL && !i_cal_time_is_null_time (dtend)) + { + ICalDuration *diff = i_cal_time_subtract (dtend, dtstart); + duration = (time_t) i_cal_duration_as_int (diff); + g_object_unref (diff); + } + else + { + prop = i_cal_component_get_first_property (comp, I_CAL_DURATION_PROPERTY); + if (prop != NULL) + { + ICalDuration *dur = i_cal_property_get_duration (prop); + duration = (time_t) i_cal_duration_as_int (dur); + g_object_unref (dur); + g_object_unref (prop); + prop = NULL; + } + } + if (dtend) g_object_unref (dtend); + if (duration < 0) duration = 0; + + /* ------------------------------------------------------------------ + * Collect EXDATE values for exclusion checking + * ------------------------------------------------------------------ */ + GSList *exdates = NULL; + for (prop = i_cal_component_get_first_property (comp, I_CAL_EXDATE_PROPERTY); + prop != NULL; + prop = i_cal_component_get_next_property (comp, I_CAL_EXDATE_PROPERTY)) + { + ICalTime *extime = i_cal_property_get_exdate (prop); + if (extime != NULL && !i_cal_time_is_null_time (extime)) + { + time_t *t = g_new (time_t, 1); + *t = i_cal_time_as_timet_with_zone (extime, zone); + exdates = g_slist_prepend (exdates, t); + } + if (extime) g_object_unref (extime); + g_object_unref (prop); + } + prop = NULL; + + /* Helper: add an occurrence if it overlaps range and isn't excluded. */ +#define MAYBE_ADD_OCC(occ_start) \ + do { \ + time_t _s = (occ_start); \ + time_t _e = _s + duration; \ + if (_s < range_end && _e > range_start) \ + { \ + gboolean _excl = FALSE; \ + for (GSList *_el = exdates; _el; _el = _el->next) \ + if (*(time_t *)_el->data == _s) { _excl = TRUE; break; } \ + if (!_excl) \ + { \ + CalendarOccurrence *_o = g_new0 (CalendarOccurrence, 1); \ + _o->start_time = _s; \ + _o->end_time = _e; \ + result = g_slist_prepend (result, _o); \ + } \ + } \ + } while (0) + + /* ------------------------------------------------------------------ + * RRULE expansion + * ------------------------------------------------------------------ */ + prop = i_cal_component_get_first_property (comp, I_CAL_RRULE_PROPERTY); + if (prop != NULL) + { + ICalRecurrence *rrule = i_cal_property_get_rrule (prop); + ICalRecurIterator *iter = i_cal_recur_iterator_new (rrule, dtstart); + + ICalTime *next; + while (TRUE) + { + next = i_cal_recur_iterator_next (iter); + if (next == NULL || i_cal_time_is_null_time (next)) + { + if (next) g_object_unref (next); + break; + } + + time_t occ_start = i_cal_time_as_timet_with_zone (next, zone); + g_object_unref (next); + + /* RRULE iterates in ascending order; stop once past range */ + if (occ_start >= range_end) + break; + + MAYBE_ADD_OCC (occ_start); + } + + g_object_unref (iter); + g_object_unref (rrule); + g_object_unref (prop); + prop = NULL; + } + else + { + /* No RRULE: single occurrence at DTSTART */ + time_t occ_start = i_cal_time_as_timet_with_zone (dtstart, zone); + MAYBE_ADD_OCC (occ_start); + + /* Plus any RDATE entries */ + for (prop = i_cal_component_get_first_property (comp, I_CAL_RDATE_PROPERTY); + prop != NULL; + prop = i_cal_component_get_next_property (comp, I_CAL_RDATE_PROPERTY)) + { + ICalDatetimeperiod *rdtp = i_cal_property_get_rdate (prop); + if (rdtp != NULL) + { + ICalTime *rdt = i_cal_datetimeperiod_get_time (rdtp); + if (rdt != NULL && !i_cal_time_is_null_time (rdt)) + { + time_t rd_start = i_cal_time_as_timet_with_zone (rdt, zone); + MAYBE_ADD_OCC (rd_start); + } + if (rdt) g_object_unref (rdt); + g_object_unref (rdtp); + } + g_object_unref (prop); + } + prop = NULL; + } + +#undef MAYBE_ADD_OCC + + g_slist_free_full (exdates, g_free); + g_object_unref (dtstart); + + return result; +} + +/* ========================================================================= + * Component → CalendarEvent conversion + * ========================================================================= */ + +/* Generic helper: get a string property value (caller owns return value). */ +static char * +get_str_prop (ICalComponent *comp, ICalPropertyKind kind, + const char *(*getter)(ICalProperty *)) +{ + ICalProperty *prop = i_cal_component_get_first_property (comp, kind); + if (!prop) + return NULL; + char *val = g_strdup (getter (prop)); + g_object_unref (prop); + return val; +} + +/* Generic helper: get a time_t from a datetime property. */ +static time_t +get_time_prop (ICalComponent *comp, + ICalPropertyKind kind, + ICalTime *(*getter)(ICalProperty *), + ICalTimezone *zone) +{ + ICalProperty *prop = i_cal_component_get_first_property (comp, kind); + if (!prop) + return 0; + ICalTime *t = getter (prop); + g_object_unref (prop); + if (!t || i_cal_time_is_null_time (t)) + { + if (t) g_object_unref (t); + return 0; + } + time_t result = i_cal_time_as_timet_with_zone (t, zone); + g_object_unref (t); + return result; +} + +/* + * Build a CalendarAppointment (wrapped in CalendarEvent) from a VEVENT + * component. Occurrences that fall within [range_start, range_end) are + * pre-expanded and stored in appointment->occurrences. + * Returns NULL if there are no occurrences in the range. + */ +static CalendarEvent * +vevent_to_appointment (ICalComponent *comp, + const char *color_string, + const char *backend_name, + ICalTimezone *zone, + time_t range_start, + time_t range_end) +{ + GSList *occurrences = expand_recurrences (comp, zone, range_start, range_end); + if (occurrences == NULL) + return NULL; + + CalendarEvent *event = g_new0 (CalendarEvent, 1); + event->type = CALENDAR_EVENT_APPOINTMENT; + + CalendarAppointment *appt = CALENDAR_APPOINTMENT (event); + appt->uid = get_str_prop (comp, I_CAL_UID_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_uid); + appt->rid = NULL; + appt->backend_name = g_strdup (backend_name); + appt->summary = get_str_prop (comp, I_CAL_SUMMARY_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_summary); + appt->description = get_str_prop (comp, I_CAL_DESCRIPTION_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_description); + appt->color_string = g_strdup (color_string); + appt->occurrences = occurrences; + + /* Set primary start/end from the first occurrence (they are overridden + * per-occurrence during filtering in get_events()). */ + CalendarOccurrence *first = (CalendarOccurrence *) occurrences->data; + appt->start_time = first->start_time; + appt->end_time = first->end_time; + appt->is_all_day = (first->end_time - first->start_time) % 86400 == 0; + + return event; +} + +/* + * Build a CalendarTask (wrapped in CalendarEvent) from a VTODO component. + */ +static CalendarEvent * +vtodo_to_task (ICalComponent *comp, + const char *color_string, + ICalTimezone *zone) +{ + CalendarEvent *event = g_new0 (CalendarEvent, 1); + event->type = CALENDAR_EVENT_TASK; + + CalendarTask *task = CALENDAR_TASK (event); + task->uid = get_str_prop (comp, I_CAL_UID_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_uid); + task->summary = get_str_prop (comp, I_CAL_SUMMARY_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_summary); + task->description = get_str_prop (comp, I_CAL_DESCRIPTION_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_description); + task->color_string = g_strdup (color_string); + task->url = get_str_prop (comp, I_CAL_URL_PROPERTY, + (const char *(*)(ICalProperty *)) i_cal_property_get_url); + + task->start_time = get_time_prop (comp, I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, zone); + task->due_time = get_time_prop (comp, I_CAL_DUE_PROPERTY, + i_cal_property_get_due, zone); + task->completed_time = get_time_prop (comp, I_CAL_COMPLETED_PROPERTY, + i_cal_property_get_completed, zone); + + ICalPropertyStatus status = i_cal_component_get_status (comp); + if (status == I_CAL_STATUS_COMPLETED) + { + task->percent_complete = 100; + } + else + { + ICalProperty *prop = + i_cal_component_get_first_property (comp, I_CAL_PERCENTCOMPLETE_PROPERTY); + if (prop) + { + task->percent_complete = CLAMP (i_cal_property_get_percentcomplete (prop), 0, 100); + g_object_unref (prop); + } + } + + ICalProperty *prio_prop = + i_cal_component_get_first_property (comp, I_CAL_PRIORITY_PROPERTY); + if (prio_prop) + { + task->priority = i_cal_property_get_priority (prio_prop); + g_object_unref (prio_prop); + } + else + { + task->priority = -1; + } + + return event; +} + +/* ========================================================================= + * Loading and caching + * ========================================================================= */ + +/* Re-build appointment_events and task_events from the loaded components + * for the current month/year window. */ +static void +vdir_rebuild_month_cache (CalendarVdirProvider *self) +{ + CalendarVdirProviderPrivate *priv = self->priv; + + if (priv->month == G_MAXUINT || priv->year == G_MAXUINT) + return; + + time_t range_start = make_month_begin (priv->month, priv->year); + time_t range_end = make_month_end (priv->month, priv->year); + + g_hash_table_remove_all (priv->appointment_events); + g_hash_table_remove_all (priv->task_events); + + const char *display_name = + (priv->display_name != NULL) ? priv->display_name : priv->directory; + + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, priv->components); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ICalComponent *vcal = I_CAL_COMPONENT (value); + /* In a vdir file the outer component is VCALENDAR; the single inner + * component is the VEVENT or VTODO. */ + ICalComponent *inner = i_cal_component_get_inner (vcal); + if (inner == NULL) + continue; + + ICalComponentKind kind = i_cal_component_isa (inner); + + if (kind == I_CAL_VEVENT_COMPONENT) + { + CalendarEvent *ev = vevent_to_appointment (inner, + priv->color_string, + display_name, + priv->zone, + range_start, + range_end); + if (ev != NULL) + { + const char *uid = CALENDAR_APPOINTMENT (ev)->uid; + /* Fall back to the filename as key if the component has no UID */ + char *uid_key = g_strdup (uid ? uid : (const char *) key); + g_hash_table_replace (priv->appointment_events, uid_key, ev); + } + } + else if (kind == I_CAL_VTODO_COMPONENT) + { + CalendarEvent *ev = vtodo_to_task (inner, priv->color_string, priv->zone); + if (ev != NULL) + { + const char *uid = CALENDAR_TASK (ev)->uid; + char *uid_key = g_strdup (uid ? uid : (const char *) key); + g_hash_table_replace (priv->task_events, uid_key, ev); + } + } + + g_object_unref (inner); + } +} + +/* Parse a single .ics file and insert / replace its component in the + * components hash table. */ +static void +vdir_load_file (CalendarVdirProvider *self, const char *basename) +{ + CalendarVdirProviderPrivate *priv = self->priv; + char *path = g_build_filename (priv->directory, basename, NULL); + char *contents = NULL; + + if (!g_file_get_contents (path, &contents, NULL, NULL)) + { + g_free (path); + return; + } + + ICalComponent *comp = i_cal_parser_parse_string (contents); + if (comp != NULL) + g_hash_table_replace (priv->components, g_strdup (basename), comp); + + g_free (contents); + g_free (path); +} + +/* Scan the vdir directory and (re)load all .ics files. */ +static void +vdir_load_all_files (CalendarVdirProvider *self) +{ + CalendarVdirProviderPrivate *priv = self->priv; + GError *error = NULL; + + g_hash_table_remove_all (priv->components); + + GDir *dir = g_dir_open (priv->directory, 0, &error); + if (dir == NULL) + { + g_warning ("CalendarVdirProvider: cannot open '%s': %s", + priv->directory, error->message); + g_error_free (error); + return; + } + + const char *name; + while ((name = g_dir_read_name (dir)) != NULL) + { + /* Per vdir spec: ignore .tmp files and files without an extension */ + if (g_str_has_suffix (name, ".tmp") || strchr (name, '.') == NULL) + continue; + if (!g_str_has_suffix (name, ".ics")) + continue; + + vdir_load_file (self, name); + } + + g_dir_close (dir); +} + +/* ========================================================================= + * GFileMonitor callback + * ========================================================================= */ + +static void +on_vdir_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + CalendarVdirProvider *self = CALENDAR_VDIR_PROVIDER (user_data); + CalendarVdirProviderPrivate *priv = self->priv; + + (void) monitor; + (void) other_file; + + char *basename = g_file_get_basename (file); + gboolean is_ics = g_str_has_suffix (basename, ".ics") && + !g_str_has_suffix (basename, ".tmp"); + gboolean is_meta = (g_strcmp0 (basename, "displayname") == 0 || + g_strcmp0 (basename, "color") == 0); + + if (!is_ics && !is_meta) + { + g_free (basename); + return; + } + + if (is_meta) + { + g_free (priv->display_name); + g_free (priv->color_string); + priv->display_name = read_metadata_file (priv->directory, "displayname"); + priv->color_string = read_metadata_file (priv->directory, "color"); + vdir_rebuild_month_cache (self); + calendar_provider_emit_appointments_changed (CALENDAR_PROVIDER (self)); + calendar_provider_emit_tasks_changed (CALENDAR_PROVIDER (self)); + g_free (basename); + return; + } + + /* .ics file event */ + switch (event_type) + { + case G_FILE_MONITOR_EVENT_CREATED: + case G_FILE_MONITOR_EVENT_CHANGED: + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: + vdir_load_file (self, basename); + break; + case G_FILE_MONITOR_EVENT_DELETED: + g_hash_table_remove (priv->components, basename); + break; + default: + g_free (basename); + return; + } + + /* Figure out whether the affected component is an appointment or task so + * we can emit the narrower signal. */ + gboolean is_appointment = TRUE; + ICalComponent *vcal = g_hash_table_lookup (priv->components, basename); + if (vcal != NULL) + { + ICalComponent *inner = i_cal_component_get_inner (vcal); + if (inner != NULL) + { + is_appointment = (i_cal_component_isa (inner) != I_CAL_VTODO_COMPONENT); + g_object_unref (inner); + } + } + + vdir_rebuild_month_cache (self); + + if (is_appointment) + calendar_provider_emit_appointments_changed (CALENDAR_PROVIDER (self)); + else + calendar_provider_emit_tasks_changed (CALENDAR_PROVIDER (self)); + + g_free (basename); +} + +/* ========================================================================= + * CalendarProvider vtable + * ========================================================================= */ + +static void +vdir_select_month (CalendarProvider *provider, guint month, guint year) +{ + CalendarVdirProvider *self = CALENDAR_VDIR_PROVIDER (provider); + CalendarVdirProviderPrivate *priv = self->priv; + + if (priv->month == month && priv->year == year && priv->loaded) + return; + + priv->month = month; + priv->year = year; + + if (!priv->loaded) + { + vdir_load_all_files (self); + priv->loaded = TRUE; + } + + vdir_rebuild_month_cache (self); +} + +static void +vdir_select_day (CalendarProvider *provider, guint day) +{ + /* Day-level filtering is handled in get_events(); nothing to cache. */ + (void) provider; + (void) day; +} + +static GSList * +vdir_get_events (CalendarProvider *provider, + CalendarEventType event_mask, + time_t start_time, + time_t end_time) +{ + CalendarVdirProvider *self = CALENDAR_VDIR_PROVIDER (provider); + CalendarVdirProviderPrivate *priv = self->priv; + GSList *result = NULL; + + if (event_mask & CALENDAR_EVENT_APPOINTMENT) + { + GSList *appts = calendar_provider_filter_events_by_range (priv->appointment_events, + CALENDAR_EVENT_APPOINTMENT, + start_time, end_time); + result = g_slist_concat (result, appts); + } + + if (event_mask & CALENDAR_EVENT_TASK) + { + GSList *tasks = calendar_provider_filter_events_by_range (priv->task_events, + CALENDAR_EVENT_TASK, + start_time, end_time); + result = g_slist_concat (result, tasks); + } + + return result; +} + +/* ========================================================================= + * GObject lifecycle + * ========================================================================= */ + +static void +calendar_vdir_provider_finalize (GObject *object) +{ + CalendarVdirProvider *self = CALENDAR_VDIR_PROVIDER (object); + CalendarVdirProviderPrivate *priv = self->priv; + + if (priv->dir_monitor != NULL) + { + g_file_monitor_cancel (priv->dir_monitor); + g_clear_object (&priv->dir_monitor); + } + + g_hash_table_destroy (priv->components); + g_hash_table_destroy (priv->appointment_events); + g_hash_table_destroy (priv->task_events); + + g_free (priv->directory); + g_free (priv->display_name); + g_free (priv->color_string); + + G_OBJECT_CLASS (calendar_vdir_provider_parent_class)->finalize (object); +} + +static void +calendar_vdir_provider_class_init (CalendarVdirProviderClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + CalendarProviderClass *provider_class = CALENDAR_PROVIDER_CLASS (klass); + + gobject_class->finalize = calendar_vdir_provider_finalize; + + provider_class->select_month = vdir_select_month; + provider_class->select_day = vdir_select_day; + provider_class->get_events = vdir_get_events; + /* set_task_completed and create_task intentionally left NULL: + * vdir collections are read-only from the applet's perspective. */ +} + +static void +calendar_vdir_provider_init (CalendarVdirProvider *self) +{ + self->priv = calendar_vdir_provider_get_instance_private (self); + CalendarVdirProviderPrivate *priv = self->priv; + + priv->month = G_MAXUINT; + priv->year = G_MAXUINT; + priv->loaded = FALSE; + + /* Default timezone: attempt to determine the local zone from the system */ + priv->zone = i_cal_timezone_get_utc_timezone (); + + priv->components = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) g_object_unref); + priv->appointment_events = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) calendar_event_free); + priv->task_events = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, + (GDestroyNotify) calendar_event_free); +} + +/* ========================================================================= + * Public constructor + * ========================================================================= */ + +CalendarProvider * +calendar_vdir_provider_new (const char *directory) +{ + g_return_val_if_fail (directory != NULL, NULL); + + if (!g_file_test (directory, G_FILE_TEST_IS_DIR)) + return NULL; + + CalendarVdirProvider *self = g_object_new (CALENDAR_TYPE_VDIR_PROVIDER, NULL); + CalendarVdirProviderPrivate *priv = self->priv; + + priv->directory = g_strdup (directory); + priv->display_name = read_metadata_file (directory, "displayname"); + priv->color_string = read_metadata_file (directory, "color"); + + /* Resolve the local timezone via the system TZ name */ + { + time_t now = time (NULL); + struct tm local = { 0, }; + localtime_r (&now, &local); + char tz_name[64]; + if (strftime (tz_name, sizeof (tz_name), "%Z", &local) > 0) + { + ICalTimezone *tz = i_cal_timezone_get_builtin_timezone (tz_name); + if (tz != NULL) + priv->zone = tz; + } + } + + /* Set up a directory monitor for live updates */ + GFile *dir_file = g_file_new_for_path (directory); + GError *error = NULL; + priv->dir_monitor = + g_file_monitor_directory (dir_file, G_FILE_MONITOR_NONE, NULL, &error); + g_object_unref (dir_file); + + if (priv->dir_monitor != NULL) + { + g_signal_connect (priv->dir_monitor, "changed", + G_CALLBACK (on_vdir_changed), self); + } + else + { + g_warning ("CalendarVdirProvider: cannot monitor '%s': %s", + directory, error != NULL ? error->message : "unknown error"); + g_clear_error (&error); + } + + return CALENDAR_PROVIDER (self); +} + +/* ========================================================================= + * Auto-discovery + * ========================================================================= + * + * Scan @base_path for subdirectories that look like vdir collections. + * A directory qualifies if it contains at least one .ics file, or has a + * 'displayname' or 'color' metadata file (an empty-but-configured calendar). + */ +GSList * +calendar_vdir_discover (const char *base_path) +{ + GSList *providers = NULL; + GError *error = NULL; + + if (base_path == NULL || !g_file_test (base_path, G_FILE_TEST_IS_DIR)) + return NULL; + + GDir *dir = g_dir_open (base_path, 0, &error); + if (dir == NULL) + { + g_debug ("CalendarVdirProvider: cannot scan '%s': %s", + base_path, error->message); + g_error_free (error); + return NULL; + } + + const char *entry; + while ((entry = g_dir_read_name (dir)) != NULL) + { + char *subdir = g_build_filename (base_path, entry, NULL); + + if (!g_file_test (subdir, G_FILE_TEST_IS_DIR)) + { + g_free (subdir); + continue; + } + + /* Check whether this looks like a vdir collection */ + gboolean looks_like_vdir = FALSE; + + /* Quick check: metadata files */ + char *meta_path = g_build_filename (subdir, "displayname", NULL); + if (g_file_test (meta_path, G_FILE_TEST_IS_REGULAR)) + looks_like_vdir = TRUE; + g_free (meta_path); + + if (!looks_like_vdir) + { + meta_path = g_build_filename (subdir, "color", NULL); + if (g_file_test (meta_path, G_FILE_TEST_IS_REGULAR)) + looks_like_vdir = TRUE; + g_free (meta_path); + } + + /* Deeper check: any .ics file */ + if (!looks_like_vdir) + { + GDir *sub = g_dir_open (subdir, 0, NULL); + if (sub != NULL) + { + const char *f; + while ((f = g_dir_read_name (sub)) != NULL) + { + if (g_str_has_suffix (f, ".ics") && + !g_str_has_suffix (f, ".tmp")) + { + looks_like_vdir = TRUE; + break; + } + } + g_dir_close (sub); + } + } + + if (looks_like_vdir) + { + CalendarProvider *p = calendar_vdir_provider_new (subdir); + if (p != NULL) + providers = g_slist_prepend (providers, p); + } + + g_free (subdir); + } + + g_dir_close (dir); + return providers; +} diff --git a/applets/clock/calendar-vdir-provider.h b/applets/clock/calendar-vdir-provider.h new file mode 100644 index 000000000..dc97f5808 --- /dev/null +++ b/applets/clock/calendar-vdir-provider.h @@ -0,0 +1,63 @@ +/* + * calendar-vdir-provider.h: vdir storage calendar source + * + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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 2 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 . + */ + +#ifndef __CALENDAR_VDIR_PROVIDER_H__ +#define __CALENDAR_VDIR_PROVIDER_H__ + +#include "calendar-provider.h" + +G_BEGIN_DECLS + +#define CALENDAR_TYPE_VDIR_PROVIDER (calendar_vdir_provider_get_type ()) +#define CALENDAR_VDIR_PROVIDER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_VDIR_PROVIDER, CalendarVdirProvider)) +#define CALENDAR_VDIR_PROVIDER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_VDIR_PROVIDER, CalendarVdirProviderClass)) +#define CALENDAR_IS_VDIR_PROVIDER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_VDIR_PROVIDER)) +#define CALENDAR_IS_VDIR_PROVIDER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_VDIR_PROVIDER)) +#define CALENDAR_VDIR_PROVIDER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_VDIR_PROVIDER, CalendarVdirProviderClass)) + +typedef struct _CalendarVdirProvider CalendarVdirProvider; +typedef struct _CalendarVdirProviderClass CalendarVdirProviderClass; +typedef struct _CalendarVdirProviderPrivate CalendarVdirProviderPrivate; + +struct _CalendarVdirProvider +{ + CalendarProvider parent; + CalendarVdirProviderPrivate *priv; +}; + +struct _CalendarVdirProviderClass +{ + CalendarProviderClass parent_class; +}; + +GType calendar_vdir_provider_get_type (void) G_GNUC_CONST; + +/* Create a provider for a single vdir collection directory. + * Reads the optional 'displayname' and 'color' metadata files from the + * directory. Returns NULL if @directory does not exist. */ +CalendarProvider *calendar_vdir_provider_new (const char *directory); + +/* Scan @base_path for vdir collections (subdirectories containing .ics + * files or a displayname/color file) and return a GSList of newly-created + * CalendarProvider* objects. The caller owns the list and its elements. */ +GSList *calendar_vdir_discover (const char *base_path); + +G_END_DECLS + +#endif /* __CALENDAR_VDIR_PROVIDER_H__ */ diff --git a/applets/clock/calendar-window.c b/applets/clock/calendar-window.c index 79780d345..6ac67b913 100644 --- a/applets/clock/calendar-window.c +++ b/applets/clock/calendar-window.c @@ -38,13 +38,13 @@ #include "clock.h" #include "clock-utils.h" #include "clock-typebuiltins.h" -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) #include "calendar-client.h" #endif #define KEY_LOCATIONS_EXPANDED "expand-locations" -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) #define KEY_SHOW_CALENDAR_EVENTS "show-calendar-events" #define KEY_SHOW_TASKS "show-tasks" #define KEY_EXPAND_CALENDAR_EVENTS "expand-calendar-events" @@ -55,7 +55,7 @@ enum { EDIT_LOCATIONS, -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) PERMISSION_READY, #endif LAST_SIGNAL @@ -80,7 +80,7 @@ struct _CalendarWindowPrivate { gulong calendar_month_changed_id; gulong calendar_day_selected_id; -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) ClockFormat time_format; CalendarClient *client; @@ -102,7 +102,7 @@ struct _CalendarWindowPrivate { /* EDS-specific signal handler IDs */ gulong client_appointments_changed_id; gulong client_tasks_changed_id; -#endif /* HAVE_EDS */ +#endif /* HAVE_EDS || HAVE_LIBICAL */ }; G_DEFINE_TYPE_WITH_PRIVATE (CalendarWindow, calendar_window, GTK_TYPE_WINDOW) @@ -127,7 +127,7 @@ static GtkWidget * create_hig_frame (CalendarWindow *calwin, const char *key, GCallback callback); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) enum { APPOINTMENT_COLUMN_UID, APPOINTMENT_COLUMN_TYPE, @@ -182,7 +182,7 @@ static void task_row_activated_cb (GtkTreeView *tree_view, GtkTreePath *path, Gt static void task_completion_toggled_cb (GtkCellRendererToggle *cell, gchar *path_str, CalendarWindow *calwin); static gboolean task_entry_key_press_cb (GtkWidget *widget, GdkEventKey *event, CalendarWindow *calwin); static void task_entry_activate_cb (GtkEntry *entry, CalendarWindow *calwin); -#endif /* HAVE_EDS */ +#endif /* HAVE_EDS || HAVE_LIBICAL */ static void calendar_mark_today(GtkCalendar *calendar) { @@ -212,7 +212,7 @@ static void calendar_month_changed_cb(GtkCalendar *calendar, gpointer user_data) gtk_calendar_clear_marks(calendar); g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, calendar_update, calendar, NULL); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Update calendar client when date changes */ CalendarWindow *calwin = CALENDAR_WINDOW (user_data); if (calwin->priv->client) { @@ -367,7 +367,7 @@ edit_locations (CalendarWindow *calwin) g_signal_emit (calwin, signals[EDIT_LOCATIONS], 0); } -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) static gboolean hide_task_entry_idle (gpointer user_data) { @@ -453,13 +453,13 @@ calendar_window_fill (CalendarWindow *calwin) if (!calwin->priv->invert_order) { gtk_box_pack_start (GTK_BOX (vbox), calwin->priv->calendar, TRUE, FALSE, 0); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) calendar_window_pack_pim (calwin, vbox); #endif calendar_window_pack_locations (calwin, vbox); } else { calendar_window_pack_locations (calwin, vbox); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) calendar_window_pack_pim (calwin, vbox); #endif gtk_box_pack_start (GTK_BOX (vbox), @@ -584,7 +584,7 @@ calendar_window_dispose (GObject *object) calwin->priv->calendar_day_selected_id = 0; } -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Disconnect client signals */ if (calwin->priv->client) { if (calwin->priv->client_appointments_changed_id > 0) { @@ -672,7 +672,7 @@ calendar_window_init (CalendarWindow *calwin) calwin->priv = calendar_window_get_instance_private (calwin); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Initialize signal handler IDs */ calwin->priv->calendar_month_changed_id = 0; calwin->priv->calendar_day_selected_id = 0; @@ -704,7 +704,7 @@ calendar_window_new (time_t *static_current_time, "prefs-path", prefs_path, NULL); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Store settings for calendar client initialization in init */ if (settings) { calwin->priv->settings = g_object_ref (settings); @@ -714,7 +714,7 @@ calendar_window_new (time_t *static_current_time, return GTK_WIDGET (calwin); } -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) static void refresh_once (gpointer user_data) { @@ -734,7 +734,7 @@ calendar_window_refresh (CalendarWindow *calwin) { g_return_if_fail (CALENDAR_IS_WINDOW (calwin)); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Reload evolution calendar data after a small delay to not slow down the UI */ if (calwin->priv->client) { g_timeout_add_once (100, refresh_once, calwin); @@ -815,7 +815,7 @@ calendar_window_get_time_format (CalendarWindow *calwin) g_return_val_if_fail (CALENDAR_IS_WINDOW (calwin), CLOCK_FORMAT_INVALID); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) return calwin->priv->time_format; #else return CLOCK_FORMAT_INVALID; @@ -879,7 +879,7 @@ calendar_window_set_prefs_path (CalendarWindow *calwin, } } -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) static char * format_time (ClockFormat format, @@ -1887,4 +1887,4 @@ calendar_window_set_client (CalendarWindow *calwin, CalendarClient *client) } } -#endif /* HAVE_EDS */ +#endif /* HAVE_EDS || HAVE_LIBICAL */ diff --git a/applets/clock/org.mate.panel.applet.clock.gschema.xml.in b/applets/clock/org.mate.panel.applet.clock.gschema.xml.in index 88ad91898..11eea6239 100644 --- a/applets/clock/org.mate.panel.applet.clock.gschema.xml.in +++ b/applets/clock/org.mate.panel.applet.clock.gschema.xml.in @@ -84,10 +84,15 @@ Speed unit The unit to use when showing wind speed. + + [] + Additional vdir calendar collection paths + A list of additional vdir calendar collection directories to display in the calendar window. Each entry should be the full path to a directory containing .ics files in vdir format (as produced by vdirsyncer). The default vdirsyncer storage path (~/.local/share/vdirsyncer/) is auto-discovered automatically. + true Show calendar events - If true, display calendar events from Evolution in the calendar window. + If true, display calendar events in the calendar window. false diff --git a/configure.ac b/configure.ac index 4ce9ae3f6..c295b76cb 100644 --- a/configure.ac +++ b/configure.ac @@ -123,6 +123,35 @@ AM_CONDITIONAL(HAVE_EDS, test "x$have_eds" = "xyes") AC_SUBST(EDS_CFLAGS) AC_SUBST(EDS_LIBS) +# Check for libical-glib for vdir calendar support (independent of EDS) +AC_ARG_ENABLE([libical], + AS_HELP_STRING([--enable-libical], [Enable vdir calendar support via libical @<:@default=auto@:>@]), + [enable_libical=$enableval], + [enable_libical=auto]) + +if test "x$enable_libical" != "xno"; then + PKG_CHECK_MODULES(LIBICAL, [libical-glib >= 3.0], have_libical=yes, have_libical=no) + if test "x$have_libical" = "xyes"; then + AC_DEFINE(HAVE_LIBICAL, 1, [Define if libical-glib is available]) + elif test "x$enable_libical" = "xyes"; then + AC_MSG_ERROR([libical support requested but libical-glib development libraries not found]) + fi +else + have_libical=no +fi + +# EDS implies libical (libecal depends on libical-glib) +if test "x$have_eds" = "xyes" && test "x$have_libical" = "xno"; then + have_libical=yes + AC_DEFINE(HAVE_LIBICAL, 1, [Define if libical-glib is available]) + LIBICAL_CFLAGS="$EDS_CFLAGS" + LIBICAL_LIBS="" +fi + +AM_CONDITIONAL(HAVE_LIBICAL, test "x$have_libical" = "xyes") +AC_SUBST(LIBICAL_CFLAGS) +AC_SUBST(LIBICAL_LIBS) + # Make it possible to compile the applets in-process PANEL_INPROCESS_NONE= PANEL_INPROCESS_ALL= @@ -406,6 +435,7 @@ echo " X11 support: ${have_x11} XRandr support: ${have_randr} Evolution Data Server support: ${have_eds} + vdir calendar (libical) support: ${have_libical} Build introspection support: ${found_introspection} Build gtk-doc documentation: ${enable_gtk_doc} From 4a49f1dac55e71bd455860761cb037bde16eda86 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 23:19:28 +0200 Subject: [PATCH 02/19] clock: fix vdir provider not loading without EDS, recurse discovery - Update #ifdef HAVE_EDS guards in clock.c to #if defined(HAVE_EDS) || defined(HAVE_LIBICAL) so the calendar client is created and passed to the calendar window when only libical is available (no EDS) - Fix calendar_vdir_discover() to recurse one level into subdirectories that are not themselves vdir collections, matching vdirsyncer's layout: /// Signed-off-by: Oz Tiram --- applets/clock/calendar-vdir-provider.c | 10 ++++++++-- applets/clock/clock.c | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/applets/clock/calendar-vdir-provider.c b/applets/clock/calendar-vdir-provider.c index 2547305dd..deddbcc74 100644 --- a/applets/clock/calendar-vdir-provider.c +++ b/applets/clock/calendar-vdir-provider.c @@ -826,8 +826,7 @@ calendar_vdir_discover (const char *base_path) GDir *dir = g_dir_open (base_path, 0, &error); if (dir == NULL) { - g_debug ("CalendarVdirProvider: cannot scan '%s': %s", - base_path, error->message); + g_debug ("CalendarVdirProvider: cannot scan '%s': %s", base_path, error->message); g_error_free (error); return NULL; } @@ -886,6 +885,13 @@ calendar_vdir_discover (const char *base_path) if (p != NULL) providers = g_slist_prepend (providers, p); } + else + { + /* Not a collection itself — recurse one level (vdirsyncer stores + * collections as ///) */ + GSList *nested = calendar_vdir_discover (subdir); + providers = g_slist_concat (providers, nested); + } g_free (subdir); } diff --git a/applets/clock/clock.c b/applets/clock/clock.c index 0c104f316..1901584b0 100644 --- a/applets/clock/clock.c +++ b/applets/clock/clock.c @@ -212,7 +212,7 @@ struct _ClockData { GDBusProxy *system_manager_proxy; -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) CalendarClient *calendar_client; #endif }; @@ -847,7 +847,7 @@ destroy_clock (GtkWidget * widget, ClockData *cd) cd->builder = NULL; } -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) if (cd->calendar_client) { g_object_unref (cd->calendar_client); cd->calendar_client = NULL; @@ -913,7 +913,7 @@ create_calendar (ClockData *cd) cd->settings); g_free (prefs_path); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) if (cd->calendar_client) { calendar_window_set_client (CALENDAR_WINDOW (window), cd->calendar_client); } @@ -2824,7 +2824,7 @@ fill_clock_applet (MatePanelApplet *applet) * hibernate). */ setup_monitor_for_resume (cd); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Initialize persistent calendar client */ cd->calendar_client = calendar_client_new (cd->settings); #endif @@ -3378,7 +3378,7 @@ fill_prefs_window (ClockData *cd) g_settings_bind (cd->settings, KEY_SHOW_TEMPERATURE, widget, "active", G_SETTINGS_BIND_DEFAULT); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) /* Set the EDS calendar event checkboxes */ widget = _clock_get_widget (cd, "show_calendar_events_check"); if (widget) { From 523802723f4ab7132a1023b0a79b68eae0334734 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 23:25:39 +0200 Subject: [PATCH 03/19] clock: document EDS and vdir calendar sources Add applets/clock/README.md describing both calendar backends: - Evolution Data Server integration (appointments and tasks) - vdir/vdirsyncer support: auto-discovery, extra paths via GSettings, setup instructions, recurring events, and collection metadata Add optional dependency entries for EDS and libical-glib to the top-level README. Signed-off-by: Oz Tiram --- README | 13 ++++++ applets/clock/README.md | 93 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 applets/clock/README.md diff --git a/README b/README index db7bf1fd9..1e0e31085 100644 --- a/README +++ b/README @@ -53,6 +53,19 @@ libmateweather-dev For work-in-progress Wayland support gtk-layer-shell (https://github.com/wmww/gtk-layer-shell) +Optional dependencies +===================== + +Evolution Data Server (EDS) — calendar and task integration in the Clock applet: +libecal-2.0 >= 3.33.2 +libedataserver-1.2 >= 3.5.3 +Enable with: --enable-eds (default: auto-detect) + +libical-glib — vdir calendar support in the Clock applet (independent of EDS): +libical-glib >= 3.0 +Enable with: --enable-libical (default: auto-detect) +See applets/clock/README.md for details on the vdir calendar feature. + How to report bugs ================== diff --git a/applets/clock/README.md b/applets/clock/README.md new file mode 100644 index 000000000..8fd529b13 --- /dev/null +++ b/applets/clock/README.md @@ -0,0 +1,93 @@ +# Clock Applet + +The clock applet displays the time and date in the MATE panel. When clicked, +it opens a calendar window that can also show upcoming appointments and tasks +from calendar data sources. + +## Calendar Data Sources + +The applet supports two calendar backends, which can be active simultaneously. + +### Evolution Data Server (EDS) + +When built with `--enable-eds` and the `libecal-2.0` / `libedataserver-1.2` +libraries are present, the applet reads appointments and tasks directly from +Evolution Data Server. This gives access to any calendar or task list +configured in GNOME Online Accounts or Evolution. + +The "Show calendar events" and "Show tasks" checkboxes in the applet +preferences control whether each type of data is displayed. + +### vdir / vdirsyncer + +When built with `--enable-libical` and `libical-glib >= 3.0` is present, the +applet can read calendars stored in **vdir format** — a directory of `.ics` +files as produced by [vdirsyncer](https://vdirsyncer.readthedocs.io/). + +#### Auto-discovery + +The applet automatically scans `$XDG_DATA_HOME/vdirsyncer/` (typically +`~/.local/share/vdirsyncer/`) for vdir collections. This matches the default +storage path used by vdirsyncer. No configuration is needed if vdirsyncer is +set up with its default paths. + +#### Additional paths + +Extra collection directories can be added via GSettings: + +``` +gsettings set org.mate.panel.applet.clock vdir-calendar-paths \ + "['/path/to/collection1', '/path/to/collection2']" +``` + +Each path should point directly to a directory containing `.ics` files +(a single vdir collection), not to a parent directory. + +#### Setting up vdirsyncer + +Install vdirsyncer and create `~/.vdirsyncer/config`: + +```ini +[general] +status_path = "~/.vdirsyncer/status/" + +[pair my_calendar] +a = "my_local" +b = "my_remote" +collections = ["from b"] +conflict_resolution = "b wins" + +[storage my_local] +type = "filesystem" +path = "~/.local/share/vdirsyncer/my_calendar/" +fileext = ".ics" + +[storage my_remote] +type = "caldav" +url = "https://your-caldav-server/path/" +username = "user@example.com" +password = "yourpassword" +``` + +Then run: + +``` +vdirsyncer discover my_calendar +vdirsyncer sync +``` + +The applet will pick up the synced events automatically on next start. Live +updates are detected via `GFileMonitor` — new or changed `.ics` files are +reflected in the calendar window without restarting the applet. + +#### Recurring events + +Recurring events (RRULE, RDATE, EXDATE) are fully expanded using libical-glib, +so repeating events appear correctly on each occurrence date. + +#### Collection metadata + +If a vdir collection directory contains a `displayname` file, its contents +are used as the backend name shown in event tooltips. A `color` file +(containing an `#RRGGBB` hex color) is used to color-code events from that +collection in the calendar window. From f7525baa4d173207601c537bea47843b3eea9d1b Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 23:33:51 +0200 Subject: [PATCH 04/19] ci: add EDS and libical-glib dependencies to build and cppcheck - Add libecal2.0-dev, libedataserver1.2-dev, libical-glib-dev to Debian/Ubuntu dependency list - Add evolution-data-server, libical to Arch dependency list - Add -DHAVE_EDS, -DHAVE_LIBICAL, -DLIBICAL_GLIB_UNSTABLE_API to cppcheck defines so the new calendar provider code paths are analysed - Add libecal-2.0 and libical-glib to cppcheck pkg-config packages for correct include paths Signed-off-by: Oz Tiram --- .github/workflows/build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40c9afb4b..a62bfbd78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,10 +26,13 @@ env: libatk1.0-dev libcairo2-dev libdconf-dev + libecal2.0-dev + libedataserver1.2-dev libgirepository1.0-dev libglib2.0-dev libgtk-3-dev libgtk-layer-shell-dev + libical-glib-dev libice-dev libmate-desktop-dev libmate-menu-dev @@ -70,6 +73,7 @@ env: ccache autoconf-archive clang + evolution-data-server gcc git glib2-devel @@ -77,6 +81,7 @@ env: gtk-layer-shell itstool libcanberra + libical libmateweather libsm libwnck3 @@ -260,6 +265,7 @@ jobs: -DHAVE_X11 -DHAVE_RANDR -DHAVE_WAYLAND -DCLOCK_INPROCESS -DFISH_INPROCESS -DNOTIFICATION_AREA_INPROCESS -DWNCKLET_INPROCESS + -DHAVE_EDS -DHAVE_LIBICAL -DLIBICAL_GLIB_UNSTABLE_API -DHAVE_LANGINFO_H -DHAVE_NL_LANGINFO -DGETTEXT_PACKAGE="mate-panel" -D__STDC_VERSION__=201112 -D_Noreturn=__attribute__((__noreturn__)) @@ -269,6 +275,8 @@ jobs: gmodule-2.0 gtk+-3.0 ice + libecal-2.0 + libical-glib libwnck-3.0 mate-desktop-2.0 sm From 0cc237073a5b69d5d19b2c9650d6d0abf630b76f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:02:06 +0200 Subject: [PATCH 05/19] fix: really expose displayname from vdirsyncer Signed-off-by: Oz Tiram --- applets/clock/calendar-window.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/applets/clock/calendar-window.c b/applets/clock/calendar-window.c index 6ac67b913..6597935f3 100644 --- a/applets/clock/calendar-window.c +++ b/applets/clock/calendar-window.c @@ -138,6 +138,7 @@ enum { APPOINTMENT_COLUMN_END_TIME, APPOINTMENT_COLUMN_ALL_DAY, APPOINTMENT_COLUMN_COLOR, + APPOINTMENT_COLUMN_BACKEND_NAME, N_APPOINTMENT_COLUMNS }; @@ -953,7 +954,8 @@ calendar_window_create_appointments_model (CalendarWindow *calwin) G_TYPE_STRING, /* APPOINTMENT_COLUMN_START_TEXT */ G_TYPE_ULONG, /* APPOINTMENT_COLUMN_END_TIME */ G_TYPE_BOOLEAN, /* APPOINTMENT_COLUMN_ALL_DAY */ - G_TYPE_STRING); /* APPOINTMENT_COLUMN_COLOR */ + G_TYPE_STRING, /* APPOINTMENT_COLUMN_COLOR */ + G_TYPE_STRING); /* APPOINTMENT_COLUMN_BACKEND_NAME */ calwin->priv->appointments_filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (GTK_TREE_MODEL (calwin->priv->appointments_model), NULL)); gtk_tree_model_filter_set_visible_func (calwin->priv->appointments_filter, @@ -1402,15 +1404,16 @@ handle_appointments_changed (CalendarWindow *calwin) gtk_list_store_append (calwin->priv->appointments_model, &iter); /* Appointment added to model */ gtk_list_store_set (calwin->priv->appointments_model, &iter, - APPOINTMENT_COLUMN_UID, appointment->uid, - APPOINTMENT_COLUMN_TYPE, APPOINTMENT_TYPE_APPOINTMENT, - APPOINTMENT_COLUMN_SUMMARY, appointment->summary, - APPOINTMENT_COLUMN_DESCRIPTION, appointment->description, - APPOINTMENT_COLUMN_START_TIME, (gint64)appointment->start_time, - APPOINTMENT_COLUMN_START_TEXT, start_text, - APPOINTMENT_COLUMN_END_TIME, (gint64)appointment->end_time, - APPOINTMENT_COLUMN_ALL_DAY, appointment->is_all_day, - APPOINTMENT_COLUMN_COLOR, appointment->color_string, + APPOINTMENT_COLUMN_UID, appointment->uid, + APPOINTMENT_COLUMN_TYPE, APPOINTMENT_TYPE_APPOINTMENT, + APPOINTMENT_COLUMN_SUMMARY, appointment->summary, + APPOINTMENT_COLUMN_DESCRIPTION, appointment->description, + APPOINTMENT_COLUMN_START_TIME, (gint64)appointment->start_time, + APPOINTMENT_COLUMN_START_TEXT, start_text, + APPOINTMENT_COLUMN_END_TIME, (gint64)appointment->end_time, + APPOINTMENT_COLUMN_ALL_DAY, appointment->is_all_day, + APPOINTMENT_COLUMN_COLOR, appointment->color_string, + APPOINTMENT_COLUMN_BACKEND_NAME, appointment->backend_name, -1); g_free (start_text); @@ -1602,7 +1605,7 @@ appointment_tooltip_query_cb (GtkWidget *widget, GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; - gchar *summary, *description, *start_text; + gchar *summary, *description, *start_text, *backend_name; gchar *tooltip_text, *end_text; gboolean all_day; gulong start_time, end_time; @@ -1619,6 +1622,7 @@ appointment_tooltip_query_cb (GtkWidget *widget, APPOINTMENT_COLUMN_START_TIME, &start_time, APPOINTMENT_COLUMN_END_TIME, &end_time, APPOINTMENT_COLUMN_ALL_DAY, &all_day, + APPOINTMENT_COLUMN_BACKEND_NAME, &backend_name, -1); if (!summary) { @@ -1657,6 +1661,12 @@ appointment_tooltip_query_cb (GtkWidget *widget, } } + if (backend_name && strlen (backend_name) > 0) { + gchar *with_source = g_markup_printf_escaped ("%s\n%s", tooltip_text, backend_name); + g_free (tooltip_text); + tooltip_text = with_source; + } + gtk_tooltip_set_markup (tooltip, tooltip_text); gtk_tree_view_set_tooltip_row (tree_view, tooltip, path); @@ -1664,6 +1674,7 @@ appointment_tooltip_query_cb (GtkWidget *widget, g_free (description); g_free (start_text); g_free (end_text); + g_free (backend_name); g_free (tooltip_text); gtk_tree_path_free (path); From 97595ea6e88e58a3957e77cd58a387baf2ff13d1 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:02:45 +0200 Subject: [PATCH 06/19] fix: escape html code coming from caldav meeting titles were displayed as event title in the tooltip. Signed-off-by: Oz Tiram --- applets/clock/calendar-window.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/applets/clock/calendar-window.c b/applets/clock/calendar-window.c index 6597935f3..4f9c4f7ff 100644 --- a/applets/clock/calendar-window.c +++ b/applets/clock/calendar-window.c @@ -1662,7 +1662,9 @@ appointment_tooltip_query_cb (GtkWidget *widget, } if (backend_name && strlen (backend_name) > 0) { - gchar *with_source = g_markup_printf_escaped ("%s\n%s", tooltip_text, backend_name); + gchar *escaped_name = g_markup_escape_text (backend_name, -1); + gchar *with_source = g_strdup_printf ("%s\n%s", tooltip_text, escaped_name); + g_free (escaped_name); g_free (tooltip_text); tooltip_text = with_source; } From 1cc89829f0c8a0541f4e2e8a0aae72c3b4e9751c Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:03:52 +0200 Subject: [PATCH 07/19] fix: eds datasource, get the calendar display name e_source_get_display_name is the correct method. Previously, the code showd the protocol type ("caldav", "local", etc.) Signed-off-by: Oz Tiram --- applets/clock/calendar-eds-provider.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/applets/clock/calendar-eds-provider.c b/applets/clock/calendar-eds-provider.c index 795d65cc0..1bb48e704 100644 --- a/applets/clock/calendar-eds-provider.c +++ b/applets/clock/calendar-eds-provider.c @@ -212,14 +212,9 @@ get_source_color (ECalClient *ecal) static char * get_source_backend_name (ECalClient *ecal) { - ESource *source = e_client_get_source (E_CLIENT (ecal)); - ECalClientSourceType stype = e_cal_client_get_source_type (ecal); - const char *ext_name = - (stype == E_CAL_CLIENT_SOURCE_TYPE_EVENTS) - ? E_SOURCE_EXTENSION_CALENDAR - : E_SOURCE_EXTENSION_TASK_LIST; - ESourceBackend *ext = e_source_get_extension (source, ext_name); - return e_source_backend_dup_backend_name (ext); + ESource *source = e_client_get_source (E_CLIENT (ecal)); + const char *name = e_source_get_display_name (source); + return g_strdup (name); } static gboolean From 7532a8d357e85e1122c46d7f40bb99a3e1d506c9 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:12:38 +0200 Subject: [PATCH 08/19] ci: fix package name in debian\ubuntu Signed-off-by: Oz Tiram --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a62bfbd78..0902a13bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ env: libglib2.0-dev libgtk-3-dev libgtk-layer-shell-dev - libical-glib-dev + libical-dev libice-dev libmate-desktop-dev libmate-menu-dev From e679e6cbbbc47b7529167a9fb75b1bc559f3a693 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:55:16 +0200 Subject: [PATCH 09/19] clock: fix libical-glib API compatibility with newer releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i_cal_time_subtract() and i_cal_duration_as_int() were removed in newer libical-glib (as shipped on Arch Linux). Replace them with: - direct i_cal_time_as_timet() subtraction for DTEND − DTSTART - a portable ical_duration_as_secs() helper built from the individual i_cal_duration_get_{weeks,days,hours,minutes,seconds}() accessors Property getter functions (i_cal_property_get_dtstart etc.) changed their parameter from ICalProperty * to const ICalProperty * in newer releases, making them incompatible as function-pointer arguments on both old and new versions simultaneously. Add thin prop_get_dt{start,end}/ prop_get_{due,completed} wrappers typed as ICalTime *(*)(ICalProperty *) so the call sites compile cleanly against either API. Signed-off-by: Oz Tiram --- applets/clock/calendar-eds-provider.c | 36 +++++++++++++++++++++----- applets/clock/calendar-vdir-provider.c | 34 +++++++++++++++++++----- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/applets/clock/calendar-eds-provider.c b/applets/clock/calendar-eds-provider.c index 1bb48e704..bfafd8236 100644 --- a/applets/clock/calendar-eds-provider.c +++ b/applets/clock/calendar-eds-provider.c @@ -39,6 +39,28 @@ #define _(x) gettext(x) #endif +/* i_cal_duration_as_int was removed in newer libical-glib; compute manually. */ +static time_t +ical_duration_as_secs (ICalDuration *dur) +{ + if (!dur) return 0; + time_t secs = (time_t) i_cal_duration_get_weeks (dur) * 7 * 86400 + + (time_t) i_cal_duration_get_days (dur) * 86400 + + (time_t) i_cal_duration_get_hours (dur) * 3600 + + (time_t) i_cal_duration_get_minutes (dur) * 60 + + (time_t) i_cal_duration_get_seconds (dur); + return i_cal_duration_is_neg (dur) ? -secs : secs; +} + +/* In newer libical-glib the property getter functions take 'const ICalProperty *' + * while older versions use 'ICalProperty *'. These thin wrappers always accept + * a non-const pointer so they can be used as function-pointer arguments on both + * old and new versions without triggering -Wincompatible-function-pointer-types. */ +static ICalTime *prop_get_dtstart (ICalProperty *p) { return i_cal_property_get_dtstart (p); } +static ICalTime *prop_get_dtend (ICalProperty *p) { return i_cal_property_get_dtend (p); } +static ICalTime *prop_get_due (ICalProperty *p) { return i_cal_property_get_due (p); } +static ICalTime *prop_get_completed (ICalProperty *p) { return i_cal_property_get_completed (p); } + /* ========================================================================= * Internal types * ========================================================================= @@ -236,7 +258,7 @@ get_component_is_all_day (ICalComponent *comp, time_t end_time = get_time_from_property (comp, I_CAL_DTEND_PROPERTY, - i_cal_property_get_dtend, default_zone); + prop_get_dtend, default_zone); if (end_time) return (end_time - start_time) % 86400 == 0; @@ -245,7 +267,7 @@ get_component_is_all_day (ICalComponent *comp, if (!prop) return FALSE; ICalDuration *dur = i_cal_property_get_duration (prop); - gboolean all_d = i_cal_duration_as_int (dur) % 86400 == 0; + gboolean all_d = ical_duration_as_secs (dur) % 86400 == 0; g_object_unref (dur); g_object_unref (prop); return all_d; @@ -287,10 +309,10 @@ appointment_init (CalendarAppointment *appt, appt->start_time = get_time_from_property (comp, I_CAL_DTSTART_PROPERTY, - i_cal_property_get_dtstart, zone); + prop_get_dtstart, zone); appt->end_time = get_time_from_property (comp, I_CAL_DTEND_PROPERTY, - i_cal_property_get_dtend, zone); + prop_get_dtend, zone); appt->is_all_day = get_component_is_all_day (comp, appt->start_time, zone); } @@ -321,13 +343,13 @@ task_init (CalendarTask *task, task->color_string = get_source_color (src->client); task->start_time = get_time_from_property (comp, I_CAL_DTSTART_PROPERTY, - i_cal_property_get_dtstart, zone); + prop_get_dtstart, zone); task->due_time = get_time_from_property (comp, I_CAL_DUE_PROPERTY, - i_cal_property_get_due, zone); + prop_get_due, zone); task->completed_time = get_time_from_property (comp, I_CAL_COMPLETED_PROPERTY, - i_cal_property_get_completed, zone); + prop_get_completed, zone); ICalPropertyStatus status = i_cal_component_get_status (comp); if (status == I_CAL_STATUS_COMPLETED) diff --git a/applets/clock/calendar-vdir-provider.c b/applets/clock/calendar-vdir-provider.c index deddbcc74..57ee8b82a 100644 --- a/applets/clock/calendar-vdir-provider.c +++ b/applets/clock/calendar-vdir-provider.c @@ -38,6 +38,28 @@ #undef CALENDAR_ENABLE_DEBUG #include "calendar-debug.h" +/* i_cal_duration_as_int was removed in newer libical-glib; compute manually. */ +static time_t +ical_duration_as_secs (ICalDuration *dur) +{ + if (!dur) return 0; + time_t secs = (time_t) i_cal_duration_get_weeks (dur) * 7 * 86400 + + (time_t) i_cal_duration_get_days (dur) * 86400 + + (time_t) i_cal_duration_get_hours (dur) * 3600 + + (time_t) i_cal_duration_get_minutes (dur) * 60 + + (time_t) i_cal_duration_get_seconds (dur); + return i_cal_duration_is_neg (dur) ? -secs : secs; +} + +/* In newer libical-glib the property getter functions take 'const ICalProperty *' + * while older versions use 'ICalProperty *'. These thin wrappers always accept + * a non-const pointer so they can be used as function-pointer arguments on both + * old and new versions without triggering -Wincompatible-function-pointer-types. */ +static ICalTime *prop_get_dtstart (ICalProperty *p) { return i_cal_property_get_dtstart (p); } +static ICalTime *prop_get_dtend (ICalProperty *p) { return i_cal_property_get_dtend (p); } +static ICalTime *prop_get_due (ICalProperty *p) { return i_cal_property_get_due (p); } +static ICalTime *prop_get_completed (ICalProperty *p) { return i_cal_property_get_completed (p); } + struct _CalendarVdirProviderPrivate { char *directory; @@ -143,9 +165,7 @@ expand_recurrences (ICalComponent *comp, dtend = i_cal_component_get_dtend (comp); if (dtend != NULL && !i_cal_time_is_null_time (dtend)) { - ICalDuration *diff = i_cal_time_subtract (dtend, dtstart); - duration = (time_t) i_cal_duration_as_int (diff); - g_object_unref (diff); + duration = i_cal_time_as_timet (dtend) - i_cal_time_as_timet (dtstart); } else { @@ -153,7 +173,7 @@ expand_recurrences (ICalComponent *comp, if (prop != NULL) { ICalDuration *dur = i_cal_property_get_duration (prop); - duration = (time_t) i_cal_duration_as_int (dur); + duration = ical_duration_as_secs (dur); g_object_unref (dur); g_object_unref (prop); prop = NULL; @@ -377,11 +397,11 @@ vtodo_to_task (ICalComponent *comp, (const char *(*)(ICalProperty *)) i_cal_property_get_url); task->start_time = get_time_prop (comp, I_CAL_DTSTART_PROPERTY, - i_cal_property_get_dtstart, zone); + prop_get_dtstart, zone); task->due_time = get_time_prop (comp, I_CAL_DUE_PROPERTY, - i_cal_property_get_due, zone); + prop_get_due, zone); task->completed_time = get_time_prop (comp, I_CAL_COMPLETED_PROPERTY, - i_cal_property_get_completed, zone); + prop_get_completed, zone); ICalPropertyStatus status = i_cal_component_get_status (comp); if (status == I_CAL_STATUS_COMPLETED) From 762ab2bf1a3e03c11937fc3f149b1fd96e7998de Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 20 Aug 2026 20:52:20 +0200 Subject: [PATCH 10/19] docs(clock): document secure vdirsyncer password storage Add guidance for protecting vdirsyncer credentials in the README: chmod 600 / umask on ~/.vdirsyncer/config, and using password.fetch with gnome-keyring as a plaintext-free alternative. Note that this brings vdir credential storage in line with the EDS backend, which already relies on libsecret/gnome-keyring by default. Signed-off-by: Oz Tiram --- applets/clock/README.md | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/applets/clock/README.md b/applets/clock/README.md index 8fd529b13..4991bec7d 100644 --- a/applets/clock/README.md +++ b/applets/clock/README.md @@ -80,6 +80,50 @@ The applet will pick up the synced events automatically on next start. Live updates are detected via `GFileMonitor` — new or changed `.ics` files are reflected in the calendar window without restarting the applet. +#### Protecting your config and credentials + +The `config` file above stores the CalDAV `password` in plain text. This is +easy to overlook, but the file should not be left world-readable: + +``` +chmod 600 ~/.vdirsyncer/config +``` + +Alternatively, set a restrictive `umask` (e.g. `umask 077`) before creating +the file so it is never created with looser permissions in the first place. + +Better still, avoid storing the password in the config file at all. +vdirsyncer supports `password.fetch`, which runs a command to retrieve the +password at sync time. On a MATE desktop with `gnome-keyring` running, you +can store the password in the keyring instead: + +``` +$ keyring set mycalendar.com myuser +Password for 'myuser' in 'mycalendar.com': ******** +``` + +Then reference it from the config instead of writing the password inline: + +```ini +[storage my_remote] +type = "caldav" +url = "https://your-caldav-server/path/" +username = "user@example.com" +password.fetch = ["command", "keyring", "get", "mycalendar.com", "myuser"] +``` + +This keeps the credential out of `~/.vdirsyncer/config` entirely, backed by +the keyring's own encrypted storage. See the +[vdirsyncer documentation on password storage](https://vdirsyncer.readthedocs.io/en/stable/config.html#supplying-passwords) +for other supported fetch backends. + +Using `password.fetch` this way brings the vdir backend's credential storage +in line with the EDS backend: EDS-managed calendar accounts (via GNOME +Online Accounts or Evolution's own account setup) already store their +passwords in the system keyring through libsecret rather than in a plaintext +file. With `password.fetch` configured, both backends end up relying on the +same keyring-backed secret storage instead of an on-disk password. + #### Recurring events Recurring events (RRULE, RDATE, EXDATE) are fully expanded using libical-glib, From f8e19a466634ba9f9b0fe13f5f029fadc31f09b8 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 20 Aug 2026 21:04:27 +0200 Subject: [PATCH 11/19] fix(clock): declare CalendarClient for libical-only builds calendar-window.h guarded the calendar-client.h include and the calendar_window_set_client prototype with #ifdef HAVE_EDS only, while calendar-window.c already used the HAVE_EDS || HAVE_LIBICAL convention used elsewhere in the applet. Building with --enable-libical --disable-eds left CalendarClient undeclared and the prototype missing, breaking clock.c compilation. Align both guards in the header with HAVE_EDS || HAVE_LIBICAL. Signed-off-by: Oz Tiram --- applets/clock/calendar-window.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applets/clock/calendar-window.h b/applets/clock/calendar-window.h index 226353bad..e0f346b65 100644 --- a/applets/clock/calendar-window.h +++ b/applets/clock/calendar-window.h @@ -31,7 +31,7 @@ #include #include "clock-utils.h" -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) #include "calendar-client.h" #endif @@ -82,7 +82,7 @@ ClockFormat calendar_window_get_time_format (CalendarWindow *calwin); void calendar_window_set_time_format (CalendarWindow *calwin, ClockFormat time_format); -#ifdef HAVE_EDS +#if defined(HAVE_EDS) || defined(HAVE_LIBICAL) void calendar_window_set_client (CalendarWindow *calwin, CalendarClient *client); #endif From 61ee4f3df449517dba411faf2f24d4bd996e8856 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 20 Aug 2026 21:38:02 +0200 Subject: [PATCH 12/19] docs(clock): document how to use fetch command with vdirsync vdirsync has a none obvious quirk discovered while testing. Document how to bypass it, without needing to fix vdirsync. Signed-off-by: Oz Tiram --- applets/clock/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/applets/clock/README.md b/applets/clock/README.md index 4991bec7d..82bd78937 100644 --- a/applets/clock/README.md +++ b/applets/clock/README.md @@ -117,6 +117,16 @@ the keyring's own encrypted storage. See the [vdirsyncer documentation on password storage](https://vdirsyncer.readthedocs.io/en/stable/config.html#supplying-passwords) for other supported fetch backends. +**Pick a plain identifier for the keyring service name — not the CalDAV +URL.** vdirsyncer runs every argument in `password.fetch` through path +normalization (to support `~`-expanding local script paths), which silently +mangles URLs: `https://host/path/` becomes `https:/host/path` (collapsed +double slash, dropped trailing slash), and `keyring get` will then look up a +service name that was never stored, failing with a non-obvious "Command +... returned non-zero exit status 1" error. Using `mycalendar.com` (as +above) rather than `https://mycalendar.com/dav/` as the service name avoids +this entirely. + Using `password.fetch` this way brings the vdir backend's credential storage in line with the EDS backend: EDS-managed calendar accounts (via GNOME Online Accounts or Evolution's own account setup) already store their From 4a3c525f9d9d28bc0d8f4713c61faa5ba47ab32b Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Fri, 21 Aug 2026 12:13:20 +0200 Subject: [PATCH 13/19] fix(clock): resolve vdir provider timezone via SystemTimezone calendar-vdir-provider.c derived the local timezone with strftime("%Z") and matched it against a builtin timezone by abbreviation. TZ abbreviations are ambiguous (e.g. "CST" maps to multiple zones), so the lookup can silently fail and fall back to UTC, shifting event times. Use SystemTimezone / system_timezone_get(), as calendar-eds-provider.c already does, to resolve an unambiguous zone name (e.g. "Europe/Berlin") from system configuration instead. Signed-off-by: Oz Tiram --- applets/clock/calendar-vdir-provider.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/applets/clock/calendar-vdir-provider.c b/applets/clock/calendar-vdir-provider.c index 57ee8b82a..d4ddd88b7 100644 --- a/applets/clock/calendar-vdir-provider.c +++ b/applets/clock/calendar-vdir-provider.c @@ -34,6 +34,7 @@ #include #include "calendar-vdir-provider.h" +#include "system-timezone.h" #undef CALENDAR_ENABLE_DEBUG #include "calendar-debug.h" @@ -790,18 +791,18 @@ calendar_vdir_provider_new (const char *directory) priv->display_name = read_metadata_file (directory, "displayname"); priv->color_string = read_metadata_file (directory, "color"); - /* Resolve the local timezone via the system TZ name */ + /* Resolve the local timezone the same way the EDS provider does: + * strftime("%Z") can yield ambiguous abbreviations (e.g. "CST" is used + * by multiple zones) that fail to match a builtin timezone, silently + * falling back to UTC. SystemTimezone resolves the unambiguous zone + * name (e.g. "Europe/Berlin") from system configuration instead. */ { - time_t now = time (NULL); - struct tm local = { 0, }; - localtime_r (&now, &local); - char tz_name[64]; - if (strftime (tz_name, sizeof (tz_name), "%Z", &local) > 0) - { - ICalTimezone *tz = i_cal_timezone_get_builtin_timezone (tz_name); - if (tz != NULL) - priv->zone = tz; - } + SystemTimezone *systz = system_timezone_new (); + const char *tz_name = system_timezone_get (systz); + ICalTimezone *tz = i_cal_timezone_get_builtin_timezone (tz_name); + if (tz != NULL) + priv->zone = tz; + g_object_unref (systz); } /* Set up a directory monitor for live updates */ From f1cb6e09a1a76fc343bebf532c1a872733a49db7 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Fri, 21 Aug 2026 22:46:29 +0200 Subject: [PATCH 14/19] build: verify libical-glib as a real dependency when EDS is enabled The EDS-implies-libical fallback in configure.ac defined HAVE_LIBICAL and set LIBICAL_CFLAGS/LIBICAL_LIBS by aliasing EDS_CFLAGS, without ever running PKG_CHECK_MODULES for libical-glib itself. This borrowed EDS_CFLAGS's transitive inclusion of libical-glib's headers, which only holds if libecal-2.0.pc lists libical-glib under a public Requires rather than Requires.private - a packaging convention, not a guarantee. Run a real PKG_CHECK_MODULES(LIBICAL, ...) whenever EDS is enabled but libical wasn't already detected on its own, so the dependency is verified rather than assumed, with a proper configure-time error if it's missing. Signed-off-by: Oz Tiram --- configure.ac | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index c295b76cb..76ad3f85a 100644 --- a/configure.ac +++ b/configure.ac @@ -140,12 +140,16 @@ else have_libical=no fi -# EDS implies libical (libecal depends on libical-glib) +# EDS depends on libical-glib itself (calendar-eds-provider.c uses its +# ICalComponent API directly), so treat it as a hard dependency whenever +# EDS is enabled rather than assuming EDS_CFLAGS happens to expose +# libical-glib's headers transitively - that only holds if libecal-2.0.pc +# lists libical-glib under a public Requires, not Requires.private, which +# is a packaging detail we shouldn't rely on. if test "x$have_eds" = "xyes" && test "x$have_libical" = "xno"; then - have_libical=yes + PKG_CHECK_MODULES(LIBICAL, [libical-glib >= 3.0], have_libical=yes, + AC_MSG_ERROR([EDS support requires libical-glib development libraries])) AC_DEFINE(HAVE_LIBICAL, 1, [Define if libical-glib is available]) - LIBICAL_CFLAGS="$EDS_CFLAGS" - LIBICAL_LIBS="" fi AM_CONDITIONAL(HAVE_LIBICAL, test "x$have_libical" = "xyes") From 5939c48b4cbd6bb3865bafc9009061f8e8c53226 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 26 Sep 2026 20:14:55 +0200 Subject: [PATCH 15/19] clock: check vdir key with g_settings_schema_has_key Replace the g_settings_list_keys() + linear-scan loop with a single g_settings_schema_has_key() call on the applet's schema, as suggested in review. Signed-off-by: Oz Tiram --- applets/clock/calendar-client.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/applets/clock/calendar-client.c b/applets/clock/calendar-client.c index b9726f260..36afe48e3 100644 --- a/applets/clock/calendar-client.c +++ b/applets/clock/calendar-client.c @@ -33,6 +33,7 @@ #include "calendar-client.h" #include "calendar-provider.h" +#include "clock.h" #ifdef HAVE_LIBICAL # include "calendar-vdir-provider.h" @@ -446,14 +447,17 @@ calendar_client_new (GSettings *settings) /* Additional paths from GSettings (if the key exists in @settings) */ if (settings != NULL) { - /* Check whether this settings instance has the vdir key before - * calling get_strv (avoids a GLib critical if key is absent). */ - gchar **all_keys = g_settings_list_keys (settings); - gboolean has_key = FALSE; - for (int ki = 0; all_keys[ki] != NULL; ki++) - if (g_strcmp0 (all_keys[ki], KEY_VDIR_CALENDAR_PATHS) == 0) - { has_key = TRUE; break; } - g_strfreev (all_keys); + /* Check whether the applet's schema has the vdir key before + * calling get_strv (avoids a GLib critical if the key is + * absent, e.g. with a stale installed schema). */ + GSettingsSchemaSource *source = g_settings_schema_source_get_default (); + GSettingsSchema *schema = (source != NULL) + ? g_settings_schema_source_lookup (source, CLOCK_SCHEMA, FALSE) + : NULL; + gboolean has_key = (schema != NULL && + g_settings_schema_has_key (schema, KEY_VDIR_CALENDAR_PATHS)); + if (schema != NULL) + g_settings_schema_unref (schema); if (has_key) { @@ -468,7 +472,7 @@ calendar_client_new (GSettings *settings) } } g_strfreev (paths); - } /* if (has_key) */ + } /* if (schema has key) */ } /* if (settings != NULL) */ } /* HAVE_LIBICAL block */ #endif /* HAVE_LIBICAL */ From cce60fbc54c7a67a2a001d63c0aff7b90784a31f Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 26 Sep 2026 20:52:42 +0200 Subject: [PATCH 16/19] clock: watch vdir-calendar-paths so changes apply at runtime Previously the 'vdir-calendar-paths' GSettings key was only read once during calendar_client_new(), so changing the value required restarting the applet to take effect. Track the providers created from the key in a separate list and, when the key changes, tear those providers down and re-create them from the new value. If a month is already selected, re-issue the selection so the fresh providers populate their caches immediately. Signed-off-by: Oz Tiram --- applets/clock/calendar-client.c | 128 ++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 31 deletions(-) diff --git a/applets/clock/calendar-client.c b/applets/clock/calendar-client.c index 36afe48e3..10acc0dc3 100644 --- a/applets/clock/calendar-client.c +++ b/applets/clock/calendar-client.c @@ -59,6 +59,11 @@ struct _CalendarClientPrivate guint day; guint month; guint year; + + /* vdir providers created from the "vdir-calendar-paths" GSettings key; + * kept separately so they can be torn down and re-created when the key + * changes at runtime */ + GSList *vdir_settings_providers; }; static void calendar_client_finalize (GObject *object); @@ -318,6 +323,9 @@ calendar_client_finalize (GObject *object) g_slist_free (client->priv->providers); client->priv->providers = NULL; + g_slist_free (client->priv->vdir_settings_providers); + client->priv->vdir_settings_providers = NULL; + G_OBJECT_CLASS (calendar_client_parent_class)->finalize (object); } @@ -398,6 +406,19 @@ calendar_client_add_provider (CalendarClient *client, G_CALLBACK (on_provider_tasks_changed), client); } +static void +calendar_client_remove_provider (CalendarClient *client, + CalendarProvider *provider) +{ + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (CALENDAR_IS_PROVIDER (provider)); + + client->priv->providers = + g_slist_remove (client->priv->providers, provider); + + g_object_unref (provider); +} + /* ========================================================================= * Auto-discovery helpers * ========================================================================= */ @@ -414,6 +435,72 @@ add_vdir_providers_from_path (CalendarClient *client, const char *path) } g_slist_free (providers); } + +/* True if the applet's schema defines the vdir paths key (avoids a GLib + * critical from g_settings_get_strv if the key is absent, e.g. with a + * stale installed schema). */ +static gboolean +clock_applet_schema_has_vdir_key (void) +{ + GSettingsSchemaSource *source = g_settings_schema_source_get_default (); + GSettingsSchema *schema = (source != NULL) + ? g_settings_schema_source_lookup (source, CLOCK_SCHEMA, FALSE) + : NULL; + gboolean has_key = (schema != NULL && + g_settings_schema_has_key (schema, KEY_VDIR_CALENDAR_PATHS)); + if (schema != NULL) + g_settings_schema_unref (schema); + return has_key; +} + +/* (Re)apply the "vdir-calendar-paths" setting: drop the vdir providers + * created from a previous value of the key and create one provider per + * currently configured path. */ +static void +apply_vdir_paths_from_settings (CalendarClient *client, + GSettings *settings) +{ + CalendarClientPrivate *priv = client->priv; + + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (settings != NULL); + + for (GSList *l = priv->vdir_settings_providers; l != NULL; l = l->next) + calendar_client_remove_provider (client, CALENDAR_PROVIDER (l->data)); + g_slist_free (priv->vdir_settings_providers); + priv->vdir_settings_providers = NULL; + + if (!clock_applet_schema_has_vdir_key ()) + return; + + gchar **paths = g_settings_get_strv (settings, KEY_VDIR_CALENDAR_PATHS); + for (gint i = 0; paths[i] != NULL; i++) + { + CalendarProvider *p = calendar_vdir_provider_new (paths[i]); + if (p != NULL) + { + priv->vdir_settings_providers = + g_slist_prepend (priv->vdir_settings_providers, p); + calendar_client_add_provider (client, p); + g_object_unref (p); + } + } + g_strfreev (paths); + + /* If a month is already selected, re-issue the selection so the new + * providers populate their caches immediately. */ + if (priv->month != G_MAXUINT && priv->year != G_MAXUINT) + calendar_client_update_appointments (client); +} + +static void +on_vdir_paths_changed (GSettings *settings, + gchar *key, + CalendarClient *client) +{ + (void) key; + apply_vdir_paths_from_settings (client, settings); +} #endif /* HAVE_LIBICAL */ /* ========================================================================= @@ -443,38 +530,17 @@ calendar_client_new (GSettings *settings) char *vdir_base = g_build_filename (data_home, VDIRSYNCER_DEFAULT_SUBDIR, NULL); add_vdir_providers_from_path (client, vdir_base); g_free (vdir_base); + } - /* Additional paths from GSettings (if the key exists in @settings) */ - if (settings != NULL) - { - /* Check whether the applet's schema has the vdir key before - * calling get_strv (avoids a GLib critical if the key is - * absent, e.g. with a stale installed schema). */ - GSettingsSchemaSource *source = g_settings_schema_source_get_default (); - GSettingsSchema *schema = (source != NULL) - ? g_settings_schema_source_lookup (source, CLOCK_SCHEMA, FALSE) - : NULL; - gboolean has_key = (schema != NULL && - g_settings_schema_has_key (schema, KEY_VDIR_CALENDAR_PATHS)); - if (schema != NULL) - g_settings_schema_unref (schema); - - if (has_key) - { - gchar **paths = g_settings_get_strv (settings, KEY_VDIR_CALENDAR_PATHS); - for (gint i = 0; paths[i] != NULL; i++) - { - CalendarProvider *p = calendar_vdir_provider_new (paths[i]); - if (p != NULL) - { - calendar_client_add_provider (client, p); - g_object_unref (p); - } - } - g_strfreev (paths); - } /* if (schema has key) */ - } /* if (settings != NULL) */ - } /* HAVE_LIBICAL block */ + /* Additional paths from GSettings (if the key exists in @settings). + * The key is watched so that changes take effect without a restart. */ + if (settings != NULL && clock_applet_schema_has_vdir_key ()) + { + apply_vdir_paths_from_settings (client, settings); + + g_signal_connect_object (G_OBJECT (settings), "changed::" KEY_VDIR_CALENDAR_PATHS, + G_CALLBACK (on_vdir_paths_changed), client, 0); + } #endif /* HAVE_LIBICAL */ (void) settings; /* suppress warning when both EDS and libical are absent */ From 2ed78caef4abdc16a1b5487863e5d1aa6ac8ae01 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 26 Sep 2026 20:53:51 +0200 Subject: [PATCH 17/19] clock: fix comment claiming discovery only recurses one level The discovery function recurses into any non-collection subdirectory, i.e. to arbitrary depth; say so in the comment. Signed-off-by: Oz Tiram --- applets/clock/calendar-vdir-provider.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/applets/clock/calendar-vdir-provider.c b/applets/clock/calendar-vdir-provider.c index d4ddd88b7..f4e60c5d8 100644 --- a/applets/clock/calendar-vdir-provider.c +++ b/applets/clock/calendar-vdir-provider.c @@ -908,8 +908,10 @@ calendar_vdir_discover (const char *base_path) } else { - /* Not a collection itself — recurse one level (vdirsyncer stores - * collections as ///) */ + /* Not a collection itself — recurse into it to look for nested + * collections (vdirsyncer stores them as + * ///, but any deeper + * hierarchy is handled the same way). */ GSList *nested = calendar_vdir_discover (subdir); providers = g_slist_concat (providers, nested); } From 91186442a33fe84f61f2f7d2c37a84dbfbc27087 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 26 Sep 2026 20:59:32 +0200 Subject: [PATCH 18/19] clock: guard against NULL rrule/iterator in recurrence expansion i_cal_property_get_rrule() may return NULL for an RRULE property that carries no value, and the resulting iterator may not be usable either. Check both, and when the iterator cannot be created fall back to a single occurrence at DTSTART so such events do not disappear. Signed-off-by: Oz Tiram --- applets/clock/calendar-vdir-provider.c | 46 +++++++++++++++++--------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/applets/clock/calendar-vdir-provider.c b/applets/clock/calendar-vdir-provider.c index f4e60c5d8..fe53a27ac 100644 --- a/applets/clock/calendar-vdir-provider.c +++ b/applets/clock/calendar-vdir-provider.c @@ -229,31 +229,45 @@ expand_recurrences (ICalComponent *comp, prop = i_cal_component_get_first_property (comp, I_CAL_RRULE_PROPERTY); if (prop != NULL) { - ICalRecurrence *rrule = i_cal_property_get_rrule (prop); - ICalRecurIterator *iter = i_cal_recur_iterator_new (rrule, dtstart); + ICalRecurrence *rrule = i_cal_property_get_rrule (prop); + ICalRecurIterator *iter = (rrule != NULL) + ? i_cal_recur_iterator_new (rrule, dtstart) + : NULL; - ICalTime *next; - while (TRUE) + if (iter != NULL) { - next = i_cal_recur_iterator_next (iter); - if (next == NULL || i_cal_time_is_null_time (next)) + ICalTime *next; + while (TRUE) { - if (next) g_object_unref (next); - break; - } + next = i_cal_recur_iterator_next (iter); + if (next == NULL || i_cal_time_is_null_time (next)) + { + if (next) g_object_unref (next); + break; + } + + time_t occ_start = i_cal_time_as_timet_with_zone (next, zone); + g_object_unref (next); - time_t occ_start = i_cal_time_as_timet_with_zone (next, zone); - g_object_unref (next); + /* RRULE iterates in ascending order; stop once past range */ + if (occ_start >= range_end) + break; - /* RRULE iterates in ascending order; stop once past range */ - if (occ_start >= range_end) - break; + MAYBE_ADD_OCC (occ_start); + } + g_object_unref (iter); + } + else + { + /* Empty or malformed RRULE: fall back to a single occurrence + * at DTSTART so the event does not disappear entirely. */ + time_t occ_start = i_cal_time_as_timet_with_zone (dtstart, zone); MAYBE_ADD_OCC (occ_start); } - g_object_unref (iter); - g_object_unref (rrule); + if (rrule != NULL) + g_object_unref (rrule); g_object_unref (prop); prop = NULL; } From 07b59606bd6a723ea2e5add063dd04ec3d303f34 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Sat, 26 Sep 2026 21:11:24 +0200 Subject: [PATCH 19/19] build: fix meson build of the clock applet calendar sources The clock applet's meson build was broken and out of sync with the autotools build: - it referenced the long-gone calendar-sources.c/h, - it only compiled the calendar client when EDS was enabled and never compiled calendar-provider.c at all, - it had no notion of libical-glib (no dependency, no HAVE_LIBICAL, no LIBICAL_GLIB_UNSTABLE_API define, no vdir provider sources). Mirror the autotools layout: the calendar client/provider sources are always built, the EDS provider is gated on EDS and the vdir provider on libical-glib. Add an enable-libical option (like --enable-libical) and treat libical-glib as a hard requirement when EDS is enabled, as configure.ac does. Signed-off-by: Oz Tiram --- applets/clock/meson.build | 30 +++++++++++++++++++++++++----- meson.build | 13 +++++++++++++ meson_options.txt | 6 ++++++ 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/applets/clock/meson.build b/applets/clock/meson.build index c0db5a174..19901875a 100644 --- a/applets/clock/meson.build +++ b/applets/clock/meson.build @@ -134,13 +134,25 @@ clock_sources = [ clock_built_sources, ] +clock_sources += [ + 'calendar-client.c', + 'calendar-client.h', + 'calendar-provider.c', + 'calendar-provider.h', + 'calendar-debug.h', +] + +if have_libical + clock_sources += [ + 'calendar-vdir-provider.c', + 'calendar-vdir-provider.h', + ] +endif + if have_eds clock_sources += [ - 'calendar-client.c', - 'calendar-client.h', - 'calendar-sources.c', - 'calendar-sources.h', - 'calendar-debug.h', + 'calendar-eds-provider.c', + 'calendar-eds-provider.h', ] endif @@ -154,6 +166,10 @@ clock_deps = [ m_dep, ] + libmate_panel_applet_deps +if have_libical + clock_deps += libical_dep +endif + if have_eds clock_deps += eds_deps endif @@ -172,6 +188,10 @@ clock_c_args = [ '-DMATEWEATHER_I_KNOW_THIS_IS_UNSTABLE', ] + disable_deprecated_flags +if have_libical + clock_c_args += '-DLIBICAL_GLIB_UNSTABLE_API' +endif + if clock_inprocess clock_applet = shared_library('clock-applet', clock_sources, diff --git a/meson.build b/meson.build index a969141e3..36af38f68 100644 --- a/meson.build +++ b/meson.build @@ -238,6 +238,18 @@ eds_deps = [ have_eds = eds_deps[0].found() and eds_deps[1].found() config_h.set('HAVE_EDS', have_eds) +# libical-glib for vdir calendar support (independent of EDS) +libical_opt = get_option('enable-libical') +libical_dep = dependency('libical-glib', version: '>= 3.0', required: libical_opt) +have_libical = libical_dep.found() + +# EDS depends on libical-glib (calendar-eds-provider.c uses its ICalComponent +# API directly), so treat it as a hard requirement whenever EDS is enabled. +if have_eds and not have_libical + error('EDS support requires libical-glib development libraries') +endif +config_h.set('HAVE_LIBICAL', have_libical) + # applets to compile in-process in_process_applets = get_option('in-process-applets') clock_inprocess = false @@ -320,6 +332,7 @@ summary({ 'X11 support': have_x11, 'XRandr support': have_randr, 'Evolution Data Server support': have_eds, + 'vdir calendar support (libical)': have_libical, 'Build introspection support': enable_introspection, 'Build gtk-doc documentation': enable_gtk_doc, }, section: 'mate-panel') diff --git a/meson_options.txt b/meson_options.txt index af77de816..f992ff4b9 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -10,6 +10,12 @@ option('enable-eds', description: 'Enable Evolution Data Server calendar integration' ) +option('enable-libical', + type: 'feature', + value: 'auto', + description: 'Enable vdir calendar support via libical-glib' +) + option('enable-x11', type: 'feature', value: 'auto',