From 574863ef5ee9b1b88f664f95c5c6b4d9e15c7370 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 22:19:01 +0200 Subject: [PATCH 1/9] 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 92dd8aae9..902d621b9 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= @@ -395,6 +424,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 d3bbd5237af4e248bc56282a228069606646fc7e Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 23:19:28 +0200 Subject: [PATCH 2/9] 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 76a77890c9fb56c236a8f040dabb8c70c2c0b4fc Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 23:25:39 +0200 Subject: [PATCH 3/9] 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 9a58669add7a5cb791d1698960f8929c26d120a1 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Wed, 10 Jun 2026 23:33:51 +0200 Subject: [PATCH 4/9] 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 d1febfd66..43a8b1c00 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 @@ -69,6 +72,7 @@ env: ccache autoconf-archive clang + evolution-data-server gcc git glib2-devel @@ -76,6 +80,7 @@ env: gtk-layer-shell itstool libcanberra + libical libmateweather libsm libwnck3 @@ -258,6 +263,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__)) @@ -267,6 +273,8 @@ jobs: gmodule-2.0 gtk+-3.0 ice + libecal-2.0 + libical-glib libwnck-3.0 mate-desktop-2.0 sm From 3917631a1ce5531b2734173e57f38565cc20761e Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:02:06 +0200 Subject: [PATCH 5/9] 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 44309f71f26acbf8ab1424af989ea0f1e5bb7ac6 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:02:45 +0200 Subject: [PATCH 6/9] 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 26f04afbb3da18dce176c22439bb88d100ed7987 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:03:52 +0200 Subject: [PATCH 7/9] 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 4030090f20baf27e27a592a3f220fbb850fef233 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:12:38 +0200 Subject: [PATCH 8/9] 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 43a8b1c00..6304284fd 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 39e201efa249f952fe386d7abbc75d589694e661 Mon Sep 17 00:00:00 2001 From: Oz Tiram Date: Thu, 11 Jun 2026 10:55:16 +0200 Subject: [PATCH 9/9] 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)