summaryrefslogtreecommitdiff
path: root/applets/clock/calendar-window.c
diff options
context:
space:
mode:
Diffstat (limited to 'applets/clock/calendar-window.c')
-rw-r--r--applets/clock/calendar-window.c1267
1 files changed, 1260 insertions, 7 deletions
diff --git a/applets/clock/calendar-window.c b/applets/clock/calendar-window.c
index b4157344..bd117a3f 100644
--- a/applets/clock/calendar-window.c
+++ b/applets/clock/calendar-window.c
@@ -38,11 +38,26 @@
#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 KEY_EXPAND_CALENDAR_EVENTS "expand-calendar-events"
+#define KEY_EXPAND_TASKS "expand-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 +75,34 @@ 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;
+ GtkWidget *appointment_tree_view;
+
+ 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 +127,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 +210,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 +250,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 +367,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)
{
@@ -290,9 +453,15 @@ calendar_window_fill (CalendarWindow *calwin)
if (!calwin->priv->invert_order) {
gtk_box_pack_start (GTK_BOX (vbox),
calwin->priv->calendar, TRUE, FALSE, 0);
+#ifdef HAVE_EDS
+ 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 +574,34 @@ 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);
+
+ /* Client is owned by ClockData, don't unref it */
+ 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,13 +704,50 @@ 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);
}
+#ifdef HAVE_EDS
+static void
+refresh_once (gpointer user_data)
+{
+ CalendarWindow *calwin = CALENDAR_WINDOW (user_data);
+ if (calwin->priv->client) {
+ calendar_client_update_appointments (calwin->priv->client);
+ calendar_client_update_tasks (calwin->priv->client);
+
+ handle_appointments_changed (calwin);
+ handle_tasks_changed (calwin);
+ }
+}
+#endif
+
void
calendar_window_refresh (CalendarWindow *calwin)
{
g_return_if_fail (CALENDAR_IS_WINDOW (calwin));
+
+#ifdef HAVE_EDS
+ /* Reload evolution calendar data after a small delay to not slow down the UI */
+ if (calwin->priv->client) {
+ g_timeout_add_once (100, refresh_once, calwin);
+ }
+
+ 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 +815,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 +873,1014 @@ 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);
+}
+
+/* Calculate relative luminance of a color to
+ * determine if the text should be light or dark */
+static gdouble
+calculate_luminance (GdkRGBA *color)
+{
+
+ /* Use the formula from https://en.wikipedia.org/wiki/Relative_luminance */
+ return 0.2126 * color->red + 0.7152 * color->green + 0.0722 * color->blue;
+}
+
+/* Get current time in the same format as stored event times.
+ * Event times are stored as local time but interpreted as UTC when displayed,
+ * so we need to create a UTC timestamp with local time components. */
+static time_t
+get_current_time_for_appointments (void)
+{
+ GDateTime *now_dt, *utc_dt;
+ time_t current_time;
+
+ now_dt = g_date_time_new_now_local();
+ utc_dt = g_date_time_new_utc(g_date_time_get_year(now_dt),
+ g_date_time_get_month(now_dt),
+ g_date_time_get_day_of_month(now_dt),
+ g_date_time_get_hour(now_dt),
+ g_date_time_get_minute(now_dt),
+ g_date_time_get_second(now_dt));
+ current_time = g_date_time_to_unix(utc_dt);
+ g_date_time_unref(now_dt);
+ g_date_time_unref(utc_dt);
+
+ return current_time;
+}
+
+static void
+appointment_cell_data_func (GtkTreeViewColumn *column,
+ GtkCellRenderer *cell,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ gchar *color_string = NULL;
+ time_t start_time = 0, end_time = 0;
+ time_t current_time;
+ gboolean is_all_day = FALSE;
+ GdkRGBA bg_color;
+ gchar *bg_color_str;
+ gdouble luminance;
+
+ gtk_tree_model_get(model, iter,
+ APPOINTMENT_COLUMN_COLOR, &color_string,
+ APPOINTMENT_COLUMN_START_TIME, &start_time,
+ APPOINTMENT_COLUMN_END_TIME, &end_time,
+ APPOINTMENT_COLUMN_ALL_DAY, &is_all_day,
+ -1);
+
+ current_time = get_current_time_for_appointments();
+
+ /* Determine if this is the current event and make it bold */
+ if (start_time <= current_time && end_time > current_time && !is_all_day) {
+ g_object_set(cell, "weight", PANGO_WEIGHT_BOLD, NULL);
+ } else {
+ g_object_set(cell, "weight", PANGO_WEIGHT_NORMAL, NULL);
+ }
+
+ /* Set background color from Evolution data */
+ if (color_string && *color_string && gdk_rgba_parse(&bg_color, color_string)) {
+ bg_color_str = gdk_rgba_to_string(&bg_color);
+ g_object_set(cell, "cell-background", bg_color_str, NULL);
+ g_free(bg_color_str);
+
+ /* Calculate luminance to determine if we need light or dark text */
+ luminance = calculate_luminance(&bg_color);
+ if (luminance > 0.5) {
+ /* Light background - use dark text */
+ g_object_set(cell, "foreground", "#000000", NULL);
+ } else {
+ /* Dark background - use light text */
+ g_object_set(cell, "foreground", "#FFFFFF", NULL);
+ }
+ } else {
+ g_object_set(cell,
+ "cell-background-set", FALSE,
+ "foreground-set", FALSE,
+ NULL);
+ }
+
+ g_free(color_string);
+}
+
+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_EXPAND_CALENDAR_EVENTS, NULL);
+
+ list = gtk_tree_view_new ();
+ gtk_tree_view_set_model (GTK_TREE_VIEW (list),
+ GTK_TREE_MODEL (calwin->priv->appointments_filter));
+
+ /* Store tree view reference for cell data function */
+ calwin->priv->appointment_tree_view = list;
+
+ 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);
+ gtk_tree_view_column_set_cell_data_func (column, cell,
+ appointment_cell_data_func,
+ calwin, 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_column_set_cell_data_func (column, cell,
+ appointment_cell_data_func,
+ calwin, 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_widget_show_all (*scrolled_window);
+ gtk_container_add (GTK_CONTAINER (frame), *scrolled_window);
+
+ *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_EXPAND_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);
+
+ /* 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);
+
+ gtk_widget_show (*scrolled_window);
+ gtk_widget_show (list);
+ gtk_container_add (GTK_CONTAINER (frame), *scrolled_window);
+
+ /* 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;
+ }
+
+ /* Connect signals if client is available */
+ if (calwin->priv->client) {
+ if (show_calendar_events && calwin->priv->client_appointments_changed_id == 0) {
+ 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 == 0) {
+ calwin->priv->client_tasks_changed_id = g_signal_connect_swapped (calwin->priv->client,
+ "tasks-changed",
+ G_CALLBACK (handle_tasks_changed),
+ calwin);
+ }
+ }
+
+
+ /* 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);
+ }
+}
+
+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));
+
+ /* Auto-scroll to next upcoming event */
+ if (calwin->priv->appointment_tree_view) {
+ GtkTreeView *tree_view = GTK_TREE_VIEW(calwin->priv->appointment_tree_view);
+ GtkTreeModel *model = GTK_TREE_MODEL(calwin->priv->appointments_filter);
+ GtkTreeIter iter;
+ time_t now;
+ gboolean found = FALSE;
+
+ now = get_current_time_for_appointments();
+
+ /* Find first current or future event */
+ if (gtk_tree_model_get_iter_first(model, &iter)) {
+ do {
+ time_t event_start, event_end;
+ gboolean is_all_day;
+ gtk_tree_model_get(model, &iter,
+ APPOINTMENT_COLUMN_START_TIME, &event_start,
+ APPOINTMENT_COLUMN_END_TIME, &event_end,
+ APPOINTMENT_COLUMN_ALL_DAY, &is_all_day,
+ -1);
+ /* Check if this is a current event (happening now) or future event */
+ if ((event_start <= now && event_end > now && !is_all_day) || event_start >= now) {
+ found = TRUE;
+ break;
+ }
+ } while (gtk_tree_model_iter_next(model, &iter));
+ }
+
+ if (found) {
+ GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
+ /* Scroll to center (0.5 puts it in the middle) */
+ gtk_tree_view_scroll_to_cell(tree_view, path, NULL, TRUE, 0.5, 0.0);
+ gtk_tree_path_free(path);
+ }
+ }
+}
+
+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");
+ }
+ }
+ }
+}
+
+void
+calendar_window_set_client (CalendarWindow *calwin, CalendarClient *client)
+{
+ g_return_if_fail (CALENDAR_IS_WINDOW (calwin));
+ calwin->priv->client = client;
+
+ /* If we have a client, initialize the calendar data */
+ if (client && calwin->priv->calendar) {
+ guint year, month, day;
+ gtk_calendar_get_date (GTK_CALENDAR (calwin->priv->calendar), &year, &month, &day);
+
+ calendar_client_select_month (calwin->priv->client, month, year);
+ calendar_client_select_day (calwin->priv->client, day);
+
+ /* Trigger initial data load if widgets exist */
+ if (calwin->priv->appointment_list) {
+ handle_appointments_changed (calwin);
+ }
+ if (calwin->priv->task_list) {
+ handle_tasks_changed (calwin);
+ }
+ }
+}
+
+#endif /* HAVE_EDS */