diff options
author | Victor Kareh <[email protected]> | 2025-07-25 07:22:50 -0400 |
---|---|---|
committer | Victor Kareh <[email protected]> | 2025-07-25 16:52:45 -0400 |
commit | dafe6ccfd17ede3af0c8a92cc06a545c4d1723ef (patch) | |
tree | 7777098fb6a2b225aa0878012f475a9ce13befd2 /applets/clock | |
parent | d62e45c2b0aaf4b15eecc2e8f1529f42fb4c8e6d (diff) | |
download | mate-panel-evolution-calendar-integration.tar.bz2 mate-panel-evolution-calendar-integration.tar.xz |
clock: Backport Evolution calendar integration from gnome-panelevolution-calendar-integration
Add Evolution Data Server (EDS) calendar integration to the MATE panel
clock applet, most of it ported from gnome-panel's clock applet. This
allows users to view calendar events and tasks directly from the clock
popup calendar.
The calendar integration is disabled by default and can be enabled
with --enable-eds during configuration. When disabled, the clock
applet functions normally without any EDS dependencies.
Diffstat (limited to 'applets/clock')
-rw-r--r-- | applets/clock/Makefile.am | 17 | ||||
-rw-r--r-- | applets/clock/calendar-client.c | 2209 | ||||
-rw-r--r-- | applets/clock/calendar-client.h | 151 | ||||
-rw-r--r-- | applets/clock/calendar-debug.h | 50 | ||||
-rw-r--r-- | applets/clock/calendar-sources.c | 503 | ||||
-rw-r--r-- | applets/clock/calendar-sources.h | 64 | ||||
-rw-r--r-- | applets/clock/calendar-window.c | 1126 | ||||
-rw-r--r-- | applets/clock/calendar-window.h | 3 | ||||
-rw-r--r-- | applets/clock/clock.c | 46 | ||||
-rw-r--r-- | applets/clock/clock.ui | 95 | ||||
-rw-r--r-- | applets/clock/org.mate.panel.applet.clock.gschema.xml.in | 10 |
11 files changed, 4262 insertions, 12 deletions
diff --git a/applets/clock/Makefile.am b/applets/clock/Makefile.am index e145dd5f..5a932422 100644 --- a/applets/clock/Makefile.am +++ b/applets/clock/Makefile.am @@ -35,6 +35,15 @@ 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-debug.h +endif + CLOCK_CPPFLAGS = \ $(AM_CPPFLAGS) \ $(LIBMATE_PANEL_APPLET_CFLAGS) \ @@ -44,6 +53,10 @@ CLOCK_CPPFLAGS = \ -DMATELOCALEDIR=\""$(datadir)/locale"\" \ -DMATEWEATHER_I_KNOW_THIS_IS_UNSTABLE +if HAVE_EDS +CLOCK_CPPFLAGS += $(EDS_CFLAGS) +endif + CLOCK_LDADD = \ ../../libmate-panel-applet/libmate-panel-applet-4.la \ $(CLOCK_LIBS) \ @@ -51,6 +64,10 @@ CLOCK_LDADD = \ libsystem-timezone.la \ -lm +if HAVE_EDS +CLOCK_LDADD += $(EDS_LIBS) +endif + test_system_timezone_SOURCES = \ test-system-timezone.c test_system_timezone_LDADD = libsystem-timezone.la diff --git a/applets/clock/calendar-client.c b/applets/clock/calendar-client.c new file mode 100644 index 00000000..c251658d --- /dev/null +++ b/applets/clock/calendar-client.c @@ -0,0 +1,2209 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Mark McLoughlin <[email protected]> + * William Jon McCann <[email protected]> + * Martin Grimme <[email protected]> + * Christian Kellner <[email protected]> + */ + +#include <config.h> + +#include "calendar-client.h" + +#include <libintl.h> +#include <string.h> +#define HANDLE_LIBICAL_MEMORY + +#ifdef HAVE_EDS + +#include <libecal/libecal.h> +#include "calendar-sources.h" +#endif + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +#ifndef _ +#define _(x) gettext(x) +#endif + +#ifndef N_ +#define N_(x) x +#endif + +#ifdef HAVE_EDS + +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; + + guint changed_signal_id; + + guint query_completed : 1; + guint query_in_progress : 1; +}; + +struct _CalendarClientPrivate +{ + CalendarSources *calendar_sources; + + GSList *appointment_sources; + GSList *task_sources; + + ICalTimezone *zone; + + guint zone_listener; + GSettings *calendar_settings; + + 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_update_appointments (CalendarClient *client); +static void +calendar_client_update_tasks (CalendarClient *client); + +enum +{ + PROP_O, + PROP_DAY, + PROP_MONTH, + PROP_YEAR +}; + +enum +{ + APPOINTMENTS_CHANGED, + TASKS_CHANGED, + LAST_SIGNAL +}; + +static guint signals [LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE_WITH_PRIVATE (CalendarClient, calendar_client, G_TYPE_OBJECT) + +static void +calendar_client_class_init (CalendarClientClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->finalize = calendar_client_finalize; + gobject_class->set_property = calendar_client_set_property; + gobject_class->get_property = calendar_client_get_property; + + 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); +} + +/* 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) +{ + return g_settings_get_string (calendar_settings, "timezone"); +} + +static ICalTimezone * +calendar_client_config_get_icaltimezone (CalendarClient *client) +{ + gchar *location = NULL; + ICalTimezone *zone = NULL; + + 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 (); + } + + zone = i_cal_timezone_get_builtin_timezone (location); + g_free (location); + + return zone; +} + +static void +calendar_client_set_timezone (CalendarClient *client) +{ + GList *list, *link; + + client->priv->zone = calendar_client_config_get_icaltimezone (client); + + list = calendar_sources_get_appointment_clients (client->priv->calendar_sources); + for (link = list; link != NULL; link = g_list_next (link)) + { + ECalClient *cal = E_CAL_CLIENT (link->data); + + e_cal_client_set_default_timezone (cal, client->priv->zone); + } + g_list_free (list); +} + +static void +calendar_client_timezone_changed_cb (GSettings *calendar_settings, + const gchar *key, + CalendarClient *client) +{ + calendar_client_set_timezone (client); +} + +static void +load_calendars (CalendarClient *client, + CalendarEventType type) +{ + GSList *l, *clients; + + 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 (); + } + + 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); + } +} + +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->month = 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; + + 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) +{ + 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); + break; + case PROP_YEAR: + 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) +{ + 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; + 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"; + if (g_settings_schema_source_lookup (schema_source, evolution_calendar_schema, FALSE)) + client->priv->calendar_settings = g_settings_new (evolution_calendar_schema); + } + + return client; +} + +/* @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) +{ + 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); +} + +static inline char * +make_isodate_for_day_begin (int day, + int month, + int year) +{ + time_t utctime; + + utctime = make_time_for_day_begin (day, month, year); + + return utctime != -1 ? isodate_from_time_t (utctime) : NULL; +} + +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); + + if (param) + { + const char *tzid; + + tzid = i_cal_parameter_get_tzid (param); + timezone = i_cal_timezone_get_builtin_timezone_from_tzid (tzid); + g_object_unref (param); + + /* 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; + } + + retval = i_cal_time_as_timet_with_zone (ical_time, timezone); + + g_object_unref (ical_time); + + return retval; +} + +static char * +get_component_uid (ICalComponent *component) +{ + return g_strdup (i_cal_component_get_uid (component)); +} + +static char * +get_component_rid (ICalComponent *component) +{ + ICalProperty *prop; + ICalTime *time; + char *rid; + + prop = i_cal_component_get_first_property (component, I_CAL_RECURRENCEID_PROPERTY); + if (!prop) + return NULL; + + time = i_cal_property_get_recurrenceid (prop); + g_object_unref (prop); + + if (!i_cal_time_is_valid_time (time) || i_cal_time_is_null_time (time)) + { + g_object_unref (time); + return NULL; + } + + rid = g_strdup (i_cal_time_as_ical_string (time)); + g_object_unref (time); + + return rid; +} + +static char * +get_component_summary (ICalComponent *component) +{ + ICalProperty *prop; + char *summary; + + prop = i_cal_component_get_first_property (component, I_CAL_SUMMARY_PROPERTY); + if (!prop) + return NULL; + + summary = g_strdup (i_cal_property_get_summary (prop)); + g_object_unref (prop); + + return summary; +} + +static char * +get_component_description (ICalComponent *component) +{ + ICalProperty *prop; + char *description; + + prop = i_cal_component_get_first_property (component, I_CAL_DESCRIPTION_PROPERTY); + if (!prop) + return NULL; + + description = g_strdup (i_cal_property_get_description (prop)); + g_object_unref (prop); + + return description; +} + +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); +} + +static inline time_t +get_component_end_time (ICalComponent *component, + ICalTimezone *default_zone) +{ + return get_time_from_property (component, + I_CAL_DTEND_PROPERTY, + i_cal_property_get_dtend, + default_zone); +} + +static gboolean +get_component_is_all_day (ICalComponent *component, + time_t start_time, + ICalTimezone *default_zone) +{ + ICalTime *dtstart; + struct tm *start_tm; + time_t end_time; + ICalProperty *prop; + ICalDuration *duration; + gboolean is_all_day; + + dtstart = i_cal_component_get_dtstart (component); + + if (dtstart && i_cal_time_is_date (dtstart)) + { + g_object_unref (dtstart); + return TRUE; + } + + 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; +} + +static inline time_t +get_component_due_time (ICalComponent *component, + ICalTimezone *default_zone) +{ + return get_time_from_property (component, + I_CAL_DUE_PROPERTY, + i_cal_property_get_due, + default_zone); +} + +static guint +get_component_percent_complete (ICalComponent *component) +{ + ICalPropertyStatus status; + ICalProperty *prop; + int percent_complete; + + status = i_cal_component_get_status (component); + if (status == I_CAL_STATUS_COMPLETED) + return 100; + + prop = i_cal_component_get_first_property (component, I_CAL_COMPLETED_PROPERTY); + + if (prop) + { + g_object_unref (prop); + return 100; + } + + 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); + + return CLAMP (percent_complete, 0, 100); +} + +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); +} + +static int +get_component_priority (ICalComponent *component) +{ + ICalProperty *prop; + int priority; + + prop = i_cal_component_get_first_property (component, I_CAL_PRIORITY_PROPERTY); + if (!prop) + return -1; + + priority = i_cal_property_get_priority (prop); + g_object_unref (prop); + + return priority; +} + +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); + + source = e_client_get_source (E_CLIENT (esource)); + source_type = e_cal_client_get_source_type (esource); + + switch (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_val_if_reached (NULL); + } + + extension = e_source_get_extension (source, extension_name); + + return e_source_selectable_dup_color (extension); +} + +static gchar * +get_source_backend_name (ECalClient *esource) +{ + 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); + + switch (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_val_if_reached (NULL); + } + + extension = e_source_get_extension (source, extension_name); + + return e_source_backend_dup_backend_name (extension); +} + +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 ICalTimezone * +resolve_timezone_id (const char *tzid, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + ECalClient *client; + ICalTimezone *retval; + + client = E_CAL_CLIENT (user_data); + retval = i_cal_timezone_get_builtin_timezone_from_tzid (tzid); + + if (!retval) + e_cal_client_get_timezone_sync (client, tzid, &retval, NULL, NULL); + + return retval; +} + +static gboolean +calendar_appointment_collect_occurrence (ICalComponent *component, + ICalTime *occurrence_start, + ICalTime *occurrence_end, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + time_t start; + time_t end; + CalendarOccurrence *occurrence; + GSList **collect_loc = user_data; + + start = i_cal_time_as_timet (occurrence_start); + end = i_cal_time_as_timet (occurrence_end); + + occurrence = g_new0 (CalendarOccurrence, 1); + occurrence->start_time = start; + occurrence->end_time = end; + + *collect_loc = g_slist_prepend (*collect_loc, occurrence); + + return TRUE; +} + +static void +calendar_appointment_generate_ocurrences (CalendarAppointment *appointment, + ICalComponent *component, + ECalClient *source, + time_t start, + time_t end, + ICalTimezone *default_zone) +{ + ICalTime *start_time; + ICalTime *end_time; + + g_assert (appointment->occurrences == NULL); + + start_time = i_cal_time_new_from_timet_with_zone (start, FALSE, NULL); + end_time = i_cal_time_new_from_timet_with_zone (end, FALSE, NULL); + + e_cal_recur_generate_instances_sync (component, + start_time, + end_time, + calendar_appointment_collect_occurrence, + &appointment->occurrences, + resolve_timezone_id, + source, + default_zone, + NULL, + NULL); + + g_object_unref (start_time); + g_object_unref (end_time); + + appointment->occurrences = g_slist_reverse (appointment->occurrences); +} + +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 void +calendar_event_generate_ocurrences (CalendarEvent *event, + ICalComponent *component, + ECalClient *source, + time_t start, + time_t end, + ICalTimezone *default_zone) +{ + if (event->type != CALENDAR_EVENT_APPOINTMENT) + return; + + calendar_appointment_generate_ocurrences (CALENDAR_APPOINTMENT (event), + component, + source, + start, + end, + default_zone); +} + +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_handle_query_completed (CalendarClientSource *source, + GError *error, + ECalClientView *view) +{ + CalendarClientQuery *query; + + query = goddamn_this_is_crack (source, view, NULL); + + if (error != NULL) + { + g_warning ("Calendar query failed: %s", error->message); + calendar_client_stop_query (source->client, source, query); + return; + } + + g_assert (source->query_in_progress != FALSE); + g_assert (query == &source->in_progress_query); + + 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; + + g_signal_emit (source->client, source->changed_signal_id, 0); +} + +static void +calendar_client_handle_query_result (CalendarClientSource *source, + GList *objects, + ECalClientView *view) +{ + CalendarClientQuery *query; + CalendarClient *client; + gboolean emit_signal; + gboolean events_changed; + GList *l; + time_t month_begin; + time_t month_end; + + client = source->client; + + query = goddamn_this_is_crack (source, view, &emit_signal); + + 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); + + events_changed = FALSE; + for (l = objects; l; l = l->next) + { + CalendarEvent *event; + CalendarEvent *old_event; + ICalComponent *component = l->data; + char *uid; + + event = calendar_event_new (component, source, client->priv->zone); + if (!event) + continue; + + calendar_event_generate_ocurrences (event, + component, + source->source, + month_begin, + month_end, + client->priv->zone); + + uid = calendar_event_get_uid (event); + + old_event = g_hash_table_lookup (query->events, uid); + + if (!calendar_event_equal (event, old_event)) + { + calendar_event_debug_dump (event); + + g_hash_table_replace (query->events, uid, event); + + events_changed = TRUE; + } + else + { + calendar_event_free (event); + g_free (uid); + } + } + + if (emit_signal && events_changed) + { + g_signal_emit (source->client, source->changed_signal_id, 0); + } +} + +static gboolean +check_object_remove (gpointer key, + gpointer value, + gpointer data) +{ + char *uid; + size_t len; + + uid = data; + len = strlen (uid); + + if (len <= strlen (key) && strncmp (uid, key, len) == 0) + { + calendar_event_debug_dump (value); + + return TRUE; + } + + return FALSE; +} + +static void +calendar_client_handle_objects_removed (CalendarClientSource *source, + GList *ids, + ECalClientView *view) +{ + CalendarClientQuery *query; + gboolean emit_signal; + gboolean events_changed; + GList *l; + + query = goddamn_this_is_crack (source, view, &emit_signal); + + events_changed = FALSE; + for (l = ids; l; l = l->next) + { + CalendarEvent *event; + ECalComponentId *id; + const char *uid; + const char *rid; + char *key; + + id = l->data; + uid = e_cal_component_id_get_uid (id); + rid = e_cal_component_id_get_rid (id); + key = g_strdup_printf ("%s%s", uid, rid ? rid : ""); + + if (!rid || !*rid) + { + guint size = g_hash_table_size (query->events); + + g_hash_table_foreach_remove (query->events, check_object_remove, (gpointer) uid); + + if (size != g_hash_table_size (query->events)) + events_changed = TRUE; + } + else if ((event = g_hash_table_lookup (query->events, key))) + { + calendar_event_debug_dump (event); + + g_assert (g_hash_table_remove (query->events, key)); + + events_changed = TRUE; + } + + g_free (key); + } + + if (emit_signal && events_changed) + { + g_signal_emit (source->client, source->changed_signal_id, 0); + } +} + +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); +} + +static void +calendar_client_start_query (CalendarClient *client, + CalendarClientSource *source, + const char *query) +{ + ECalClientView *view = NULL; + GError *error = NULL; + + if (!e_cal_client_get_view_sync (source->source, query, &view, NULL, &error)) + { + g_warning ("Error preparing the query: '%s': %s", + query, error->message); + + g_error_free (error); + return; + } + + g_assert (view != NULL); + + 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 = view; + source->in_progress_query.events = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) calendar_event_free); + + g_signal_connect_swapped (view, "objects-added", + G_CALLBACK (calendar_client_handle_query_result), + source); + g_signal_connect_swapped (view, "objects-modified", + G_CALLBACK (calendar_client_handle_query_result), + source); + g_signal_connect_swapped (view, "objects-removed", + G_CALLBACK (calendar_client_handle_objects_removed), + source); + g_signal_connect_swapped (view, "complete", + G_CALLBACK (calendar_client_handle_query_completed), + source); + + e_cal_client_view_start (view, NULL); +} + +static 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? + */ +static 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->next)) + { + 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 new file mode 100644 index 00000000..7747d23f --- /dev/null +++ b/applets/clock/calendar-client.h @@ -0,0 +1,151 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Mark McLoughlin <[email protected]> + * William Jon McCann <[email protected]> + * Martin Grimme <[email protected]> + * Christian Kellner <[email protected]> + */ + +#ifndef __CALENDAR_CLIENT_H__ +#define __CALENDAR_CLIENT_H__ + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +typedef enum +{ + CALENDAR_EVENT_APPOINTMENT = 1 << 0, + CALENDAR_EVENT_TASK = 1 << 1, + CALENDAR_EVENT_ALL = (1 << 2) - 1 +} CalendarEventType; + +#define CALENDAR_TYPE_CLIENT (calendar_client_get_type ()) +#define CALENDAR_CLIENT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_CLIENT, CalendarClient)) +#define CALENDAR_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_CLIENT, CalendarClientClass)) +#define CALENDAR_IS_CLIENT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_CLIENT)) +#define CALENDAR_IS_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_CLIENT)) +#define CALENDAR_CLIENT_GET_CLASS(o)(G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_CLIENT, CalendarClientClass)) + +typedef struct _CalendarClient CalendarClient; +typedef struct _CalendarClientClass CalendarClientClass; +typedef struct _CalendarClientPrivate CalendarClientPrivate; + +struct _CalendarClient +{ + GObject parent; + CalendarClientPrivate *priv; +}; + +struct _CalendarClientClass +{ + GObjectClass parent_class; + + void (* appointments_changed) (CalendarClient *client); + void (* tasks_changed) (CalendarClient *client); +}; + + +typedef struct +{ + time_t start_time; + time_t end_time; +} CalendarOccurrence; + +typedef struct +{ + char *uid; + char *rid; + char *backend_name; + char *summary; + char *description; + char *color_string; + time_t start_time; + time_t end_time; + guint is_all_day : 1; + + /* Only used internally */ + GSList *occurrences; +} CalendarAppointment; + +typedef struct +{ + char *uid; + char *summary; + char *description; + char *color_string; + char *url; + time_t start_time; + time_t due_time; + guint percent_complete; + time_t completed_time; + int priority; +} CalendarTask; + +typedef struct +{ + union + { + CalendarAppointment appointment; + CalendarTask task; + } event; + CalendarEventType type; +} CalendarEvent; + +#define CALENDAR_EVENT(e) ((CalendarEvent *)(e)) +#define CALENDAR_APPOINTMENT(e) ((CalendarAppointment *)(e)) +#define CALENDAR_TASK(e) ((CalendarTask *)(e)) + +typedef void (* CalendarDayIter) (CalendarClient *client, + guint day, + gpointer user_data); + + +GType calendar_client_get_type (void) G_GNUC_CONST; +CalendarClient *calendar_client_new (GSettings *settings); + +void calendar_client_get_date (CalendarClient *client, + guint *year, + guint *month, + guint *day); +void calendar_client_select_month (CalendarClient *client, + guint month, + guint year); +void calendar_client_select_day (CalendarClient *client, + guint day); + +GSList *calendar_client_get_events (CalendarClient *client, + CalendarEventType event_mask); +void calendar_client_foreach_appointment_day (CalendarClient *client, + CalendarDayIter iter_func, + gpointer user_data); + +void calendar_client_set_task_completed (CalendarClient *client, + char *task_uid, + gboolean task_completed, + guint percent_complete); + +gboolean calendar_client_create_task (CalendarClient *client, + const char *summary); + +void calendar_event_free (CalendarEvent *event); + +G_END_DECLS + +#endif /* __CALENDAR_CLIENT_H__ */ diff --git a/applets/clock/calendar-debug.h b/applets/clock/calendar-debug.h new file mode 100644 index 00000000..39befd74 --- /dev/null +++ b/applets/clock/calendar-debug.h @@ -0,0 +1,50 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Mark McLoughlin <[email protected]> + */ + +#ifndef __CALENDAR_DEBUG_H__ +#define __CALENDAR_DEBUG_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +#ifdef CALENDAR_ENABLE_DEBUG + +#include <stdio.h> + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) fprintf (stderr, __VA_ARGS__); +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) fprintf (stderr, args); +#endif + +#else /* if !defined (CALENDAR_DEBUG) */ + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) +#endif + +#endif /* CALENDAR_ENABLE_DEBUG */ + +G_END_DECLS + +#endif /* __CALENDAR_DEBUG_H__ */ diff --git a/applets/clock/calendar-sources.c b/applets/clock/calendar-sources.c new file mode 100644 index 00000000..54c6dc8c --- /dev/null +++ b/applets/clock/calendar-sources.c @@ -0,0 +1,503 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Mark McLoughlin <[email protected]> + * William Jon McCann <[email protected]> + * Martin Grimme <[email protected]> + * Christian Kellner <[email protected]> + */ + +#include <config.h> + +#include "calendar-sources.h" + +#include <libintl.h> +#include <string.h> +#define HANDLE_LIBICAL_MEMORY + + +#include <libecal/libecal.h> + +#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 new file mode 100644 index 00000000..1dfc7445 --- /dev/null +++ b/applets/clock/calendar-sources.h @@ -0,0 +1,64 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Mark McLoughlin <[email protected]> + * William Jon McCann <[email protected]> + * Martin Grimme <[email protected]> + * Christian Kellner <[email protected]> + */ + +#ifndef __CALENDAR_SOURCES_H__ +#define __CALENDAR_SOURCES_H__ + +#include <glib-object.h> + +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-window.c b/applets/clock/calendar-window.c index b4157344..ae9f1d7c 100644 --- a/applets/clock/calendar-window.c +++ b/applets/clock/calendar-window.c @@ -38,11 +38,24 @@ #include "clock.h" #include "clock-utils.h" #include "clock-typebuiltins.h" +#ifdef HAVE_EDS +#include "calendar-client.h" +#endif #define KEY_LOCATIONS_EXPANDED "expand-locations" +#ifdef HAVE_EDS +#define KEY_SHOW_CALENDAR_EVENTS "show-calendar-events" +#define KEY_SHOW_TASKS "show-tasks" +#define SCHEMA_CALENDAR_APP "org.mate.desktop.default-applications.office.calendar" +#define SCHEMA_TASKS_APP "org.mate.desktop.default-applications.office.tasks" +#endif + enum { EDIT_LOCATIONS, +#ifdef HAVE_EDS + PERMISSION_READY, +#endif LAST_SIGNAL }; @@ -60,6 +73,33 @@ struct _CalendarWindowPrivate { GtkWidget *locations_list; GSettings *settings; + + /* Signal handler IDs for proper cleanup */ + gulong calendar_month_changed_id; + gulong calendar_day_selected_id; + +#ifdef HAVE_EDS + ClockFormat time_format; + + CalendarClient *client; + + GtkWidget *appointment_list; + + GtkListStore *appointments_model; + GtkListStore *tasks_model; + + GtkTreeSelection *previous_selection; + + GtkTreeModelFilter *appointments_filter; + GtkTreeModelFilter *tasks_filter; + + GtkWidget *task_list; + GtkWidget *task_entry; + + /* EDS-specific signal handler IDs */ + gulong client_appointments_changed_id; + gulong client_tasks_changed_id; +#endif /* HAVE_EDS */ }; G_DEFINE_TYPE_WITH_PRIVATE (CalendarWindow, calendar_window, GTK_TYPE_WINDOW) @@ -84,6 +124,63 @@ static GtkWidget * create_hig_frame (CalendarWindow *calwin, const char *key, GCallback callback); +#ifdef HAVE_EDS +enum { + APPOINTMENT_COLUMN_UID, + APPOINTMENT_COLUMN_TYPE, + APPOINTMENT_COLUMN_SUMMARY, + APPOINTMENT_COLUMN_DESCRIPTION, + APPOINTMENT_COLUMN_START_TIME, + APPOINTMENT_COLUMN_START_TEXT, + APPOINTMENT_COLUMN_END_TIME, + APPOINTMENT_COLUMN_ALL_DAY, + APPOINTMENT_COLUMN_COLOR, + N_APPOINTMENT_COLUMNS +}; + +enum { + TASK_COLUMN_UID, + TASK_COLUMN_TYPE, + TASK_COLUMN_SUMMARY, + TASK_COLUMN_DESCRIPTION, + TASK_COLUMN_START_TIME, + TASK_COLUMN_START_TEXT, + TASK_COLUMN_DUE_TIME, + TASK_COLUMN_DUE_TEXT, + TASK_COLUMN_PERCENT_COMPLETE, + TASK_COLUMN_PERCENT_COMPLETE_TEXT, + TASK_COLUMN_COMPLETED, + TASK_COLUMN_COMPLETED_TIME, + TASK_COLUMN_PRIORITY, + TASK_COLUMN_COLOR, + N_TASK_COLUMNS +}; + +enum { + APPOINTMENT_TYPE_APPOINTMENT, + TASK_TYPE_TASK +}; + +static void calendar_window_pack_pim (CalendarWindow *calwin, GtkWidget *vbox); +static char *format_time (ClockFormat format, time_t t, gint year, gint month, gint day); +static void update_frame_visibility (GtkWidget *frame, GtkTreeModel *model); +static GtkWidget *create_appointment_list (CalendarWindow *calwin, GtkWidget **tree_view, GtkWidget **scrolled_window); +static GtkWidget *create_task_list (CalendarWindow *calwin, GtkWidget **tree_view, GtkWidget **scrolled_window); +static void calendar_window_create_appointments_model (CalendarWindow *calwin); +static void calendar_window_create_tasks_model (CalendarWindow *calwin); +static void handle_appointments_changed (CalendarWindow *calwin); +static void handle_tasks_changed (CalendarWindow *calwin); +static void mark_day_on_calendar (CalendarClient *client, guint day, CalendarWindow *calwin); +static gboolean is_for_filter (GtkTreeModel *model, GtkTreeIter *iter, gpointer data); +static gboolean appointment_tooltip_query_cb (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data); +static gboolean task_tooltip_query_cb (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data); +static void appointment_row_activated_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data); +static void task_row_activated_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data); +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 */ + static void calendar_mark_today(GtkCalendar *calendar) { time_t now; @@ -110,7 +207,22 @@ static gboolean calendar_update(gpointer user_data) 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, user_data, NULL); + g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, calendar_update, calendar, NULL); + +#ifdef HAVE_EDS + /* Update calendar client when date changes */ + CalendarWindow *calwin = CALENDAR_WINDOW (user_data); + if (calwin->priv->client) { + guint year, month, day; + gtk_calendar_get_date (calendar, &year, &month, &day); + calendar_client_select_month (calwin->priv->client, month, year); + calendar_client_select_day (calwin->priv->client, day); + + /* Refresh appointments and tasks for the new date */ + handle_appointments_changed (calwin); + handle_tasks_changed (calwin); + } +#endif } static GtkWidget * @@ -135,8 +247,10 @@ calendar_window_create_calendar (CalendarWindow *calwin) gtk_calendar_select_day (GTK_CALENDAR (calendar), (guint) tm1.tm_mday); calendar_mark_today (GTK_CALENDAR(calendar)); - g_signal_connect(calendar, "month-changed", - G_CALLBACK(calendar_month_changed_cb), calendar); + calwin->priv->calendar_month_changed_id = g_signal_connect(calendar, "month-changed", + G_CALLBACK(calendar_month_changed_cb), calwin); + calwin->priv->calendar_day_selected_id = g_signal_connect(calendar, "day-selected", + G_CALLBACK(calendar_month_changed_cb), calwin); return calendar; } @@ -250,6 +364,52 @@ edit_locations (CalendarWindow *calwin) g_signal_emit (calwin, signals[EDIT_LOCATIONS], 0); } +#ifdef HAVE_EDS +static gboolean +hide_task_entry_idle (gpointer user_data) +{ + CalendarWindow *calwin = CALENDAR_WINDOW (user_data); + if (calwin->priv->task_entry) { + gtk_widget_hide (calwin->priv->task_entry); + } + return FALSE; /* Remove the idle source */ +} + +static gboolean +focus_task_entry_idle (gpointer user_data) +{ + CalendarWindow *calwin = CALENDAR_WINDOW (user_data); + if (calwin->priv->task_entry && gtk_widget_get_visible (calwin->priv->task_entry)) { + gtk_widget_grab_focus (calwin->priv->task_entry); + } + return FALSE; /* Remove the idle source */ +} + +static void +add_task (CalendarWindow *calwin) +{ + if (calwin->priv->task_entry) { + gtk_widget_show (calwin->priv->task_entry); + gtk_widget_set_can_focus (calwin->priv->task_entry, TRUE); + gtk_widget_set_sensitive (calwin->priv->task_entry, TRUE); + + /* Make sure parent window is active */ + gtk_window_present (GTK_WINDOW (calwin)); + + /* Ensure widget is realized */ + if (!gtk_widget_get_realized (calwin->priv->task_entry)) { + gtk_widget_realize (calwin->priv->task_entry); + } + + /* Try to grab focus immediately */ + gtk_widget_grab_focus (calwin->priv->task_entry); + + /* Also try to grab focus in idle callback in case immediate focus fails */ + g_idle_add (focus_task_entry_idle, calwin); + } +} +#endif + static void calendar_window_pack_locations (CalendarWindow *calwin, GtkWidget *vbox) { @@ -287,12 +447,22 @@ calendar_window_fill (CalendarWindow *calwin) calwin->priv->calendar = calendar_window_create_calendar (calwin); gtk_widget_show (calwin->priv->calendar); +#ifdef HAVE_EDS + /* Calendar client will be initialized later in calendar_window_pack_pim */ +#endif + if (!calwin->priv->invert_order) { gtk_box_pack_start (GTK_BOX (vbox), calwin->priv->calendar, TRUE, FALSE, 0); +#ifdef HAVE_EDS + calendar_window_pack_pim (calwin, vbox); +#endif calendar_window_pack_locations (calwin, vbox); } else { calendar_window_pack_locations (calwin, vbox); +#ifdef HAVE_EDS + calendar_window_pack_pim (calwin, vbox); +#endif gtk_box_pack_start (GTK_BOX (vbox), calwin->priv->calendar, TRUE, FALSE, 0); } @@ -405,6 +575,33 @@ calendar_window_dispose (GObject *object) g_clear_pointer (&calwin->priv->prefs_path, g_free); + /* Disconnect calendar signals */ + if (calwin->priv->calendar && calwin->priv->calendar_month_changed_id > 0) { + g_signal_handler_disconnect (calwin->priv->calendar, calwin->priv->calendar_month_changed_id); + calwin->priv->calendar_month_changed_id = 0; + } + if (calwin->priv->calendar && calwin->priv->calendar_day_selected_id > 0) { + g_signal_handler_disconnect (calwin->priv->calendar, calwin->priv->calendar_day_selected_id); + calwin->priv->calendar_day_selected_id = 0; + } + +#ifdef HAVE_EDS + /* Disconnect client signals */ + if (calwin->priv->client) { + if (calwin->priv->client_appointments_changed_id > 0) { + g_signal_handler_disconnect (calwin->priv->client, calwin->priv->client_appointments_changed_id); + calwin->priv->client_appointments_changed_id = 0; + } + if (calwin->priv->client_tasks_changed_id > 0) { + g_signal_handler_disconnect (calwin->priv->client, calwin->priv->client_tasks_changed_id); + calwin->priv->client_tasks_changed_id = 0; + } + g_signal_handlers_disconnect_by_data (calwin->priv->client, calwin); + g_object_unref (calwin->priv->client); + calwin->priv->client = NULL; + } +#endif + if (calwin->priv->settings) g_object_unref (calwin->priv->settings); calwin->priv->settings = NULL; @@ -475,6 +672,13 @@ calendar_window_init (CalendarWindow *calwin) calwin->priv = calendar_window_get_instance_private (calwin); +#ifdef HAVE_EDS + /* Initialize signal handler IDs */ + calwin->priv->calendar_month_changed_id = 0; + calwin->priv->calendar_day_selected_id = 0; + calwin->priv->client_appointments_changed_id = 0; +#endif + window = GTK_WINDOW (calwin); gtk_window_set_type_hint (window, GDK_WINDOW_TYPE_HINT_DOCK); gtk_window_set_decorated (window, FALSE); @@ -488,7 +692,8 @@ calendar_window_init (CalendarWindow *calwin) GtkWidget * calendar_window_new (time_t *static_current_time, const char *prefs_path, - gboolean invert_order) + gboolean invert_order, + GSettings *settings) { CalendarWindow *calwin; @@ -499,6 +704,13 @@ calendar_window_new (time_t *static_current_time, "prefs-path", prefs_path, NULL); +#ifdef HAVE_EDS + /* Store settings for calendar client initialization in init */ + if (settings) { + calwin->priv->settings = g_object_ref (settings); + } +#endif + return GTK_WIDGET (calwin); } @@ -506,6 +718,16 @@ void calendar_window_refresh (CalendarWindow *calwin) { g_return_if_fail (CALENDAR_IS_WINDOW (calwin)); + +#ifdef HAVE_EDS + if (calwin->priv->appointments_filter && calwin->priv->appointment_list) + gtk_tree_model_filter_refilter (calwin->priv->appointments_filter); + + /* Update frame visibility based on model content */ + if (calwin->priv->appointment_list && calwin->priv->appointments_filter) + update_frame_visibility (calwin->priv->appointment_list, + GTK_TREE_MODEL (calwin->priv->appointments_filter)); +#endif } gboolean @@ -573,7 +795,11 @@ calendar_window_get_time_format (CalendarWindow *calwin) g_return_val_if_fail (CALENDAR_IS_WINDOW (calwin), CLOCK_FORMAT_INVALID); +#ifdef HAVE_EDS + return calwin->priv->time_format; +#else return CLOCK_FORMAT_INVALID; +#endif } static time_t * @@ -627,7 +853,893 @@ calendar_window_set_prefs_path (CalendarWindow *calwin, g_object_notify (G_OBJECT (calwin), "prefs-path"); - if (calwin->priv->settings) - g_object_unref (calwin->priv->settings); - calwin->priv->settings = g_settings_new_with_path (CLOCK_SCHEMA, calwin->priv->prefs_path); + /* Only create new settings if we don't already have shared settings */ + if (!calwin->priv->settings) { + calwin->priv->settings = g_settings_new_with_path (CLOCK_SCHEMA, calwin->priv->prefs_path); + } +} + +#ifdef HAVE_EDS + +static char * +format_time (ClockFormat format, + time_t t, + gint year, + gint month, + gint day) +{ + GDateTime *dt; + gchar *time; + + if (!t) + return NULL; + + /* Evolution timestamps are in UTC but represent local appointment times + * Since TZID lookup failed, treat UTC timestamp as local time directly */ + dt = g_date_time_new_from_unix_utc (t); + time = NULL; + + if (!dt) + return NULL; + + /* Always show time since we're filtering by selected date */ + if (format == CLOCK_FORMAT_12) { + /* Translators: This is a strftime format string. + * It is used to display the time in 12-hours format + * (eg, like in the US: 8:10 am). The %p expands to + * am/pm. + */ + time = g_date_time_format (dt, _("%l:%M %p")); + } else { + /* Translators: This is a strftime format string. + * It is used to display the time in 24-hours format + * (eg, like in France: 20:10). + */ + time = g_date_time_format (dt, _("%H:%M")); + } + + g_date_time_unref (dt); + return time; +} + +static void +update_frame_visibility (GtkWidget *frame, + GtkTreeModel *model) +{ + GtkTreeIter iter; + gboolean model_empty; + + if (!frame) + return; + + model_empty = !gtk_tree_model_get_iter_first (model, &iter); + + if (model_empty) + gtk_widget_hide (frame); + else + gtk_widget_show (frame); +} + + +static void +calendar_window_create_appointments_model (CalendarWindow *calwin) +{ + calwin->priv->appointments_model = gtk_list_store_new (N_APPOINTMENT_COLUMNS, + G_TYPE_STRING, /* APPOINTMENT_COLUMN_UID */ + G_TYPE_INT, /* APPOINTMENT_COLUMN_TYPE */ + G_TYPE_STRING, /* APPOINTMENT_COLUMN_SUMMARY */ + G_TYPE_STRING, /* APPOINTMENT_COLUMN_DESCRIPTION */ + G_TYPE_ULONG, /* APPOINTMENT_COLUMN_START_TIME */ + 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 */ + + 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, + (GtkTreeModelFilterVisibleFunc) is_for_filter, + GINT_TO_POINTER (APPOINTMENT_TYPE_APPOINTMENT), + NULL); +} + +static void +calendar_window_create_tasks_model (CalendarWindow *calwin) +{ + calwin->priv->tasks_model = gtk_list_store_new (N_TASK_COLUMNS, + G_TYPE_STRING, /* TASK_COLUMN_UID */ + G_TYPE_INT, /* TASK_COLUMN_TYPE */ + G_TYPE_STRING, /* TASK_COLUMN_SUMMARY */ + G_TYPE_STRING, /* TASK_COLUMN_DESCRIPTION */ + G_TYPE_ULONG, /* TASK_COLUMN_START_TIME */ + G_TYPE_STRING, /* TASK_COLUMN_START_TEXT */ + G_TYPE_ULONG, /* TASK_COLUMN_DUE_TIME */ + G_TYPE_STRING, /* TASK_COLUMN_DUE_TEXT */ + G_TYPE_INT, /* TASK_COLUMN_PERCENT_COMPLETE */ + G_TYPE_STRING, /* TASK_COLUMN_PERCENT_COMPLETE_TEXT */ + G_TYPE_BOOLEAN, /* TASK_COLUMN_COMPLETED */ + G_TYPE_ULONG, /* TASK_COLUMN_COMPLETED_TIME */ + G_TYPE_INT, /* TASK_COLUMN_PRIORITY */ + G_TYPE_STRING); /* TASK_COLUMN_COLOR */ + + calwin->priv->tasks_filter = GTK_TREE_MODEL_FILTER (gtk_tree_model_filter_new (GTK_TREE_MODEL (calwin->priv->tasks_model), NULL)); + gtk_tree_model_filter_set_visible_func (calwin->priv->tasks_filter, + (GtkTreeModelFilterVisibleFunc) is_for_filter, + GINT_TO_POINTER (TASK_TYPE_TASK), + NULL); +} + +static GtkWidget * +create_hig_calendar_frame (CalendarWindow *calwin, + const char *title, + const char *button_label, + const char *key, + GCallback callback) +{ + return create_hig_frame (calwin, title, button_label, key, callback); +} + + +static GtkWidget * +create_appointment_list (CalendarWindow *calwin, + GtkWidget **tree_view, + GtkWidget **scrolled_window) +{ + GtkWidget *frame; + GtkWidget *list; + GtkTreeViewColumn *column; + GtkCellRenderer *cell; + + frame = create_hig_calendar_frame (calwin, _("Appointments"), NULL, + KEY_SHOW_CALENDAR_EVENTS, NULL); + + list = gtk_tree_view_new (); + gtk_tree_view_set_model (GTK_TREE_VIEW (list), + GTK_TREE_MODEL (calwin->priv->appointments_filter)); + + column = gtk_tree_view_column_new (); + cell = gtk_cell_renderer_text_new (); + gtk_tree_view_column_pack_start (column, cell, FALSE); + gtk_tree_view_column_set_attributes (column, cell, + "text", APPOINTMENT_COLUMN_START_TEXT, + NULL); + + cell = gtk_cell_renderer_text_new (); + g_object_set (cell, + "wrap-mode", PANGO_WRAP_WORD_CHAR, + "wrap-width", 200, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + gtk_tree_view_column_pack_start (column, cell, TRUE); + gtk_tree_view_column_set_attributes (column, cell, + "text", APPOINTMENT_COLUMN_SUMMARY, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (list), column); + + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (list), FALSE); + gtk_widget_set_has_tooltip (list, TRUE); + g_signal_connect (list, "query-tooltip", G_CALLBACK (appointment_tooltip_query_cb), calwin); + g_signal_connect (list, "row-activated", G_CALLBACK (appointment_row_activated_cb), calwin); + + *scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (*scrolled_window), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (*scrolled_window), + GTK_SHADOW_IN); + gtk_widget_set_size_request (*scrolled_window, -1, 150); + gtk_container_add (GTK_CONTAINER (*scrolled_window), list); + + gtk_container_add (GTK_CONTAINER (frame), *scrolled_window); + + /* Ensure the scrolled window and tree view are visible */ + gtk_widget_show (*scrolled_window); + gtk_widget_show (list); + + /* Appointment list widgets created */ + + *tree_view = list; + return frame; +} + +static GtkWidget * +create_task_list (CalendarWindow *calwin, + GtkWidget **tree_view, + GtkWidget **scrolled_window) +{ + GtkWidget *frame; + GtkWidget *list; + GtkTreeViewColumn *column; + GtkCellRenderer *cell; + + frame = create_hig_calendar_frame (calwin, _("Tasks"), _("Add"), + KEY_SHOW_TASKS, G_CALLBACK (add_task)); + + list = gtk_tree_view_new (); + gtk_tree_view_set_model (GTK_TREE_VIEW (list), + GTK_TREE_MODEL (calwin->priv->tasks_filter)); + + column = gtk_tree_view_column_new (); + + /* Completion checkbox */ + cell = gtk_cell_renderer_toggle_new (); + gtk_tree_view_column_pack_start (column, cell, FALSE); + gtk_tree_view_column_set_attributes (column, cell, + "active", TASK_COLUMN_COMPLETED, + NULL); + g_signal_connect (cell, "toggled", G_CALLBACK (task_completion_toggled_cb), calwin); + + + /* Task summary */ + cell = gtk_cell_renderer_text_new (); + g_object_set (cell, + "wrap-mode", PANGO_WRAP_WORD_CHAR, + "wrap-width", 200, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + gtk_tree_view_column_pack_start (column, cell, TRUE); + gtk_tree_view_column_set_attributes (column, cell, + "text", TASK_COLUMN_SUMMARY, + "strikethrough", TASK_COLUMN_COMPLETED, + NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (list), column); + + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (list), FALSE); + gtk_widget_set_has_tooltip (list, TRUE); + g_signal_connect (list, "query-tooltip", G_CALLBACK (task_tooltip_query_cb), calwin); + g_signal_connect (list, "row-activated", G_CALLBACK (task_row_activated_cb), calwin); + + *scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (*scrolled_window), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (*scrolled_window), + GTK_SHADOW_IN); + gtk_widget_set_size_request (*scrolled_window, -1, 150); + gtk_container_add (GTK_CONTAINER (*scrolled_window), list); + + gtk_container_add (GTK_CONTAINER (frame), *scrolled_window); + + /* Create task entry field */ + calwin->priv->task_entry = gtk_entry_new (); + gtk_entry_set_placeholder_text (GTK_ENTRY (calwin->priv->task_entry), _("Enter task description...")); + gtk_widget_set_can_focus (calwin->priv->task_entry, TRUE); + gtk_widget_set_sensitive (calwin->priv->task_entry, TRUE); + g_signal_connect (calwin->priv->task_entry, "key-press-event", G_CALLBACK (task_entry_key_press_cb), calwin); + g_signal_connect (calwin->priv->task_entry, "activate", G_CALLBACK (task_entry_activate_cb), calwin); + gtk_container_add (GTK_CONTAINER (frame), calwin->priv->task_entry); + + /* Ensure the scrolled window and tree view are visible */ + gtk_widget_show (*scrolled_window); + gtk_widget_show (list); + + /* Hide task entry after all show operations are complete */ + g_idle_add (hide_task_entry_idle, calwin); + + *tree_view = list; + return frame; +} + +static void +calendar_window_pack_pim (CalendarWindow *calwin, + GtkWidget *vbox) +{ + GtkWidget *list; + GtkWidget *tree_view; + GtkWidget *scrolled_window; + gboolean show_calendar_events; + gboolean show_tasks; + + /* Check if calendar events should be shown */ + show_calendar_events = g_settings_get_boolean (calwin->priv->settings, KEY_SHOW_CALENDAR_EVENTS); + show_tasks = g_settings_get_boolean (calwin->priv->settings, KEY_SHOW_TASKS); + + if (!show_calendar_events && !show_tasks) { + return; + } + + /* Initialize calendar client if not already done */ + if (!calwin->priv->client && calwin->priv->settings) { + calwin->priv->client = calendar_client_new (calwin->priv->settings); + + if (calwin->priv->client) { + if (show_calendar_events) { + calwin->priv->client_appointments_changed_id = g_signal_connect_swapped (calwin->priv->client, + "appointments-changed", + G_CALLBACK (handle_appointments_changed), + calwin); + } + if (show_tasks) { + calwin->priv->client_tasks_changed_id = g_signal_connect_swapped (calwin->priv->client, + "tasks-changed", + G_CALLBACK (handle_tasks_changed), + calwin); + } + } + } + + if (!calwin->priv->client) { + g_warning ("Failed to create calendar client in calendar_window_pack_pim"); + return; + } + + /* Create and pack appointments list if enabled */ + if (show_calendar_events) { + calendar_window_create_appointments_model (calwin); + list = create_appointment_list (calwin, &tree_view, &scrolled_window); + update_frame_visibility (list, + GTK_TREE_MODEL (calwin->priv->appointments_filter)); + calwin->priv->appointment_list = list; + + gtk_box_pack_start (GTK_BOX (vbox), + calwin->priv->appointment_list, + TRUE, TRUE, 0); + } + + /* Create and pack tasks list if enabled */ + if (show_tasks) { + calendar_window_create_tasks_model (calwin); + list = create_task_list (calwin, &tree_view, &scrolled_window); + update_frame_visibility (list, + GTK_TREE_MODEL (calwin->priv->tasks_filter)); + calwin->priv->task_list = list; + + gtk_box_pack_start (GTK_BOX (vbox), + calwin->priv->task_list, + TRUE, TRUE, 0); + } + + /* Initialize calendar client with current date now that client is ready */ + if (calwin->priv->client && calwin->priv->calendar) { + guint year, month, day; + gtk_calendar_get_date (GTK_CALENDAR (calwin->priv->calendar), &year, &month, &day); + /* Set a flag to indicate we're initializing to prevent redundant calls */ + g_object_set_data (G_OBJECT (calwin), "initializing", GINT_TO_POINTER (1)); + + calendar_client_select_month (calwin->priv->client, month, year); + calendar_client_select_day (calwin->priv->client, day); + + /* Clear the initialization flag and trigger initial load */ + g_object_set_data (G_OBJECT (calwin), "initializing", GINT_TO_POINTER (0)); + + /* Now trigger the initial appointments and tasks load */ + if (show_calendar_events) { + handle_appointments_changed (calwin); + } + if (show_tasks) { + handle_tasks_changed (calwin); + } + } } + +static gboolean +is_for_filter (GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data) +{ + gint type; + gint expected_type = GPOINTER_TO_INT (data); + + /* Check if this is a task model or appointment model */ + if (expected_type == TASK_TYPE_TASK) { + gtk_tree_model_get (model, iter, TASK_COLUMN_TYPE, &type, -1); + } else { + gtk_tree_model_get (model, iter, APPOINTMENT_COLUMN_TYPE, &type, -1); + } + return type == expected_type; +} + +static void +mark_day_on_calendar (CalendarClient *client, + guint day, + CalendarWindow *calwin) +{ + gtk_calendar_mark_day (GTK_CALENDAR (calwin->priv->calendar), day); +} + + + +static gint +compare_appointments_by_time (const CalendarAppointment *a, const CalendarAppointment *b) +{ + /* Sort by start time - earlier appointments first */ + if (a->start_time < b->start_time) + return -1; + else if (a->start_time > b->start_time) + return 1; + else + return 0; +} + +static gint +compare_tasks_by_due_time (const CalendarTask *a, const CalendarTask *b) +{ + /* Sort by due time - earlier due dates first, then by priority */ + if (a->due_time && b->due_time) { + if (a->due_time < b->due_time) + return -1; + else if (a->due_time > b->due_time) + return 1; + } else if (a->due_time && !b->due_time) { + return -1; /* Tasks with due dates come first */ + } else if (!a->due_time && b->due_time) { + return 1; + } + + /* If due times are equal or both missing, sort by priority (higher priority first) */ + if (a->priority > b->priority) + return -1; + else if (a->priority < b->priority) + return 1; + else + return 0; +} + +static void +handle_appointments_changed (CalendarWindow *calwin) +{ + GSList *events, *l; + guint year, month, day; + + /* Skip redundant calls during initialization */ + if (g_object_get_data (G_OBJECT (calwin), "initializing")) { + return; + } + + if (calwin->priv->calendar) { + gtk_calendar_clear_marks (GTK_CALENDAR (calwin->priv->calendar)); + + calendar_client_foreach_appointment_day (calwin->priv->client, + (CalendarDayIter) mark_day_on_calendar, + calwin); + } + + gtk_list_store_clear (calwin->priv->appointments_model); + + calendar_client_get_date (calwin->priv->client, &year, &month, &day); + + events = calendar_client_get_events (calwin->priv->client, + CALENDAR_EVENT_APPOINTMENT); + + /* Sort appointments by start time for better display order */ + events = g_slist_sort (events, (GCompareFunc) compare_appointments_by_time); + + /* Found appointments for current date */ + for (l = events; l; l = l->next) { + CalendarAppointment *appointment = l->data; + GtkTreeIter iter; + char *start_text; + + g_assert (CALENDAR_EVENT (appointment)->type == CALENDAR_EVENT_APPOINTMENT); + + if (appointment->is_all_day) + start_text = g_strdup (_("All Day")); + else + start_text = format_time (calendar_window_get_time_format (calwin), + appointment->start_time, + year, month, day); + + 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, + -1); + + g_free (start_text); + } + + /* Refresh filter before checking visibility */ + if (calwin->priv->appointments_filter) + gtk_tree_model_filter_refilter (calwin->priv->appointments_filter); + + update_frame_visibility (calwin->priv->appointment_list, + GTK_TREE_MODEL (calwin->priv->appointments_filter)); +} + +static void +handle_tasks_changed (CalendarWindow *calwin) +{ + GSList *events, *l; + guint year, month, day; + + /* Skip redundant calls during initialization */ + if (g_object_get_data (G_OBJECT (calwin), "initializing")) { + return; + } + + if (!calwin->priv->tasks_model) { + return; + } + + gtk_list_store_clear (calwin->priv->tasks_model); + + calendar_client_get_date (calwin->priv->client, &year, &month, &day); + + events = calendar_client_get_events (calwin->priv->client, + CALENDAR_EVENT_TASK); + + /* Sort tasks by due time for better display order */ + events = g_slist_sort (events, (GCompareFunc) compare_tasks_by_due_time); + + /* Found tasks for current date */ + for (l = events; l; l = l->next) { + CalendarTask *task = (CalendarTask *) l->data; + GtkTreeIter iter; + char *start_text = NULL; + char *due_text = NULL; + char *percent_complete_text = NULL; + gboolean completed; + + g_assert (CALENDAR_EVENT (task)->type == CALENDAR_EVENT_TASK); + + if (task->start_time) { + start_text = format_time (calendar_window_get_time_format (calwin), + task->start_time, + year, month, day); + } else { + start_text = g_strdup (""); + } + + if (task->due_time) { + due_text = format_time (calendar_window_get_time_format (calwin), + task->due_time, + year, month, day); + } else { + due_text = g_strdup (""); + } + + /* Format percent complete as text */ + if (task->percent_complete > 0) { + percent_complete_text = g_strdup_printf ("%d%%", task->percent_complete); + } else { + percent_complete_text = g_strdup (""); + } + + completed = (task->percent_complete == 100); + + gtk_list_store_append (calwin->priv->tasks_model, &iter); + gtk_list_store_set (calwin->priv->tasks_model, &iter, + TASK_COLUMN_UID, task->uid, + TASK_COLUMN_TYPE, TASK_TYPE_TASK, + TASK_COLUMN_SUMMARY, task->summary, + TASK_COLUMN_DESCRIPTION, task->description, + TASK_COLUMN_START_TIME, (gint64)task->start_time, + TASK_COLUMN_START_TEXT, start_text, + TASK_COLUMN_DUE_TIME, (gint64)task->due_time, + TASK_COLUMN_DUE_TEXT, due_text, + TASK_COLUMN_PERCENT_COMPLETE, task->percent_complete, + TASK_COLUMN_PERCENT_COMPLETE_TEXT, percent_complete_text, + TASK_COLUMN_COMPLETED, completed, + TASK_COLUMN_COMPLETED_TIME, (gint64)task->completed_time, + TASK_COLUMN_PRIORITY, task->priority, + TASK_COLUMN_COLOR, task->color_string, + -1); + + g_free (start_text); + g_free (due_text); + g_free (percent_complete_text); + } + + /* Refresh filter before checking visibility */ + if (calwin->priv->tasks_filter) + gtk_tree_model_filter_refilter (calwin->priv->tasks_filter); + + update_frame_visibility (calwin->priv->task_list, + GTK_TREE_MODEL (calwin->priv->tasks_filter)); +} + +static void +appointment_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + gpointer user_data) +{ + GAppInfo *app_info; + GError *error = NULL; + + /* Launch Evolution calendar */ + app_info = g_app_info_get_default_for_type ("text/calendar", FALSE); + if (!app_info) { + /* Try launching evolution directly if no calendar app is set */ + app_info = g_app_info_create_from_commandline ("evolution -c calendar", + "Evolution Calendar", + G_APP_INFO_CREATE_NONE, + &error); + } + + if (app_info) { + if (!g_app_info_launch (app_info, NULL, NULL, &error)) { + g_warning ("Failed to launch calendar application: %s", error->message); + g_error_free (error); + } + g_object_unref (app_info); + } else { + g_warning ("No calendar application found"); + if (error) { + g_warning ("Error: %s", error->message); + g_error_free (error); + } + } +} + +static gboolean +appointment_tooltip_query_cb (GtkWidget *widget, + gint x, + gint y, + gboolean keyboard_mode, + GtkTooltip *tooltip, + gpointer user_data) +{ + GtkTreeView *tree_view = GTK_TREE_VIEW (widget); + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + gchar *summary, *description, *start_text; + gchar *tooltip_text, *end_text; + gboolean all_day; + gulong start_time, end_time; + + if (!gtk_tree_view_get_tooltip_context (tree_view, &x, &y, keyboard_mode, + &model, &path, &iter)) { + return FALSE; + } + + gtk_tree_model_get (model, &iter, + APPOINTMENT_COLUMN_SUMMARY, &summary, + APPOINTMENT_COLUMN_DESCRIPTION, &description, + APPOINTMENT_COLUMN_START_TEXT, &start_text, + APPOINTMENT_COLUMN_START_TIME, &start_time, + APPOINTMENT_COLUMN_END_TIME, &end_time, + APPOINTMENT_COLUMN_ALL_DAY, &all_day, + -1); + + if (!summary) { + gtk_tree_path_free (path); + return FALSE; + } + + /* Format end time */ + if (!all_day && end_time > 0) { + GDateTime *end_dt = g_date_time_new_from_unix_utc (end_time); + if (end_dt) { + end_text = g_date_time_format (end_dt, "%H:%M"); + g_date_time_unref (end_dt); + } else { + end_text = NULL; + } + } else { + end_text = NULL; + } + + if (description && strlen (description) > 0) { + if (all_day) { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\n%s\nAll Day", summary, description); + } else if (end_text) { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\n%s\n%s - %s", summary, description, start_text ? start_text : "", end_text); + } else { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\n%s\n%s", summary, description, start_text ? start_text : ""); + } + } else { + if (all_day) { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\nAll Day", summary); + } else if (end_text) { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\n%s - %s", summary, start_text ? start_text : "", end_text); + } else { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\n%s", summary, start_text ? start_text : ""); + } + } + + gtk_tooltip_set_markup (tooltip, tooltip_text); + gtk_tree_view_set_tooltip_row (tree_view, tooltip, path); + + g_free (summary); + g_free (description); + g_free (start_text); + g_free (end_text); + g_free (tooltip_text); + gtk_tree_path_free (path); + + return TRUE; +} + +static gboolean +task_tooltip_query_cb (GtkWidget *widget, + gint x, + gint y, + gboolean keyboard_mode, + GtkTooltip *tooltip, + gpointer user_data) +{ + GtkTreeView *tree_view = GTK_TREE_VIEW (widget); + GtkTreeModel *model; + GtkTreePath *path; + GtkTreeIter iter; + gchar *summary, *description, *start_text, *due_text; + gchar *tooltip_text; + gint percent_complete, priority; + + if (!gtk_tree_view_get_tooltip_context (tree_view, &x, &y, keyboard_mode, + &model, &path, &iter)) { + return FALSE; + } + + gtk_tree_model_get (model, &iter, + TASK_COLUMN_SUMMARY, &summary, + TASK_COLUMN_DESCRIPTION, &description, + TASK_COLUMN_START_TEXT, &start_text, + TASK_COLUMN_DUE_TEXT, &due_text, + TASK_COLUMN_PERCENT_COMPLETE, &percent_complete, + TASK_COLUMN_PRIORITY, &priority, + -1); + + if (!summary) { + gtk_tree_path_free (path); + return FALSE; + } + + /* Build tooltip with task information */ + if (description && strlen (description) > 0) { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\n%s\nProgress: %d%%", + summary, description, percent_complete); + } else { + tooltip_text = g_markup_printf_escaped ("<b>%s</b>\nProgress: %d%%", + summary, percent_complete); + } + + /* Add due date if available */ + if (due_text && strlen (due_text) > 0) { + gchar *temp = tooltip_text; + tooltip_text = g_markup_printf_escaped ("%s\nDue: %s", temp, due_text); + g_free (temp); + } + + gtk_tooltip_set_markup (tooltip, tooltip_text); + gtk_tree_view_set_tooltip_row (tree_view, tooltip, path); + + g_free (summary); + g_free (description); + g_free (start_text); + g_free (due_text); + g_free (tooltip_text); + gtk_tree_path_free (path); + + return TRUE; +} + +static void +task_row_activated_cb (GtkTreeView *tree_view, + GtkTreePath *path, + GtkTreeViewColumn *column, + gpointer user_data) +{ + GAppInfo *app_info; + GError *error = NULL; + + /* Launch Evolution tasks */ + app_info = g_app_info_get_default_for_uri_scheme ("task"); + if (!app_info) { + /* Fallback to Evolution tasks directly */ + app_info = g_app_info_create_from_commandline ("evolution --component=tasks", + "Evolution Tasks", + G_APP_INFO_CREATE_NONE, + &error); + } + + if (app_info) { + g_app_info_launch (app_info, NULL, NULL, &error); + g_object_unref (app_info); + } + + if (error) { + g_warning ("Failed to launch Evolution tasks: %s", error->message); + g_error_free (error); + } +} + +static void +task_completion_toggled_cb (GtkCellRendererToggle *cell, + gchar *path_str, + CalendarWindow *calwin) +{ + GtkTreePath *path; + GtkTreeIter iter; + gchar *task_uid; + gboolean completed; + gint percent_complete; + + path = gtk_tree_path_new_from_string (path_str); + + if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (calwin->priv->tasks_filter), &iter, path)) { + gtk_tree_path_free (path); + return; + } + + gtk_tree_model_get (GTK_TREE_MODEL (calwin->priv->tasks_filter), &iter, + TASK_COLUMN_UID, &task_uid, + TASK_COLUMN_COMPLETED, &completed, + TASK_COLUMN_PERCENT_COMPLETE, &percent_complete, + -1); + + /* Toggle completion state */ + completed = !completed; + percent_complete = completed ? 100 : 0; + + /* Update the Evolution task */ + if (calwin->priv->client && task_uid) { + calendar_client_set_task_completed (calwin->priv->client, + task_uid, + completed, + percent_complete); + } + + g_free (task_uid); + gtk_tree_path_free (path); +} + +static gboolean +task_entry_key_press_cb (GtkWidget *widget, + GdkEventKey *event, + CalendarWindow *calwin) +{ + const gchar *text; + + if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) { + /* Get the text from the entry */ + text = gtk_entry_get_text (GTK_ENTRY (widget)); + + /* Create task if text is not empty */ + if (text && *text != '\0') { + if (calwin->priv->client) { + gboolean success = calendar_client_create_task (calwin->priv->client, text); + if (success) { + /* Clear the entry and hide it */ + gtk_entry_set_text (GTK_ENTRY (widget), ""); + gtk_widget_hide (widget); + } else { + g_warning ("Failed to create task"); + } + } + } + return TRUE; /* Event handled */ + } else if (event->keyval == GDK_KEY_Escape) { + /* Clear the entry and hide it */ + gtk_entry_set_text (GTK_ENTRY (widget), ""); + gtk_widget_hide (widget); + return TRUE; /* Event handled */ + } + + return FALSE; /* Let other handlers process the event */ +} + +static void +task_entry_activate_cb (GtkEntry *entry, + CalendarWindow *calwin) +{ + const gchar *text; + + /* Get the text from the entry */ + text = gtk_entry_get_text (entry); + + /* Create task if text is not empty */ + if (text && *text != '\0') { + if (calwin->priv->client) { + gboolean success = calendar_client_create_task (calwin->priv->client, text); + if (success) { + /* Clear the entry and hide it */ + gtk_entry_set_text (entry, ""); + gtk_widget_hide (GTK_WIDGET (entry)); + } else { + g_warning ("Failed to create task"); + } + } + } +} + +#endif /* HAVE_EDS */ diff --git a/applets/clock/calendar-window.h b/applets/clock/calendar-window.h index d4ef49dd..e24fdfc3 100644 --- a/applets/clock/calendar-window.h +++ b/applets/clock/calendar-window.h @@ -61,7 +61,8 @@ struct _CalendarWindowClass { GType calendar_window_get_type (void) G_GNUC_CONST; GtkWidget *calendar_window_new (time_t *static_current_time, const char *prefs_dir, - gboolean invert_order); + gboolean invert_order, + GSettings *settings); void calendar_window_refresh (CalendarWindow *calwin); diff --git a/applets/clock/clock.c b/applets/clock/clock.c index 0eee5766..41bb49b0 100644 --- a/applets/clock/clock.c +++ b/applets/clock/clock.c @@ -89,6 +89,8 @@ #define KEY_CITIES "cities" #define KEY_TEMPERATURE_UNIT "temperature-unit" #define KEY_SPEED_UNIT "speed-unit" +#define KEY_SHOW_CALENDAR_EVENTS "show-calendar-events" +#define KEY_SHOW_TASKS "show-tasks" /* For watching for when the system resumes from sleep mode (e.g. suspend) * and updating the clock as soon as that happens. */ @@ -127,6 +129,7 @@ struct _ClockData { GtkWidget *props; GtkWidget *calendar_popup; + gboolean calendar_popup_destroying; GtkWidget *clock_vbox; GtkSizeGroup *clock_group; @@ -788,9 +791,12 @@ destroy_clock (GtkWidget * widget, ClockData *cd) gtk_widget_destroy (cd->props); cd->props = NULL; - if (cd->calendar_popup) + if (cd->calendar_popup && !cd->calendar_popup_destroying) { + cd->calendar_popup_destroying = TRUE; gtk_widget_destroy (cd->calendar_popup); + } cd->calendar_popup = NULL; + cd->calendar_popup_destroying = FALSE; g_free (cd->timeformat); @@ -861,7 +867,8 @@ create_calendar (ClockData *cd) prefs_path = mate_panel_applet_get_preferences_path (MATE_PANEL_APPLET (cd->applet)); window = calendar_window_new (&cd->current_time, prefs_path, - cd->orient == MATE_PANEL_APPLET_ORIENT_UP); + cd->orient == MATE_PANEL_APPLET_ORIENT_UP, + cd->settings); g_free (prefs_path); calendar_window_set_show_weeks (CALENDAR_WINDOW (window), @@ -1348,9 +1355,11 @@ static void update_calendar_popup (ClockData *cd) { if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (cd->panel_button))) { - if (cd->calendar_popup) { + if (cd->calendar_popup && !cd->calendar_popup_destroying) { + cd->calendar_popup_destroying = TRUE; gtk_widget_destroy (cd->calendar_popup); cd->calendar_popup = NULL; + cd->calendar_popup_destroying = FALSE; cd->cities_section = NULL; cd->map_widget = NULL; cd->clock_vbox = NULL; @@ -1363,7 +1372,7 @@ update_calendar_popup (ClockData *cd) return; } - if (!cd->calendar_popup) { + if (!cd->calendar_popup && !cd->calendar_popup_destroying) { cd->calendar_popup = create_calendar (cd); g_object_add_weak_pointer (G_OBJECT (cd->calendar_popup), (gpointer *) &cd->calendar_popup); @@ -3286,6 +3295,35 @@ fill_prefs_window (ClockData *cd) g_settings_bind (cd->settings, KEY_SHOW_TEMPERATURE, widget, "active", G_SETTINGS_BIND_DEFAULT); +#ifdef HAVE_EDS + /* Set the EDS calendar event checkboxes */ + widget = _clock_get_widget (cd, "show_calendar_events_check"); + if (widget) { + /* Bind to gsettings - this should sync with calendar window automatically */ + g_settings_bind (cd->settings, KEY_SHOW_CALENDAR_EVENTS, widget, "active", + G_SETTINGS_BIND_DEFAULT); + } else { + g_warning ("Could not find show_calendar_events_check widget in preferences"); + } + + /* Set the EDS tasks checkboxes */ + widget = _clock_get_widget (cd, "show_tasks_check"); + if (widget) { + /* Bind to gsettings - this should sync with calendar window automatically */ + g_settings_bind (cd->settings, KEY_SHOW_TASKS, widget, "active", + G_SETTINGS_BIND_DEFAULT); + } else { + g_warning ("Could not find show_tasks_check widget in preferences"); + } + +#else + /* Hide the Calendar tab if EDS is not available */ + widget = _clock_get_widget (cd, "vbox-calendar"); + gtk_widget_hide (widget); + widget = _clock_get_widget (cd, "label-calendar-tab"); + gtk_widget_hide (widget); +#endif + /* Fill the Cities list */ widget = _clock_get_widget (cd, "cities_list"); diff --git a/applets/clock/clock.ui b/applets/clock/clock.ui index b4c4f1b8..2f9cfa78 100644 --- a/applets/clock/clock.ui +++ b/applets/clock/clock.ui @@ -891,6 +891,101 @@ <property name="tab-fill">False</property> </packing> </child> + <child> + <object class="GtkBox" id="vbox-calendar"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="border-width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <child> + <object class="GtkBox" id="calendar-options"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label-calendar"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Evolution Calendar Integration</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="vbox-calendar-options"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkCheckButton" id="show_calendar_events_check"> + <property name="label" translatable="yes">Show calendar _events</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="use-underline">True</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="show_tasks_check"> + <property name="label" translatable="yes">Show _tasks</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="use-underline">True</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel" id="label-calendar-tab"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Calendar</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab-fill">False</property> + </packing> + </child> </object> <packing> <property name="expand">False</property> 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 ee1b6902..ad822e46 100644 --- a/applets/clock/org.mate.panel.applet.clock.gschema.xml.in +++ b/applets/clock/org.mate.panel.applet.clock.gschema.xml.in @@ -84,5 +84,15 @@ <summary>Speed unit</summary> <description>The unit to use when showing wind speed.</description> </key> + <key name="show-calendar-events" type="b"> + <default>true</default> + <summary>Show calendar events</summary> + <description>If true, display calendar events from Evolution in the calendar window.</description> + </key> + <key name="show-tasks" type="b"> + <default>false</default> + <summary>Show tasks</summary> + <description>If true, display tasks from Evolution in the calendar window.</description> + </key> </schema> </schemalist> |