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/calendar-window.c | |
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/calendar-window.c')
-rw-r--r-- | applets/clock/calendar-window.c | 1126 |
1 files changed, 1119 insertions, 7 deletions
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 */ |