/* gtd-task-list-eds.c * * Copyright (C) 2015-2020 Georges Basile Stavracas Neto * * 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 3 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 . */ #define G_LOG_DOMAIN "GtdTaskListEds" #include "e-source-endeavour.h" #include "gtd-debug.h" #include "gtd-eds-autoptr.h" #include "gtd-provider-eds.h" #include "gtd-task-eds.h" #include "gtd-task-list-eds.h" #include struct _GtdTaskListEds { GtdTaskList parent; ECalClient *client; ECalClientView *client_view; ESource *source; GCancellable *cancellable; }; typedef struct { GtdProvider *provider; ESource *source; ECalClient *client; } NewTaskListData; static void on_client_objects_modified_for_migration_cb (GObject *object, GAsyncResult *result, gpointer user_data); G_DEFINE_TYPE (GtdTaskListEds, gtd_task_list_eds, GTD_TYPE_TASK_LIST) enum { PROP_0, PROP_CLIENT, PROP_SOURCE, N_PROPS }; /* * Auxiliary methods */ static void new_task_list_data_free (gpointer data) { NewTaskListData *list_data = data; if (!list_data) return; g_clear_object (&list_data->provider); g_clear_object (&list_data->source); g_clear_object (&list_data->client); g_free (list_data); } static void update_changed_tasks (GtdTaskListEds *self, GHashTable *changed_tasks) { g_autoptr (GSList) components = NULL; GHashTableIter iter; GtdProvider *provider; GtdTask *task; guint n_changed_tasks; GTD_ENTRY; n_changed_tasks = g_hash_table_size (changed_tasks); provider = gtd_task_list_get_provider (GTD_TASK_LIST (self)); /* Nothing changed, list is ready */ if (n_changed_tasks == 0) { gtd_object_pop_loading (GTD_OBJECT (provider)); g_signal_emit_by_name (provider, "list-added", self); GTD_RETURN (); } GTD_TRACE_MSG ("%u task(s) changed", n_changed_tasks); g_hash_table_iter_init (&iter, changed_tasks); while (g_hash_table_iter_next (&iter, (gpointer *) &task, NULL)) { ICalComponent *ical_comp; ECalComponent *comp; comp = gtd_task_eds_get_component (GTD_TASK_EDS (task)); ical_comp = e_cal_component_get_icalcomponent (comp); components = g_slist_prepend (components, ical_comp); } e_cal_client_modify_objects (self->client, components, E_CAL_OBJ_MOD_THIS, E_CAL_OPERATION_FLAG_NONE, self->cancellable, on_client_objects_modified_for_migration_cb, self); GTD_EXIT; } static void migrate_to_v1 (GtdTaskListEds *self, GHashTable *changed_tasks) { GListModel *model; guint n_tasks; guint i; model = G_LIST_MODEL (self); n_tasks = g_list_model_get_n_items (model); for (i = 0; i < n_tasks; i++) { g_autoptr (GtdTask) task = NULL; task = g_list_model_get_item (model, i); /* Don't notify to avoid carpet-bombing GtdTaskList */ g_object_freeze_notify (G_OBJECT (task)); gtd_task_set_position (task, i); g_hash_table_add (changed_tasks, task); } for (i = 0; i < n_tasks; i++) { g_autoptr (GtdTask) task = NULL; task = g_list_model_get_item (model, i); g_object_thaw_notify (G_OBJECT (task)); } } struct { guint api_version; void (* migrate) (GtdTaskListEds *self, GHashTable *changed_tasks); } migration_vtable[] = { { 0, migrate_to_v1 }, }; static void maybe_migrate_todo_api_version (GtdTaskListEds *self) { g_autoptr (GHashTable) changed_tasks = NULL; ESourceEndeavour *endeavour_extension; gboolean api_version_changed; guint api_version; guint i; GTD_ENTRY; /* * Ensure the type so that it is available for introspection when * calling e_source_get_extension(). */ g_type_ensure (E_TYPE_SOURCE_ENDEAVOUR); api_version_changed = FALSE; endeavour_extension = e_source_get_extension (self->source, E_SOURCE_EXTENSION_ENDEAVOUR); api_version = e_source_endeavour_get_api_version (endeavour_extension); changed_tasks = g_hash_table_new (g_direct_hash, g_direct_equal); g_debug ("%s: Endeavour API version %u", gtd_task_list_get_name (GTD_TASK_LIST (self)), api_version); for (i = 0; i < G_N_ELEMENTS (migration_vtable); i++) { guint new_api_version = i + 1; if (api_version > migration_vtable[i].api_version) continue; g_debug (" Migrating task list to Endeavour API v%u", new_api_version); migration_vtable[i].migrate (self, changed_tasks); e_source_endeavour_set_api_version (endeavour_extension, new_api_version); api_version_changed = TRUE; } if (api_version_changed) { g_debug ("Saving new API version"); e_source_write (self->source, NULL, NULL, NULL); } update_changed_tasks (self, changed_tasks); GTD_EXIT; } /* * Callbacks */ static gboolean new_task_list_in_idle_cb (gpointer data) { g_autoptr (GtdTaskListEds) list_eds = NULL; g_autoptr (GTask) task = data; NewTaskListData *list_data; list_data = g_task_get_task_data (task); list_eds = g_object_new (GTD_TYPE_TASK_LIST_EDS, "provider", list_data->provider, "source", list_data->source, "client", list_data->client, NULL); g_task_return_pointer (task, g_steal_pointer (&list_eds), NULL); return G_SOURCE_REMOVE; } static void on_client_objects_modified_for_migration_cb (GObject *object, GAsyncResult *result, gpointer user_data) { g_autoptr (GError) error = NULL; GtdProvider *provider; GtdTaskListEds *self; GTD_ENTRY; self = GTD_TASK_LIST_EDS (user_data); provider = gtd_task_list_get_provider (GTD_TASK_LIST (self)); e_cal_client_modify_objects_finish (self->client, result, &error); if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning ("Error migrating tasks to new API version: %s", error->message); gtd_object_pop_loading (GTD_OBJECT (provider)); g_signal_emit_by_name (provider, "list-added", self); GTD_EXIT; } static void on_view_objects_added_cb (ECalClientView *view, const GSList *objects, GtdTaskList *self) { g_autoptr (ECalClient) client = NULL; GSList *l; GTD_ENTRY; client = e_cal_client_view_ref_client (view); for (l = (GSList*) objects; l; l = l->next) { g_autoptr (ECalComponent) component = NULL; GtdTask *task; const gchar *uid; component = e_cal_component_new_from_icalcomponent (i_cal_component_clone (l->data)); uid = e_cal_component_get_uid (component); task = gtd_task_list_get_task_by_id (self, uid); /* If the task already exists, we must instead update its component */ if (task) { gtd_task_eds_set_component (GTD_TASK_EDS (task), component); gtd_task_list_update_task (self, task); GTD_TRACE_MSG ("Updated task '%s' to tasklist '%s'", gtd_task_get_title (task), gtd_task_list_get_name (self)); continue; } /* Add the new task */ task = gtd_task_eds_new (component); gtd_task_set_list (task, self); gtd_task_list_add_task (self, task); GTD_TRACE_MSG ("Added task '%s' (%s) to tasklist '%s'", gtd_task_get_title (task), gtd_object_get_uid (GTD_OBJECT (task)), gtd_task_list_get_name (self)); } GTD_EXIT; } static void on_view_objects_modified_cb (ECalClientView *view, const GSList *objects, GtdTaskList *self) { g_autoptr (ECalClient) client = NULL; GSList *l; GTD_ENTRY; client = e_cal_client_view_ref_client (view); for (l = (GSList*) objects; l; l = l->next) { g_autoptr (ECalComponent) component = NULL; GtdTask *task; const gchar *uid; component = e_cal_component_new_from_icalcomponent (i_cal_component_clone (l->data)); uid = e_cal_component_get_uid (component); task = gtd_task_list_get_task_by_id (self, uid); if (!task) continue; gtd_task_eds_set_component (GTD_TASK_EDS (task), component); GTD_TRACE_MSG ("Updated task '%s' from tasklist '%s'", gtd_task_get_title (GTD_TASK (task)), gtd_task_list_get_name (self)); } GTD_EXIT; } static void on_view_objects_removed_cb (ECalClientView *view, const GSList *uids, GtdTaskList *self) { GSList *l; GTD_ENTRY; for (l = (GSList*) uids; l; l = l->next) { ECalComponentId *id; GtdTask *task; id = l->data; task = gtd_task_list_get_task_by_id (self, e_cal_component_id_get_uid (id)); if (!task) continue; gtd_task_list_remove_task (self, task); GTD_TRACE_MSG ("Removed task '%s' from tasklist '%s'", gtd_task_get_title (task), gtd_task_list_get_name (self)); } GTD_EXIT; } static void on_view_completed_cb (ECalClientView *view, const GError *error, GtdTaskList *self) { gtd_object_pop_loading (GTD_OBJECT (gtd_manager_get_default ())); gtd_object_pop_loading (GTD_OBJECT (self)); if (error) { g_warning ("Error fetching tasks from list: %s", error->message); gtd_manager_emit_error_message (gtd_manager_get_default (), _("Error fetching tasks from list"), error->message, NULL, NULL); return; } maybe_migrate_todo_api_version (GTD_TASK_LIST_EDS (self)); } static void on_client_view_acquired_cb (GObject *client, GAsyncResult *result, gpointer user_data) { g_autoptr (GError) error = NULL; GtdTaskListEds *self; self = GTD_TASK_LIST_EDS (user_data); e_cal_client_get_view_finish (E_CAL_CLIENT (client), result, &self->client_view, &error); if (error) { g_warning ("Error fetching tasks from list: %s", error->message); gtd_manager_emit_error_message (gtd_manager_get_default (), _("Error fetching tasks from list"), error->message, NULL, NULL); return; } g_debug ("ECalClientView for tasklist '%s' successfully acquired", gtd_task_list_get_name (GTD_TASK_LIST (self))); g_signal_connect (self->client_view, "objects-added", G_CALLBACK (on_view_objects_added_cb), self); g_signal_connect (self->client_view, "objects-removed", G_CALLBACK (on_view_objects_removed_cb), self); g_signal_connect (self->client_view, "objects-modified", G_CALLBACK (on_view_objects_modified_cb), self); g_signal_connect (self->client_view, "complete", G_CALLBACK (on_view_completed_cb), self); gtd_object_push_loading (GTD_OBJECT (self)); e_cal_client_view_start (self->client_view, &error); if (error) { g_warning ("Error starting view: %s", error->message); gtd_manager_emit_error_message (gtd_manager_get_default (), _("Error fetching tasks from list"), error->message, NULL, NULL); } } static void on_source_removable_changed_cb (GtdTaskListEds *list) { gtd_task_list_set_is_removable (GTD_TASK_LIST (list), e_source_get_removable (list->source) || e_source_get_remote_deletable (list->source)); } static void on_source_selectable_selected_changed_cb (ESourceSelectable *selectable, GParamSpec *pspec, GtdTaskListEds *self) { g_debug ("%s (%s): ESourceSelectable:selected changed, notifying...", e_source_get_uid (self->source), e_source_get_display_name (self->source)); g_object_notify (G_OBJECT (self), "archived"); } static void on_save_task_list_finished_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GtdTaskListEds *list; GError *error; list = user_data; error = NULL; gtd_object_pop_loading (GTD_OBJECT (list)); e_source_write_finish (E_SOURCE (source), result, &error); if (error) { g_warning ("%s: %s: %s", G_STRFUNC, "Error saving task list", error->message); g_clear_error (&error); } } static void save_task_list (GtdTaskListEds *list) { if (e_source_get_writable (list->source)) { if (!list->cancellable) list->cancellable = g_cancellable_new (); gtd_object_push_loading (GTD_OBJECT (list)); e_source_write (list->source, list->cancellable, on_save_task_list_finished_cb, list); } } static gboolean color_to_string (GBinding *binding, const GValue *from_value, GValue *to_value, gpointer user_data) { GdkRGBA *color; gchar *color_str; color = g_value_get_boxed (from_value); color_str = gdk_rgba_to_string (color); g_value_set_string (to_value, color_str); g_free (color_str); return TRUE; } static gboolean string_to_color (GBinding *binding, const GValue *from_value, GValue *to_value, gpointer user_data) { GdkRGBA color; if (!gdk_rgba_parse (&color, g_value_get_string (from_value))) gdk_rgba_parse (&color, "#ffffff"); /* calendar default colour */ g_value_set_boxed (to_value, &color); return TRUE; } /* * GtdTaskList overrides */ static gboolean gtd_task_list_eds_get_archived (GtdTaskList *list) { ESourceSelectable *selectable; GtdTaskListEds *self; self = GTD_TASK_LIST_EDS (list); selectable = e_source_get_extension (self->source, E_SOURCE_EXTENSION_TASK_LIST); return !e_source_selectable_get_selected (selectable); } static void gtd_task_list_eds_set_archived (GtdTaskList *list, gboolean archived) { ESourceSelectable *selectable; GtdTaskListEds *self; GTD_ENTRY; self = GTD_TASK_LIST_EDS (list); selectable = e_source_get_extension (self->source, E_SOURCE_EXTENSION_TASK_LIST); g_signal_handlers_block_by_func (selectable, on_source_selectable_selected_changed_cb, self); e_source_selectable_set_selected (selectable, !archived); g_signal_handlers_unblock_by_func (selectable, on_source_selectable_selected_changed_cb, self); GTD_EXIT; } /* * GtdObject overrides */ static const gchar* gtd_task_list_eds_get_uid (GtdObject *object) { GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); return e_source_get_uid (self->source); } static void gtd_task_list_eds_set_uid (GtdObject *object, const gchar *uid) { g_assert_not_reached (); } /* * GObject overrides */ static void gtd_task_list_eds_finalize (GObject *object) { GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); g_cancellable_cancel (self->cancellable); g_clear_object (&self->cancellable); g_clear_object (&self->client); g_clear_object (&self->client_view); g_clear_object (&self->source); G_OBJECT_CLASS (gtd_task_list_eds_parent_class)->finalize (object); } static void gtd_task_list_eds_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); switch (prop_id) { case PROP_CLIENT: g_value_set_object (value, self->client); break; case PROP_SOURCE: g_value_set_object (value, self->source); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtd_task_list_eds_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtdTaskListEds *self = GTD_TASK_LIST_EDS (object); switch (prop_id) { case PROP_CLIENT: if (!g_set_object (&self->client, g_value_get_object (value)) || !self->client) return; e_cal_client_get_view (self->client, "#t", self->cancellable, on_client_view_acquired_cb, self); g_object_notify (object, "client"); break; case PROP_SOURCE: gtd_task_list_eds_set_source (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gtd_task_list_eds_class_init (GtdTaskListEdsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtdObjectClass *gtd_object_class = GTD_OBJECT_CLASS (klass); GtdTaskListClass *task_list_class = GTD_TASK_LIST_CLASS (klass); task_list_class->get_archived = gtd_task_list_eds_get_archived; task_list_class->set_archived = gtd_task_list_eds_set_archived; gtd_object_class->get_uid = gtd_task_list_eds_get_uid; gtd_object_class->set_uid = gtd_task_list_eds_set_uid; object_class->finalize = gtd_task_list_eds_finalize; object_class->get_property = gtd_task_list_eds_get_property; object_class->set_property = gtd_task_list_eds_set_property; /** * GtdTaskListEds::client: * * The #ECalClient of this #GtdTaskListEds */ g_object_class_install_property (object_class, PROP_CLIENT, g_param_spec_object ("client", "ECalClient of this list", "The ECalClient of this list", E_TYPE_CAL_CLIENT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); /** * GtdTaskListEds::source: * * The #ESource of this #GtdTaskListEds */ g_object_class_install_property (object_class, PROP_SOURCE, g_param_spec_object ("source", "ESource of this list", "The ESource of this list", E_TYPE_SOURCE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT)); } static void gtd_task_list_eds_init (GtdTaskListEds *self) { } void gtd_task_list_eds_new (GtdProvider *provider, ESource *source, ECalClient *client, GAsyncReadyCallback callback, GCancellable *cancellable, gpointer user_data) { g_autoptr (GTask) task = NULL; NewTaskListData *data; data = g_new (NewTaskListData, 1); data->provider = g_object_ref (provider); data->source = g_object_ref (source); data->client = g_object_ref (client); task = g_task_new (NULL, cancellable, callback, user_data); g_task_set_task_data (task, data, new_task_list_data_free); g_task_set_source_tag (task, gtd_task_list_eds_new); g_idle_add (new_task_list_in_idle_cb, g_steal_pointer (&task)); } GtdTaskListEds* gtd_task_list_eds_new_finish (GAsyncResult *result, GError **error) { g_return_val_if_fail (G_IS_TASK (result), NULL); g_return_val_if_fail (!error || !*error, NULL); return g_task_propagate_pointer (G_TASK (result), error); } ESource* gtd_task_list_eds_get_source (GtdTaskListEds *list) { g_return_val_if_fail (GTD_IS_TASK_LIST_EDS (list), NULL); return list->source; } void gtd_task_list_eds_set_source (GtdTaskListEds *self, ESource *source) { ESourceSelectable *selectable; ESourceRefresh *refresh; GdkRGBA color; gboolean is_inbox; g_return_if_fail (GTD_IS_TASK_LIST_EDS (self)); if (!g_set_object (&self->source, source)) return; is_inbox = g_str_equal (e_source_get_uid (source), GTD_PROVIDER_EDS_INBOX_ID); /* Setup color */ selectable = E_SOURCE_SELECTABLE (e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST)); if (!gdk_rgba_parse (&color, e_source_selectable_get_color (selectable))) gdk_rgba_parse (&color, "#ffffff"); /* calendar default color */ gtd_task_list_set_color (GTD_TASK_LIST (self), &color); g_object_bind_property_full (self, "color", selectable, "color", G_BINDING_BIDIRECTIONAL, color_to_string, string_to_color, self, NULL); g_signal_connect_object (selectable, "notify::selected", G_CALLBACK (on_source_selectable_selected_changed_cb), self, 0); /* Setup tasklist name */ if (is_inbox) gtd_task_list_set_name (GTD_TASK_LIST (self), _("Inbox")); else gtd_task_list_set_name (GTD_TASK_LIST (self), e_source_get_display_name (source)); g_object_bind_property (source, "display-name", self, "name", G_BINDING_BIDIRECTIONAL); /* Save the task list every time something changes */ g_signal_connect_swapped (source, "notify", G_CALLBACK (save_task_list), self); /* Update ::is-removable property */ gtd_task_list_set_is_removable (GTD_TASK_LIST (self), e_source_get_removable (source) || e_source_get_remote_deletable (source)); g_signal_connect_swapped (source, "notify::removable", G_CALLBACK (on_source_removable_changed_cb), self); g_signal_connect_swapped (source, "notify::remote-deletable", G_CALLBACK (on_source_removable_changed_cb), self); /* Refresh timeout */ refresh = e_source_get_extension (source, E_SOURCE_EXTENSION_REFRESH); e_source_refresh_set_enabled (refresh, TRUE); e_source_refresh_set_interval_minutes (refresh, 5); e_source_write (source, NULL, NULL, NULL); g_object_notify (G_OBJECT (self), "source"); } ECalClient* gtd_task_list_eds_get_client (GtdTaskListEds *self) { g_return_val_if_fail (GTD_IS_TASK_LIST_EDS (self), NULL); return self->client; }