diff options
| author | Matthew Fennell <matthew@fennell.dev> | 2025-12-27 12:40:20 +0000 |
|---|---|---|
| committer | Matthew Fennell <matthew@fennell.dev> | 2025-12-27 12:40:20 +0000 |
| commit | 5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 (patch) | |
| tree | ed28aefed8add0da1c55c08fdf80b23c4346e0dc /src/plugins | |
Import Upstream version 43.0upstream/latest
Diffstat (limited to 'src/plugins')
85 files changed, 11641 insertions, 0 deletions
diff --git a/src/plugins/all-tasks-panel/all-tasks-panel-plugin.c b/src/plugins/all-tasks-panel/all-tasks-panel-plugin.c new file mode 100644 index 0000000..93aedfe --- /dev/null +++ b/src/plugins/all-tasks-panel/all-tasks-panel-plugin.c @@ -0,0 +1,34 @@ +/* gtd-plugin-all-tasks-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdPluginAllTasksPanel" + +#include "endeavour.h" + +#include "gtd-all-tasks-panel.h" + +G_MODULE_EXPORT void +all_tasks_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_ALL_TASKS_PANEL); +} diff --git a/src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml b/src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml new file mode 100644 index 0000000..608338f --- /dev/null +++ b/src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/all-tasks-panel"> + <file>all-tasks-panel.plugin</file> + </gresource> +</gresources> diff --git a/src/plugins/all-tasks-panel/all-tasks-panel.plugin b/src/plugins/all-tasks-panel/all-tasks-panel.plugin new file mode 100644 index 0000000..ac184ca --- /dev/null +++ b/src/plugins/all-tasks-panel/all-tasks-panel.plugin @@ -0,0 +1,13 @@ +[Plugin] +Name = All Tasks +Module = all-tasks-panel +Description = A panel to show all open tasks +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = all_tasks_panel_plugin_register_types +Depends = diff --git a/src/plugins/all-tasks-panel/gtd-all-tasks-panel.c b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.c new file mode 100644 index 0000000..a4b4ad3 --- /dev/null +++ b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.c @@ -0,0 +1,491 @@ +/* gtd-all-tasks-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdAllTasksPanel" + +#include "gtd-all-tasks-panel.h" + +#include "endeavour.h" + +#include "gtd-debug.h" + +#include <glib/gi18n.h> +#include <math.h> + + +#define GTD_ALL_TASKS_PANEL_NAME "all-tasks-panel" +#define GTD_ALL_TASKS_PANEL_PRIORITY 000 + +struct _GtdAllTasksPanel +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkSortListModel *sort_model; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdAllTasksPanel, gtd_all_tasks_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + + +static void +get_date_offset (GDateTime *dt, + gint *days_diff, + gint *years_diff) +{ + g_autoptr (GDateTime) now = NULL; + GDate now_date, dt_date; + + g_date_clear (&dt_date, 1); + g_date_set_dmy (&dt_date, + g_date_time_get_day_of_month (dt), + g_date_time_get_month (dt), + g_date_time_get_year (dt)); + + now = g_date_time_new_now_local (); + + g_date_clear (&now_date, 1); + g_date_set_dmy (&now_date, + g_date_time_get_day_of_month (now), + g_date_time_get_month (now), + g_date_time_get_year (now)); + + + if (days_diff) + *days_diff = g_date_days_between (&now_date, &dt_date); + + if (years_diff) + *years_diff = g_date_time_get_year (dt) - g_date_time_get_year (now); +} + +static gchar* +get_string_for_date (GDateTime *dt, + gint *span) +{ + gchar *str; + gint days_diff; + gint years_diff; + + if (!dt) + return g_strdup (_("No date set")); + + days_diff = years_diff = 0; + + get_date_offset (dt, &days_diff, &years_diff); + + if (days_diff < -1) + { + /* Translators: This message will never be used with '1 day ago' + * but the singular form is required because some languages do not + * have plurals, some languages reuse the singular form for numbers + * like 21, 31, 41, etc. + */ + str = g_strdup_printf (g_dngettext (NULL, "%d day ago", "%d days ago", -days_diff), -days_diff); + } + else if (days_diff == -1) + { + str = g_strdup (_("Yesterday")); + } + else if (days_diff == 0) + { + str = g_strdup (_("Today")); + } + else if (days_diff == 1) + { + str = g_strdup (_("Tomorrow")); + } + else if (days_diff > 1 && days_diff < 7) + { + str = g_date_time_format (dt, "%A"); // Weekday name + } + else if (days_diff >= 7 && years_diff == 0) + { + str = g_date_time_format (dt, "%OB"); // Full month name + } + else + { + str = g_strdup_printf ("%d", g_date_time_get_year (dt)); + } + + if (span) + *span = days_diff; + + return str; +} + +static GtkWidget* +create_label (const gchar *text, + gint span, + gboolean first_header) +{ + GtkWidget *label; + GtkWidget *box; + + label = g_object_new (GTK_TYPE_LABEL, + "label", text, + "margin-top", first_header ? 6 : 18, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, span < 0 ? "date-overdue" : "date-scheduled"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + gtk_box_append (GTK_BOX (box), label); + + return box; +} + +static gint +compare_by_date (GDateTime *d1, + GDateTime *d2) +{ + if (!d1 && !d2) + return 0; + else if (!d1) + return 1; + else if (!d2) + return -1; + + if (g_date_time_get_year (d1) != g_date_time_get_year (d2)) + return g_date_time_get_year (d1) - g_date_time_get_year (d2); + + return g_date_time_get_day_of_year (d1) - g_date_time_get_day_of_year (d2); +} + + +/* + * Callbacks + */ + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + GtdAllTasksPanel *self) +{ + g_autoptr (GDateTime) dt = NULL; + g_autofree gchar *text = NULL; + gint span; + + dt = gtd_task_get_due_date (task); + + if (previous_task) + { + g_autoptr (GDateTime) before_dt = NULL; + gint diff; + + before_dt = gtd_task_get_due_date (previous_task); + diff = compare_by_date (before_dt, dt); + + if (diff != 0) + text = get_string_for_date (dt, &span); + } + else + { + text = get_string_for_date (dt, &span); + } + + return text ? create_label (text, span, !previous_task) : NULL; +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + g_autoptr (GDateTime) dt1 = NULL; + g_autoptr (GDateTime) dt2 = NULL; + GtdTask *task1; + GtdTask *task2; + GDate dates[2]; + gint result; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + if (!dt1 && !dt2) + return gtd_task_compare (task1, task2); + else if (!dt1) + return 1; + else if (!dt2) + return -1; + + g_date_clear (dates, 2); + + g_date_set_dmy (&dates[0], + g_date_time_get_day_of_month (dt1), + g_date_time_get_month (dt1), + g_date_time_get_year (dt1)); + + g_date_set_dmy (&dates[1], + g_date_time_get_day_of_month (dt2), + g_date_time_get_month (dt2), + g_date_time_get_year (dt2)); + + result = g_date_days_between (&dates[1], &dates[0]); + + if (result != 0) + return result; + + return gtd_task_compare (task1, task2); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdAllTasksPanel *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added); + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdAllTasksPanel *self) +{ + g_autoptr (GDateTime) now = NULL; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); +} + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_all_tasks_get_panel_name (GtdPanel *panel) +{ + return GTD_ALL_TASKS_PANEL_NAME; +} + +static const gchar* +gtd_panel_all_tasks_get_panel_title (GtdPanel *panel) +{ + return _("All"); +} + +static GList* +gtd_panel_all_tasks_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_all_tasks_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_all_tasks_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_ALL_TASKS_PANEL (panel)->icon); +} + +static guint32 +gtd_panel_all_tasks_get_priority (GtdPanel *panel) +{ + return GTD_ALL_TASKS_PANEL_PRIORITY; +} + +static gchar* +gtd_panel_all_tasks_get_subtitle (GtdPanel *panel) +{ + GtdAllTasksPanel *self = GTD_ALL_TASKS_PANEL (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_all_tasks_get_panel_name; + iface->get_panel_title = gtd_panel_all_tasks_get_panel_title; + iface->get_header_widgets = gtd_panel_all_tasks_get_header_widgets; + iface->get_menu = gtd_panel_all_tasks_get_menu; + iface->get_icon = gtd_panel_all_tasks_get_icon; + iface->get_priority = gtd_panel_all_tasks_get_priority; + iface->get_subtitle = gtd_panel_all_tasks_get_subtitle; +} + + +/* + * GObject overrides + */ + +static void +gtd_all_tasks_panel_finalize (GObject *object) +{ + GtdAllTasksPanel *self = (GtdAllTasksPanel *)object; + + g_clear_object (&self->icon); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_all_tasks_panel_parent_class)->finalize (object); +} + +static void +gtd_all_tasks_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdAllTasksPanel *self = GTD_ALL_TASKS_PANEL (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_ALL_TASKS_PANEL_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_ALL_TASKS_PANEL_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_all_tasks_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_all_tasks_panel_class_init (GtdAllTasksPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_all_tasks_panel_finalize; + object_class->get_property = gtd_all_tasks_panel_get_property; + object_class->set_property = gtd_all_tasks_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_all_tasks_panel_init (GtdAllTasksPanel *self) +{ + GtdManager *manager = gtd_manager_get_default (); + GtkCustomSorter *sorter; + + self->icon = g_themed_icon_new ("view-tasks-all-symbolic"); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_SORTER (sorter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (GTD_TASK_LIST_VIEW (self->view), G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (GTD_TASK_LIST_VIEW (self->view), TRUE); + gtd_task_list_view_set_show_due_date (GTD_TASK_LIST_VIEW (self->view), FALSE); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (GTD_TASK_LIST_VIEW (self->view), + (GtdTaskListViewHeaderFunc) header_func, + self); + + g_signal_connect_object (self->sort_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); +} + +GtkWidget* +gtd_all_tasks_panel_new (void) +{ + return g_object_new (GTD_TYPE_ALL_TASKS_PANEL, NULL); +} diff --git a/src/plugins/all-tasks-panel/gtd-all-tasks-panel.h b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.h new file mode 100644 index 0000000..f6efec0 --- /dev/null +++ b/src/plugins/all-tasks-panel/gtd-all-tasks-panel.h @@ -0,0 +1,34 @@ +/* gtd-all-tasks-panel.h + * + * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_ALL_TASKS_PANEL (gtd_all_tasks_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdAllTasksPanel, gtd_all_tasks_panel, GTD, ALL_TASKS_PANEL, GtkBox) + +GtkWidget* gtd_all_tasks_panel_new (void); + +G_END_DECLS diff --git a/src/plugins/all-tasks-panel/meson.build b/src/plugins/all-tasks-panel/meson.build new file mode 100644 index 0000000..9b4954f --- /dev/null +++ b/src/plugins/all-tasks-panel/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=all_tasks_panel_plugin_register_types'] + +plugins_sources += files( + 'all-tasks-panel-plugin.c', + 'gtd-all-tasks-panel.c' +) + +plugins_sources += gnome.compile_resources( + 'all-tasks-panel-resources', + 'all-tasks-panel.gresource.xml', + c_name: 'all_tasks_panel_plugin', +) diff --git a/src/plugins/eds/e-source-endeavour.c b/src/plugins/eds/e-source-endeavour.c new file mode 100644 index 0000000..3ca8846 --- /dev/null +++ b/src/plugins/eds/e-source-endeavour.c @@ -0,0 +1,128 @@ +/* gtd-task-list-eds.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "e-source-endeavour.h" + +struct _ESourceEndeavour +{ + ESourceExtension parent; + + guint api_version; +}; + +G_DEFINE_TYPE (ESourceEndeavour, e_source_endeavour, E_TYPE_SOURCE_EXTENSION) + +enum +{ + PROP_0, + PROP_API_VERSION, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + + +/* + * GObject overrides + */ + +static void +e_source_endeavour_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ESourceEndeavour *self = E_SOURCE_ENDEAVOUR (object); + + switch (prop_id) + { + case PROP_API_VERSION: + g_value_set_uint (value, self->api_version); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +e_source_endeavour_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ESourceEndeavour *self = E_SOURCE_ENDEAVOUR (object); + + switch (prop_id) + { + case PROP_API_VERSION: + self->api_version = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +e_source_endeavour_class_init (ESourceEndeavourClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ESourceExtensionClass *extension_class = E_SOURCE_EXTENSION_CLASS (klass); + + object_class->get_property = e_source_endeavour_get_property; + object_class->set_property = e_source_endeavour_set_property; + + extension_class->name = E_SOURCE_EXTENSION_ENDEAVOUR; + + properties[PROP_API_VERSION] = g_param_spec_uint ("api-version", + "API Version", + "API Version", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | E_SOURCE_PARAM_SETTING | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +e_source_endeavour_init (ESourceEndeavour *self) +{ + self->api_version = 0; +} + +guint +e_source_endeavour_get_api_version (ESourceEndeavour *self) +{ + g_return_val_if_fail (E_IS_SOURCE_ENDEAVOUR (self), 0); + + return self->api_version; +} + +void +e_source_endeavour_set_api_version (ESourceEndeavour *self, + guint api_version) +{ + g_return_if_fail (E_IS_SOURCE_ENDEAVOUR (self)); + + e_source_extension_property_lock (E_SOURCE_EXTENSION (self)); + self->api_version = api_version; + e_source_extension_property_unlock (E_SOURCE_EXTENSION (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_API_VERSION]); +} diff --git a/src/plugins/eds/e-source-endeavour.h b/src/plugins/eds/e-source-endeavour.h new file mode 100644 index 0000000..ca70c18 --- /dev/null +++ b/src/plugins/eds/e-source-endeavour.h @@ -0,0 +1,35 @@ +/* gtd-task-list-eds.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "gtd-eds.h" + +G_BEGIN_DECLS + +#define E_SOURCE_EXTENSION_ENDEAVOUR "Endeavour" + +#define E_TYPE_SOURCE_ENDEAVOUR (e_source_endeavour_get_type()) +G_DECLARE_FINAL_TYPE (ESourceEndeavour, e_source_endeavour, E, SOURCE_ENDEAVOUR, ESourceExtension) + +guint e_source_endeavour_get_api_version (ESourceEndeavour *self); + +void e_source_endeavour_set_api_version (ESourceEndeavour *self, + guint api_version); + +G_END_DECLS diff --git a/src/plugins/eds/eds-plugin.c b/src/plugins/eds/eds-plugin.c new file mode 100644 index 0000000..df172d0 --- /dev/null +++ b/src/plugins/eds/eds-plugin.c @@ -0,0 +1,30 @@ +/* eds-plugin.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "endeavour.h" +#include "gtd-plugin-eds.h" + +G_MODULE_EXPORT void +gtd_plugin_eds_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_ACTIVATABLE, + GTD_TYPE_PLUGIN_EDS); +} diff --git a/src/plugins/eds/eds.gresource.xml b/src/plugins/eds/eds.gresource.xml new file mode 100644 index 0000000..4b578f9 --- /dev/null +++ b/src/plugins/eds/eds.gresource.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/eds"> + <file>eds.plugin</file> + </gresource> +</gresources> diff --git a/src/plugins/eds/eds.plugin b/src/plugins/eds/eds.plugin new file mode 100644 index 0000000..b7f28cf --- /dev/null +++ b/src/plugins/eds/eds.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Core +Module = eds +Description = Evolution-data-server plugin for Endeavour +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +Hidden = true +License = GPL +Loader = C +Embedded = gtd_plugin_eds_register_types +Depends = diff --git a/src/plugins/eds/gtd-eds-autoptr.h b/src/plugins/eds/gtd-eds-autoptr.h new file mode 100644 index 0000000..99c6129 --- /dev/null +++ b/src/plugins/eds/gtd-eds-autoptr.h @@ -0,0 +1,27 @@ +/* gtd-eds-autoptr.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-eds.h" + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ECalComponent, g_object_unref); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ECalComponentId, e_cal_component_id_free); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ECalClient, g_object_unref); diff --git a/src/plugins/eds/gtd-eds.h b/src/plugins/eds/gtd-eds.h new file mode 100644 index 0000000..a48827c --- /dev/null +++ b/src/plugins/eds/gtd-eds.h @@ -0,0 +1,32 @@ +/* gtd-eds.h + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib.h> + +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + +#include <libecal/libecal.h> +#include <libedataserver/libedataserver.h> + +G_GNUC_END_IGNORE_DEPRECATIONS diff --git a/src/plugins/eds/gtd-plugin-eds.c b/src/plugins/eds/gtd-plugin-eds.c new file mode 100644 index 0000000..6200916 --- /dev/null +++ b/src/plugins/eds/gtd-plugin-eds.c @@ -0,0 +1,323 @@ +/* gtd-plugin-eds.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdPluginEds" + +#include "gtd-plugin-eds.h" +#include "gtd-provider-goa.h" +#include "gtd-provider-local.h" + +#include <glib/gi18n.h> +#include <glib-object.h> + +/** + * The #GtdPluginEds is a class that loads all the + * essential providers of Endeavour. + * + * It basically loads #ESourceRegistry which provides + * #GtdProviderLocal. Immediately after that, it loads + * #GoaClient which provides one #GtdProviderGoa per + * supported account. + * + * The currently supported Online Accounts are Google, + * ownCloud and Microsoft Exchange ones. + */ + +struct _GtdPluginEds +{ + GObject parent; + + ESourceRegistry *registry; + + /* Providers */ + GList *providers; +}; + +enum +{ + PROP_0, + PROP_PREFERENCES_PANEL, + LAST_PROP +}; + +const gchar *supported_accounts[] = { + "exchange", + "google", + "owncloud", + NULL +}; + +static void gtd_activatable_iface_init (GtdActivatableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdPluginEds, gtd_plugin_eds, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTD_TYPE_ACTIVATABLE, gtd_activatable_iface_init)) + +/* + * GtdActivatable interface implementation + */ +static void +gtd_plugin_eds_activate (GtdActivatable *activatable) +{ + ; +} + +static void +gtd_plugin_eds_deactivate (GtdActivatable *activatable) +{ + ; +} + +static GtkWidget* +gtd_plugin_eds_get_preferences_panel (GtdActivatable *activatable) +{ + return NULL; +} + +static void +gtd_activatable_iface_init (GtdActivatableInterface *iface) +{ + iface->activate = gtd_plugin_eds_activate; + iface->deactivate = gtd_plugin_eds_deactivate; + iface->get_preferences_panel = gtd_plugin_eds_get_preferences_panel; +} + + +/* + * Init + */ + +static void +gtd_plugin_eds_goa_account_removed_cb (GoaClient *client, + GoaObject *object, + GtdPluginEds *self) +{ + GtdManager *manager; + GoaAccount *account; + GList *l; + + account = goa_object_peek_account (object); + manager = gtd_manager_get_default (); + + if (!g_strv_contains (supported_accounts, goa_account_get_provider_type (account))) + return; + + for (l = self->providers; l != NULL; l = l->next) + { + if (!GTD_IS_PROVIDER_GOA (l->data)) + continue; + + if (account == gtd_provider_goa_get_account (l->data)) + { + GtdProviderGoa *provider = GTD_PROVIDER_GOA (l->data); + + self->providers = g_list_remove (self->providers, l->data); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + break; + } + } +} + +static void +gtd_plugin_eds_goa_account_added_cb (GoaClient *client, + GoaObject *object, + GtdPluginEds *self) +{ + GtdManager *manager; + GoaAccount *account; + + account = goa_object_get_account (object); + manager = gtd_manager_get_default (); + + if (g_strv_contains (supported_accounts, goa_account_get_provider_type (account))) + { + GtdProviderGoa *provider; + + provider = gtd_provider_goa_new (self->registry, account); + + self->providers = g_list_append (self->providers, provider); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + } +} + +static void +gtd_plugin_eds_goa_client_finish_cb (GObject *client, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdPluginEds *self; + GtdManager *manager; + GoaClient *goa_client; + GList *accounts; + GList *l; + + self = GTD_PLUGIN_EDS (user_data); + goa_client = goa_client_new_finish (result, &error); + manager = gtd_manager_get_default (); + + if (error) + { + g_warning ("%s: %s: %s", + G_STRFUNC, + "Error loading GNOME Online Accounts", + error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Error loading GNOME Online Accounts"), + error->message, + NULL, + NULL); + g_clear_error (&error); + } + + /* Load each supported GoaAccount into a GtdProviderGoa */ + accounts = goa_client_get_accounts (goa_client); + + for (l = accounts; l != NULL; l = l->next) + { + GtdProviderGoa *provider; + GoaAccount *account; + GoaObject *object; + + object = l->data; + account = goa_object_get_account (object); + + if (!g_strv_contains (supported_accounts, goa_account_get_provider_type (account))) + { + g_object_unref (account); + continue; + } + + g_debug ("Creating new provider for account '%s'", goa_account_get_identity (account)); + + /* Create the new GOA provider */ + provider = gtd_provider_goa_new (self->registry, account); + + self->providers = g_list_append (self->providers, provider); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + + g_object_unref (account); + } + + /* Connect GoaClient signals */ + g_signal_connect (goa_client, + "account-added", + G_CALLBACK (gtd_plugin_eds_goa_account_added_cb), + user_data); + + g_signal_connect (goa_client, + "account-removed", + G_CALLBACK (gtd_plugin_eds_goa_account_removed_cb), + user_data); + + g_list_free_full (accounts, g_object_unref); +} + + + +static void +gtd_plugin_eds_source_registry_finish_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GtdPluginEds *self = GTD_PLUGIN_EDS (user_data); + GtdProviderLocal *provider; + ESourceRegistry *registry; + GtdManager *manager; + GError *error = NULL; + + manager = gtd_manager_get_default (); + registry = e_source_registry_new_finish (result, &error); + self->registry = registry; + + /* Abort on error */ + if (error) + { + g_warning ("%s: %s", + "Error loading Evolution-Data-Server backend", + error->message); + + g_clear_error (&error); + return; + } + + /* Load the local provider */ + provider = gtd_provider_local_new (registry); + + self->providers = g_list_append (self->providers, provider); + gtd_manager_add_provider (manager, GTD_PROVIDER (provider)); + + /* We only start loading Goa accounts after + * ESourceRegistry is get, since it'd be way + * too hard to synchronize these two asynchronous + * calls. + */ + goa_client_new (NULL, + (GAsyncReadyCallback) gtd_plugin_eds_goa_client_finish_cb, + self); +} + +static void +gtd_plugin_eds_finalize (GObject *object) +{ + GtdPluginEds *self = (GtdPluginEds *)object; + + g_list_free_full (self->providers, g_object_unref); + self->providers = NULL; + + G_OBJECT_CLASS (gtd_plugin_eds_parent_class)->finalize (object); +} + +static void +gtd_plugin_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_PREFERENCES_PANEL: + g_value_set_object (value, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_plugin_eds_class_init (GtdPluginEdsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_plugin_eds_finalize; + object_class->get_property = gtd_plugin_eds_get_property; + + g_object_class_override_property (object_class, + PROP_PREFERENCES_PANEL, + "preferences-panel"); +} + +static void +gtd_plugin_eds_init (GtdPluginEds *self) +{ + /* load the source registry */ + e_source_registry_new (NULL, + (GAsyncReadyCallback) gtd_plugin_eds_source_registry_finish_cb, + self); +} diff --git a/src/plugins/eds/gtd-plugin-eds.h b/src/plugins/eds/gtd-plugin-eds.h new file mode 100644 index 0000000..6eea2c4 --- /dev/null +++ b/src/plugins/eds/gtd-plugin-eds.h @@ -0,0 +1,28 @@ +/* gtd-eds-plugin.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_PLUGIN_EDS (gtd_plugin_eds_get_type()) +G_DECLARE_FINAL_TYPE (GtdPluginEds, gtd_plugin_eds, GTD, PLUGIN_EDS, PeasExtensionBase) + +G_END_DECLS diff --git a/src/plugins/eds/gtd-provider-eds.c b/src/plugins/eds/gtd-provider-eds.c new file mode 100644 index 0000000..d46d70e --- /dev/null +++ b/src/plugins/eds/gtd-provider-eds.c @@ -0,0 +1,1157 @@ +/* gtd-provider-eds.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdProviderEds" + +#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 <glib/gi18n.h> + +/** + * #GtdProviderEds is the base class of #GtdProviderLocal + * and #GtdProviderGoa. It provides the common functionality + * shared between these two providers. + * + * The subclasses basically have to implement GtdProviderEds->should_load_source + * which decides whether a given #ESource should be loaded (and added to the + * sources list) or not. #GtdProviderLocal for example would filter out + * sources whose backend is not "local". + */ + +typedef struct +{ + GtdTaskList *list; + GDateTime *due_date; + gchar *title; + ESource *source; + + /* Update Task */ + ECalComponent *component; + GtdTask *task; +} AsyncData; + +typedef struct +{ + GHashTable *task_lists; + + ESourceRegistry *source_registry; + + GCancellable *cancellable; + + gint lazy_load_id; +} GtdProviderEdsPrivate; + + +static void gtd_provider_iface_init (GtdProviderInterface *iface); + + +G_DEFINE_TYPE_WITH_CODE (GtdProviderEds, gtd_provider_eds, GTD_TYPE_OBJECT, + G_ADD_PRIVATE (GtdProviderEds) + G_IMPLEMENT_INTERFACE (GTD_TYPE_PROVIDER, gtd_provider_iface_init)) + + +enum +{ + PROP_0, + PROP_ENABLED, + PROP_DESCRIPTION, + PROP_ICON, + PROP_ID, + PROP_NAME, + PROP_PROVIDER_TYPE, + PROP_REGISTRY, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +async_data_free (gpointer data) +{ + AsyncData *async_data = data; + + g_clear_pointer (&async_data->due_date, g_date_time_unref); + g_clear_pointer (&async_data->title, g_free); + g_clear_object (&async_data->source); + g_clear_object (&async_data->list); + g_clear_object (&async_data->task); + g_clear_object (&async_data->component); + g_free (async_data); +} + +static void +set_default_list (GtdProviderEds *self, + GtdTaskList *list) +{ + GtdProviderEdsPrivate *priv; + GtdManager *manager; + ESource *source; + + priv = gtd_provider_eds_get_instance_private (self); + source = gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)); + manager = gtd_manager_get_default (); + + e_source_registry_set_default_task_list (priv->source_registry, source); + + if (gtd_manager_get_default_provider (manager) != (GtdProvider*) self) + gtd_manager_set_default_provider (manager, GTD_PROVIDER (self)); +} + +static void +ensure_offline_sync (GtdProviderEds *self, + ESource *source) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + ESourceOffline *extension; + + extension = e_source_get_extension (source, E_SOURCE_EXTENSION_OFFLINE); + e_source_offline_set_stay_synchronized (extension, TRUE); + + e_source_registry_commit_source (priv->source_registry, source, NULL, NULL, NULL); +} + + +/* + * Callbacks + */ + +static void +on_task_list_eds_loaded_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + GtdTaskListEds *list; + ESource *source; + + self = GTD_PROVIDER_EDS (user_data); + priv = gtd_provider_eds_get_instance_private (self); + list = gtd_task_list_eds_new_finish (result, &error); + + if (error) + { + g_warning ("Error creating task list: %s", error->message); + return; + } + + source = gtd_task_list_eds_get_source (list); + + g_hash_table_insert (priv->task_lists, e_source_dup_uid (source), g_object_ref (list)); + g_object_set_data (G_OBJECT (source), "task-list", list); + + g_debug ("Task list '%s' successfully connected", e_source_get_display_name (source)); +} + +static void +on_client_connected_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + ECalClient *client; + ESource *source; + + self = GTD_PROVIDER_EDS (user_data); + priv = gtd_provider_eds_get_instance_private (self); + source = e_client_get_source (E_CLIENT (source_object)); + client = E_CAL_CLIENT (e_cal_client_connect_finish (result, &error)); + + if (error) + { + g_warning ("Failed to connect to task list '%s': %s", e_source_get_uid (source), error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("Failed to connect to task list"), + error->message, + NULL, + NULL); + gtd_object_pop_loading (GTD_OBJECT (self)); + return; + } + + ensure_offline_sync (self, source); + + /* creates a new task list */ + gtd_task_list_eds_new (GTD_PROVIDER (self), + source, + client, + on_task_list_eds_loaded_cb, + priv->cancellable, + self); +} + +static void +on_source_added_cb (GtdProviderEds *provider, + ESource *source) +{ + /* Don't load the source if it's not a tasklist */ + if (!e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST) || + !GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->should_load_source (provider, source)) + { + GTD_TRACE_MSG ("Ignoring source %s (%s)", + e_source_get_display_name (source), + e_source_get_uid (source)); + return; + } + + /* + * The pop_loading() is actually emited by GtdTaskListEds, after the + * ECalClientView sends the :complete signal. + */ + gtd_object_push_loading (GTD_OBJECT (provider)); + gtd_object_push_loading (GTD_OBJECT (gtd_manager_get_default ())); + + e_cal_client_connect (source, + E_CAL_CLIENT_SOURCE_TYPE_TASKS, + 15, /* seconds to wait */ + NULL, + on_client_connected_cb, + provider); +} + +static void +on_source_removed_cb (GtdProviderEds *provider, + ESource *source) +{ + GtdProviderEdsPrivate *priv; + GtdTaskList *list; + + GTD_ENTRY; + + priv = gtd_provider_eds_get_instance_private (provider); + list = g_object_get_data (G_OBJECT (source), "task-list"); + + if (!g_hash_table_remove (priv->task_lists, gtd_object_get_uid (GTD_OBJECT (list)))) + GTD_RETURN (); + + /* + * Since all subclasses will have this signal given that they + * are all GtdProvider implementations, it's not that bad + * to let it stay here. + */ + g_signal_emit_by_name (provider, "list-removed", list); + + GTD_EXIT; +} + +static void +on_source_refreshed_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (user_data); + g_autoptr (GError) error = NULL; + + GTD_ENTRY; + + e_source_registry_refresh_backend_finish (priv->source_registry, result, &error); + + if (error) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error refreshing source: %s", error->message); + GTD_RETURN (); + } + + GTD_EXIT; +} + +static void +create_task_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (ECalComponent) component = NULL; + g_autoptr (GError) error = NULL; + g_autofree gchar *new_uid = NULL; + ECalComponentText *new_summary; + GtdTaskListEds *tasklist; + ECalClient *client; + AsyncData *data; + GtdTask *new_task; + + GTD_ENTRY; + + data = task_data; + tasklist = GTD_TASK_LIST_EDS (data->list); + client = gtd_task_list_eds_get_client (tasklist); + + /* Create the new task */ + component = e_cal_component_new (); + e_cal_component_set_new_vtype (component, E_CAL_COMPONENT_TODO); + + new_summary = e_cal_component_text_new (data->title, NULL); + e_cal_component_set_summary (component, new_summary); + + if (data->due_date) + { + ECalComponentDateTime *comp_dt; + ICalTime *idt; + + idt = i_cal_time_new_null_time (); + i_cal_time_set_date (idt, + g_date_time_get_year (data->due_date), + g_date_time_get_month (data->due_date), + g_date_time_get_day_of_month (data->due_date)); + i_cal_time_set_time (idt, + g_date_time_get_hour (data->due_date), + g_date_time_get_minute (data->due_date), + g_date_time_get_seconds (data->due_date)); + i_cal_time_set_is_date (idt, + i_cal_time_get_hour (idt) == 0 && + i_cal_time_get_minute (idt) == 0 && + i_cal_time_get_second (idt) == 0); + + comp_dt = e_cal_component_datetime_new_take (idt, g_strdup ("UTC")); + e_cal_component_set_due (component, comp_dt); + e_cal_component_commit_sequence (component); + + e_cal_component_datetime_free (comp_dt); + } + + e_cal_client_create_object_sync (client, + e_cal_component_get_icalcomponent (component), + E_CAL_OPERATION_FLAG_NONE, + &new_uid, + cancellable, + &error); + + e_cal_component_text_free (new_summary); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + new_task = gtd_task_eds_new (component); + gtd_task_set_position (new_task, g_list_model_get_n_items (G_LIST_MODEL (tasklist))); + + /* + * In the case the task UID changes because of creation proccess, + * reapply it to the task. + */ + if (new_uid) + gtd_object_set_uid (GTD_OBJECT (new_task), new_uid); + + /* Effectively apply the updated component */ + gtd_task_eds_apply (GTD_TASK_EDS (new_task)); + + g_task_return_pointer (task, g_object_ref (new_task), g_object_unref); + + GTD_EXIT; +} + +static void +update_task_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (GError) error = NULL; + GtdTaskListEds *tasklist; + ECalClient *client; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + tasklist = GTD_TASK_LIST_EDS (gtd_task_get_list (data->task)); + client = gtd_task_list_eds_get_client (tasklist); + + e_cal_client_modify_object_sync (client, + e_cal_component_get_icalcomponent (data->component), + E_CAL_OBJ_MOD_THIS, + E_CAL_OPERATION_FLAG_NONE, + cancellable, + &error); + + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + +static void +remove_task_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (ECalComponentId) id = NULL; + g_autoptr (GError) error = NULL; + GtdTaskListEds *tasklist; + ECalClient *client; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + tasklist = GTD_TASK_LIST_EDS (gtd_task_get_list (data->task)); + client = gtd_task_list_eds_get_client (tasklist); + id = e_cal_component_get_id (data->component); + + e_cal_client_remove_object_sync (client, + e_cal_component_id_get_uid (id), + e_cal_component_id_get_rid (id), + E_CAL_OBJ_MOD_THIS, + E_CAL_OPERATION_FLAG_NONE, + cancellable, + &error); + + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + +static void +create_or_update_task_list_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GtdProviderEdsPrivate *priv; + g_autoptr (GError) error = NULL; + GtdProviderEds *self; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + self = GTD_PROVIDER_EDS (source_object); + priv = gtd_provider_eds_get_instance_private (self); + + e_source_registry_commit_source_sync (priv->source_registry, + data->source, + cancellable, + &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + + +static void +remove_task_list_in_thread_cb (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr (GError) error = NULL; + AsyncData *data; + + GTD_ENTRY; + + data = task_data; + + e_source_remove_sync (data->source, cancellable, &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + GTD_RETURN (); + } + + g_task_return_boolean (task, TRUE); + + GTD_EXIT; +} + + +/* + * GtdProvider iface + */ + +static const gchar* +gtd_provider_eds_get_id (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_id (GTD_PROVIDER_EDS (provider)); +} + +static const gchar* +gtd_provider_eds_get_name (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_name (GTD_PROVIDER_EDS (provider)); +} + +static const gchar* +gtd_provider_eds_get_provider_type (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_provider_type (GTD_PROVIDER_EDS (provider)); +} + +static const gchar* +gtd_provider_eds_get_description (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_description (GTD_PROVIDER_EDS (provider)); +} + + +static gboolean +gtd_provider_eds_get_enabled (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), FALSE); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_enabled (GTD_PROVIDER_EDS (provider)); +} + +static void +gtd_provider_eds_refresh (GtdProvider *provider) +{ + g_autoptr (GHashTable) collections = NULL; + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + GHashTableIter iter; + GtdTaskListEds *list; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_PROVIDER_EDS (provider)); + + self = GTD_PROVIDER_EDS (provider); + priv = gtd_provider_eds_get_instance_private (self); + collections = g_hash_table_new (g_direct_hash, g_direct_equal); + + g_hash_table_iter_init (&iter, priv->task_lists); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &list)) + { + g_autoptr (ESource) collection = NULL; + ESource *source; + + source = gtd_task_list_eds_get_source (list); + collection = e_source_registry_find_extension (priv->source_registry, + source, + E_SOURCE_EXTENSION_COLLECTION); + + if (!collection || g_hash_table_contains (collections, collection)) + continue; + + GTD_TRACE_MSG ("Refreshing collection %s", e_source_get_uid (collection)); + + e_source_registry_refresh_backend (priv->source_registry, + e_source_get_uid (collection), + priv->cancellable, + on_source_refreshed_cb, + g_object_ref (self)); + + g_hash_table_add (collections, collection); + } + + GTD_EXIT; +} + +static GIcon* +gtd_provider_eds_get_icon (GtdProvider *provider) +{ + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + return GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->get_icon (GTD_PROVIDER_EDS (provider)); +} + +static void +gtd_provider_eds_create_task (GtdProvider *provider, + GtdTaskList *list, + const gchar *title, + GDateTime *due_date, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + GtdProviderEds *self; + AsyncData *data; + + g_return_if_fail (GTD_IS_TASK_LIST_EDS (list)); + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + + data = g_new0 (AsyncData, 1); + data->list = g_object_ref (list); + data->title = g_strdup (title); + data->due_date = due_date ? g_date_time_ref (due_date) : NULL; + + gtd_object_push_loading (GTD_OBJECT (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gtd_provider_eds_create_task); + g_task_set_task_data (task, data, async_data_free); + g_task_run_in_thread (task, create_task_in_thread_cb); + + GTD_EXIT; +} + +static GtdTask* +gtd_provider_eds_create_task_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + g_autoptr (GtdTask) new_task = NULL; + GtdProviderEds *self; + GtdTaskList *list; + AsyncData *data; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + data = g_task_get_task_data (G_TASK (result)); + list = data->list; + + gtd_object_pop_loading (GTD_OBJECT (self)); + + new_task = g_task_propagate_pointer (G_TASK (result), error); + + if (new_task) + { + gtd_task_set_list (new_task, list); + gtd_task_list_add_task (list, new_task); + set_default_list (self, list); + } + + GTD_RETURN (new_task); +} + +static void +gtd_provider_eds_update_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + ECalComponent *component; + AsyncData *data; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_TASK (task)); + g_return_if_fail (GTD_IS_TASK_LIST_EDS (gtd_task_get_list (task))); + + component = gtd_task_eds_get_component (GTD_TASK_EDS (task)); + + e_cal_component_commit_sequence (component); + + /* The task is not ready until we finish the operation */ + gtd_object_push_loading (GTD_OBJECT (task)); + gtd_object_push_loading (GTD_OBJECT (provider)); + + data = g_new0 (AsyncData, 1); + data->task = g_object_ref (task); + data->component = e_cal_component_clone (component); + + gtask = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_provider_eds_update_task); + g_task_set_task_data (gtask, data, async_data_free); + g_task_run_in_thread (gtask, update_task_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_update_task_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GtdProviderEds *self; + AsyncData *data; + GtdTask *task; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + data = g_task_get_task_data (G_TASK (result)); + task = data->task; + + gtd_object_pop_loading (GTD_OBJECT (self)); + gtd_object_pop_loading (GTD_OBJECT (task)); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + { + gtd_task_eds_revert (GTD_TASK_EDS (task)); + GTD_RETURN (FALSE); + } + + gtd_task_eds_apply (GTD_TASK_EDS (task)); + gtd_task_list_update_task (gtd_task_get_list (task), task); + + GTD_RETURN (TRUE); +} + +static void +gtd_provider_eds_remove_task (GtdProvider *provider, + GtdTask *task, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + ECalComponent *component; + AsyncData *data; + + GTD_ENTRY; + + g_return_if_fail (GTD_IS_TASK (task)); + g_return_if_fail (GTD_IS_TASK_LIST_EDS (gtd_task_get_list (task))); + + component = gtd_task_eds_get_component (GTD_TASK_EDS (task)); + + gtd_object_push_loading (GTD_OBJECT (provider)); + + data = g_new0 (AsyncData, 1); + data->task = g_object_ref (task); + data->component = e_cal_component_clone (component); + + gtask = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_provider_eds_remove_task); + g_task_set_task_data (gtask, data, async_data_free); + g_task_run_in_thread (gtask, remove_task_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_remove_task_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GTD_ENTRY; + + gtd_object_pop_loading (GTD_OBJECT (provider)); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static void +gtd_provider_eds_create_task_list (GtdProvider *provider, + const gchar *name, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + GtdProviderEds *self; + AsyncData *data; + ESource *source; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + source = NULL; + + /* Create an ESource */ + if (!GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->create_source) { + g_debug ("Can't create task list: not supported by %s", G_OBJECT_TYPE_NAME (provider)); + return; + } + + source = GTD_PROVIDER_EDS_CLASS (G_OBJECT_GET_CLASS (provider))->create_source (self); + if (!source) { + g_debug ("Can't create task list: create_source() returned NULL"); + return; + } + + /* EDS properties */ + e_source_set_display_name (source, name); + + data = g_new0 (AsyncData, 1); + data->title = g_strdup (name); + data->source = g_object_ref (source); + + gtd_object_push_loading (GTD_OBJECT (provider)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, gtd_provider_eds_create_task_list); + g_task_set_task_data (task, data, async_data_free); + g_task_run_in_thread (task, create_or_update_task_list_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_create_task_list_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GtdProviderEds *self; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + gtd_object_pop_loading (GTD_OBJECT (self)); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static void +gtd_provider_eds_update_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + AsyncData *data; + ESource *source; + + GTD_ENTRY; + + g_assert (GTD_IS_TASK_LIST_EDS (list)); + g_assert (gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)) != NULL); + + source = gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)); + + gtd_object_push_loading (GTD_OBJECT (provider)); + gtd_object_push_loading (GTD_OBJECT (list)); + + data = g_new0 (AsyncData, 1); + data->list = g_object_ref (list); + data->source = g_object_ref (source); + + task = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (task, gtd_provider_eds_update_task_list); + g_task_set_task_data (task, data, async_data_free); + g_task_run_in_thread (task, create_or_update_task_list_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_update_task_list_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GtdProviderEds *self; + AsyncData *data; + + GTD_ENTRY; + + self = GTD_PROVIDER_EDS (provider); + data = g_task_get_task_data (G_TASK (result)); + + gtd_object_pop_loading (GTD_OBJECT (data->list)); + gtd_object_pop_loading (GTD_OBJECT (self)); + + g_signal_emit_by_name (self, "list-changed", data->list); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static void +gtd_provider_eds_remove_task_list (GtdProvider *provider, + GtdTaskList *list, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) gtask = NULL; + AsyncData *data; + ESource *source; + + GTD_ENTRY; + + g_assert (GTD_IS_TASK_LIST_EDS (list)); + g_assert (gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)) != NULL); + + source = gtd_task_list_eds_get_source (GTD_TASK_LIST_EDS (list)); + + gtd_object_push_loading (GTD_OBJECT (provider)); + + data = g_new0 (AsyncData, 1); + data->source = g_object_ref (source); + + gtask = g_task_new (provider, cancellable, callback, user_data); + g_task_set_source_tag (gtask, gtd_provider_eds_remove_task_list); + g_task_set_task_data (gtask, data, async_data_free); + g_task_run_in_thread (gtask, remove_task_list_in_thread_cb); + + GTD_EXIT; +} + +static gboolean +gtd_provider_eds_remove_task_list_finish (GtdProvider *provider, + GAsyncResult *result, + GError **error) +{ + GTD_ENTRY; + + gtd_object_pop_loading (GTD_OBJECT (provider)); + + GTD_RETURN (g_task_propagate_boolean (G_TASK (result), error)); +} + +static GList* +gtd_provider_eds_get_task_lists (GtdProvider *provider) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (GTD_PROVIDER_EDS (provider)); + + return g_hash_table_get_values (priv->task_lists); +} + +static GtdTaskList* +gtd_provider_eds_get_inbox (GtdProvider *provider) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (GTD_PROVIDER_EDS (provider)); + + return g_hash_table_lookup (priv->task_lists, GTD_PROVIDER_EDS_INBOX_ID); +} + +static void +gtd_provider_iface_init (GtdProviderInterface *iface) +{ + iface->get_id = gtd_provider_eds_get_id; + iface->get_name = gtd_provider_eds_get_name; + iface->get_provider_type = gtd_provider_eds_get_provider_type; + iface->get_description = gtd_provider_eds_get_description; + iface->get_enabled = gtd_provider_eds_get_enabled; + iface->refresh = gtd_provider_eds_refresh; + iface->get_icon = gtd_provider_eds_get_icon; + iface->create_task = gtd_provider_eds_create_task; + iface->create_task_finish = gtd_provider_eds_create_task_finish; + iface->update_task = gtd_provider_eds_update_task; + iface->update_task_finish = gtd_provider_eds_update_task_finish; + iface->remove_task = gtd_provider_eds_remove_task; + iface->remove_task_finish = gtd_provider_eds_remove_task_finish; + iface->create_task_list = gtd_provider_eds_create_task_list; + iface->create_task_list_finish = gtd_provider_eds_create_task_list_finish; + iface->update_task_list = gtd_provider_eds_update_task_list; + iface->update_task_list_finish = gtd_provider_eds_update_task_list_finish; + iface->remove_task_list = gtd_provider_eds_remove_task_list; + iface->remove_task_list_finish = gtd_provider_eds_remove_task_list_finish; + iface->get_task_lists = gtd_provider_eds_get_task_lists; + iface->get_inbox = gtd_provider_eds_get_inbox; +} + + +/* + * GObject overrides + */ + +static void +gtd_provider_eds_finalize (GObject *object) +{ + GtdProviderEds *self = (GtdProviderEds *)object; + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->cancellable); + g_clear_object (&priv->source_registry); + g_clear_pointer (&priv->task_lists, g_hash_table_destroy); + + G_OBJECT_CLASS (gtd_provider_eds_parent_class)->finalize (object); +} + +static void +gtd_provider_eds_constructed (GObject *object) +{ + GtdProviderEdsPrivate *priv; + GtdProviderEds *self; + g_autoptr (GError) error = NULL; + GList *sources; + GList *l; + + self = GTD_PROVIDER_EDS (object); + priv = gtd_provider_eds_get_instance_private (self); + + if (error) + { + g_warning ("%s: %s", "Error loading task manager", error->message); + return; + } + + /* Load task list sources */ + sources = e_source_registry_list_sources (priv->source_registry, E_SOURCE_EXTENSION_TASK_LIST); + + for (l = sources; l != NULL; l = l->next) + on_source_added_cb (self, l->data); + + g_list_free_full (sources, g_object_unref); + + /* listen to the signals, so new sources don't slip by */ + g_signal_connect_swapped (priv->source_registry, + "source-added", + G_CALLBACK (on_source_added_cb), + self); + + g_signal_connect_swapped (priv->source_registry, + "source-removed", + G_CALLBACK (on_source_removed_cb), + self); +} + +static void +gtd_provider_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdProvider *provider = GTD_PROVIDER (object); + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (GTD_PROVIDER_EDS (object)); + + + switch (prop_id) + { + case PROP_DESCRIPTION: + g_value_set_string (value, gtd_provider_eds_get_description (provider)); + break; + + case PROP_ENABLED: + g_value_set_boolean (value, gtd_provider_eds_get_enabled (provider)); + break; + + case PROP_ICON: + g_value_set_object (value, gtd_provider_eds_get_icon (provider)); + break; + + case PROP_ID: + g_value_set_string (value, gtd_provider_eds_get_id (provider)); + break; + + case PROP_NAME: + g_value_set_string (value, gtd_provider_eds_get_name (provider)); + break; + + case PROP_PROVIDER_TYPE: + g_value_set_string (value, gtd_provider_eds_get_provider_type (provider)); + break; + + case PROP_REGISTRY: + g_value_set_object (value, priv->source_registry); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_eds_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdProviderEds *self = GTD_PROVIDER_EDS (object); + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + + switch (prop_id) + { + case PROP_REGISTRY: + if (g_set_object (&priv->source_registry, g_value_get_object (value))) + g_object_notify (object, "registry"); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_eds_class_init (GtdProviderEdsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_provider_eds_finalize; + object_class->constructed = gtd_provider_eds_constructed; + object_class->get_property = gtd_provider_eds_get_property; + object_class->set_property = gtd_provider_eds_set_property; + + g_object_class_override_property (object_class, PROP_DESCRIPTION, "description"); + g_object_class_override_property (object_class, PROP_ENABLED, "enabled"); + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_ID, "id"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PROVIDER_TYPE, "provider-type"); + + g_object_class_install_property (object_class, + PROP_REGISTRY, + g_param_spec_object ("registry", + "Source registry", + "The EDS source registry object", + E_TYPE_SOURCE_REGISTRY, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} + +static void +gtd_provider_eds_init (GtdProviderEds *self) +{ + GtdProviderEdsPrivate *priv = gtd_provider_eds_get_instance_private (self); + + priv->cancellable = g_cancellable_new (); + priv->task_lists = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); +} + +GtdProviderEds* +gtd_provider_eds_new (ESourceRegistry *registry) +{ + return g_object_new (GTD_TYPE_PROVIDER_EDS, + "registry", registry, + NULL); +} + +ESourceRegistry* +gtd_provider_eds_get_registry (GtdProviderEds *provider) +{ + GtdProviderEdsPrivate *priv; + + g_return_val_if_fail (GTD_IS_PROVIDER_EDS (provider), NULL); + + priv = gtd_provider_eds_get_instance_private (provider); + + return priv->source_registry; +} diff --git a/src/plugins/eds/gtd-provider-eds.h b/src/plugins/eds/gtd-provider-eds.h new file mode 100644 index 0000000..5ffe4ff --- /dev/null +++ b/src/plugins/eds/gtd-provider-eds.h @@ -0,0 +1,61 @@ +/* gtd-provider-eds.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "endeavour.h" + +#include "gtd-eds.h" + +#include <glib.h> + +G_BEGIN_DECLS + +#define GTD_PROVIDER_EDS_INBOX_ID "system-task-list" + +#define GTD_TYPE_PROVIDER_EDS (gtd_provider_eds_get_type()) + +G_DECLARE_DERIVABLE_TYPE (GtdProviderEds, gtd_provider_eds, GTD, PROVIDER_EDS, GtdObject) + +struct _GtdProviderEdsClass +{ + GtdObjectClass parent; + + const gchar* (*get_id) (GtdProviderEds *self); + + const gchar* (*get_name) (GtdProviderEds *self); + + const gchar* (*get_provider_type) (GtdProviderEds *self); + + const gchar* (*get_description) (GtdProviderEds *self); + + gboolean (*get_enabled) (GtdProviderEds *self); + + GIcon* (*get_icon) (GtdProviderEds *self); + + ESource* (*create_source) (GtdProviderEds *self); + + gboolean (*should_load_source) (GtdProviderEds *provider, + ESource *source); +}; + +GtdProviderEds* gtd_provider_eds_new (ESourceRegistry *registry); + +ESourceRegistry* gtd_provider_eds_get_registry (GtdProviderEds *local); + +G_END_DECLS diff --git a/src/plugins/eds/gtd-provider-goa.c b/src/plugins/eds/gtd-provider-goa.c new file mode 100644 index 0000000..05cacd0 --- /dev/null +++ b/src/plugins/eds/gtd-provider-goa.c @@ -0,0 +1,262 @@ +/* gtd-provider-goa.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdProviderGoa" + +#include "gtd-eds-autoptr.h" +#include "gtd-provider-eds.h" +#include "gtd-provider-goa.h" + +#include <glib/gi18n.h> + +struct _GtdProviderGoa +{ + GtdProviderEds parent; + + GoaAccount *account; + GIcon *icon; + + gchar *id; +}; + +G_DEFINE_TYPE (GtdProviderGoa, gtd_provider_goa, GTD_TYPE_PROVIDER_EDS) + +enum +{ + PROP_0, + PROP_ACCOUNT, + N_PROPS +}; + + +/* + * GtdProviderEds overrides + */ + +static const gchar* +gtd_provider_goa_get_id (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return self->id; +} + +static const gchar* +gtd_provider_goa_get_name (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return goa_account_get_provider_name (self->account); +} + +static const gchar* +gtd_provider_goa_get_provider_type (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return goa_account_get_provider_type (self->account); +} + +static const gchar* +gtd_provider_goa_get_description (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return goa_account_get_identity (self->account); +} + +static gboolean +gtd_provider_goa_get_enabled (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return !goa_account_get_calendar_disabled (self->account); +} + +static GIcon* +gtd_provider_goa_get_icon (GtdProviderEds *provider) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (provider); + + return self->icon; +} + +static void +gtd_provider_goa_set_account (GtdProviderGoa *provider, + GoaAccount *account) +{ + g_autofree gchar *icon_name = NULL; + + if (provider->account == account) + return; + + g_set_object (&provider->account, account); + g_object_notify (G_OBJECT (provider), "account"); + + g_debug ("Setting up Online Account: %s (%s)", + goa_account_get_identity (account), + goa_account_get_id (account)); + + /* Update icon */ + icon_name = g_strdup_printf ("goa-account-%s", goa_account_get_provider_type (provider->account)); + g_set_object (&provider->icon, g_themed_icon_new (icon_name)); + g_object_notify (G_OBJECT (provider), "icon"); + + /* Provider id */ + provider->id = g_strdup_printf ("%s@%s", + goa_account_get_provider_type (provider->account), + goa_account_get_id (provider->account)); +} + + +/* + * GObject overrides + */ + +static void +gtd_provider_goa_finalize (GObject *object) +{ + GtdProviderGoa *self = (GtdProviderGoa *)object; + + g_clear_pointer (&self->id, g_free); + + g_clear_object (&self->account); + g_clear_object (&self->icon); + + G_OBJECT_CLASS (gtd_provider_goa_parent_class)->finalize (object); +} + +static void +gtd_provider_goa_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (object); + + switch (prop_id) + { + + case PROP_ACCOUNT: + g_value_set_object (value, self->account); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_provider_goa_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdProviderGoa *self = GTD_PROVIDER_GOA (object); + + switch (prop_id) + { + case PROP_ACCOUNT: + gtd_provider_goa_set_account (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +gtd_provider_goa_should_load_source (GtdProviderEds *provider, + ESource *source) +{ + g_autoptr (ESource) ancestor = NULL; + GtdProviderGoa *self; + gboolean retval; + + self = GTD_PROVIDER_GOA (provider); + retval = FALSE; + + ancestor = e_source_registry_find_extension (gtd_provider_eds_get_registry (provider), + source, + E_SOURCE_EXTENSION_GOA); + + /* If we detect that the given source is provided by a GOA account, check the account id */ + if (ancestor) + { + ESourceExtension *extension; + const gchar *ancestor_id; + const gchar *account_id; + + extension = e_source_get_extension (ancestor, E_SOURCE_EXTENSION_GOA); + ancestor_id = e_source_goa_get_account_id (E_SOURCE_GOA (extension)); + account_id = goa_account_get_id (self->account); + + /* When the ancestor's GOA id matches the current account's id, we shall load this list */ + retval = g_strcmp0 (ancestor_id, account_id) == 0; + } + + return retval; +} + +static void +gtd_provider_goa_class_init (GtdProviderGoaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtdProviderEdsClass *eds_class = GTD_PROVIDER_EDS_CLASS (klass); + + eds_class->get_id = gtd_provider_goa_get_id; + eds_class->get_name = gtd_provider_goa_get_name; + eds_class->get_provider_type = gtd_provider_goa_get_provider_type; + eds_class->get_description = gtd_provider_goa_get_description; + eds_class->get_enabled = gtd_provider_goa_get_enabled; + eds_class->get_icon = gtd_provider_goa_get_icon; + eds_class->should_load_source = gtd_provider_goa_should_load_source; + + object_class->finalize = gtd_provider_goa_finalize; + object_class->get_property = gtd_provider_goa_get_property; + object_class->set_property = gtd_provider_goa_set_property; + + g_object_class_install_property (object_class, + PROP_ACCOUNT, + g_param_spec_object ("account", + "Account of the provider", + "The Online Account of the provider", + GOA_TYPE_ACCOUNT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +gtd_provider_goa_init (GtdProviderGoa *self) +{ +} + +GtdProviderGoa* +gtd_provider_goa_new (ESourceRegistry *registry, + GoaAccount *account) +{ + return g_object_new (GTD_TYPE_PROVIDER_GOA, + "account", account, + "registry", registry, + NULL); +} + +GoaAccount* +gtd_provider_goa_get_account (GtdProviderGoa *provider) +{ + return provider->account; +} diff --git a/src/plugins/eds/gtd-provider-goa.h b/src/plugins/eds/gtd-provider-goa.h new file mode 100644 index 0000000..42625cd --- /dev/null +++ b/src/plugins/eds/gtd-provider-goa.h @@ -0,0 +1,43 @@ +/* gtd-provider-goa.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_PROVIDER_GOA_H +#define GTD_PROVIDER_GOA_H + +#define GOA_API_IS_SUBJECT_TO_CHANGE 1 + +#include "endeavour.h" +#include "gtd-provider-eds.h" + +#include <glib.h> +#include <goa/goa.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_GOA (gtd_provider_goa_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderGoa, gtd_provider_goa, GTD, PROVIDER_GOA, GtdProviderEds) + +GtdProviderGoa* gtd_provider_goa_new (ESourceRegistry *registry, + GoaAccount *account); + +GoaAccount* gtd_provider_goa_get_account (GtdProviderGoa *provider); + +G_END_DECLS + +#endif /* GTD_PROVIDER_GOA_H */ diff --git a/src/plugins/eds/gtd-provider-local.c b/src/plugins/eds/gtd-provider-local.c new file mode 100644 index 0000000..c5651ab --- /dev/null +++ b/src/plugins/eds/gtd-provider-local.c @@ -0,0 +1,150 @@ +/* gtd-provider-local.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdProviderLocal" + +#include "gtd-provider-local.h" +#include "gtd-task-list-eds.h" + +#include <glib/gi18n.h> + +struct _GtdProviderLocal +{ + GtdProviderEds parent; + + GIcon *icon; + GList *tasklists; +}; + +G_DEFINE_TYPE (GtdProviderLocal, gtd_provider_local, GTD_TYPE_PROVIDER_EDS) + + +/* + * GtdProviderEds overrides + */ + +static const gchar* +gtd_provider_local_get_id (GtdProviderEds *provider) +{ + return "local"; +} + +static const gchar* +gtd_provider_local_get_name (GtdProviderEds *provider) +{ + return _("On This Computer"); +} + +static const gchar* +gtd_provider_local_get_provider_type (GtdProviderEds *provider) +{ + return "local"; +} + +static const gchar* +gtd_provider_local_get_description (GtdProviderEds *provider) +{ + return _("Local"); +} + +static gboolean +gtd_provider_local_get_enabled (GtdProviderEds *provider) +{ + return TRUE; +} + +static GIcon* +gtd_provider_local_get_icon (GtdProviderEds *provider) +{ + GtdProviderLocal *self = GTD_PROVIDER_LOCAL (provider); + + return self->icon; +} + +static ESource* +gtd_provider_local_create_source (GtdProviderEds *provider) +{ + ESourceExtension *extension; + ESource *source; + + /* Create the source */ + source = e_source_new (NULL, NULL, NULL); + + if (!source) + return NULL; + + /* Make it a local source */ + extension = e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST); + + e_source_set_parent (source, "local-stub"); + e_source_backend_set_backend_name (E_SOURCE_BACKEND (extension), "local"); + + return source; +} + +static void +gtd_provider_local_finalize (GObject *object) +{ + GtdProviderLocal *self = (GtdProviderLocal *)object; + + g_clear_object (&self->icon); + + G_OBJECT_CLASS (gtd_provider_local_parent_class)->finalize (object); +} + +static gboolean +gtd_provider_local_should_load_source (GtdProviderEds *provider, + ESource *source) +{ + if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + return g_strcmp0 (e_source_get_parent (source), "local-stub") == 0; + + return FALSE; +} + +static void +gtd_provider_local_class_init (GtdProviderLocalClass *klass) +{ + GtdProviderEdsClass *eds_class = GTD_PROVIDER_EDS_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + eds_class->get_id = gtd_provider_local_get_id; + eds_class->get_name = gtd_provider_local_get_name; + eds_class->get_provider_type = gtd_provider_local_get_provider_type; + eds_class->get_description = gtd_provider_local_get_description; + eds_class->get_enabled = gtd_provider_local_get_enabled; + eds_class->get_icon = gtd_provider_local_get_icon; + eds_class->create_source = gtd_provider_local_create_source; + eds_class->should_load_source = gtd_provider_local_should_load_source; + + object_class->finalize = gtd_provider_local_finalize; +} + +static void +gtd_provider_local_init (GtdProviderLocal *self) +{ + self->icon = G_ICON (g_themed_icon_new_with_default_fallbacks ("computer-symbolic")); +} + +GtdProviderLocal* +gtd_provider_local_new (ESourceRegistry *registry) +{ + return g_object_new (GTD_TYPE_PROVIDER_LOCAL, + "registry", registry, + NULL); +} diff --git a/src/plugins/eds/gtd-provider-local.h b/src/plugins/eds/gtd-provider-local.h new file mode 100644 index 0000000..90ff8a6 --- /dev/null +++ b/src/plugins/eds/gtd-provider-local.h @@ -0,0 +1,37 @@ +/* gtd-provider-local.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_PROVIDER_LOCAL_H +#define GTD_PROVIDER_LOCAL_H + +#include "endeavour.h" +#include "gtd-provider-eds.h" + +#include <glib.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_PROVIDER_LOCAL (gtd_provider_local_get_type()) + +G_DECLARE_FINAL_TYPE (GtdProviderLocal, gtd_provider_local, GTD, PROVIDER_LOCAL, GtdProviderEds) + +GtdProviderLocal* gtd_provider_local_new (ESourceRegistry *source_registry); + +G_END_DECLS + +#endif /* GTD_PROVIDER_LOCAL_H */ diff --git a/src/plugins/eds/gtd-task-eds.c b/src/plugins/eds/gtd-task-eds.c new file mode 100644 index 0000000..5dc667f --- /dev/null +++ b/src/plugins/eds/gtd-task-eds.c @@ -0,0 +1,650 @@ +/* gtd-task-eds.c + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdTaskEds" + +#include "gtd-eds-autoptr.h" +#include "gtd-task-eds.h" + +#define ICAL_X_ENDEAVOUR_POSITION "X-ENDEAVOUR-POSITION" + +struct _GtdTaskEds +{ + GtdTask parent; + + ECalComponent *component; + ECalComponent *new_component; + + gchar *description; +}; + +G_DEFINE_TYPE (GtdTaskEds, gtd_task_eds, GTD_TYPE_TASK) + +enum +{ + PROP_0, + PROP_COMPONENT, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static GDateTime* +convert_icaltime (const ICalTime *date) +{ + GDateTime *dt; + + if (!date) + return NULL; + + dt = g_date_time_new_utc (i_cal_time_get_year (date), + i_cal_time_get_month (date), + i_cal_time_get_day (date), + i_cal_time_is_date (date) ? 0 : i_cal_time_get_hour (date), + i_cal_time_is_date (date) ? 0 : i_cal_time_get_minute (date), + i_cal_time_is_date (date) ? 0 : i_cal_time_get_second (date)); + + return dt; +} + +static void +set_description (GtdTaskEds *self, + const gchar *description) +{ + ECalComponentText *text; + GSList note; + + text = e_cal_component_text_new (description ? description : "", NULL); + + note.data = text; + note.next = NULL; + + g_clear_pointer (&self->description, g_free); + self->description = g_strdup (description); + + e_cal_component_set_descriptions (self->new_component, (description && *description) ? ¬e : NULL); + + e_cal_component_text_free (text); +} + +static void +setup_description (GtdTaskEds *self) +{ + g_autofree gchar *desc = NULL; + GSList *text_list; + GSList *l; + + /* concatenates the multiple descriptions a task may have */ + text_list = e_cal_component_get_descriptions (self->new_component); + + for (l = text_list; l != NULL; l = l->next) + { + if (l->data != NULL) + { + ECalComponentText *text; + gchar *carrier; + + text = l->data; + + if (desc) + { + carrier = g_strconcat (desc, + "\n", + e_cal_component_text_get_value (text), + NULL); + g_free (desc); + desc = carrier; + } + else + { + desc = g_strdup (e_cal_component_text_get_value (text)); + } + } + } + + set_description (self, desc); + + g_slist_free_full (text_list, e_cal_component_text_free); +} + + +/* + * GtdObject overrides + */ + +static const gchar* +gtd_task_eds_get_uid (GtdObject *object) +{ + GtdTaskEds *self; + const gchar *uid; + + g_return_val_if_fail (GTD_IS_TASK (object), NULL); + + self = GTD_TASK_EDS (object); + + if (self->new_component) + uid = e_cal_component_get_uid (self->new_component); + else + uid = NULL; + + return uid; +} + +static void +gtd_task_eds_set_uid (GtdObject *object, + const gchar *uid) +{ + GtdTaskEds *self; + const gchar *current_uid; + + g_return_if_fail (GTD_IS_TASK (object)); + + self = GTD_TASK_EDS (object); + + if (!self->new_component) + return; + + current_uid = e_cal_component_get_uid (self->new_component); + + if (g_strcmp0 (current_uid, uid) != 0) + { + e_cal_component_set_uid (self->new_component, uid); + + g_object_notify (G_OBJECT (object), "uid"); + } +} + + +/* + * GtdTask overrides + */ + +static GDateTime* +gtd_task_eds_get_completion_date (GtdTask *task) +{ + ICalTime *idt; + GtdTaskEds *self; + GDateTime *dt; + + self = GTD_TASK_EDS (task); + dt = NULL; + + idt = e_cal_component_get_completed (self->new_component); + + if (idt) + dt = convert_icaltime (idt); + + g_clear_object (&idt); + + return dt; +} + +static void +gtd_task_eds_set_completion_date (GtdTask *task, + GDateTime *dt) +{ + GtdTaskEds *self; + ICalTime *idt; + + self = GTD_TASK_EDS (task); + + idt = i_cal_time_new_null_time (); + i_cal_time_set_date (idt, + g_date_time_get_year (dt), + g_date_time_get_month (dt), + g_date_time_get_day_of_month (dt)); + i_cal_time_set_time (idt, + g_date_time_get_hour (dt), + g_date_time_get_minute (dt), + g_date_time_get_seconds (dt)); + i_cal_time_set_timezone (idt, i_cal_timezone_get_utc_timezone ()); + + /* convert timezone + * + * FIXME: This does not do anything until we have an ical + * timezone associated with the task + */ + i_cal_time_convert_timezone (idt, NULL, i_cal_timezone_get_utc_timezone ()); + + e_cal_component_set_completed (self->new_component, idt); + + g_object_unref (idt); +} + +static gboolean +gtd_task_eds_get_complete (GtdTask *task) +{ + ICalPropertyStatus status; + GtdTaskEds *self; + gboolean completed; + + g_return_val_if_fail (GTD_IS_TASK_EDS (task), FALSE); + + self = GTD_TASK_EDS (task); + + status = e_cal_component_get_status (self->new_component); + completed = status == I_CAL_STATUS_COMPLETED; + + return completed; +} + +static void +gtd_task_eds_set_complete (GtdTask *task, + gboolean complete) +{ + ICalPropertyStatus status; + GtdTaskEds *self; + gint percent; + g_autoptr (GDateTime) now = NULL; + + self = GTD_TASK_EDS (task); + now = complete ? g_date_time_new_now_local () : NULL; + + if (complete) + { + percent = 100; + status = I_CAL_STATUS_COMPLETED; + } + else + { + percent = 0; + status = I_CAL_STATUS_NEEDSACTION; + } + + e_cal_component_set_percent_complete (self->new_component, percent); + e_cal_component_set_status (self->new_component, status); + gtd_task_eds_set_completion_date (task, now); +} + +static GDateTime* +gtd_task_eds_get_creation_date (GtdTask *task) +{ + ICalTime *idt; + GtdTaskEds *self; + GDateTime *dt; + + self = GTD_TASK_EDS (task); + dt = NULL; + + idt = e_cal_component_get_created (self->new_component); + + if (idt) + dt = convert_icaltime (idt); + + g_clear_object (&idt); + + return dt; +} + +static void +gtd_task_eds_set_creation_date (GtdTask *task, + GDateTime *dt) +{ + g_assert_not_reached (); +} + +static const gchar* +gtd_task_eds_get_description (GtdTask *task) +{ + GtdTaskEds *self = GTD_TASK_EDS (task); + + return self->description ? self->description : ""; +} + +static void +gtd_task_eds_set_description (GtdTask *task, + const gchar *description) +{ + set_description (GTD_TASK_EDS (task), description); +} + +static GDateTime* +gtd_task_eds_get_due_date (GtdTask *task) +{ + ECalComponentDateTime *comp_dt; + GtdTaskEds *self; + GDateTime *date; + + g_return_val_if_fail (GTD_IS_TASK_EDS (task), NULL); + + self = GTD_TASK_EDS (task); + + comp_dt = e_cal_component_get_due (self->new_component); + if (!comp_dt) + return NULL; + + date = convert_icaltime (e_cal_component_datetime_get_value (comp_dt)); + e_cal_component_datetime_free (comp_dt); + + return date; +} + +static void +gtd_task_eds_set_due_date (GtdTask *task, + GDateTime *dt) +{ + GtdTaskEds *self; + GDateTime *current_dt; + + g_assert (GTD_IS_TASK_EDS (task)); + + self = GTD_TASK_EDS (task); + + current_dt = gtd_task_get_due_date (task); + + if (dt != current_dt) + { + ECalComponentDateTime *comp_dt; + ICalTime *idt; + + comp_dt = NULL; + idt = NULL; + + if (!current_dt || + (current_dt && + dt && + g_date_time_compare (current_dt, dt) != 0)) + { + idt = i_cal_time_new_null_time (); + + g_date_time_ref (dt); + + /* Copy the given dt */ + i_cal_time_set_date (idt, + g_date_time_get_year (dt), + g_date_time_get_month (dt), + g_date_time_get_day_of_month (dt)); + i_cal_time_set_time (idt, + g_date_time_get_hour (dt), + g_date_time_get_minute (dt), + g_date_time_get_seconds (dt)); + i_cal_time_set_is_date (idt, + i_cal_time_get_hour (idt) == 0 && + i_cal_time_get_minute (idt) == 0 && + i_cal_time_get_second (idt) == 0); + + comp_dt = e_cal_component_datetime_new_take (idt, g_strdup ("UTC")); + + e_cal_component_set_due (self->new_component, comp_dt); + + e_cal_component_datetime_free (comp_dt); + + g_date_time_unref (dt); + } + else if (!dt) + { + e_cal_component_set_due (self->new_component, NULL); + } + } + + g_clear_pointer (¤t_dt, g_date_time_unref); +} + +static gboolean +gtd_task_eds_get_important (GtdTask *task) +{ + GtdTaskEds *self = GTD_TASK_EDS (task); + + return e_cal_component_get_priority (self->new_component) > 0; +} + +static void +gtd_task_eds_set_important (GtdTask *task, + gboolean important) +{ + GtdTaskEds *self = GTD_TASK_EDS (task); + + e_cal_component_set_priority (self->new_component, important ? 3 : -1); +} + +static gint64 +gtd_task_eds_get_position (GtdTask *task) +{ + g_autofree gchar *value = NULL; + ICalComponent *ical_comp; + gint64 position = -1; + GtdTaskEds *self; + + self = GTD_TASK_EDS (task); + ical_comp = e_cal_component_get_icalcomponent (self->new_component); + + value = e_cal_util_component_dup_x_property (ical_comp, ICAL_X_ENDEAVOUR_POSITION); + if (value) + position = g_ascii_strtoll (value, NULL, 10); + + return position; +} + +void +gtd_task_eds_set_position (GtdTask *task, + gint64 position) +{ + g_autofree gchar *value = NULL; + ICalComponent *ical_comp; + GtdTaskEds *self; + + self = GTD_TASK_EDS (task); + if (position != -1) + value = g_strdup_printf ("%" G_GINT64_FORMAT, position); + ical_comp = e_cal_component_get_icalcomponent (self->new_component); + + e_cal_util_component_set_x_property (ical_comp, ICAL_X_ENDEAVOUR_POSITION, value); +} + +static const gchar* +gtd_task_eds_get_title (GtdTask *task) +{ + GtdTaskEds *self; + + g_return_val_if_fail (GTD_IS_TASK_EDS (task), NULL); + + self = GTD_TASK_EDS (task); + + return i_cal_component_get_summary (e_cal_component_get_icalcomponent (self->new_component)); +} + +static void +gtd_task_eds_set_title (GtdTask *task, + const gchar *title) +{ + ECalComponentText *new_summary; + GtdTaskEds *self; + + g_return_if_fail (GTD_IS_TASK_EDS (task)); + g_return_if_fail (g_utf8_validate (title, -1, NULL)); + + self = GTD_TASK_EDS (task); + + new_summary = e_cal_component_text_new (title, NULL); + + e_cal_component_set_summary (self->new_component, new_summary); + + e_cal_component_text_free (new_summary); +} + + +/* + * GObject overrides + */ + +static void +gtd_task_eds_finalize (GObject *object) +{ + GtdTaskEds *self = (GtdTaskEds *)object; + + g_clear_object (&self->component); + g_clear_object (&self->new_component); + + G_OBJECT_CLASS (gtd_task_eds_parent_class)->finalize (object); +} + +static void +gtd_task_eds_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdTaskEds *self = GTD_TASK_EDS (object); + + switch (prop_id) + { + case PROP_COMPONENT: + g_value_set_object (value, self->component); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_eds_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdTaskEds *self = GTD_TASK_EDS (object); + + switch (prop_id) + { + case PROP_COMPONENT: + gtd_task_eds_set_component (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_eds_class_init (GtdTaskEdsClass *klass) +{ + GtdObjectClass *gtd_object_class = GTD_OBJECT_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtdTaskClass *task_class = GTD_TASK_CLASS (klass); + + object_class->finalize = gtd_task_eds_finalize; + object_class->get_property = gtd_task_eds_get_property; + object_class->set_property = gtd_task_eds_set_property; + + task_class->get_complete = gtd_task_eds_get_complete; + task_class->set_complete = gtd_task_eds_set_complete; + task_class->get_creation_date = gtd_task_eds_get_creation_date; + task_class->set_creation_date = gtd_task_eds_set_creation_date; + task_class->get_completion_date = gtd_task_eds_get_completion_date; + task_class->set_completion_date = gtd_task_eds_set_completion_date; + task_class->get_description = gtd_task_eds_get_description; + task_class->set_description = gtd_task_eds_set_description; + task_class->get_due_date = gtd_task_eds_get_due_date; + task_class->set_due_date = gtd_task_eds_set_due_date; + task_class->get_important = gtd_task_eds_get_important; + task_class->set_important = gtd_task_eds_set_important; + task_class->get_position = gtd_task_eds_get_position; + task_class->set_position = gtd_task_eds_set_position; + task_class->get_title = gtd_task_eds_get_title; + task_class->set_title = gtd_task_eds_set_title; + + gtd_object_class->get_uid = gtd_task_eds_get_uid; + gtd_object_class->set_uid = gtd_task_eds_set_uid; + + properties[PROP_COMPONENT] = g_param_spec_object ("component", + "Component", + "Component", + E_TYPE_CAL_COMPONENT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gtd_task_eds_init (GtdTaskEds *self) +{ +} + +GtdTask* +gtd_task_eds_new (ECalComponent *component) +{ + return g_object_new (GTD_TYPE_TASK_EDS, + "component", component, + NULL); +} + +ECalComponent* +gtd_task_eds_get_component (GtdTaskEds *self) +{ + g_return_val_if_fail (GTD_IS_TASK_EDS (self), NULL); + + return self->new_component; +} + +void +gtd_task_eds_set_component (GtdTaskEds *self, + ECalComponent *component) +{ + GObject *object; + + g_return_if_fail (GTD_IS_TASK_EDS (self)); + g_return_if_fail (E_IS_CAL_COMPONENT (component)); + + if (!g_set_object (&self->component, component)) + return; + + object = G_OBJECT (self); + + g_clear_object (&self->new_component); + self->new_component = e_cal_component_clone (component); + + setup_description (self); + + g_object_notify (object, "complete"); + g_object_notify (object, "creation-date"); + g_object_notify (object, "description"); + g_object_notify (object, "due-date"); + g_object_notify (object, "important"); + g_object_notify (object, "position"); + g_object_notify (object, "title"); + g_object_notify_by_pspec (object, properties[PROP_COMPONENT]); +} + +void +gtd_task_eds_apply (GtdTaskEds *self) +{ + g_return_if_fail (GTD_IS_TASK_EDS (self)); + + e_cal_component_commit_sequence (self->new_component); + + /* Make new_component the actual component */ + gtd_task_eds_set_component (self, self->new_component); +} + +void +gtd_task_eds_revert (GtdTaskEds *self) +{ + g_autoptr (ECalComponent) component = NULL; + + g_return_if_fail (GTD_IS_TASK_EDS (self)); + + component = e_cal_component_clone (self->component); + + gtd_task_eds_set_component (self, component); +} diff --git a/src/plugins/eds/gtd-task-eds.h b/src/plugins/eds/gtd-task-eds.h new file mode 100644 index 0000000..078f585 --- /dev/null +++ b/src/plugins/eds/gtd-task-eds.h @@ -0,0 +1,46 @@ +/* gtd-task-eds.h + * + * Copyright (C) 2017-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_TASK_EDS_H +#define GTD_TASK_EDS_H + +#include "endeavour.h" + +#include "gtd-eds.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_EDS (gtd_task_eds_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskEds, gtd_task_eds, GTD, TASK_EDS, GtdTask) + +GtdTask* gtd_task_eds_new (ECalComponent *component); + +ECalComponent* gtd_task_eds_get_component (GtdTaskEds *self); + +void gtd_task_eds_set_component (GtdTaskEds *self, + ECalComponent *component); + +void gtd_task_eds_apply (GtdTaskEds *self); + +void gtd_task_eds_revert (GtdTaskEds *self); + +G_END_DECLS + +#endif /* GTD_TASK_EDS_H */ + diff --git a/src/plugins/eds/gtd-task-list-eds.c b/src/plugins/eds/gtd-task-list-eds.c new file mode 100644 index 0000000..19bcdab --- /dev/null +++ b/src/plugins/eds/gtd-task-list-eds.c @@ -0,0 +1,875 @@ +/* gtd-task-list-eds.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <glib/gi18n.h> + +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; +} diff --git a/src/plugins/eds/gtd-task-list-eds.h b/src/plugins/eds/gtd-task-list-eds.h new file mode 100644 index 0000000..5a71295 --- /dev/null +++ b/src/plugins/eds/gtd-task-list-eds.h @@ -0,0 +1,53 @@ +/* gtd-task-list-eds.h + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_TASK_LIST_EDS_H +#define GTD_TASK_LIST_EDS_H + +#include "endeavour.h" + +#include "gtd-eds.h" + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST_EDS (gtd_task_list_eds_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListEds, gtd_task_list_eds, GTD, TASK_LIST_EDS, GtdTaskList) + +void gtd_task_list_eds_new (GtdProvider *provider, + ESource *source, + ECalClient *client, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data); + +GtdTaskListEds* gtd_task_list_eds_new_finish (GAsyncResult *result, + GError **error); + +ESource* gtd_task_list_eds_get_source (GtdTaskListEds *list); + +void gtd_task_list_eds_set_source (GtdTaskListEds *list, + ESource *source); + +ECalClient* gtd_task_list_eds_get_client (GtdTaskListEds *self); + +G_END_DECLS + +#endif /* GTD_TASK_LIST_EDS_H */ diff --git a/src/plugins/eds/meson.build b/src/plugins/eds/meson.build new file mode 100644 index 0000000..50705a3 --- /dev/null +++ b/src/plugins/eds/meson.build @@ -0,0 +1,27 @@ +plugins_ldflags += ['-Wl,--undefined=gtd_plugin_eds_register_types'] + +################ +# Dependencies # +################ + +plugins_deps += [ + dependency('libecal-2.0', version: '>= 3.33.2'), + dependency('libedataserver-1.2', version: '>= 3.32.0'), +] + +plugins_sources += files( + 'e-source-endeavour.c', + 'gtd-plugin-eds.c', + 'gtd-provider-eds.c', + 'gtd-provider-goa.c', + 'gtd-provider-local.c', + 'gtd-task-eds.c', + 'gtd-task-list-eds.c', + 'eds-plugin.c', +) + +plugins_sources += gnome.compile_resources( + 'eds-resources', + 'eds.gresource.xml', + c_name: 'eds_plugin', +) diff --git a/src/plugins/inbox-panel/gtd-inbox-panel.c b/src/plugins/inbox-panel/gtd-inbox-panel.c new file mode 100644 index 0000000..3b1bc9a --- /dev/null +++ b/src/plugins/inbox-panel/gtd-inbox-panel.c @@ -0,0 +1,275 @@ +/* gtd-inbox-panel.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdInboxPanel" + +#include "gtd-inbox-panel.h" + +#include "endeavour.h" + +#include <glib/gi18n.h> +#include <math.h> + + +#define GTD_INBOX_PANEL_NAME "inbox-panel" +#define GTD_INBOX_PANEL_PRIORITY 2000 + +struct _GtdInboxPanel +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdInboxPanel, gtd_inbox_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + GtdTask *task; + + task = (GtdTask*) item; + + /* + * The Inbox task list is not explicitly declared here because it's included + * in the filter already and should be filtered out in other lists + */ + return !gtd_task_get_complete (task); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdInboxPanel *self) +{ + guint n_items = g_list_model_get_n_items (model); + + if (self->number_of_tasks == n_items) + return; + + self->number_of_tasks = n_items; + g_object_notify (G_OBJECT (self), "subtitle"); +} + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_inbox_get_panel_name (GtdPanel *panel) +{ + return GTD_INBOX_PANEL_NAME; +} + +static const gchar* +gtd_panel_inbox_get_panel_title (GtdPanel *panel) +{ + return _("Inbox"); +} + +static GList* +gtd_panel_inbox_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_inbox_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_inbox_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_INBOX_PANEL (panel)->icon); +} + +static guint32 +gtd_panel_inbox_get_priority (GtdPanel *panel) +{ + return GTD_INBOX_PANEL_PRIORITY; +} + +static gchar* +gtd_panel_inbox_get_subtitle (GtdPanel *panel) +{ + GtdInboxPanel *self = GTD_INBOX_PANEL (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_inbox_get_panel_name; + iface->get_panel_title = gtd_panel_inbox_get_panel_title; + iface->get_header_widgets = gtd_panel_inbox_get_header_widgets; + iface->get_menu = gtd_panel_inbox_get_menu; + iface->get_icon = gtd_panel_inbox_get_icon; + iface->get_priority = gtd_panel_inbox_get_priority; + iface->get_subtitle = gtd_panel_inbox_get_subtitle; +} + + +/* + * GObject overrides + */ + +static void +gtd_inbox_panel_finalize (GObject *object) +{ + GtdInboxPanel *self = (GtdInboxPanel *)object; + + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + + G_OBJECT_CLASS (gtd_inbox_panel_parent_class)->finalize (object); +} + +static void +gtd_inbox_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdInboxPanel *self = GTD_INBOX_PANEL (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_INBOX_PANEL_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_INBOX_PANEL_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_inbox_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_inbox_panel_class_init (GtdInboxPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_inbox_panel_finalize; + object_class->get_property = gtd_inbox_panel_get_property; + object_class->set_property = gtd_inbox_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_inbox_panel_init (GtdInboxPanel *self) +{ + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *filter; + + self->icon = g_themed_icon_new ("mail-inbox-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (GTD_TASK_LIST_VIEW (self->view), G_LIST_MODEL (self->filter_model)); + gtd_task_list_view_set_show_list_name (GTD_TASK_LIST_VIEW (self->view), FALSE); + gtd_task_list_view_set_show_due_date (GTD_TASK_LIST_VIEW (self->view), FALSE); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + g_signal_connect_object (self->filter_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); +} + +GtkWidget* +gtd_inbox_panel_new (void) +{ + return g_object_new (GTD_TYPE_INBOX_PANEL, NULL); +} + diff --git a/src/plugins/inbox-panel/gtd-inbox-panel.h b/src/plugins/inbox-panel/gtd-inbox-panel.h new file mode 100644 index 0000000..779e6f2 --- /dev/null +++ b/src/plugins/inbox-panel/gtd-inbox-panel.h @@ -0,0 +1,32 @@ +/* gtd-inbox-panel.h + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_INBOX_PANEL (gtd_inbox_panel_get_type()) +G_DECLARE_FINAL_TYPE (GtdInboxPanel, gtd_inbox_panel, GTD, INBOX_PANEL, GtkBox) + +GtkWidget* gtd_inbox_panel_new (void); + +G_END_DECLS diff --git a/src/plugins/inbox-panel/inbox-panel-plugin.c b/src/plugins/inbox-panel/inbox-panel-plugin.c new file mode 100644 index 0000000..b5a180c --- /dev/null +++ b/src/plugins/inbox-panel/inbox-panel-plugin.c @@ -0,0 +1,32 @@ +/* gtd-plugin-inbox-panel.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdPluginInboxPanel" + +#include "endeavour.h" +#include "gtd-inbox-panel.h" + +G_MODULE_EXPORT void +inbox_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_INBOX_PANEL); +} diff --git a/src/plugins/inbox-panel/inbox-panel.gresource.xml b/src/plugins/inbox-panel/inbox-panel.gresource.xml new file mode 100644 index 0000000..b8e9454 --- /dev/null +++ b/src/plugins/inbox-panel/inbox-panel.gresource.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/inbox-panel"> + <file>inbox-panel.plugin</file> + </gresource> +</gresources> diff --git a/src/plugins/inbox-panel/inbox-panel.plugin b/src/plugins/inbox-panel/inbox-panel.plugin new file mode 100644 index 0000000..1ee0114 --- /dev/null +++ b/src/plugins/inbox-panel/inbox-panel.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Inbox +Module = inbox-panel +Description = A panel to show tasks in the inbox +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Hidden = true +Builtin = true +License = GPL +Loader = C +Embedded = inbox_panel_plugin_register_types +Depends = diff --git a/src/plugins/inbox-panel/meson.build b/src/plugins/inbox-panel/meson.build new file mode 100644 index 0000000..06c2463 --- /dev/null +++ b/src/plugins/inbox-panel/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=inbox_panel_plugin_register_types'] + +plugins_sources += files( + 'gtd-inbox-panel.c', + 'inbox-panel-plugin.c' +) + +plugins_sources += gnome.compile_resources( + 'inbox-panel-resources', + 'inbox-panel.gresource.xml', + c_name: 'inbox_panel_plugin', +) diff --git a/src/plugins/meson.build b/src/plugins/meson.build new file mode 100644 index 0000000..01b37a2 --- /dev/null +++ b/src/plugins/meson.build @@ -0,0 +1,36 @@ +plugins_incs = [ + incs, + include_directories('..'), +] + +# Ensure enum types header is generated before building plugins +plugins_sources = [ gtd_enum_types[1] ] +plugins_deps = [ endeavour_deps ] +plugins_ldflags = [] +plugins_libs = [] +plugins_confs = [] + +plugins_conf = configuration_data() +plugins_conf.set('VERSION', endeavour_version) + +subdir('all-tasks-panel') +subdir('eds') +subdir('inbox-panel') +subdir('next-week-panel') +subdir('peace') +subdir('scheduled-panel') +subdir('task-lists-workspace') +subdir('today-panel') + +plugins_lib = static_library( + 'plugins', + plugins_sources, + dependencies: plugins_deps, + include_directories: plugins_incs, + link_with: plugins_libs, + link_args: plugins_ldflags, +) + +plugins_dep = declare_dependency( + link_whole: plugins_lib, +) diff --git a/src/plugins/next-week-panel/gtd-next-week-panel.c b/src/plugins/next-week-panel/gtd-next-week-panel.c new file mode 100644 index 0000000..33ea031 --- /dev/null +++ b/src/plugins/next-week-panel/gtd-next-week-panel.c @@ -0,0 +1,573 @@ +/* gtd-next-week-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + + +#define G_LOG_DOMAIN "GtdNextWeekPanel" + +#include "gtd-next-week-panel.h" + +#include "endeavour.h" + +#include <glib/gi18n.h> +#include <math.h> + + +#define GTD_NEXT_WEEK_PANEL_NAME "next-week-panel" +#define GTD_NEXT_WEEK_PANEL_PRIORITY 700 + +struct _GtdNextWeekPanel +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; + GtkFilterListModel *incomplete_model; + GtkSortListModel *sort_model; + + GtkCssProvider *css_provider; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdNextWeekPanel, gtd_next_week_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +load_css_provider (GtdNextWeekPanel *self) +{ + g_autoptr (GSettings) settings = NULL; + g_autoptr (GFile) css_file = NULL; + g_autofree gchar *theme_name = NULL; + g_autofree gchar *theme_uri = NULL; + + /* Load CSS provider */ + settings = g_settings_new ("org.gnome.desktop.interface"); + theme_name = g_settings_get_string (settings, "gtk-theme"); + theme_uri = g_build_filename ("resource:///org/gnome/todo/plugins/next-week-panel/theme", theme_name, ".css", NULL); + css_file = g_file_new_for_uri (theme_uri); + + self->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (self->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (g_file_query_exists (css_file, NULL)) + gtk_css_provider_load_from_file (self->css_provider, css_file); + else + gtk_css_provider_load_from_resource (self->css_provider, "/org/gnome/todo/plugins/next-week-panel/theme/Adwaita.css"); +} + +static gboolean +get_date_offset (GDateTime *dt, + gint *days_diff, + gint *years_diff) +{ + g_autoptr (GDateTime) now = NULL; + GDate now_date, dt_date; + + g_date_clear (&dt_date, 1); + g_date_set_dmy (&dt_date, + g_date_time_get_day_of_month (dt), + g_date_time_get_month (dt), + g_date_time_get_year (dt)); + + now = g_date_time_new_now_local (); + + g_date_clear (&now_date, 1); + g_date_set_dmy (&now_date, + g_date_time_get_day_of_month (now), + g_date_time_get_month (now), + g_date_time_get_year (now)); + + + if (days_diff) + *days_diff = g_date_days_between (&now_date, &dt_date); + + if (years_diff) + *years_diff = g_date_time_get_year (dt) - g_date_time_get_year (now); + + return TRUE; +} + +static gchar* +get_string_for_date (GDateTime *dt, + gint *span) +{ + gchar *str; + gint days_diff; + gint years_diff; + + /* This case should never happen */ + if (!dt) + return g_strdup (_("No date set")); + + days_diff = years_diff = 0; + + get_date_offset (dt, &days_diff, &years_diff); + + if (days_diff < 0) + { + str = g_strdup (_("Overdue")); + } + else if (days_diff == 0) + { + str = g_strdup (_("Today")); + } + else if (days_diff == 1) + { + str = g_strdup (_("Tomorrow")); + } + else if (days_diff > 1 && days_diff < 7) + { + str = g_date_time_format (dt, "%A"); // Weekday name + } + else if (days_diff >= 7 && years_diff == 0) + { + str = g_date_time_format (dt, "%OB"); // Full month name + } + else + { + str = g_strdup_printf ("%d", g_date_time_get_year (dt)); + } + + if (span) + *span = days_diff; + + return str; +} + +static GtkWidget* +create_label (const gchar *text, + gint span, + gboolean first_header) +{ + GtkWidget *label; + GtkWidget *box; + + label = g_object_new (GTK_TYPE_LABEL, + "label", text, + "margin-top", first_header ? 6 : 18, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, span < 0 ? "date-overdue" : "date-scheduled"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + gtk_box_append (GTK_BOX (box), label); + + return box; +} + +static gint +compare_by_date (GDateTime *d1, + GDateTime *d2) +{ + if (g_date_time_get_year (d1) != g_date_time_get_year (d2)) + return g_date_time_get_year (d1) - g_date_time_get_year (d2); + + return g_date_time_get_day_of_year (d1) - g_date_time_get_day_of_year (d2); +} + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + GtdNextWeekPanel *self) +{ + g_autoptr (GDateTime) dt = NULL; + g_autofree gchar *text = NULL; + gint span; + + dt = gtd_task_get_due_date (task); + + if (previous_task) + { + g_autoptr (GDateTime) before_dt = NULL; + gint before_diff, current_diff; + + before_dt = gtd_task_get_due_date (previous_task); + + get_date_offset (before_dt, &before_diff, NULL); + get_date_offset (dt, ¤t_diff, NULL); + + if ((before_diff < 0 && current_diff >= 0) || + (before_diff >= 0 && current_diff >= 0 && before_diff != current_diff)) + { + text = get_string_for_date (dt, &span); + } + } + else + { + text = get_string_for_date (dt, &span); + } + + return text ? create_label (text, span, !previous_task) : NULL; +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GDateTime *dt1; + GDateTime *dt2; + GtdTask *task1; + GtdTask *task2; + gint retval; + gchar *t1; + gchar *t2; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + /* First, compare by ::due-date. */ + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = compare_by_date (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Third, compare by ::creation-date. */ + dt1 = gtd_task_get_creation_date (task1); + dt2 = gtd_task_get_creation_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = g_date_time_compare (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Finally, compare by ::title. */ + t1 = t2 = NULL; + + t1 = g_utf8_casefold (gtd_task_get_title (task1), -1); + t2 = g_utf8_casefold (gtd_task_get_title (task2), -1); + + retval = g_strcmp0 (t1, t2); + + g_free (t1); + g_free (t2); + + return retval; +} + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + GtdTask *task; + gboolean complete; + gint days_offset; + + task = (GtdTask*) item; + complete = gtd_task_get_complete (task); + task_dt = gtd_task_get_due_date (task); + + return task_dt != NULL && + get_date_offset (task_dt, &days_offset, NULL) && + days_offset < 7 && + ((days_offset < 0 && !complete) || days_offset >= 0); +} + +static gboolean +filter_complete_func (gpointer item, + gpointer user_data) +{ + GtdTask *task = (GtdTask*) item; + + return !gtd_task_get_complete (task); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdNextWeekPanel *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdNextWeekPanel *self) +{ + g_autoptr (GDateTime) now = NULL; + GtkFilter *filter; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_next_week_get_panel_name (GtdPanel *panel) +{ + return GTD_NEXT_WEEK_PANEL_NAME; +} + +static const gchar* +gtd_panel_next_week_get_panel_title (GtdPanel *panel) +{ + return _("Next 7 Days"); +} + +static GList* +gtd_panel_next_week_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_next_week_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_next_week_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_NEXT_WEEK_PANEL (panel)->icon); +} + +static guint32 +gtd_panel_next_week_get_priority (GtdPanel *panel) +{ + return GTD_NEXT_WEEK_PANEL_PRIORITY; +} + +static gchar* +gtd_panel_next_week_get_subtitle (GtdPanel *panel) +{ + GtdNextWeekPanel *self = GTD_NEXT_WEEK_PANEL (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_next_week_get_panel_name; + iface->get_panel_title = gtd_panel_next_week_get_panel_title; + iface->get_header_widgets = gtd_panel_next_week_get_header_widgets; + iface->get_menu = gtd_panel_next_week_get_menu; + iface->get_icon = gtd_panel_next_week_get_icon; + iface->get_priority = gtd_panel_next_week_get_priority; + iface->get_subtitle = gtd_panel_next_week_get_subtitle; +} + + +/* + * GObject overrides + */ + +static void +gtd_next_week_panel_finalize (GObject *object) +{ + GtdNextWeekPanel *self = (GtdNextWeekPanel *)object; + + g_clear_object (&self->css_provider); + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + g_clear_object (&self->incomplete_model); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_next_week_panel_parent_class)->finalize (object); +} + +static void +gtd_next_week_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdNextWeekPanel *self = GTD_NEXT_WEEK_PANEL (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_NEXT_WEEK_PANEL_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_NEXT_WEEK_PANEL_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_next_week_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_next_week_panel_class_init (GtdNextWeekPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_next_week_panel_finalize; + object_class->get_property = gtd_next_week_panel_get_property; + object_class->set_property = gtd_next_week_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_next_week_panel_init (GtdNextWeekPanel *self) +{ + g_autoptr (GDateTime) now = g_date_time_new_now_local (); + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *incomplete_filter; + GtkCustomFilter *filter; + GtkCustomSorter *sorter; + + self->icon = g_themed_icon_new ("view-tasks-week-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (G_LIST_MODEL (self->filter_model), + GTK_SORTER (sorter)); + + incomplete_filter = gtk_custom_filter_new (filter_complete_func, self, NULL); + self->incomplete_model = gtk_filter_list_model_new (G_LIST_MODEL (self->sort_model), + GTK_FILTER (incomplete_filter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (GTD_TASK_LIST_VIEW (self->view), G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (GTD_TASK_LIST_VIEW (self->view), TRUE); + gtd_task_list_view_set_show_due_date (GTD_TASK_LIST_VIEW (self->view), FALSE); + gtd_task_list_view_set_default_date (self->view, now); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (GTD_TASK_LIST_VIEW (self->view), + (GtdTaskListViewHeaderFunc) header_func, + self); + + g_signal_connect_object (self->incomplete_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); + load_css_provider (self); +} + +GtkWidget* +gtd_next_week_panel_new (void) +{ + return g_object_new (GTD_TYPE_NEXT_WEEK_PANEL, NULL); +} diff --git a/src/plugins/next-week-panel/gtd-next-week-panel.h b/src/plugins/next-week-panel/gtd-next-week-panel.h new file mode 100644 index 0000000..cfda880 --- /dev/null +++ b/src/plugins/next-week-panel/gtd-next-week-panel.h @@ -0,0 +1,34 @@ +/* gtd-next-week-panel.h + * + * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_NEXT_WEEK_PANEL (gtd_next_week_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdNextWeekPanel, gtd_next_week_panel, GTD, NEXT_WEEK_PANEL, GtkBox) + +GtkWidget* gtd_next_week_panel_new (void); + +G_END_DECLS diff --git a/src/plugins/next-week-panel/meson.build b/src/plugins/next-week-panel/meson.build new file mode 100644 index 0000000..618c266 --- /dev/null +++ b/src/plugins/next-week-panel/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=next_week_panel_plugin_register_types'] + +plugins_sources += files( + 'gtd-next-week-panel.c', + 'next-week-panel-plugin.c' +) + +plugins_sources += gnome.compile_resources( + 'next-week-panel-resources', + 'next-week-panel.gresource.xml', + c_name: 'next_week_panel_plugin', +) diff --git a/src/plugins/next-week-panel/next-week-panel-plugin.c b/src/plugins/next-week-panel/next-week-panel-plugin.c new file mode 100644 index 0000000..29af419 --- /dev/null +++ b/src/plugins/next-week-panel/next-week-panel-plugin.c @@ -0,0 +1,33 @@ +/* gtd-plugin-next-week-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdPluginNextWeekPanel" + +#include "gtd-next-week-panel.h" + +#include "endeavour.h" + +G_MODULE_EXPORT void +next_week_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_NEXT_WEEK_PANEL); +} diff --git a/src/plugins/next-week-panel/next-week-panel.gresource.xml b/src/plugins/next-week-panel/next-week-panel.gresource.xml new file mode 100644 index 0000000..86d42c8 --- /dev/null +++ b/src/plugins/next-week-panel/next-week-panel.gresource.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/next-week-panel"> + <file>next-week-panel.plugin</file> + <file>theme/Adwaita.css</file> + </gresource> +</gresources> diff --git a/src/plugins/next-week-panel/next-week-panel.plugin b/src/plugins/next-week-panel/next-week-panel.plugin new file mode 100644 index 0000000..0f029ad --- /dev/null +++ b/src/plugins/next-week-panel/next-week-panel.plugin @@ -0,0 +1,13 @@ +[Plugin] +Name = Next 7 days +Module = next-week-panel +Description = A panel to show tasks scheduled for the next 7 days +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = next_week_panel_plugin_register_types +Depends = diff --git a/src/plugins/next-week-panel/theme/Adwaita.css b/src/plugins/next-week-panel/theme/Adwaita.css new file mode 100644 index 0000000..546b582 --- /dev/null +++ b/src/plugins/next-week-panel/theme/Adwaita.css @@ -0,0 +1,11 @@ +label.date-scheduled { + color: #4a90d9; + font-size: 16px; + font-weight: bold; +} + +label.date-overdue { + color: #ee2222; + font-size: 16px; + font-weight: bold; +} diff --git a/src/plugins/peace/gtd-peace-omni-area-addin.c b/src/plugins/peace/gtd-peace-omni-area-addin.c new file mode 100644 index 0000000..77c4e25 --- /dev/null +++ b/src/plugins/peace/gtd-peace-omni-area-addin.c @@ -0,0 +1,209 @@ +/* gtd-peace-omni-area-addin.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-peace-omni-area-addin.h" + +#include "config.h" + +#include <glib/gi18n.h> + +#define MESSAGE_ID "peace-message-id" + +#define SWITCH_MESSAGE_TIMEOUT 20 * 60 +#define REMOVE_MESSAGE_TIMEOUT 1 * 60 + +struct _GtdPeaceOmniAreaAddin +{ + GObject parent; + + GtdOmniArea *omni_area; + + guint timeout_id; +}; + +static gboolean switch_message_cb (gpointer user_data); + +static void gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdPeaceOmniAreaAddin, gtd_peace_omni_area_addin, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTD_TYPE_OMNI_AREA_ADDIN, gtd_omni_area_addin_iface_init)) + +typedef struct +{ + const gchar *text; + const gchar *icon_name; +} str_pair; + +const str_pair mindful_questions[] = +{ + { N_("Did you drink some water today?"), NULL }, + { N_("What are your goals for today?"), NULL }, + { N_("Can you let your creativity flow?"), NULL }, + { N_("How are you feeling right now?"), NULL }, + { N_("At what point is it good enough?"), NULL }, +}; + +const str_pair reminders[] = +{ + { N_("Remember to breathe. Good. Don't stop."), NULL }, + { N_("Don't forget to drink some water"), NULL }, + { N_("Remember to take some time off"), NULL }, + { N_("Eat fruits if you can 🍐️"), NULL }, + { N_("Take care of yourself"), NULL }, + { N_("Remember to have some fun"), NULL }, + { N_("You're doing great"), NULL }, +}; + +const str_pair inspiring_quotes[] = +{ + { N_("Smile, breathe and go slowly"), NULL }, + { N_("Wherever you go, there you are"), NULL }, + { N_("Working hard is always rewarded"), NULL }, + { N_("Keep calm"), NULL }, + { N_("You can do it"), NULL }, + { N_("Meanwhile, spread the love ♥️"), NULL }, +}; + + +/* + * Callbacks + */ + +static gboolean +remove_message_cb (gpointer user_data) +{ + GtdPeaceOmniAreaAddin *self = GTD_PEACE_OMNI_AREA_ADDIN (user_data); + gint factor = g_random_int_range (2, 6); + + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + + self->timeout_id = g_timeout_add_seconds (SWITCH_MESSAGE_TIMEOUT * factor, switch_message_cb, self); + + return G_SOURCE_REMOVE; +} + +static gboolean +switch_message_cb (gpointer user_data) +{ + GtdPeaceOmniAreaAddin *self; + g_autoptr (GIcon) icon = NULL; + const gchar *message; + const gchar *icon_name; + gint source; + + self = GTD_PEACE_OMNI_AREA_ADDIN (user_data); + source = g_random_int_range (0, 3); + + if (source == 0) + { + gint i = g_random_int_range (0, G_N_ELEMENTS (mindful_questions)); + + message = gettext (mindful_questions[i].text); + icon_name = mindful_questions[i].icon_name; + } + else if (source == 1) + { + gint i = g_random_int_range (0, G_N_ELEMENTS (reminders)); + + message = gettext (reminders[i].text); + icon_name = reminders[i].icon_name; + } + else + { + gint i = g_random_int_range (0, G_N_ELEMENTS (inspiring_quotes)); + + message = gettext (inspiring_quotes[i].text); + icon_name = inspiring_quotes[i].icon_name; + } + + if (icon_name) + icon = g_themed_icon_new (icon_name); + + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + gtd_omni_area_push_message (self->omni_area, MESSAGE_ID, message, NULL); + + self->timeout_id = g_timeout_add_seconds (REMOVE_MESSAGE_TIMEOUT, remove_message_cb, self); + + return G_SOURCE_REMOVE; +} + + +/* + * GtdOmniAreaAddin iface + */ + +static void +gtd_today_omni_area_addin_omni_area_addin_load (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdPeaceOmniAreaAddin *self = GTD_PEACE_OMNI_AREA_ADDIN (addin); + + self->omni_area = omni_area; + + g_clear_handle_id (&self->timeout_id, g_source_remove); + self->timeout_id = g_timeout_add_seconds (SWITCH_MESSAGE_TIMEOUT, switch_message_cb, self); +} + +static void +gtd_today_omni_area_addin_omni_area_addin_unload (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdPeaceOmniAreaAddin *self = GTD_PEACE_OMNI_AREA_ADDIN (addin); + + gtd_omni_area_withdraw_message (omni_area, MESSAGE_ID); + + g_clear_handle_id (&self->timeout_id, g_source_remove); + self->omni_area = NULL; +} + +static void +gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface) +{ + iface->load = gtd_today_omni_area_addin_omni_area_addin_load; + iface->unload = gtd_today_omni_area_addin_omni_area_addin_unload; +} + + +/* + * GObject overrides + */ + +static void +gtd_peace_omni_area_addin_finalize (GObject *object) +{ + GtdPeaceOmniAreaAddin *self = (GtdPeaceOmniAreaAddin *)object; + + g_clear_handle_id (&self->timeout_id, g_source_remove); + + G_OBJECT_CLASS (gtd_peace_omni_area_addin_parent_class)->finalize (object); +} + +static void +gtd_peace_omni_area_addin_class_init (GtdPeaceOmniAreaAddinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_peace_omni_area_addin_finalize; +} + +static void +gtd_peace_omni_area_addin_init (GtdPeaceOmniAreaAddin *self) +{ +} diff --git a/src/plugins/peace/gtd-peace-omni-area-addin.h b/src/plugins/peace/gtd-peace-omni-area-addin.h new file mode 100644 index 0000000..8b83653 --- /dev/null +++ b/src/plugins/peace/gtd-peace-omni-area-addin.h @@ -0,0 +1,30 @@ +/* gtd-peace-omni-area-addin.h + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_PEACE_OMNI_AREA_ADDIN (gtd_peace_omni_area_addin_get_type()) +G_DECLARE_FINAL_TYPE (GtdPeaceOmniAreaAddin, gtd_peace_omni_area_addin, GTD, PEACE_OMNI_AREA_ADDIN, GObject) + +G_END_DECLS diff --git a/src/plugins/peace/meson.build b/src/plugins/peace/meson.build new file mode 100644 index 0000000..3cba81b --- /dev/null +++ b/src/plugins/peace/meson.build @@ -0,0 +1,12 @@ +plugins_ldflags += ['-Wl,--undefined=peace_plugin_register_types'] + +plugins_sources += files( + 'gtd-peace-omni-area-addin.c', + 'peace-plugin.c', +) + +plugins_sources += gnome.compile_resources( + 'peace-resources', + 'peace.gresource.xml', + c_name: 'peace_plugin', +) diff --git a/src/plugins/peace/peace-plugin.c b/src/plugins/peace/peace-plugin.c new file mode 100644 index 0000000..5ece6fa --- /dev/null +++ b/src/plugins/peace/peace-plugin.c @@ -0,0 +1,31 @@ +/* zen-plugin.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include <libpeas/peas.h> + +#include "gtd-peace-omni-area-addin.h" + +G_MODULE_EXPORT void +peace_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_OMNI_AREA_ADDIN, + GTD_TYPE_PEACE_OMNI_AREA_ADDIN); +} diff --git a/src/plugins/peace/peace.gresource.xml b/src/plugins/peace/peace.gresource.xml new file mode 100644 index 0000000..13132ed --- /dev/null +++ b/src/plugins/peace/peace.gresource.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/peace"> + <file>peace.plugin</file> + </gresource> +</gresources> diff --git a/src/plugins/peace/peace.plugin b/src/plugins/peace/peace.plugin new file mode 100644 index 0000000..d7e85ea --- /dev/null +++ b/src/plugins/peace/peace.plugin @@ -0,0 +1,12 @@ +[Plugin] +Name = Peace +Module = peace +Description = Smile, breathe and go slowly +Authors = Georges Basile Stavracas Neto <georges.stavracas@gmail.com> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = peace_plugin_register_types +Depends = diff --git a/src/plugins/scheduled-panel/gtd-panel-scheduled.c b/src/plugins/scheduled-panel/gtd-panel-scheduled.c new file mode 100644 index 0000000..9b3e57e --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-panel-scheduled.c @@ -0,0 +1,518 @@ +/* gtd-panel-scheduled.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdPanelScheduled" + +#include "endeavour.h" + +#include "gtd-panel-scheduled.h" + +#include <glib/gi18n.h> +#include <math.h> + +struct _GtdPanelScheduled +{ + GtkBox parent; + + GIcon *icon; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; + GtkSortListModel *sort_model; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_EXTENDED (GtdPanelScheduled, gtd_panel_scheduled, GTK_TYPE_BOX, + 0, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, + gtd_panel_iface_init)) + +#define GTD_PANEL_SCHEDULED_NAME "panel-scheduled" +#define GTD_PANEL_SCHEDULED_PRIORITY 500 + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +static void +get_date_offset (GDateTime *dt, + gint *days_diff, + gint *years_diff) +{ + g_autoptr (GDateTime) now = NULL; + GDate now_date, dt_date; + + g_date_clear (&dt_date, 1); + g_date_set_dmy (&dt_date, + g_date_time_get_day_of_month (dt), + g_date_time_get_month (dt), + g_date_time_get_year (dt)); + + now = g_date_time_new_now_local (); + + g_date_clear (&now_date, 1); + g_date_set_dmy (&now_date, + g_date_time_get_day_of_month (now), + g_date_time_get_month (now), + g_date_time_get_year (now)); + + + if (days_diff) + *days_diff = g_date_days_between (&now_date, &dt_date); + + if (years_diff) + *years_diff = g_date_time_get_year (dt) - g_date_time_get_year (now); +} + +static gchar* +get_string_for_date (GDateTime *dt, + gint *span) +{ + gchar *str; + gint days_diff; + gint years_diff; + + /* This case should never happen */ + if (!dt) + return g_strdup (_("No date set")); + + days_diff = years_diff = 0; + + get_date_offset (dt, &days_diff, &years_diff); + + if (days_diff < -1) + { + /* Translators: This message will never be used with '1 day ago' + * but the singular form is required because some languages do not + * have plurals, some languages reuse the singular form for numbers + * like 21, 31, 41, etc. + */ + str = g_strdup_printf (g_dngettext (NULL, "%d day ago", "%d days ago", -days_diff), -days_diff); + } + else if (days_diff == -1) + { + str = g_strdup (_("Yesterday")); + } + else if (days_diff == 0) + { + str = g_strdup (_("Today")); + } + else if (days_diff == 1) + { + str = g_strdup (_("Tomorrow")); + } + else if (days_diff > 1 && days_diff < 7) + { + str = g_date_time_format (dt, "%A"); // Weekday name + } + else if (days_diff >= 7 && years_diff == 0) + { + str = g_date_time_format (dt, "%OB"); // Full month name + } + else + { + str = g_strdup_printf ("%d", g_date_time_get_year (dt)); + } + + if (span) + *span = days_diff; + + return str; +} + +static GtkWidget* +create_label (const gchar *text, + gint span, + gboolean first_header) +{ + GtkWidget *label; + GtkWidget *box; + + label = g_object_new (GTK_TYPE_LABEL, + "label", text, + "margin-start", 6, + "margin-bottom", 6, + "margin-top", first_header ? 6 : 18, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, span < 0 ? "date-overdue" : "date-scheduled"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + gtk_box_append (GTK_BOX (box), label); + + return box; +} + +static gint +compare_by_date (GDateTime *d1, + GDateTime *d2) +{ + if (g_date_time_get_year (d1) != g_date_time_get_year (d2)) + return g_date_time_get_year (d1) - g_date_time_get_year (d2); + + return g_date_time_get_day_of_year (d1) - g_date_time_get_day_of_year (d2); +} + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + GtdPanelScheduled *panel) +{ + g_autoptr (GDateTime) dt = NULL; + g_autofree gchar *text = NULL; + gint span; + + dt = gtd_task_get_due_date (task); + + if (previous_task) + { + g_autoptr (GDateTime) before_dt = NULL; + gint diff; + + before_dt = gtd_task_get_due_date (previous_task); + diff = compare_by_date (before_dt, dt); + + if (diff != 0) + text = get_string_for_date (dt, &span); + } + else + { + text = get_string_for_date (dt, &span); + } + + return text ? create_label (text, span, !previous_task) : NULL; +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + GDateTime *dt1; + GDateTime *dt2; + GtdTask *task1; + GtdTask *task2; + gint retval; + gchar *t1; + gchar *t2; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + /* First, compare by ::due-date. */ + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = compare_by_date (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Second, compare by ::complete. */ + retval = gtd_task_get_complete (task1) - gtd_task_get_complete (task2); + + if (retval != 0) + return retval; + + /* Third, compare by ::creation-date. */ + dt1 = gtd_task_get_creation_date (task1); + dt2 = gtd_task_get_creation_date (task2); + + if (!dt1 && !dt2) + retval = 0; + else if (!dt1) + retval = 1; + else if (!dt2) + retval = -1; + else + retval = g_date_time_compare (dt1, dt2); + + g_clear_pointer (&dt1, g_date_time_unref); + g_clear_pointer (&dt2, g_date_time_unref); + + if (retval != 0) + return retval; + + /* Finally, compare by ::title. */ + t1 = t2 = NULL; + + t1 = g_utf8_casefold (gtd_task_get_title (task1), -1); + t2 = g_utf8_casefold (gtd_task_get_title (task2), -1); + + retval = g_strcmp0 (t1, t2); + + g_free (t1); + g_free (t2); + + return retval; +} + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + GtdTask *task; + + task = (GtdTask*) item; + task_dt = gtd_task_get_due_date (task); + + return !gtd_task_get_complete (task) && task_dt != NULL; +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdPanelScheduled *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdPanelScheduled *self) +{ + g_autoptr (GDateTime) now = NULL; + GtkFilter *filter; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + + +/********************** + * GtdPanel iface init + **********************/ +static const gchar* +gtd_panel_scheduled_get_panel_name (GtdPanel *panel) +{ + return GTD_PANEL_SCHEDULED_NAME; +} + +static const gchar* +gtd_panel_scheduled_get_panel_title (GtdPanel *panel) +{ + return _("Scheduled"); +} + +static GList* +gtd_panel_scheduled_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_scheduled_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_scheduled_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_PANEL_SCHEDULED (panel)->icon); +} + +static guint32 +gtd_panel_scheduled_get_priority (GtdPanel *panel) +{ + return GTD_PANEL_SCHEDULED_PRIORITY; +} + +static gchar* +gtd_panel_scheduled_get_subtitle (GtdPanel *panel) +{ + GtdPanelScheduled *self = GTD_PANEL_SCHEDULED (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_scheduled_get_panel_name; + iface->get_panel_title = gtd_panel_scheduled_get_panel_title; + iface->get_header_widgets = gtd_panel_scheduled_get_header_widgets; + iface->get_menu = gtd_panel_scheduled_get_menu; + iface->get_icon = gtd_panel_scheduled_get_icon; + iface->get_priority = gtd_panel_scheduled_get_priority; + iface->get_subtitle = gtd_panel_scheduled_get_subtitle; +} + +static void +gtd_panel_scheduled_finalize (GObject *object) +{ + GtdPanelScheduled *self = (GtdPanelScheduled *)object; + + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_panel_scheduled_parent_class)->finalize (object); +} + +static void +gtd_panel_scheduled_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdPanelScheduled *self = GTD_PANEL_SCHEDULED (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_PANEL_SCHEDULED_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_PANEL_SCHEDULED_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_panel_scheduled_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_panel_scheduled_class_init (GtdPanelScheduledClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_panel_scheduled_finalize; + object_class->get_property = gtd_panel_scheduled_get_property; + object_class->set_property = gtd_panel_scheduled_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_panel_scheduled_init (GtdPanelScheduled *self) +{ + g_autoptr (GDateTime) now = g_date_time_new_now_local (); + GtdManager *manager = gtd_manager_get_default (); + GtkCustomFilter *filter; + GtkCustomSorter *sorter; + + self->icon = g_themed_icon_new ("alarm-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (G_LIST_MODEL (self->filter_model), + GTK_SORTER (sorter)); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (self->view, G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (self->view, TRUE); + gtd_task_list_view_set_show_due_date (self->view, FALSE); + gtd_task_list_view_set_default_date (self->view, now); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (self->view, + (GtdTaskListViewHeaderFunc) header_func, + self); + + g_signal_connect_object (self->sort_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); +} + +GtkWidget* +gtd_panel_scheduled_new (void) +{ + return g_object_new (GTD_TYPE_PANEL_SCHEDULED, NULL); +} + diff --git a/src/plugins/scheduled-panel/gtd-panel-scheduled.h b/src/plugins/scheduled-panel/gtd-panel-scheduled.h new file mode 100644 index 0000000..59af3b5 --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-panel-scheduled.h @@ -0,0 +1,35 @@ +/* gtd-panel-scheduled.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_PANEL_SCHEDULED_H +#define GTD_PANEL_SCHEDULED_H + +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_PANEL_SCHEDULED (gtd_panel_scheduled_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPanelScheduled, gtd_panel_scheduled, GTD, PANEL_SCHEDULED, GtkBox) + +GtkWidget* gtd_panel_scheduled_new (void); + +G_END_DECLS + +#endif /* GTD_PANEL_SCHEDULED_H */ diff --git a/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c new file mode 100644 index 0000000..348cfd1 --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c @@ -0,0 +1,153 @@ +/* gtd-plugin-scheduled-panel.c + * + * Copyright (C) 2016-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdPluginScheduledPanel" + +#include "gtd-panel-scheduled.h" + +#include "gtd-plugin-scheduled-panel.h" + +#include <glib/gi18n.h> +#include <glib-object.h> + +struct _GtdPluginScheduledPanel +{ + PeasExtensionBase parent; + + GtkCssProvider *css_provider; +}; + +static void gtd_activatable_iface_init (GtdActivatableInterface *iface); + +G_DEFINE_DYNAMIC_TYPE_EXTENDED (GtdPluginScheduledPanel, gtd_plugin_scheduled_panel, PEAS_TYPE_EXTENSION_BASE, + 0, + G_IMPLEMENT_INTERFACE_DYNAMIC (GTD_TYPE_ACTIVATABLE, + gtd_activatable_iface_init)) + +enum { + PROP_0, + PROP_PREFERENCES_PANEL, + N_PROPS +}; + +/* + * GtdActivatable interface implementation + */ +static void +gtd_plugin_scheduled_panel_activate (GtdActivatable *activatable) +{ + ; +} + +static void +gtd_plugin_scheduled_panel_deactivate (GtdActivatable *activatable) +{ + ; +} + +static GtkWidget* +gtd_plugin_scheduled_panel_get_preferences_panel (GtdActivatable *activatable) +{ + return NULL; +} + +static void +gtd_activatable_iface_init (GtdActivatableInterface *iface) +{ + iface->activate = gtd_plugin_scheduled_panel_activate; + iface->deactivate = gtd_plugin_scheduled_panel_deactivate; + iface->get_preferences_panel = gtd_plugin_scheduled_panel_get_preferences_panel; +} + +static void +gtd_plugin_scheduled_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_PREFERENCES_PANEL: + g_value_set_object (value, NULL); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_plugin_scheduled_panel_class_init (GtdPluginScheduledPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = gtd_plugin_scheduled_panel_get_property; + + g_object_class_override_property (object_class, + PROP_PREFERENCES_PANEL, + "preferences-panel"); +} + +static void +gtd_plugin_scheduled_panel_init (GtdPluginScheduledPanel *self) +{ + GSettings *settings; + GFile* css_file; + gchar *theme_name; + gchar *theme_uri; + + /* Load CSS provider */ + settings = g_settings_new ("org.gnome.desktop.interface"); + theme_name = g_settings_get_string (settings, "gtk-theme"); + theme_uri = g_build_filename ("resource:///org/gnome/todo/theme/scheduled-panel", theme_name, ".css", NULL); + css_file = g_file_new_for_uri (theme_uri); + + self->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (self->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (g_file_query_exists (css_file, NULL)) + gtk_css_provider_load_from_file (self->css_provider, css_file); + else + gtk_css_provider_load_from_resource (self->css_provider, "/org/gnome/todo/plugins/scheduled-panel/theme/Adwaita.css"); + + g_object_unref (settings); + g_object_unref (css_file); + g_free (theme_name); + g_free (theme_uri); +} + +static void +gtd_plugin_scheduled_panel_class_finalize (GtdPluginScheduledPanelClass *klass) +{ +} + +G_MODULE_EXPORT void +gtd_plugin_scheduled_panel_register_types (PeasObjectModule *module) +{ + gtd_plugin_scheduled_panel_register_type (G_TYPE_MODULE (module)); + + peas_object_module_register_extension_type (module, + GTD_TYPE_ACTIVATABLE, + GTD_TYPE_PLUGIN_SCHEDULED_PANEL); + + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_PANEL_SCHEDULED); +} diff --git a/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h new file mode 100644 index 0000000..e5391f3 --- /dev/null +++ b/src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h @@ -0,0 +1,37 @@ +/* gtd-plugin-scheduled-panel.h + * + * Copyright (C) 2016 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_PLUGIN_SCHEDULED_PANEL_H +#define GTD_PLUGIN_SCHEDULED_PANEL_H + +#include "endeavour.h" + +#include <glib.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_PLUGIN_SCHEDULED_PANEL (gtd_plugin_scheduled_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPluginScheduledPanel, gtd_plugin_scheduled_panel, GTD, PLUGIN_SCHEDULED_PANEL, PeasExtensionBase) + +G_MODULE_EXPORT void gtd_plugin_scheduled_panel_register_types (PeasObjectModule *module); + +G_END_DECLS + +#endif /* GTD_PLUGIN_SCHEDULED_PANEL_H */ + diff --git a/src/plugins/scheduled-panel/meson.build b/src/plugins/scheduled-panel/meson.build new file mode 100644 index 0000000..e720573 --- /dev/null +++ b/src/plugins/scheduled-panel/meson.build @@ -0,0 +1,6 @@ +plugins_ldflags += ['-Wl,--undefined=gtd_plugin_scheduled_panel_register_types'] + +plugins_sources += files( + 'gtd-panel-scheduled.c', + 'gtd-plugin-scheduled-panel.c' +) diff --git a/src/plugins/scheduled-panel/scheduled-panel.gresource.xml b/src/plugins/scheduled-panel/scheduled-panel.gresource.xml new file mode 100644 index 0000000..21b04ae --- /dev/null +++ b/src/plugins/scheduled-panel/scheduled-panel.gresource.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/scheduled-panel"> + <file>scheduled-panel.plugin</file> + <file>theme/Adwaita.css</file> + </gresource> +</gresources> diff --git a/src/plugins/scheduled-panel/scheduled-panel.plugin b/src/plugins/scheduled-panel/scheduled-panel.plugin new file mode 100644 index 0000000..2cb78d8 --- /dev/null +++ b/src/plugins/scheduled-panel/scheduled-panel.plugin @@ -0,0 +1,13 @@ +[Plugin] +Name = Scheduled tasks +Module = scheduled-panel +Description = A panel to show scheduled tasks +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +License = GPL +Loader = C +Embedded = gtd_plugin_scheduled_panel_register_types +Depends = diff --git a/src/plugins/scheduled-panel/theme/Adwaita.css b/src/plugins/scheduled-panel/theme/Adwaita.css new file mode 100644 index 0000000..546b582 --- /dev/null +++ b/src/plugins/scheduled-panel/theme/Adwaita.css @@ -0,0 +1,11 @@ +label.date-scheduled { + color: #4a90d9; + font-size: 16px; + font-weight: bold; +} + +label.date-overdue { + color: #ee2222; + font-size: 16px; + font-weight: bold; +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-list-row.c b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.c new file mode 100644 index 0000000..5919665 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.c @@ -0,0 +1,333 @@ +/* gtd-sidebar-list-row.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdSidebarListRow" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-notification.h" +#include "gtd-provider.h" +#include "gtd-sidebar-list-row.h" +#include "gtd-task.h" +#include "gtd-task-list.h" +#include "gtd-utils.h" + +#include <math.h> +#include <glib/gi18n.h> + +struct _GtdSidebarListRow +{ + GtkListBoxRow parent; + + GtkImage *color_icon; + GtkLabel *name_label; + GtkLabel *tasks_counter_label; + + GtdTaskList *list; +}; + + +static void on_list_changed_cb (GtdSidebarListRow *self); + +static void on_list_color_changed_cb (GtdTaskList *list, + GParamSpec *pspec, + GtdSidebarListRow *self); + +G_DEFINE_TYPE (GtdSidebarListRow, gtd_sidebar_list_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_LIST, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static void +update_color_icon (GtdSidebarListRow *self) +{ + g_autoptr (GdkPaintable) paintable = NULL; + g_autoptr (GdkRGBA) color = NULL; + + color = gtd_task_list_get_color (self->list); + paintable = gtd_create_circular_paintable (color, 12); + + gtk_image_set_from_paintable (self->color_icon, paintable); +} + +static void +update_counter_label (GtdSidebarListRow *self) +{ + g_autofree gchar *label = NULL; + GListModel *model; + guint counter = 0; + guint i; + + model = G_LIST_MODEL (self->list); + + for (i = 0; i < g_list_model_get_n_items (model); i++) + counter += !gtd_task_get_complete (g_list_model_get_item (model, i)); + + label = counter > 0 ? g_strdup_printf ("%u", counter) : g_strdup (""); + + gtk_label_set_label (self->tasks_counter_label, label); +} + +static void +set_list (GtdSidebarListRow *self, + GtdTaskList *list) +{ + g_assert (list != NULL); + g_assert (self->list == NULL); + + self->list = g_object_ref (list); + + g_object_bind_property (list, + "name", + self->name_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (gtd_task_list_get_provider (list), + "enabled", + self, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + /* Always keep the counter label updated */ + g_signal_connect_object (list, "task-added", G_CALLBACK (on_list_changed_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (list, "task-updated", G_CALLBACK (on_list_changed_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (list, "task-removed", G_CALLBACK (on_list_changed_cb), self, G_CONNECT_SWAPPED); + + update_counter_label (self); + + /* And also the color icon */ + g_signal_connect_object (list, "notify::color", G_CALLBACK (on_list_color_changed_cb), self, 0); + + update_color_icon (self); +} + + +/* + * Callbacks + */ + +static void +on_import_task_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_task_list_import_task_finish (GTD_TASK_LIST (object), result, &error); + + if (error) + g_warning ("Error updating task: %s", error->message); +} + +static void +on_list_changed_cb (GtdSidebarListRow *self) +{ + update_counter_label (self); +} + +static void +on_list_color_changed_cb (GtdTaskList *list, + GParamSpec *pspec, + GtdSidebarListRow *self) +{ + update_color_icon (self); +} + +static void +on_rename_popover_hidden_cb (GtkPopover *popover, + GtdSidebarListRow *self) +{ + /* + * Remove the relative to, to remove the popover from the widget + * list and avoid parsing any CSS for it. It's a small performance + * improvement. + */ + gtk_widget_set_parent (GTK_WIDGET (popover), NULL); +} + +static gboolean +on_task_drop (GtkDropTarget *target, + const GValue *value, + double x, + double y, + GtdSidebarListRow *self) +{ + GtdTask *task; + + GTD_ENTRY; + + task = g_value_get_object (value); + gtd_task_list_import_task (self->list, + task, + NULL, + on_import_task_cb, + self); + + GTD_RETURN (TRUE); +} + + +static gboolean +on_task_enter_drop (GtkDropTarget *target, + double x, + double y, + GtdSidebarListRow *self) +{ + GTD_ENTRY; + + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_SELECTED , FALSE); + + GTD_RETURN (TRUE); +} + +static gboolean +on_task_leave_drop (GtkDropTarget *target, + double x, + double y, + GtdSidebarListRow *self) +{ + GTD_ENTRY; + + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_SELECTED); + + GTD_RETURN (TRUE); +} + +/* + * GObject overrides + */ + +static void +gtd_sidebar_list_row_finalize (GObject *object) +{ + GtdSidebarListRow *self = (GtdSidebarListRow *)object; + + g_clear_object (&self->list); + + G_OBJECT_CLASS (gtd_sidebar_list_row_parent_class)->finalize (object); +} + +static void +gtd_sidebar_list_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdSidebarListRow *self = GTD_SIDEBAR_LIST_ROW (object); + + switch (prop_id) + { + case PROP_LIST: + g_value_set_object (value, self->list); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_list_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdSidebarListRow *self = GTD_SIDEBAR_LIST_ROW (object); + + switch (prop_id) + { + case PROP_LIST: + set_list (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_list_row_class_init (GtdSidebarListRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_sidebar_list_row_finalize; + object_class->get_property = gtd_sidebar_list_row_get_property; + object_class->set_property = gtd_sidebar_list_row_set_property; + + properties[PROP_LIST] = g_param_spec_object ("list", + "List", + "The task list this row represents", + GTD_TYPE_TASK_LIST, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar-list-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebarListRow, color_icon); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarListRow, name_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarListRow, tasks_counter_label); + + gtk_widget_class_bind_template_callback (widget_class, on_rename_popover_hidden_cb); +} + +static void +gtd_sidebar_list_row_init (GtdSidebarListRow *self) +{ + GtkDropTarget *target; + + gtk_widget_init_template (GTK_WIDGET (self)); + + target = gtk_drop_target_new (GTD_TYPE_TASK, GDK_ACTION_MOVE); + gtk_drop_target_set_preload (target, TRUE); + g_signal_connect (target, "drop", G_CALLBACK (on_task_drop), self); + g_signal_connect (target, "enter", G_CALLBACK (on_task_enter_drop), self); + g_signal_connect (target, "leave", G_CALLBACK (on_task_leave_drop), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (target)); +} + +GtkWidget* +gtd_sidebar_list_row_new (GtdTaskList *list) +{ + return g_object_new (GTD_TYPE_SIDEBAR_LIST_ROW, + "list", list, + NULL); +} + +GtdTaskList* +gtd_sidebar_list_row_get_task_list (GtdSidebarListRow *self) +{ + g_return_val_if_fail (GTD_IS_SIDEBAR_LIST_ROW (self), NULL); + + return self->list; +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-list-row.h b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.h new file mode 100644 index 0000000..3660608 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.h @@ -0,0 +1,37 @@ +/* gtd-sidebar-list-row.h + * + * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR_LIST_ROW (gtd_sidebar_list_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdSidebarListRow, gtd_sidebar_list_row, GTD, SIDEBAR_LIST_ROW, GtkListBoxRow) + +GtkWidget* gtd_sidebar_list_row_new (GtdTaskList *list); + +GtdTaskList* gtd_sidebar_list_row_get_task_list (GtdSidebarListRow *self); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui new file mode 100644 index 0000000..380d1ed --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="GtdSidebarListRow" parent="GtkListBoxRow"> + <property name="can_focus">1</property> + <child> + <object class="GtkBox"> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="spacing">6</property> + <child> + <object class="GtkImage" id="color_icon"> + <property name="width-request">12</property> + <property name="height-request">12</property> + <property name="halign">center</property> + <property name="valign">center</property> + <style> + <class name="color-circle-icon"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="name_label"> + <property name="hexpand">1</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="tasks_counter_label"> + <style> + <class name="caption"/> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c new file mode 100644 index 0000000..a379f24 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c @@ -0,0 +1,180 @@ +/* gtd-sidebar-panel-row.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-panel.h" +#include "gtd-sidebar-panel-row.h" + +struct _GtdSidebarPanelRow +{ + GtkListBoxRow parent; + + GtkWidget *panel_icon; + GtkWidget *subtitle_label; + GtkWidget *title_label; + + GtdPanel *panel; +}; + +G_DEFINE_TYPE (GtdSidebarPanelRow, gtd_sidebar_panel_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_PANEL, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static void +set_panel (GtdSidebarPanelRow *self, + GtdPanel *panel) +{ + g_assert (panel != NULL); + g_assert (self->panel == NULL); + + self->panel = g_object_ref (panel); + + /* Bind panel properties to the row widgets */ + g_object_bind_property (self->panel, + "icon", + self->panel_icon, + "gicon", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->panel, + "title", + self->title_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->panel, + "subtitle", + self->subtitle_label, + "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PANEL]); +} + + +/* + * GObject overrides + */ + +static void +gtd_sidebar_panel_row_finalize (GObject *object) +{ + GtdSidebarPanelRow *self = (GtdSidebarPanelRow *)object; + + g_clear_object (&self->panel); + + G_OBJECT_CLASS (gtd_sidebar_panel_row_parent_class)->finalize (object); +} + +static void +gtd_sidebar_panel_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdSidebarPanelRow *self = GTD_SIDEBAR_PANEL_ROW (object); + + switch (prop_id) + { + case PROP_PANEL: + g_value_set_object (value, self->panel); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_panel_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdSidebarPanelRow *self = GTD_SIDEBAR_PANEL_ROW (object); + + switch (prop_id) + { + case PROP_PANEL: + set_panel (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_panel_row_class_init (GtdSidebarPanelRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_sidebar_panel_row_finalize; + object_class->get_property = gtd_sidebar_panel_row_get_property; + object_class->set_property = gtd_sidebar_panel_row_set_property; + + properties[PROP_PANEL] = g_param_spec_object ("panel", + "Panel", + "The panel this row represents", + GTD_TYPE_PANEL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebarPanelRow, panel_icon); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarPanelRow, subtitle_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarPanelRow, title_label); +} + +static void +gtd_sidebar_panel_row_init (GtdSidebarPanelRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_sidebar_panel_row_new (GtdPanel *panel) +{ + return g_object_new (GTD_TYPE_SIDEBAR_PANEL_ROW, + "panel", panel, + NULL); +} + +GtdPanel* +gtd_sidebar_panel_row_get_panel (GtdSidebarPanelRow *self) +{ + g_return_val_if_fail (GTD_IS_SIDEBAR_PANEL_ROW (self), NULL); + + return self->panel; +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h new file mode 100644 index 0000000..58decde --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h @@ -0,0 +1,37 @@ +/* gtd-sidebar-panel-row.h + * + * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR_PANEL_ROW (gtd_sidebar_panel_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdSidebarPanelRow, gtd_sidebar_panel_row, GTD, SIDEBAR_PANEL_ROW, GtkListBoxRow) + +GtkWidget* gtd_sidebar_panel_row_new (GtdPanel *panel); + +GtdPanel* gtd_sidebar_panel_row_get_panel (GtdSidebarPanelRow *self); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui new file mode 100644 index 0000000..5722bd8 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="GtdSidebarPanelRow" parent="GtkListBoxRow"> + <property name="can_focus">1</property> + <child> + <object class="GtkBox"> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage" id="panel_icon"/> + </child> + <child> + <object class="GtkLabel" id="title_label"> + <property name="hexpand">1</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="subtitle_label"> + <property name="xalign">1.0</property> + <style> + <class name="caption"/> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c new file mode 100644 index 0000000..19bb256 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c @@ -0,0 +1,300 @@ +/* gtd-sidebar-provider-row.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdSidebarProviderRow" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-provider.h" +#include "gtd-sidebar-provider-row.h" + +struct _GtdSidebarProviderRow +{ + GtkListBoxRow parent; + + GtkWidget *loading_label; + GtkLabel *provider_label; + GtkStack *stack; + + GtdProvider *provider; +}; + +static void on_provider_changed_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebarProviderRow *self); + +static void on_provider_lists_changed_cb (GtdProvider *provider, + GtdTaskList *list, + GtdSidebarProviderRow *self); + +static void on_provider_notify_loading_cb (GtdProvider *provider, + GParamSpec *pspec, + GtdSidebarProviderRow *self); + +G_DEFINE_TYPE (GtdSidebarProviderRow, gtd_sidebar_provider_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_PROVIDER, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Auxiliary methods + */ + +static void +update_provider_label (GtdSidebarProviderRow *self) +{ + g_autoptr (GList) providers = NULL; + GtdManager *manager; + GList *l; + gboolean is_unique; + const gchar *title; + + manager = gtd_manager_get_default (); + providers = gtd_manager_get_providers (manager); + is_unique = TRUE; + + /* + * We need to check if there is another provider with + * the same GType of the provider. + */ + for (l = providers; l; l = l->next) + { + GtdProvider *provider = l->data; + + if (self->provider != provider && + g_str_equal (gtd_provider_get_provider_type (self->provider), gtd_provider_get_provider_type (provider))) + { + is_unique = FALSE; + break; + } + } + + if (is_unique) + title = gtd_provider_get_name (self->provider); + else + title = gtd_provider_get_description (self->provider); + + gtk_label_set_label (self->provider_label, title); +} + +static void +update_loading_state (GtdSidebarProviderRow *self) +{ + g_autoptr (GList) lists = NULL; + gboolean is_loading; + gboolean has_lists; + + g_assert (self->provider != NULL); + + lists = gtd_provider_get_task_lists (self->provider); + is_loading = gtd_object_get_loading (GTD_OBJECT (self->provider)); + has_lists = lists != NULL; + + GTD_TRACE_MSG ("'%s' (%s): is_loading: %d, has_lists: %d", + gtd_provider_get_name (self->provider), + gtd_provider_get_id (self->provider), + is_loading, + has_lists); + + gtk_stack_set_visible_child_name (self->stack, is_loading ? "spinner" : "empty"); + gtk_widget_set_visible (self->loading_label, is_loading && !has_lists); +} + +static void +set_provider (GtdSidebarProviderRow *self, + GtdProvider *provider) +{ + GtdManager *manager; + + g_assert (provider != NULL); + g_assert (self->provider == NULL); + + self->provider = g_object_ref (provider); + + g_object_bind_property (provider, + "enabled", + self, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + /* Setup the title label */ + manager = gtd_manager_get_default (); + + g_signal_connect_object (manager, "provider-added", G_CALLBACK (on_provider_changed_cb), self, 0); + g_signal_connect_object (manager, "provider-removed", G_CALLBACK (on_provider_changed_cb), self, 0); + + update_provider_label (self); + + /* And the icon */ + g_signal_connect_object (provider, "notify::loading", G_CALLBACK (on_provider_notify_loading_cb), self, 0); + g_signal_connect_object (provider, "list-added", G_CALLBACK (on_provider_lists_changed_cb), self, 0); + g_signal_connect_object (provider, "list-removed", G_CALLBACK (on_provider_lists_changed_cb), self, 0); + + update_loading_state (self); +} + + +/* + * Callbacks + */ + +static void +on_provider_changed_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebarProviderRow *self) +{ + update_provider_label (self); +} + +static void +on_provider_lists_changed_cb (GtdProvider *provider, + GtdTaskList *list, + GtdSidebarProviderRow *self) +{ + update_loading_state (self); +} + +static void +on_provider_notify_loading_cb (GtdProvider *provider, + GParamSpec *pspec, + GtdSidebarProviderRow *self) +{ + update_loading_state (self); +} + + +/* + * GObject overrides + */ + +static void +gtd_sidebar_provider_row_finalize (GObject *object) +{ + GtdSidebarProviderRow *self = (GtdSidebarProviderRow *)object; + + g_clear_object (&self->provider); + + G_OBJECT_CLASS (gtd_sidebar_provider_row_parent_class)->finalize (object); +} + +static void +gtd_sidebar_provider_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdSidebarProviderRow *self = GTD_SIDEBAR_PROVIDER_ROW (object); + + switch (prop_id) + { + case PROP_PROVIDER: + g_value_set_object (value, self->provider); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_sidebar_provider_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtdSidebarProviderRow *self = GTD_SIDEBAR_PROVIDER_ROW (object); + + switch (prop_id) + { + case PROP_PROVIDER: + set_provider (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +/* + * GtkWidget overrides + */ + +static void +gtd_sidebar_provider_row_class_init (GtdSidebarProviderRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtd_sidebar_provider_row_finalize; + object_class->get_property = gtd_sidebar_provider_row_get_property; + object_class->set_property = gtd_sidebar_provider_row_set_property; + + properties[PROP_PROVIDER] = g_param_spec_object ("provider", + "Provider", + "Provider of the row", + GTD_TYPE_PROVIDER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebarProviderRow, loading_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarProviderRow, provider_label); + gtk_widget_class_bind_template_child (widget_class, GtdSidebarProviderRow, stack); +} + +static void +gtd_sidebar_provider_row_init (GtdSidebarProviderRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtd_sidebar_provider_row_new (GtdProvider *provider) +{ + return g_object_new (GTD_TYPE_SIDEBAR_PROVIDER_ROW, + "provider", provider, + NULL); +} + +GtdProvider* +gtd_sidebar_provider_row_get_provider (GtdSidebarProviderRow *self) +{ + g_return_val_if_fail (GTD_IS_SIDEBAR_PROVIDER_ROW (self), NULL); + + return self->provider; +} + +void +gtd_sidebar_provider_row_popup_menu (GtdSidebarProviderRow *self) +{ + g_assert (GTD_IS_SIDEBAR_PROVIDER_ROW (self)); + + /* TODO: Implement me */ +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h new file mode 100644 index 0000000..640e6b2 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h @@ -0,0 +1,39 @@ +/* gtd-sidebar-provider-row.h + * + * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR_PROVIDER_ROW (gtd_sidebar_provider_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtdSidebarProviderRow, gtd_sidebar_provider_row, GTD, SIDEBAR_PROVIDER_ROW, GtkListBoxRow) + +GtkWidget* gtd_sidebar_provider_row_new (GtdProvider *provider); + +GtdProvider* gtd_sidebar_provider_row_get_provider (GtdSidebarProviderRow *self); + +void gtd_sidebar_provider_row_popup_menu (GtdSidebarProviderRow *self); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui new file mode 100644 index 0000000..e97e318 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="GtdSidebarProviderRow" parent="GtkListBoxRow"> + <property name="can_focus">1</property> + <property name="selectable">0</property> + <property name="activatable">0</property> + <property name="margin_top">6</property> + <child> + <object class="GtkBox"> + <property name="margin_top">6</property> + <property name="spacing">3</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="margin_start">6</property> + <property name="margin_end">6</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel" id="provider_label"> + <property name="hexpand">1</property> + <property name="xalign">0.0</property> + <style> + <class name="caption-heading"/> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="transition-type">crossfade</property> + <child> + <object class="GtkStackPage"> + <property name="name">spinner</property> + <property name="child"> + <object class="GtkSpinner"> + <property name="spinning">true</property> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="name">empty</property> + <property name="child"> + <object class="GtkBox"/> + </property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkSeparator"/> + </child> + <child> + <object class="GtkLabel" id="loading_label"> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="margin-top">6</property> + <property name="margin-bottom">12</property> + <property name="xalign">0.0</property> + <property name="label" translatable="yes">Loading…</property> + <style> + <class name="caption"/> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/plugins/task-lists-workspace/gtd-sidebar.c b/src/plugins/task-lists-workspace/gtd-sidebar.c new file mode 100644 index 0000000..0f5760b --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar.c @@ -0,0 +1,929 @@ +/* gtd-sidebar.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdSidebar" + +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-max-size-layout.h" +#include "gtd-notification.h" +#include "gtd-panel.h" +#include "gtd-provider.h" +#include "gtd-sidebar.h" +#include "gtd-sidebar-list-row.h" +#include "gtd-sidebar-panel-row.h" +#include "gtd-sidebar-provider-row.h" +#include "gtd-task-list.h" +#include "gtd-task-list-panel.h" +#include "gtd-utils.h" + +#include <glib/gi18n.h> + +struct _GtdSidebar +{ + GtdWidget parent; + + GtkListBox *archive_listbox; + GtkListBoxRow *archive_row; + GtkListBox *listbox; + GtkStack *stack; + + GtkStack *panel_stack; + GtdPanel *task_list_panel; + + GSimpleActionGroup *action_group; +}; + +G_DEFINE_TYPE (GtdSidebar, gtd_sidebar, GTD_TYPE_WIDGET) + + +/* + * Auxiliary methods + */ + +static gboolean +activate_row_below (GtdSidebar *self, + GtdSidebarListRow *current_row) +{ + GtkWidget *next_row; + GtkWidget *parent; + GtkWidget *child; + gboolean after_deleted; + + parent = gtk_widget_get_parent (GTK_WIDGET (current_row)); + after_deleted = FALSE; + next_row = NULL; + + for (child = gtk_widget_get_first_child (parent); + child; + child = gtk_widget_get_next_sibling (child)) + { + if (child == (GtkWidget*) current_row) + { + after_deleted = TRUE; + continue; + } + + if (!gtk_widget_get_visible (child) || + !gtk_list_box_row_get_activatable (GTK_LIST_BOX_ROW (child))) + { + continue; + } + + next_row = child; + + if (after_deleted) + break; + } + + if (next_row) + g_signal_emit_by_name (next_row, "activate"); + + return next_row != NULL; +} + +static void +add_task_list (GtdSidebar *self, + GtdTaskList *list) +{ + if (gtd_task_list_is_inbox (list)) + return; + + g_debug ("Adding task list '%s'", gtd_task_list_get_name (list)); + + if (!gtd_task_list_get_archived (list)) + { + gtk_list_box_prepend (self->listbox, gtd_sidebar_list_row_new (list)); + gtk_list_box_invalidate_filter (self->listbox); + } + else + { + gtk_list_box_prepend (self->archive_listbox, gtd_sidebar_list_row_new (list)); + gtk_list_box_invalidate_filter (self->archive_listbox); + } +} + +static void +add_panel (GtdSidebar *self, + GtdPanel *panel) +{ + GtkWidget *row; + + g_debug ("Adding panel '%s'", gtd_panel_get_panel_name (panel)); + + row = gtd_sidebar_panel_row_new (panel); + + gtk_list_box_prepend (self->listbox, row); +} + +static void +add_provider (GtdSidebar *self, + GtdProvider *provider) +{ + g_debug ("Adding provider '%s'", gtd_provider_get_name (provider)); + + gtk_list_box_prepend (self->listbox, gtd_sidebar_provider_row_new (provider)); + gtk_list_box_prepend (self->archive_listbox, gtd_sidebar_provider_row_new (provider)); +} + +static gint +compare_panels (GtdSidebarPanelRow *row_a, + GtdSidebarPanelRow *row_b) +{ + GtdPanel *panel_a; + GtdPanel *panel_b; + + panel_a = gtd_sidebar_panel_row_get_panel (row_a); + panel_b = gtd_sidebar_panel_row_get_panel (row_b); + + return gtd_panel_get_priority (panel_b) - gtd_panel_get_priority (panel_a); +} + +static gint +compare_providers (GtdSidebarProviderRow *row_a, + GtdSidebarProviderRow *row_b) +{ + GtdProvider *provider_a; + GtdProvider *provider_b; + + provider_a = gtd_sidebar_provider_row_get_provider (row_a); + provider_b = gtd_sidebar_provider_row_get_provider (row_b); + + return gtd_provider_compare (provider_a, provider_b); +} + +static gint +compare_lists (GtdSidebarListRow *row_a, + GtdSidebarListRow *row_b) +{ + GtdTaskList *list_a; + GtdTaskList *list_b; + gint result; + + list_a = gtd_sidebar_list_row_get_task_list (row_a); + list_b = gtd_sidebar_list_row_get_task_list (row_b); + + /* First, compare by their providers */ + result = gtd_provider_compare (gtd_task_list_get_provider (list_a), gtd_task_list_get_provider (list_b)); + + if (result != 0) + return result; + + return gtd_collate_compare_strings (gtd_task_list_get_name (list_a), gtd_task_list_get_name (list_b)); +} + +typedef gpointer (*GetDataFunc) (gpointer data); + +static gpointer +get_row_internal (GtdSidebar *self, + GtkListBox *listbox, + GType type, + GetDataFunc get_data_func, + gpointer data) +{ + GtkWidget *child; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (listbox)); + child; + child = gtk_widget_get_next_sibling (child)) + { + if (g_type_is_a (G_OBJECT_TYPE (child), type) && get_data_func (child) == data) + return child; + } + + return NULL; +} + +static GtkListBoxRow* +get_row_for_panel (GtdSidebar *self, + GtdPanel *panel) +{ + return get_row_internal (self, + self->listbox, + GTD_TYPE_SIDEBAR_PANEL_ROW, + (GetDataFunc) gtd_sidebar_panel_row_get_panel, + panel); +} + +static GtkListBoxRow* +get_row_for_provider (GtdSidebar *self, + GtkListBox *listbox, + GtdProvider *provider) +{ + return get_row_internal (self, + listbox, + GTD_TYPE_SIDEBAR_PROVIDER_ROW, + (GetDataFunc) gtd_sidebar_provider_row_get_provider, + provider); +} + +static GtkListBoxRow* +get_row_for_task_list (GtdSidebar *self, + GtkListBox *listbox, + GtdTaskList *list) +{ + return get_row_internal (self, + listbox, + GTD_TYPE_SIDEBAR_LIST_ROW, + (GetDataFunc) gtd_sidebar_list_row_get_task_list, + list); +} + +static void +activate_appropriate_row (GtdSidebar *self, + GtkListBoxRow *row) +{ + GtkListBoxRow *to_be_activated; + + if (activate_row_below (self, GTD_SIDEBAR_LIST_ROW (row))) + return; + + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.toggle-archive", + "b", + FALSE); + + to_be_activated = gtk_list_box_get_row_at_index (self->listbox, 0); + g_signal_emit_by_name (to_be_activated, "activate"); +} + +/* + * Callbacks + */ + +static void +on_action_move_up_activated_cb (GSimpleAction *simple, + GVariant *parameters, + gpointer user_data) +{ + GtkListBoxRow *selected_row; + GtkListBoxRow *previous_row; + GtdSidebar *self; + gint selected_row_index; + + GTD_ENTRY; + + self = GTD_SIDEBAR (user_data); + selected_row = gtk_list_box_get_selected_row (self->listbox); + g_assert (selected_row != NULL); + + selected_row_index = gtk_list_box_row_get_index (selected_row); + if (selected_row_index == 0) + return; + + do + { + previous_row = gtk_list_box_get_row_at_index (self->listbox, + --selected_row_index); + } + while (previous_row && + (previous_row == self->archive_row || + !gtk_list_box_row_get_activatable (previous_row))); + + + if (previous_row) + g_signal_emit_by_name (previous_row, "activate"); + + GTD_EXIT; +} + +static void +on_action_move_down_activated_cb (GSimpleAction *simple, + GVariant *parameters, + gpointer user_data) +{ + GtkListBoxRow *selected_row; + GtkListBoxRow *next_row; + GtdSidebar *self; + gint selected_row_index; + + GTD_ENTRY; + + self = GTD_SIDEBAR (user_data); + selected_row = gtk_list_box_get_selected_row (self->listbox); + g_assert (selected_row != NULL); + + selected_row_index = gtk_list_box_row_get_index (selected_row); + + do + { + next_row = gtk_list_box_get_row_at_index (self->listbox, + ++selected_row_index); + } + while (next_row && + (next_row == self->archive_row || + !gtk_list_box_row_get_activatable (next_row))); + + + if (next_row) + g_signal_emit_by_name (next_row, "activate"); + + GTD_EXIT; +} + +static void +on_panel_added_cb (GtdManager *manager, + GtdPanel *panel, + GtdSidebar *self) +{ + add_panel (self, panel); +} + +static void +on_panel_removed_cb (GtdManager *manager, + GtdPanel *panel, + GtdSidebar *self) +{ + GtkListBoxRow *row = get_row_for_panel (self, panel); + + g_debug ("Removing panel '%s'", gtd_panel_get_panel_name (panel)); + + if (row) + gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); +} + +static void +on_provider_task_list_removed_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_remove_task_list_finish (GTD_PROVIDER (source), result, &error); +} + +static void +delete_list_cb (GtdNotification *notification, + gpointer user_data) +{ + GtdTaskList *list; + GtdProvider *provider; + + list = GTD_TASK_LIST (user_data); + provider = gtd_task_list_get_provider (list); + + g_assert (provider != NULL); + g_assert (gtd_task_list_is_removable (list)); + + gtd_provider_remove_task_list (provider, + list, + NULL, + on_provider_task_list_removed_cb, + NULL); +} + +static void +undo_delete_list_cb (GtdNotification *notification, + gpointer user_data) +{ + g_assert (GTD_IS_SIDEBAR_LIST_ROW (user_data)); + + gtk_widget_show (GTK_WIDGET (user_data)); +} + +static void +on_task_list_panel_list_deleted_cb (GtdTaskListPanel *panel, + GtdTaskList *list, + GtdSidebar *self) +{ + GtdSidebarListRow *row; + GtdNotification *notification; + g_autofree gchar *title = NULL; + + if (gtd_task_list_get_archived (list)) + row = (GtdSidebarListRow*) get_row_for_task_list (self, self->archive_listbox, list); + else + row = (GtdSidebarListRow*) get_row_for_task_list (self, self->listbox, list); + + g_assert (row != NULL && GTD_IS_SIDEBAR_LIST_ROW (row)); + + GTD_TRACE_MSG ("Removing task list row from sidebar"); + + title = g_strdup_printf (_("Task list <b>%s</b> removed"), gtd_task_list_get_name (list)); + notification = gtd_notification_new (title); + gtd_notification_set_dismissal_action (notification, delete_list_cb, list); + gtd_notification_set_secondary_action (notification, _("Undo"), undo_delete_list_cb, row); + + gtd_manager_send_notification (gtd_manager_get_default (), notification); + + /* + * If the deleted list is selected, go to the next one (or previous, if + * there are no other task list after this one). + */ + if (gtk_list_box_row_is_selected (GTK_LIST_BOX_ROW (row))) + activate_appropriate_row (self, GTK_LIST_BOX_ROW (row)); + + gtk_widget_hide (GTK_WIDGET (row)); +} + +static void +on_listbox_row_activated_cb (GtkListBox *panels_listbox, + GtkListBoxRow *row, + GtdSidebar *self) +{ + if (GTD_IS_SIDEBAR_PANEL_ROW (row)) + { + GtdPanel *panel = gtd_sidebar_panel_row_get_panel (GTD_SIDEBAR_PANEL_ROW (row)); + + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.activate-panel", + "(sv)", + gtd_panel_get_panel_name (panel), + g_variant_new_maybe (G_VARIANT_TYPE_VARIANT, NULL)); + } + else if (GTD_IS_SIDEBAR_PROVIDER_ROW (row)) + { + /* Do nothing */ + } + else if (GTD_IS_SIDEBAR_LIST_ROW (row)) + { + GVariantBuilder builder; + GtdProvider *provider; + GtdTaskList *list; + + list = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row)); + provider = gtd_task_list_get_provider (list); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, "{sv}", + "provider-id", + g_variant_new_string (gtd_provider_get_id (provider))); + g_variant_builder_add (&builder, "{sv}", + "task-list-id", + g_variant_new_string (gtd_object_get_uid (GTD_OBJECT (list)))); + + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.activate-panel", + "(sv)", + "task-list-panel", + g_variant_builder_end (&builder)); + } + else if (row == self->archive_row) + { + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.toggle-archive", + "b", + TRUE); + } + else + { + g_assert_not_reached (); + } +} + +static void +on_panel_stack_visible_child_changed_cb (GtkStack *panel_stack, + GParamSpec *pspec, + GtdSidebar *self) +{ + GtkListBoxRow *panel_row; + GtkListBox *listbox; + GtdPanel *visible_panel; + + GTD_ENTRY; + + g_assert (GTD_IS_PANEL (gtk_stack_get_visible_child (panel_stack))); + + visible_panel = GTD_PANEL (gtk_stack_get_visible_child (panel_stack)); + listbox = self->listbox; + + /* + * If the currently visible panel is the tasklist panel, we + * should choose the tasklist that is visible. Otherwise, + * just select the panel. + */ + if (visible_panel == self->task_list_panel) + { + GtdTaskList *task_list; + + task_list = gtd_task_list_panel_get_task_list (GTD_TASK_LIST_PANEL (self->task_list_panel)); + g_assert (task_list != NULL); + + panel_row = get_row_for_task_list (self, self->listbox, task_list); + + if (!panel_row) + { + panel_row = get_row_for_task_list (self, self->archive_listbox, task_list); + listbox = self->archive_listbox; + } + } + else + { + panel_row = get_row_for_panel (self, visible_panel); + } + + /* Select the row if it's not already selected*/ + if (!gtk_list_box_row_is_selected (panel_row)) + gtk_list_box_select_row (listbox, panel_row); + + GTD_EXIT; +} + +static void +on_provider_added_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebar *self) +{ + add_provider (self, provider); +} + +static void +on_provider_removed_cb (GtdManager *manager, + GtdProvider *provider, + GtdSidebar *self) +{ + GtkListBoxRow *row; + + g_debug ("Removing provider '%s'", gtd_provider_get_name (provider)); + + row = get_row_for_provider (self, self->listbox, provider); + gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); + + row = get_row_for_provider (self, self->archive_listbox, provider); + gtk_list_box_remove (self->archive_listbox, GTK_WIDGET (row)); +} + + +static void +on_task_list_added_cb (GtdManager *manager, + GtdTaskList *list, + GtdSidebar *self) +{ + add_task_list (self, list); +} + +static void +on_task_list_changed_cb (GtdManager *manager, + GtdTaskList *list, + GtdSidebar *self) +{ + GtkListBoxRow *row; + GtkListBox *listbox; + gboolean archived; + + archived = gtd_task_list_get_archived (list); + listbox = archived ? self->archive_listbox : self->listbox; + row = get_row_for_task_list (self, listbox, list); + + /* + * The task was either archived or unarchived; remove it and add to + * the appropriate listbox. + */ + if (!row) + { + listbox = archived ? self->listbox : self->archive_listbox; + row = get_row_for_task_list (self, listbox, list); + + if (!row) + goto out; + + /* Change to another panel or taklist */ + if (gtk_list_box_row_is_selected (row)) + activate_appropriate_row (self, row); + + /* Destroy the old row */ + gtk_list_box_remove (listbox, GTK_WIDGET (row)); + + /* Add a new row */ + add_task_list (self, list); + } + +out: + gtk_list_box_invalidate_filter (listbox); +} + +static void +on_task_list_removed_cb (GtdManager *manager, + GtdTaskList *list, + GtdSidebar *self) +{ + GtkListBoxRow *row; + GtkListBox *listbox; + + g_debug ("Removing task list '%s'", gtd_task_list_get_name (list)); + + g_assert (!gtd_task_list_is_inbox (list)); + + if (!gtd_task_list_get_archived (list)) + listbox = self->listbox; + else + listbox = self->archive_listbox; + + row = get_row_for_task_list (self, listbox, list); + if (!row) + return; + + gtk_list_box_remove (listbox, GTK_WIDGET (row)); + gtk_list_box_invalidate_filter (listbox); +} + +static gboolean +filter_archive_listbox_cb (GtkListBoxRow *row, + gpointer user_data) +{ + if (GTD_IS_SIDEBAR_LIST_ROW (row)) + { + GtdTaskList *list; + + list = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row)); + return gtd_task_list_get_archived (list); + } + else if (GTD_IS_SIDEBAR_PROVIDER_ROW (row)) + { + g_autoptr (GList) lists = NULL; + GtdProvider *provider; + GList *l; + + provider = gtd_sidebar_provider_row_get_provider (GTD_SIDEBAR_PROVIDER_ROW (row)); + lists = gtd_provider_get_task_lists (provider); + + for (l = lists; l; l = l->next) + { + if (gtd_task_list_get_archived (l->data)) + return TRUE; + } + + return FALSE; + } + else + { + g_assert_not_reached (); + } + + return FALSE; +} + +static gboolean +filter_listbox_cb (GtkListBoxRow *row, + gpointer user_data) +{ + GtdTaskList *list; + + if (!GTD_IS_SIDEBAR_LIST_ROW (row)) + return TRUE; + + list = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row)); + return !gtd_task_list_get_archived (list); +} + +static gint +sort_listbox_cb (GtkListBoxRow *row_a, + GtkListBoxRow *row_b, + gpointer user_data) +{ + GtdSidebar *self = GTD_SIDEBAR (user_data); + + /* Special-case the Archive row */ + if (row_a == self->archive_row || row_b == self->archive_row) + { + if (GTD_IS_SIDEBAR_PANEL_ROW (row_b)) + return 1; + else + return -1; + } + + if (G_OBJECT_TYPE (row_a) != G_OBJECT_TYPE (row_b)) + { + gint result; + + /* Panels go above everything else */ + if (GTD_IS_SIDEBAR_PANEL_ROW (row_b) != GTD_IS_SIDEBAR_PANEL_ROW (row_a)) + return GTD_IS_SIDEBAR_PANEL_ROW (row_b) - GTD_IS_SIDEBAR_PANEL_ROW (row_a); + + /* + * At this point, we know that row_a and row_b are either provider rows, or + * tasklist rows. We also know that they're different, i.e. if row_a is a + * provider row, row_b will be a list one, and vice-versa. + */ + if (GTD_IS_SIDEBAR_PROVIDER_ROW (row_a)) + { + GtdProvider *provider_a; + GtdTaskList *list_b; + + provider_a = gtd_sidebar_provider_row_get_provider (GTD_SIDEBAR_PROVIDER_ROW (row_a)); + list_b = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row_b)); + + /* + * If the providers are different, respect the provider order. If the providers are the + * same, we must put the provider row above the tasklist row. + */ + result = gtd_provider_compare (provider_a, gtd_task_list_get_provider (list_b)); + + if (result != 0) + return result; + + return -1; + } + else + { + GtdTaskList *list_a; + GtdProvider *provider_b; + + list_a = gtd_sidebar_list_row_get_task_list (GTD_SIDEBAR_LIST_ROW (row_a)); + provider_b = gtd_sidebar_provider_row_get_provider (GTD_SIDEBAR_PROVIDER_ROW (row_b)); + + /* See comment above */ + result = gtd_provider_compare (gtd_task_list_get_provider (list_a), provider_b); + + if (result != 0) + return result; + + return 1; + } + } + else + { + /* + * We only reach this section of the code if both rows are of the same type, + * so it doesn't matter which one we get the type from. + */ + + if (GTD_IS_SIDEBAR_PANEL_ROW (row_a)) + return compare_panels (GTD_SIDEBAR_PANEL_ROW (row_a), GTD_SIDEBAR_PANEL_ROW (row_b)); + + if (GTD_IS_SIDEBAR_PROVIDER_ROW (row_a)) + return compare_providers (GTD_SIDEBAR_PROVIDER_ROW (row_a), GTD_SIDEBAR_PROVIDER_ROW (row_b)); + + if (GTD_IS_SIDEBAR_LIST_ROW (row_a)) + return compare_lists (GTD_SIDEBAR_LIST_ROW (row_a), GTD_SIDEBAR_LIST_ROW (row_b)); + } + + return 0; +} + + +/* + * GObject overrides + */ + +static void +gtd_sidebar_constructed (GObject *object) +{ + g_autoptr (GList) providers = NULL; + GListModel *lists; + GtdManager *manager; + GtdSidebar *self; + GList *l; + guint i; + + self = (GtdSidebar *)object; + manager = gtd_manager_get_default (); + + G_OBJECT_CLASS (gtd_sidebar_parent_class)->constructed (object); + + /* Add providers */ + providers = gtd_manager_get_providers (manager); + + for (l = providers; l; l = l->next) + add_provider (self, l->data); + + g_signal_connect (manager, "provider-added", G_CALLBACK (on_provider_added_cb), self); + g_signal_connect (manager, "provider-removed", G_CALLBACK (on_provider_removed_cb), self); + + /* Add task lists */ + lists = gtd_manager_get_task_lists_model (manager); + + for (i = 0; i < g_list_model_get_n_items (lists); i++) + { + g_autoptr (GtdTaskList) list = g_list_model_get_item (lists, i); + + add_task_list (self, list); + } + + g_signal_connect (manager, "list-added", G_CALLBACK (on_task_list_added_cb), self); + g_signal_connect (manager, "list-changed", G_CALLBACK (on_task_list_changed_cb), self); + g_signal_connect (manager, "list-removed", G_CALLBACK (on_task_list_removed_cb), self); +} + +static void +gtd_sidebar_class_init (GtdSidebarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = gtd_sidebar_constructed; + + g_type_ensure (GTD_TYPE_MAX_SIZE_LAYOUT); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-sidebar.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, archive_listbox); + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, archive_row); + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, listbox); + gtk_widget_class_bind_template_child (widget_class, GtdSidebar, stack); + + gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb); + + gtk_widget_class_set_css_name (widget_class, "sidebar"); +} + +static void +gtd_sidebar_init (GtdSidebar *self) +{ + static const GActionEntry entries[] = { + { "move-up", on_action_move_up_activated_cb }, + { "move-down", on_action_move_down_activated_cb }, + }; + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_sort_func (self->listbox, sort_listbox_cb, self, NULL); + gtk_list_box_set_filter_func (self->listbox, filter_listbox_cb, self, NULL); + + gtk_list_box_set_sort_func (self->archive_listbox, sort_listbox_cb, self, NULL); + gtk_list_box_set_filter_func (self->archive_listbox, filter_archive_listbox_cb, self, NULL); + + self->action_group = g_simple_action_group_new (); + + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + entries, + G_N_ELEMENTS (entries), + self); + + gtk_widget_insert_action_group (GTK_WIDGET (self), + "sidebar", + G_ACTION_GROUP (self->action_group)); +} + +void +gtd_sidebar_set_panel_stack (GtdSidebar *self, + GtkStack *stack) +{ + g_return_if_fail (GTD_IS_SIDEBAR (self)); + g_return_if_fail (GTK_IS_STACK (stack)); + + g_assert (self->panel_stack == NULL); + + self->panel_stack = g_object_ref (stack); + + g_signal_connect_object (stack, + "notify::visible-child", + G_CALLBACK (on_panel_stack_visible_child_changed_cb), + self, + 0); +} + + +void +gtd_sidebar_set_task_list_panel (GtdSidebar *self, + GtdPanel *task_list_panel) +{ + g_return_if_fail (GTD_IS_SIDEBAR (self)); + g_return_if_fail (GTD_IS_PANEL (task_list_panel)); + + g_assert (self->task_list_panel == NULL); + + self->task_list_panel = g_object_ref (task_list_panel); + g_signal_connect_object (self->task_list_panel, + "list-deleted", + G_CALLBACK (on_task_list_panel_list_deleted_cb), + self, + 0); +} + +void +gtd_sidebar_activate (GtdSidebar *self) +{ + GtkListBoxRow *first_row; + + g_assert (GTD_IS_SIDEBAR (self)); + + first_row = gtk_list_box_get_row_at_index (self->listbox, 0); + g_signal_emit_by_name (first_row, "activate"); +} + +void +gtd_sidebar_set_archive_visible (GtdSidebar *self, + gboolean show_archive) +{ + g_assert (GTD_IS_SIDEBAR (self)); + + if (show_archive) + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->archive_listbox)); + else + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->listbox)); +} + +void +gtd_sidebar_connect (GtdSidebar *self, + GtkWidget *workspace) +{ + g_signal_connect (workspace, "panel-added", G_CALLBACK (on_panel_added_cb), self); + g_signal_connect (workspace, "panel-removed", G_CALLBACK (on_panel_removed_cb), self); +} diff --git a/src/plugins/task-lists-workspace/gtd-sidebar.h b/src/plugins/task-lists-workspace/gtd-sidebar.h new file mode 100644 index 0000000..d85a996 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar.h @@ -0,0 +1,44 @@ +/* gtd-sidebar.h + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_SIDEBAR (gtd_sidebar_get_type()) +G_DECLARE_FINAL_TYPE (GtdSidebar, gtd_sidebar, GTD, SIDEBAR, GtdWidget) + +void gtd_sidebar_set_panel_stack (GtdSidebar *self, + GtkStack *stack); + +void gtd_sidebar_set_task_list_panel (GtdSidebar *self, + GtdPanel *task_list_panel); + +void gtd_sidebar_activate (GtdSidebar *self); + +void gtd_sidebar_set_archive_visible (GtdSidebar *self, + gboolean show_archive); + +void gtd_sidebar_connect (GtdSidebar *self, + GtkWidget *workspace); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-sidebar.ui b/src/plugins/task-lists-workspace/gtd-sidebar.ui new file mode 100644 index 0000000..356eb9c --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-sidebar.ui @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GtdSidebar" parent="GtdWidget"> + <property name="hexpand">0</property> + <property name="layout-manager"> + <object class="GtdMaxSizeLayout"> + <property name="width-chars">35</property> + <property name="max-width-chars">35</property> + </object> + </property> + + <child> + <object class="GtkShortcutController"> + <property name="name">Sidebar Keyboard Shortcuts</property> + <property name="scope">global</property> + <child> + <object class="GtkShortcut"> + <property name="trigger"><Control>Page_Down</property> + <property name="action">action(sidebar.move-down)</property> + </object> + </child> + <child> + <object class="GtkShortcut"> + <property name="trigger"><Alt>Down</property> + <property name="action">action(sidebar.move-down)</property> + </object> + </child> + <child> + <object class="GtkShortcut"> + <property name="trigger"><Control>Page_Up</property> + <property name="action">action(sidebar.move-up)</property> + </object> + </child> + <child> + <object class="GtkShortcut"> + <property name="trigger"><Alt>Up</property> + <property name="action">action(sidebar.move-up)</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkScrolledWindow"> + <property name="can_focus">1</property> + <property name="min-content-width">300</property> + <property name="hscrollbar-policy">never</property> + <child> + <object class="GtkStack" id="stack"> + <property name="hexpand">true</property> + <property name="vexpand">true</property> + <property name="hhomogeneous">true</property> + <property name="vhomogeneous">false</property> + <property name="transition-type">slide-left-right</property> + + <!-- Main Listbox --> + <child> + <object class="GtkStackPage"> + <property name="name">main</property> + <property name="child"> + <object class="GtkListBox" id="listbox"> + <property name="hexpand">true</property> + <property name="vexpand">true</property> + <property name="selection_mode">browse</property> + <signal name="row-activated" handler="on_listbox_row_activated_cb" object="GtdSidebar" swapped="no"/> + <style> + <class name="navigation-sidebar"/> + </style> + + <!-- Archive row --> + <child> + <object class="GtkListBoxRow" id="archive_row"> + <property name="can_focus">1</property> + <child> + <object class="GtkBox"> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">6</property> + <property name="margin-end">6</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage"> + <property name="icon-name">folder-symbolic</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" comments="Translators: 'archived' as in 'archived task lists'">Archived</property> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + + </object> + </property> + </object> + </child> + + <!-- Archived lists --> + <child> + <object class="GtkStackPage"> + <property name="name">archive</property> + <property name="child"> + <object class="GtkListBox" id="archive_listbox"> + <property name="hexpand">true</property> + <property name="vexpand">true</property> + <property name="selection_mode">browse</property> + <signal name="row-activated" handler="on_listbox_row_activated_cb" object="GtdSidebar" swapped="no"/> + <style> + <class name="navigation-sidebar"/> + </style> + + <child type="placeholder"> + <object class="AdwStatusPage"> + <property name="title" translatable="yes">No Archived Lists</property> + <property name="icon_name">folder-symbolic</property> + <style> + <class name="compact"/> + </style> + </object> + </child> + </object> + </property> + </object> + </child> + + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/plugins/task-lists-workspace/gtd-task-list-panel.c b/src/plugins/task-lists-workspace/gtd-task-list-panel.c new file mode 100644 index 0000000..111c777 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-list-panel.c @@ -0,0 +1,636 @@ +/* gtd-task-list-panel.c + * + * Copyright 2018-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdTaskListPanel" + +#include <glib/gi18n.h> + +#include "gtd-color-button.h" +#include "gtd-debug.h" +#include "gtd-manager.h" +#include "gtd-panel.h" +#include "gtd-provider.h" +#include "gtd-task-list.h" +#include "gtd-task-list-panel.h" +#include "gtd-task-list-view.h" +#include "gtd-utils.h" + +struct _GtdTaskListPanel +{ + GtkBox parent; + + GtkButton *archive_button; + GtkFlowBox *colors_flowbox; + GtkPopover *popover; + GtkStack *popover_stack; + GtkWidget *rename_button; + GtkEditable *rename_entry; + GtdTaskListView *task_list_view; + + GtkWidget *previous_color_button; +}; + + +static void on_colors_flowbox_child_activated_cb (GtkFlowBox *colors_flowbox, + GtkFlowBoxChild *child, + GtdTaskListPanel *self); + +static void on_task_list_updated_cb (GObject *source, + GAsyncResult *result, + gpointer user_data); + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + + +G_DEFINE_TYPE_WITH_CODE (GtdTaskListPanel, gtd_task_list_panel, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, gtd_panel_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + +enum +{ + LIST_DELETED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS] = { 0, }; + +/* + * Auxiliary methods + */ + +static const gchar * const colors[] = +{ + "#3584e4", + "#33d17a", + "#f6d32d", + "#ff7800", + "#e01b24", + "#9141ac", + "#986a44", + "#3d3846", + "#ffffff", +}; + +static void +populate_color_grid (GtdTaskListPanel *self) +{ + guint i; + + for (i = 0; i < G_N_ELEMENTS (colors); i++) + { + GtkWidget *button; + GdkRGBA color; + + gdk_rgba_parse (&color, colors[i]); + + button = gtd_color_button_new (&color); + gtk_widget_set_size_request (button, -1, 24); + + gtk_flow_box_insert (self->colors_flowbox, button, -1); + } +} + +static void +update_selected_color (GtdTaskListPanel *self) +{ + g_autoptr (GdkRGBA) color = NULL; + GtdTaskList *list; + GtkWidget *button; + guint i; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + color = gtd_task_list_get_color (list); + button = NULL; + + for (i = 0; i < G_N_ELEMENTS (colors); i++) + { + GdkRGBA c; + + gdk_rgba_parse (&c, colors[i]); + + if (gdk_rgba_equal (&c, color)) + { + button = GTK_WIDGET (gtk_flow_box_get_child_at_index (self->colors_flowbox, i)); + break; + } + } + + if (button) + { + g_signal_handlers_block_by_func (button, on_colors_flowbox_child_activated_cb, self); + g_signal_emit_by_name (button, "activate"); + g_signal_handlers_unblock_by_func (button, on_colors_flowbox_child_activated_cb, self); + } + else if (self->previous_color_button) + { + gtk_widget_unset_state_flags (self->previous_color_button, GTK_STATE_FLAG_SELECTED); + self->previous_color_button = NULL; + } +} + +static void +rename_list (GtdTaskListPanel *self) +{ + g_autofree gchar *new_name = NULL; + GtdTaskList *list; + + g_assert (gtk_widget_get_visible (GTK_WIDGET (self->popover))); + g_assert (g_utf8_validate (gtk_editable_get_text (self->rename_entry), -1, NULL)); + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + new_name = g_strdup (gtk_editable_get_text (self->rename_entry)); + new_name = g_strstrip (new_name); + + /* + * Even though the Rename button is insensitive, we may still reach here + * by activating the entry. + */ + if (!new_name || new_name[0] == '\0') + return; + + if (g_strcmp0 (new_name, gtd_task_list_get_name (list)) != 0) + { + gtd_task_list_set_name (list, new_name); + gtd_provider_update_task_list (gtd_task_list_get_provider (list), + list, + NULL, + on_task_list_updated_cb, + self); + } + + gtk_popover_popdown (self->popover); + gtk_editable_set_text (self->rename_entry, ""); +} + +static void +update_archive_button (GtdTaskListPanel *self) +{ + GtdTaskList *list; + gboolean archived; + + GTD_ENTRY; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + archived = gtd_task_list_get_archived (list); + g_object_set (self->archive_button, + "text", archived ? _("Unarchive") : _("Archive"), + NULL); + + GTD_EXIT; +} + + +/* + * Callbacks + */ + +static void +on_task_list_updated_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + gtd_provider_update_task_list_finish (GTD_PROVIDER (source), result, &error); + + if (error) + { + g_warning ("Error creating task: %s", error->message); + + gtd_manager_emit_error_message (gtd_manager_get_default (), + _("An error occurred while updating a task"), + error->message, + NULL, + NULL); + } +} + +static void +on_archive_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + GtdProvider *provider; + GtdTaskList *list; + gboolean archived; + + GTD_ENTRY; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + archived = gtd_task_list_get_archived (list); + gtd_task_list_set_archived (list, !archived); + + update_archive_button (self); + + provider = gtd_task_list_get_provider (list); + gtd_provider_update_task_list (provider, + list, + NULL, + on_task_list_updated_cb, + self); + + GTD_EXIT; +} + +static void +on_colors_flowbox_child_activated_cb (GtkFlowBox *colors_flowbox, + GtkFlowBoxChild *child, + GtdTaskListPanel *self) +{ + const GdkRGBA *color; + GtdTaskList *list; + GtkWidget *color_button; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + + g_assert (list != NULL); + + color_button = gtk_flow_box_child_get_child (child); + + if (self->previous_color_button == color_button) + return; + + gtk_widget_set_state_flags (color_button, GTK_STATE_FLAG_SELECTED, FALSE); + + if (self->previous_color_button) + gtk_widget_unset_state_flags (self->previous_color_button, GTK_STATE_FLAG_SELECTED); + + g_debug ("Setting new color for task list '%s'", gtd_task_list_get_name (list)); + + color = gtd_color_button_get_color (GTD_COLOR_BUTTON (color_button)); + gtd_task_list_set_color (list, color); + + gtd_provider_update_task_list (gtd_task_list_get_provider (list), + list, + NULL, + on_task_list_updated_cb, + self); + + self->previous_color_button = color_button; +} + +static void +on_delete_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + GtdTaskList *list; + + list = GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); + g_assert (list != NULL); + + GTD_TRACE_MSG ("Emitting GtdTaskListPanel:list-deleted"); + + g_signal_emit (self, signals[LIST_DELETED], 0, list); +} + +static void +on_go_to_rename_page_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + gtk_stack_set_visible_child_name (self->popover_stack, "rename"); +} + +static void +on_popover_hidden_cb (GtkPopover *popover, + GtdTaskListPanel *self) +{ + gtk_editable_set_text (self->rename_entry, ""); + gtk_stack_set_visible_child_name (self->popover_stack, "main"); +} + +static void +on_rename_button_clicked_cb (GtkButton *button, + GtdTaskListPanel *self) +{ + rename_list (self); +} + +static void +on_rename_entry_activated_cb (GtkEntry *entry, + GtdTaskListPanel *self) +{ + rename_list (self); +} + +static void +on_rename_entry_text_changed_cb (GtkEditable *entry, + GParamSpec *pspec, + GtdTaskListPanel *self) +{ + g_autofree gchar *new_name = NULL; + gboolean valid; + + new_name = g_strdup (gtk_editable_get_text (entry)); + new_name = g_strstrip (new_name); + + valid = new_name && new_name[0] != '\0'; + + gtk_widget_set_sensitive (self->rename_button, valid); +} + + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_task_list_panel_get_panel_name (GtdPanel *panel) +{ + return "task-list-panel"; +} + +static const gchar* +gtd_task_list_panel_get_panel_title (GtdPanel *panel) +{ + GtdTaskListPanel *self; + GtdTaskList *list; + + self = GTD_TASK_LIST_PANEL (panel); + list = (GtdTaskList *) gtd_task_list_view_get_model (self->task_list_view); + + return list ? gtd_task_list_get_name (list) : ""; +} + +static GList* +gtd_task_list_panel_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_task_list_panel_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_task_list_panel_get_icon (GtdPanel *panel) +{ + return NULL; +} + +static GtkPopover* +gtd_task_list_panel_get_popover (GtdPanel *panel) +{ + GtdTaskListPanel *self = GTD_TASK_LIST_PANEL (panel); + return self->popover; +} + + +static guint32 +gtd_task_list_panel_get_priority (GtdPanel *panel) +{ + return 0; +} + +static gchar* +gtd_task_list_panel_get_subtitle (GtdPanel *panel) +{ + return NULL; +} + +static void +gtd_task_list_panel_activate (GtdPanel *panel, + GVariant *parameters) +{ + GtdTaskListPanel *self; + GVariantDict dict; + GtdTaskList *list; + GListModel *model; + const gchar *task_list_id; + const gchar *provider_id; + guint i; + + GTD_ENTRY; + + self = GTD_TASK_LIST_PANEL (panel); + + /* + * The task list panel must receive an a{sv} and looks for: + * + * * provider-id: the id of the provider + * * task-list-id: the id of the task list + * + * So it can find the task list from the GtdManager. + */ + + g_variant_dict_init (&dict, parameters); + g_variant_dict_lookup (&dict, "provider-id", "&s", &provider_id); + g_variant_dict_lookup (&dict, "task-list-id", "&s", &task_list_id); + + GTD_TRACE_MSG ("Activating %s with 'provider-id': %s and 'task-list-id': %s", + G_OBJECT_TYPE_NAME (self), + provider_id, + task_list_id); + + model = gtd_manager_get_task_lists_model (gtd_manager_get_default ()); + list = NULL; + + for (i = 0; i < g_list_model_get_n_items (model); i++) + { + g_autoptr (GtdTaskList) task_list = NULL; + GtdProvider *provider; + + task_list = g_list_model_get_item (model, i); + if (g_strcmp0 (gtd_object_get_uid (GTD_OBJECT (task_list)), task_list_id) != 0) + continue; + + provider = gtd_task_list_get_provider (task_list); + if (g_strcmp0 (gtd_provider_get_id (provider), provider_id) != 0) + return; + + list = task_list; + break; + } + + g_assert (list != NULL); + + gtd_task_list_panel_set_task_list (self, list); + + GTD_EXIT; +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_task_list_panel_get_panel_name; + iface->get_panel_title = gtd_task_list_panel_get_panel_title; + iface->get_header_widgets = gtd_task_list_panel_get_header_widgets; + iface->get_menu = gtd_task_list_panel_get_menu; + iface->get_icon = gtd_task_list_panel_get_icon; + iface->get_popover = gtd_task_list_panel_get_popover; + iface->get_priority = gtd_task_list_panel_get_priority; + iface->get_subtitle = gtd_task_list_panel_get_subtitle; + iface->activate = gtd_task_list_panel_activate; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_list_panel_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, NULL); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, "task-list-panel"); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, 0); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, NULL); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (object))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_list_panel_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + + +static void +gtd_task_list_panel_class_init (GtdTaskListPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = gtd_task_list_panel_get_property; + object_class->set_property = gtd_task_list_panel_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); + + signals[LIST_DELETED] = g_signal_new ("list-deleted", + GTD_TYPE_TASK_LIST_PANEL, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_TASK_LIST); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-task-list-panel.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, archive_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, colors_flowbox); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, popover); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, popover_stack); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, rename_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, rename_entry); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, rename_entry); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListPanel, task_list_view); + + gtk_widget_class_bind_template_callback (widget_class, on_archive_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_colors_flowbox_child_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_delete_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_go_to_rename_page_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_popover_hidden_cb); + gtk_widget_class_bind_template_callback (widget_class, on_rename_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_rename_entry_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_rename_entry_text_changed_cb); +} + +static void +gtd_task_list_panel_init (GtdTaskListPanel *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + populate_color_grid (self); +} + +GtkWidget* +gtd_task_list_panel_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LIST_PANEL, NULL); +} + +GtdTaskList* +gtd_task_list_panel_get_task_list (GtdTaskListPanel *self) +{ + g_return_val_if_fail (GTD_IS_TASK_LIST_PANEL (self), NULL); + + return GTD_TASK_LIST (gtd_task_list_view_get_model (self->task_list_view)); +} + +void +gtd_task_list_panel_set_task_list (GtdTaskListPanel *self, + GtdTaskList *list) +{ + g_return_if_fail (GTD_IS_TASK_LIST_PANEL (self)); + g_return_if_fail (GTD_IS_TASK_LIST (list)); + + gtd_task_list_view_set_model (self->task_list_view, G_LIST_MODEL (list)); + + update_selected_color (self); + update_archive_button (self); + + g_object_notify (G_OBJECT (self), "title"); +} + diff --git a/src/plugins/task-lists-workspace/gtd-task-list-panel.h b/src/plugins/task-lists-workspace/gtd-task-list-panel.h new file mode 100644 index 0000000..1db7e70 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-list-panel.h @@ -0,0 +1,40 @@ +/* gtd-task-list-panel.h + * + * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gtd-types.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LIST_PANEL (gtd_task_list_panel_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListPanel, gtd_task_list_panel, GTD, TASK_LIST_PANEL, GtkBox) + +GtkWidget* gtd_task_list_panel_new (void); + +GtdTaskList* gtd_task_list_panel_get_task_list (GtdTaskListPanel *self); + +void gtd_task_list_panel_set_task_list (GtdTaskListPanel *self, + GtdTaskList *list); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-task-list-panel.ui b/src/plugins/task-lists-workspace/gtd-task-list-panel.ui new file mode 100644 index 0000000..f5db9d7 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-list-panel.ui @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GtdTaskListPanel" parent="GtkBox"> + <child> + <object class="GtdTaskListView" id="task_list_view"/> + </child> + </template> + <object class="GtkPopover" id="popover"> + <property name="visible">0</property> + <signal name="hide" handler="on_popover_hidden_cb" object="GtdTaskListPanel" swapped="no"/> + <style> + <class name="menu" /> + </style> + + <child> + <object class="GtkStack" id="popover_stack"> + + <!-- Main Page --> + <child> + <object class="GtkStackPage"> + <property name="name">main</property> + <property name="child"> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="GtkFlowBox" id="colors_flowbox"> + <property name="hexpand">true</property> + <property name="vexpand">true</property> + <property name="selection-mode">none</property> + <property name="min-children-per-line">3</property> + <property name="max-children-per-line">3</property> + <signal name="child-activated" handler="on_colors_flowbox_child_activated_cb" object="GtdTaskListPanel" swapped="no"/> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="text" translatable="yes">Rename</property> + <signal name="clicked" handler="on_go_to_rename_page_button_clicked_cb" object="GtdTaskListPanel" swapped="no"/> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="text" translatable="yes">Clear completed tasks…</property> + <property name="action-name">list.clear-completed-tasks</property> + </object> + </child> + <child> + <object class="GtkSeparator"/> + </child> + <child> + <object class="GtkModelButton" id="archive_button"> + <property name="text" translatable="yes">Archive</property> + <signal name="clicked" handler="on_archive_button_clicked_cb" object="GtdTaskListPanel" swapped="no"/> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="text" translatable="yes">Delete</property> + <signal name="clicked" handler="on_delete_button_clicked_cb" object="GtdTaskListPanel" swapped="no"/> + </object> + </child> + </object> + </property> + </object> + </child> + + <!-- Rename Page --> + <child> + <object class="GtkStackPage"> + <property name="name">rename</property> + <property name="child"> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <child> + <object class="GtkModelButton" id="rename_header_button"> + <property name="text" translatable="yes">Rename</property> + <property name="role">title</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="spacing">12</property> + <child> + <object class="GtkEntry" id="rename_entry"> + <signal name="activate" handler="on_rename_entry_activated_cb" object="GtdTaskListPanel" swapped="no"/> + <signal name="notify::text" handler="on_rename_entry_text_changed_cb" object="GtdTaskListPanel" swapped="no"/> + </object> + </child> + <child> + <object class="GtkButton" id="rename_button"> + <property name="label" translatable="yes">Rename</property> + <signal name="clicked" handler="on_rename_button_clicked_cb" object="GtdTaskListPanel" swapped="no"/> + <style> + <class name="destructive-action"/> + </style> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + + </object> + </child> + </object> +</interface> diff --git a/src/plugins/task-lists-workspace/gtd-task-lists-workspace.c b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.c new file mode 100644 index 0000000..407de89 --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.c @@ -0,0 +1,711 @@ +/* gtd-task-lists-workspace.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GtdTaskListsWorkspace" + +#include "gtd-task-lists-workspace.h" + +#include "gtd-debug.h" +#include "task-lists-workspace.h" +#include "gtd-sidebar.h" +#include "gtd-task-list-panel.h" + +#include <libpeas/peas.h> +#include <glib/gi18n.h> + +struct _GtdTaskListsWorkspace +{ + GtkBox parent; + + GtkWidget *back_button; + GtkWidget *content_box; + GtkMenuButton *gear_menu_button; + AdwLeaflet *leaflet; + GtkWidget *new_list_button; + GtkBox *panel_box_end; + GtkBox *panel_box_start; + GtkMenuButton *primary_menu_button; + GtkStack *stack; + GtdSidebar *sidebar; + GtkWidget *sidebar_box; + + AdwToastOverlay *sidebar_overlay; + AdwToastOverlay *content_overlay; + + GtdPanel *active_panel; + GtdPanel *task_list_panel; + + GHashTable *notification_list; + + PeasExtensionSet *panels_set; + GSimpleActionGroup *action_group; +}; + +typedef struct +{ + GtdWindow *window; + gchar *primary_text; + gchar *secondary_text; +} ErrorData; + +static void gtd_workspace_iface_init (GtdWorkspaceInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdTaskListsWorkspace, gtd_task_lists_workspace, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GTD_TYPE_WORKSPACE, gtd_workspace_iface_init)) + +enum +{ + PROP_0, + PROP_ICON, + PROP_TITLE, + N_PROPS +}; + +enum +{ + PANEL_ADDED, + PANEL_REMOVED, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + + +/* + * Auxiliary methods + */ + +static void +error_data_free (ErrorData *error_data) +{ + g_free (error_data->primary_text); + g_free (error_data->secondary_text); + g_free (error_data); +} + +static void +add_widgets (GtdTaskListsWorkspace *self, + GList *widgets) +{ + GList *l; + + for (l = widgets; l; l = l->next) + { + switch (gtk_widget_get_halign (l->data)) + { + case GTK_ALIGN_END: + gtk_box_append (self->panel_box_end, l->data); + break; + + case GTK_ALIGN_START: + case GTK_ALIGN_BASELINE: + case GTK_ALIGN_FILL: + default: + gtk_box_append (self->panel_box_start, l->data); + break; + } + } +} + +static void +remove_widgets (GtdTaskListsWorkspace *self, + GList *widgets) +{ + GList *l; + + for (l = widgets; l; l = l->next) + { + GtkBox *box; + + if (gtk_widget_get_halign (l->data) == GTK_ALIGN_END) + box = self->panel_box_end; + else + box = self->panel_box_start; + + g_object_ref (l->data); + gtk_box_remove (box, l->data); + } +} + +static void +update_panel_menu (GtdTaskListsWorkspace *self) +{ + GtkPopover *popover; + const GMenu *menu; + + popover = gtd_panel_get_popover (self->active_panel); + menu = gtd_panel_get_menu (self->active_panel); + + gtk_widget_set_visible (GTK_WIDGET (self->gear_menu_button), popover || menu); + + if (popover) + { + gtk_menu_button_set_popover (self->gear_menu_button, GTK_WIDGET (popover)); + } + else + { + gtk_menu_button_set_popover (self->gear_menu_button, NULL); + gtk_menu_button_set_menu_model (self->gear_menu_button, G_MENU_MODEL (menu)); + } +} + + +/* + * Callbacks + */ + +static void +on_action_activate_panel_activated_cb (GSimpleAction *simple, + GVariant *parameters, + gpointer user_data) +{ + GtdTaskListsWorkspace *self; + g_autoptr (GVariant) panel_parameters = NULL; + g_autofree gchar *panel_id = NULL; + GtdPanel *panel; + + self = GTD_TASK_LISTS_WORKSPACE (user_data); + + g_variant_get (parameters, + "(sv)", + &panel_id, + &panel_parameters); + + g_debug ("Activating panel '%s'", panel_id); + + panel = (GtdPanel *) gtk_stack_get_child_by_name (self->stack, panel_id); + g_return_if_fail (panel && GTD_IS_PANEL (panel)); + + gtd_panel_activate (panel, panel_parameters); + + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (panel)); + adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_FORWARD); +} + +static void +on_action_toggle_archive_activated_cb (GSimpleAction *simple, + GVariant *state, + gpointer user_data) +{ + GtdTaskListsWorkspace *self; + gboolean archive_visible; + + self = GTD_TASK_LISTS_WORKSPACE (user_data); + archive_visible = g_variant_get_boolean (state); + + gtk_widget_set_visible (self->new_list_button, !archive_visible); + gtd_sidebar_set_archive_visible (self->sidebar, archive_visible); +} + +static void +on_back_sidebar_button_clicked_cb (GtkButton *button, + GtdTaskListsWorkspace *self) +{ + adw_leaflet_navigate (self->leaflet, ADW_NAVIGATION_DIRECTION_BACK); +} + +static void +on_back_button_clicked_cb (GtkButton *button, + GtdTaskListsWorkspace *self) +{ + gtk_widget_activate_action (GTK_WIDGET (self), + "task-lists-workspace.toggle-archive", + "b", + FALSE); +} + +static void +on_panel_added_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdPanel *panel, + GtdTaskListsWorkspace *self) +{ + gtk_stack_add_titled (self->stack, + GTK_WIDGET (g_object_ref_sink (panel)), + gtd_panel_get_panel_name (panel), + gtd_panel_get_panel_title (panel)); + + g_signal_emit (self, signals[PANEL_ADDED], 0, panel); +} + +static void +on_panel_removed_cb (PeasExtensionSet *extension_set, + PeasPluginInfo *plugin_info, + GtdPanel *panel, + GtdTaskListsWorkspace *self) +{ + g_object_ref (panel); + + gtk_stack_remove (self->stack, GTK_WIDGET (panel)); + g_signal_emit (self, signals[PANEL_REMOVED], 0, panel); + + g_object_unref (panel); +} + +static void +on_panel_menu_changed_cb (GObject *object, + GParamSpec *pspec, + GtdTaskListsWorkspace *self) +{ + if (GTD_PANEL (object) != self->active_panel) + return; + + update_panel_menu (self); +} +static void +on_stack_visible_child_cb (GtdTaskListsWorkspace *self, + GParamSpec *pspec, + GtkStack *stack) +{ + GtkWidget *visible_child; + GtdPanel *panel; + GList *header_widgets; + + GTD_ENTRY; + + visible_child = gtk_stack_get_visible_child (stack); + panel = GTD_PANEL (visible_child); + + /* Remove previous panel's widgets */ + if (self->active_panel) + { + header_widgets = gtd_panel_get_header_widgets (self->active_panel); + + /* Disconnect signals */ + g_signal_handlers_disconnect_by_func (self->active_panel, + on_panel_menu_changed_cb, + self); + + remove_widgets (self, header_widgets); + + g_list_free (header_widgets); + } + + /* Add current panel's header widgets */ + header_widgets = gtd_panel_get_header_widgets (panel); + add_widgets (self, header_widgets); + + g_list_free (header_widgets); + + g_signal_connect (panel, "notify::menu", G_CALLBACK (on_panel_menu_changed_cb), self); + + /* Set panel as the new active panel */ + g_set_object (&self->active_panel, panel); + + /* Setup the panel's menu */ + update_panel_menu (self); + + GTD_EXIT; +} + +void +on_toast_dismissed_cb (AdwToast *self, + gpointer user_data) +{ + GtdNotification *notification = user_data; + + GTD_ENTRY; + + gtd_notification_execute_dismissal_action (notification); + + GTD_EXIT; +} + +void +toast_activated_cb (GtdTaskListsWorkspace *self, + const char *action_name, + GVariant *parameter) +{ + AdwToast *toast; + GtdNotification *notification; + + GTD_ENTRY; + + toast = g_hash_table_lookup (self->notification_list, g_variant_get_string (parameter, NULL)); + + if (toast != NULL) { + notification = g_object_get_data (G_OBJECT (toast), "notification"); + + g_signal_handlers_block_by_func (toast, on_toast_dismissed_cb, notification); + gtd_notification_execute_secondary_action (notification); + } + + GTD_EXIT; +} + +static void +on_show_notification_cb (GtdManager *manager, + GtdNotification *notification, + GtdTaskListsWorkspace *self) +{ + AdwToast *toast; + GValue btn_label = G_VALUE_INIT; + + g_object_get_property (G_OBJECT (notification), "secondary-action-name", &btn_label); + + /* Convert GtdNotification to AdwToast */ + toast = adw_toast_new (gtd_notification_get_text (notification)); + adw_toast_set_button_label (toast, g_value_get_string (&btn_label)); + adw_toast_set_action_name (toast, "toast.activated"); + adw_toast_set_action_target_value (toast, g_variant_new_string (g_value_get_string (&btn_label))); + g_object_set_data (G_OBJECT (toast), "notification", notification); + + g_hash_table_insert (self->notification_list, (char *) g_value_get_string (&btn_label), toast); + g_signal_connect (toast, "dismissed", G_CALLBACK (on_toast_dismissed_cb), notification); + + if (adw_leaflet_get_folded (self->leaflet)) { + if (adw_leaflet_get_visible_child (self->leaflet) == self->content_box) + adw_toast_overlay_add_toast (self->content_overlay, toast); + if (adw_leaflet_get_visible_child (self->leaflet) == self->sidebar_box) + adw_toast_overlay_add_toast (self->sidebar_overlay, toast); + } else { + adw_toast_overlay_add_toast (self->content_overlay, toast); + } +} + +static void +error_message_notification_primary_action (GtdNotification *notification, + gpointer user_data) +{ + error_data_free (user_data); +} + +static void +error_message_notification_secondary_action (GtdNotification *notification, + gpointer user_data) +{ + GtkWidget *dialog; + ErrorData *data; + + data = user_data; + dialog = adw_message_dialog_new (GTK_WINDOW (data->window), + data->primary_text, + NULL); + + adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog), + "%s", + data->secondary_text); + + adw_message_dialog_add_response (ADW_MESSAGE_DIALOG (dialog), + "close", _("Close")); + + g_signal_connect (dialog, + "response", + G_CALLBACK (gtk_window_destroy), + NULL); + + gtk_widget_show (dialog); + + error_data_free (data); +} + +static void +on_show_error_message_cb (GtdManager *manager, + const gchar *primary_text, + const gchar *secondary_text, + GtdNotificationActionFunc function, + gpointer user_data, + GtdTaskListsWorkspace *self) +{ + GtdNotification *notification; + ErrorData *error_data; + + error_data = g_new0 (ErrorData, 1); + notification = gtd_notification_new (primary_text); + + error_data->window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))); + error_data->primary_text = g_strdup (primary_text); + error_data->secondary_text = g_strdup (secondary_text); + + gtd_notification_set_dismissal_action (notification, + error_message_notification_primary_action, + error_data); + + if (!function) + { + gtd_notification_set_secondary_action (notification, + _("Details"), + error_message_notification_secondary_action, + error_data); + } + else + { + gtd_notification_set_secondary_action (notification, secondary_text, function, user_data); + } + + gtd_manager_send_notification (gtd_manager_get_default (), notification); +} + +/* + * GtdWorkspace implementation + */ + +static const gchar* +gtd_task_lists_workspace_get_id (GtdWorkspace *workspace) +{ + return "task-lists"; +} + +static const gchar* +gtd_task_lists_workspace_get_title (GtdWorkspace *workspace) +{ + return _("Task Lists"); +} + +static gint +gtd_task_lists_workspace_get_priority (GtdWorkspace *workspace) +{ + return 1000; +} + +static GIcon* +gtd_task_lists_workspace_get_icon (GtdWorkspace *workspace) +{ + return g_themed_icon_new ("view-list-symbolic"); +} + +static void +gtd_task_lists_workspace_activate (GtdWorkspace *workspace, + GVariant *parameters) +{ + GtdTaskListsWorkspace *self = GTD_TASK_LISTS_WORKSPACE (workspace); + + if (parameters) + { + const gchar *panel_id = g_variant_get_string (parameters, NULL); + + g_debug ("Activating panel '%s'", panel_id); + gtk_stack_set_visible_child_name (self->stack, panel_id); + } + else + { + gtd_sidebar_activate (self->sidebar); + } +} + +static void +gtd_workspace_iface_init (GtdWorkspaceInterface *iface) +{ + iface->get_id = gtd_task_lists_workspace_get_id; + iface->get_title = gtd_task_lists_workspace_get_title; + iface->get_priority = gtd_task_lists_workspace_get_priority; + iface->get_icon = gtd_task_lists_workspace_get_icon; + iface->activate = gtd_task_lists_workspace_activate; +} + + +/* + * GObject overrides + */ + +static void +gtd_task_lists_workspace_dispose (GObject *object) +{ + GtdTaskListsWorkspace *self = (GtdTaskListsWorkspace *)object; + + G_OBJECT_CLASS (gtd_task_lists_workspace_parent_class)->dispose (object); + + g_signal_handlers_disconnect_by_func (self->panels_set, on_panel_added_cb, self); + g_signal_handlers_disconnect_by_func (self->panels_set, on_panel_removed_cb, self); + g_clear_object (&self->panels_set); +} + +static void +gtd_task_lists_workspace_constructed (GObject *object) +{ + GtdManager *manager = gtd_manager_get_default (); + GtdTaskListsWorkspace *self = (GtdTaskListsWorkspace *)object; + + G_OBJECT_CLASS (gtd_task_lists_workspace_parent_class)->constructed (object); + + /* Add loaded panels */ + self->panels_set = peas_extension_set_new (peas_engine_get_default (), + GTD_TYPE_PANEL, + NULL); + + peas_extension_set_foreach (self->panels_set, + (PeasExtensionSetForeachFunc) on_panel_added_cb, + self); + + g_signal_connect (self->panels_set, "extension-added", G_CALLBACK (on_panel_added_cb), self); + g_signal_connect (self->panels_set, "extension-removed", G_CALLBACK (on_panel_removed_cb), self); + g_signal_connect (manager, "show-notification", G_CALLBACK (on_show_notification_cb), self); + g_signal_connect (manager, "show-error-message", G_CALLBACK (on_show_error_message_cb), self); +} + +static void +gtd_task_lists_workspace_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdWorkspace *workspace = GTD_WORKSPACE (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_take_object (value, gtd_workspace_get_icon (workspace)); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_workspace_get_title (workspace)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_task_lists_workspace_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_task_lists_workspace_class_init (GtdTaskListsWorkspaceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + g_resources_register (task_lists_workspace_get_resource ()); + + object_class->dispose = gtd_task_lists_workspace_dispose; + object_class->constructed = gtd_task_lists_workspace_constructed; + object_class->get_property = gtd_task_lists_workspace_get_property; + object_class->set_property = gtd_task_lists_workspace_set_property; + + + /** + * GtdTaskListsWorkspace::panel-added: + * @manager: a #GtdManager + * @panel: a #GtdPanel + * + * The ::panel-added signal is emmited after a #GtdPanel + * is added. + */ + signals[PANEL_ADDED] = g_signal_new ("panel-added", + GTD_TYPE_TASK_LISTS_WORKSPACE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PANEL); + + /** + * GtdTaskListsWorkspace::panel-removed: + * @manager: a #GtdManager + * @panel: a #GtdPanel + * + * The ::panel-removed signal is emmited after a #GtdPanel + * is removed from the list. + */ + signals[PANEL_REMOVED] = g_signal_new ("panel-removed", + GTD_TYPE_TASK_LISTS_WORKSPACE, + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + GTD_TYPE_PANEL); + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); + + g_type_ensure (GTD_TYPE_PROVIDER_POPOVER); + g_type_ensure (GTD_TYPE_SIDEBAR); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/plugins/task-lists-workspace/gtd-task-lists-workspace.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, back_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, content_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, gear_menu_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, leaflet); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, new_list_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, panel_box_end); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, panel_box_start); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, primary_menu_button); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, sidebar); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, sidebar_box); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, stack); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, sidebar_overlay); + gtk_widget_class_bind_template_child (widget_class, GtdTaskListsWorkspace, content_overlay); + + gtk_widget_class_bind_template_callback (widget_class, on_back_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_back_sidebar_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_stack_visible_child_cb); + + gtk_widget_class_install_action (widget_class, "toast.activated", "s", (GtkWidgetActionActivateFunc) toast_activated_cb); +} + +static void +gtd_task_lists_workspace_init (GtdTaskListsWorkspace *self) +{ + GtkApplication *application; + GMenu *primary_menu; + + static const GActionEntry entries[] = { + { "activate-panel", on_action_activate_panel_activated_cb, "(sv)" }, + { "toggle-archive", on_action_toggle_archive_activated_cb, "b" }, + }; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->notification_list = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + self->action_group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + entries, + G_N_ELEMENTS (entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self), + "task-lists-workspace", + G_ACTION_GROUP (self->action_group)); + + gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->back_button), + g_variant_new_boolean (FALSE)); + + /* Task list panel */ + self->task_list_panel = GTD_PANEL (gtd_task_list_panel_new ()); + on_panel_added_cb (NULL, NULL, self->task_list_panel, self); + + gtd_sidebar_connect (self->sidebar, GTK_WIDGET (self)); + gtd_sidebar_set_panel_stack (self->sidebar, self->stack); + gtd_sidebar_set_task_list_panel (self->sidebar, self->task_list_panel); + + /* Fancy primary menu */ + application = GTK_APPLICATION (g_application_get_default ()); + primary_menu = gtk_application_get_menu_by_id (application, "primary-menu"); + gtk_menu_button_set_menu_model (self->primary_menu_button, G_MENU_MODEL (primary_menu)); +} + +GtdWorkspace* +gtd_task_lists_workspace_new (void) +{ + return g_object_new (GTD_TYPE_TASK_LISTS_WORKSPACE, NULL); +} diff --git a/src/plugins/task-lists-workspace/gtd-task-lists-workspace.h b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.h new file mode 100644 index 0000000..0cbc97a --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.h @@ -0,0 +1,35 @@ +/* gtd-task-lists-workspace.h + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "endeavour.h" + +G_BEGIN_DECLS + +#define GTD_TYPE_TASK_LISTS_WORKSPACE (gtd_task_lists_workspace_get_type()) + +G_DECLARE_FINAL_TYPE (GtdTaskListsWorkspace, gtd_task_lists_workspace, GTD, TASK_LISTS_WORKSPACE, GtkBox) + +GtdWorkspace* gtd_task_lists_workspace_new (void); + +G_END_DECLS diff --git a/src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui new file mode 100644 index 0000000..7d63d1d --- /dev/null +++ b/src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GtdTaskListsWorkspace" parent="GtkBox"> + + <!-- Main leaflet --> + <child> + <object class="AdwLeaflet" id="leaflet"> + <property name="can-navigate-back">true</property> + <property name="width-request">360</property> + + <child> + <object class="GtkBox" id="sidebar_box"> + <property name="orientation">vertical</property> + <property name="hexpand">False</property> + + <child> + <object class="AdwHeaderBar" id="start_headerbar"> + <property name="hexpand">1</property> + <property name="show-start-title-buttons">True</property> + <property name="show-end-title-buttons" bind-source="leaflet" bind-property="folded" bind-flags="sync-create" /> + + <property name="title-widget"> + <object class="AdwWindowTitle"> + <property name="visible">False</property> + </object> + </property> + + <!-- New List --> + <child> + <object class="GtkMenuButton" id="new_list_button"> + <property name="can_focus">1</property> + <property name="label" translatable="yes">New List</property> + <property name="receives_default">1</property> + <property name="popover">new_list_popover</property> + <property name="halign">start</property> + </object> + </child> + + <child> + <object class="GtkButton" id="back_button"> + <property name="visible" bind-source="new_list_button" bind-property="visible" bind-flags="sync-create|invert-boolean" /> + <property name="can_focus">1</property> + <property name="receives_default">1</property> + <property name="halign">start</property> + <property name="icon-name">go-previous-symbolic</property> + <signal name="clicked" handler="on_back_button_clicked_cb" object="GtdTaskListsWorkspace" swapped="no" /> + </object> + </child> + + <child type="end"> + <object class="GtkMenuButton" id="primary_menu_button"> + <property name="icon-name">open-menu-symbolic</property> + </object> + </child> + + </object> + </child> + + <child> + <object class="AdwToastOverlay" id="sidebar_overlay"> + <child> + <object class="GtdSidebar" id="sidebar"> + <property name="can_focus">False</property> + <property name="vexpand">True</property> + </object> + </child> + </object> + </child> + + </object> + </child> + + <child> + <object class="AdwLeafletPage"> + <property name="navigatable">False</property> + <property name="child"> + <object class="GtkSeparator"/> + </property> + </object> + </child> + + <child> + <object class="GtkBox" id="content_box"> + <property name="orientation">vertical</property> + <property name="hexpand">true</property> + + <child> + <object class="AdwHeaderBar" id="headerbar"> + <property name="hexpand">1</property> + <property name="show-start-title-buttons" bind-source="leaflet" bind-property="folded" bind-flags="sync-create" /> + <property name="show-end-title-buttons">True</property> + + <child> + <object class="GtkButton" id="back_sidebar_button"> + <property name="visible" bind-source="leaflet" bind-property="folded" bind-flags="sync-create" /> + <property name="can_focus">1</property> + <property name="receives_default">1</property> + <property name="halign">start</property> + <property name="icon-name">go-previous-symbolic</property> + <signal name="clicked" handler="on_back_sidebar_button_clicked_cb" object="GtdTaskListsWorkspace" swapped="no" /> + </object> + </child> + + <child> + <object class="GtkBox" id="panel_box_start"> + <property name="spacing">6</property> + </object> + </child> + + <!-- Omni Area --> + <child type="title"> + <object class="GtdOmniArea" id="omni_area"> + </object> + </child> + + <child type="end"> + <object class="GtkMenuButton" id="gear_menu_button"> + <property name="can_focus">1</property> + <property name="icon-name">view-more-symbolic</property> + </object> + </child> + + <child type="end"> + <object class="GtkBox" id="panel_box_end"> + <property name="spacing">6</property> + </object> + </child> + + </object> + </child> + + <child> + <object class="AdwToastOverlay" id="content_overlay"> + <!-- Panels Stack --> + <child> + <object class="GtkStack" id="stack"> + <property name="hexpand">true</property> + <property name="vexpand">true</property> + <property name="transition_duration">250</property> + <property name="transition_type">crossfade</property> + <signal name="notify::visible-child" handler="on_stack_visible_child_cb" object="GtdTaskListsWorkspace" swapped="yes"/> + <style> + <class name="background"/> + </style> + </object> + </child> + </object> + </child> + + + + + </object> + </child> + + </object> + </child> + + </template> + + <object class="GtdProviderPopover" id="new_list_popover"> + <property name="position">bottom</property> + </object> +</interface> diff --git a/src/plugins/task-lists-workspace/meson.build b/src/plugins/task-lists-workspace/meson.build new file mode 100644 index 0000000..b80dc50 --- /dev/null +++ b/src/plugins/task-lists-workspace/meson.build @@ -0,0 +1,20 @@ +plugins_ldflags += ['-Wl,--undefined=task_lists_workspace_plugin_register_types'] + +task_lists_panel_sources = files( + 'gtd-sidebar.c', + 'gtd-sidebar-list-row.c', + 'gtd-sidebar-panel-row.c', + 'gtd-sidebar-provider-row.c', + 'gtd-task-list-panel.c', + 'gtd-task-lists-workspace.c', + 'task-lists-workspace-plugin.c', +) + +task_lists_panel_sources += gnome.compile_resources( + 'task-lists-workspace', + 'task-lists-workspace.gresource.xml', + c_name: 'task_lists_workspace', + export: true, +) + +plugins_sources += task_lists_panel_sources diff --git a/src/plugins/task-lists-workspace/task-lists-workspace-plugin.c b/src/plugins/task-lists-workspace/task-lists-workspace-plugin.c new file mode 100644 index 0000000..78d071d --- /dev/null +++ b/src/plugins/task-lists-workspace/task-lists-workspace-plugin.c @@ -0,0 +1,31 @@ +/* gtd-plugin-task-lists-workspace.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "endeavour.h" + +#include "gtd-task-lists-workspace.h" + +G_MODULE_EXPORT void +task_lists_workspace_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_WORKSPACE, + GTD_TYPE_TASK_LISTS_WORKSPACE); +} diff --git a/src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml b/src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml new file mode 100644 index 0000000..cf76721 --- /dev/null +++ b/src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/task-lists-workspace"> + <file>gtd-sidebar.ui</file> + <file>gtd-sidebar-list-row.ui</file> + <file>gtd-sidebar-panel-row.ui</file> + <file>gtd-sidebar-provider-row.ui</file> + <file>gtd-task-list-panel.ui</file> + <file>gtd-task-lists-workspace.ui</file> + <file>task-lists-workspace.plugin</file> + </gresource> +</gresources> diff --git a/src/plugins/task-lists-workspace/task-lists-workspace.plugin b/src/plugins/task-lists-workspace/task-lists-workspace.plugin new file mode 100644 index 0000000..2b2f090 --- /dev/null +++ b/src/plugins/task-lists-workspace/task-lists-workspace.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Task Lists Workspace +Module = task-lists-workspace +Description = Plugin implementing the task lists workspace +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +Hidden = true +License = GPL +Loader = C +Embedded = task_lists_workspace_plugin_register_types +Depends = diff --git a/src/plugins/today-panel/gtd-panel-today.c b/src/plugins/today-panel/gtd-panel-today.c new file mode 100644 index 0000000..b6cac7a --- /dev/null +++ b/src/plugins/today-panel/gtd-panel-today.c @@ -0,0 +1,480 @@ +/* gtd-panel-today.c + * + * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define G_LOG_DOMAIN "GtdPanelToday" + +#include <endeavour.h> +#include "gtd-panel-today.h" + +#include <glib/gi18n.h> + +struct _GtdPanelToday +{ + GtkBox parent; + + GIcon *icon; + + gint day_change_callback_id; + + guint number_of_tasks; + GtdTaskListView *view; + + GtkFilterListModel *filter_model; + GtkFilterListModel *incomplete_model; + GtkSortListModel *sort_model; + + GtkCssProvider *css_provider; +}; + +static void gtd_panel_iface_init (GtdPanelInterface *iface); + +G_DEFINE_TYPE_EXTENDED (GtdPanelToday, gtd_panel_today, GTK_TYPE_BOX, + 0, + G_IMPLEMENT_INTERFACE (GTD_TYPE_PANEL, + gtd_panel_iface_init)) + + +#define GTD_PANEL_TODAY_NAME "panel-today" +#define GTD_PANEL_TODAY_PRIORITY 1000 + +enum +{ + PROP_0, + PROP_ICON, + PROP_MENU, + PROP_NAME, + PROP_PRIORITY, + PROP_SUBTITLE, + PROP_TITLE, + N_PROPS +}; + + +/* + * Auxiliary methods + */ + +static void +load_css_provider (GtdPanelToday *self) +{ + g_autofree gchar *theme_name = NULL; + g_autofree gchar *theme_uri = NULL; + g_autoptr (GSettings) settings = NULL; + g_autoptr (GFile) css_file = NULL; + + /* Load CSS provider */ + settings = g_settings_new ("org.gnome.desktop.interface"); + theme_name = g_settings_get_string (settings, "gtk-theme"); + theme_uri = g_build_filename ("resource:///org/gnome/todo/theme/today-panel", theme_name, ".css", NULL); + css_file = g_file_new_for_uri (theme_uri); + + self->css_provider = gtk_css_provider_new (); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (self->css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + if (g_file_query_exists (css_file, NULL)) + gtk_css_provider_load_from_file (self->css_provider, css_file); + else + gtk_css_provider_load_from_resource (self->css_provider, "/org/gnome/todo/plugins/today-panel/theme/Adwaita.css"); +} + +static gboolean +is_overdue (GDateTime *today, + GDateTime *dt) +{ + if (!dt) + return FALSE; + + if (g_date_time_get_year (dt) > g_date_time_get_year (today)) + return FALSE; + + if (g_date_time_get_year (dt) < g_date_time_get_year (today)) + return TRUE; + + return g_date_time_get_day_of_year (dt) < g_date_time_get_day_of_year (today); +} + +static gboolean +is_today (GDateTime *today, + GDateTime *dt) +{ + if (!dt) + return FALSE; + + if (g_date_time_get_year (dt) == g_date_time_get_year (today) && + g_date_time_get_day_of_year (dt) == g_date_time_get_day_of_year (today)) + { + return TRUE; + } + + return FALSE; +} + +static GtkWidget* +create_label (const gchar *text, + gboolean overdue) +{ + GtkWidget *label; + + label = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "label", text, + "margin-top", overdue ? 6 : 18, + "margin-bottom", 6, + "margin-start", 6, + "margin-end", 6, + "xalign", 0.0, + "hexpand", TRUE, + NULL); + + gtk_widget_add_css_class (label, overdue ? "date-overdue" : "date-scheduled"); + + return label; +} + +static GtkWidget* +header_func (GtdTask *task, + GtdTask *previous_task, + gpointer user_data) +{ + g_autoptr (GDateTime) now = NULL; + g_autoptr (GDateTime) dt = NULL; + GtkWidget *header = NULL; + + now = g_date_time_new_now_local (); + dt = gtd_task_get_due_date (task); + + /* Only show a header if the we have overdue tasks */ + if (!previous_task && is_overdue (now, dt)) + { + header = create_label (_("Overdue"), TRUE); + } + else if (previous_task) + { + g_autoptr (GDateTime) previous_dt = NULL; + + previous_dt = gtd_task_get_due_date (previous_task); + + if (is_today (now, dt) != is_today (now, previous_dt)) + header = create_label (_("Today"), FALSE); + } + + return header; +} + + +/* + * Callbacks + */ + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + g_autoptr (GDateTime) now = NULL; + GtdTask *task; + gboolean complete; + + task = (GtdTask*) item; + now = g_date_time_new_now_local (); + task_dt = gtd_task_get_due_date (task); + + complete = gtd_task_get_complete (task); + + return is_today (now, task_dt) || (!complete && is_overdue (now, task_dt)); +} + +static gboolean +filter_complete_func (gpointer item, + gpointer user_data) +{ + GtdTask *task = (GtdTask*) item; + + return !gtd_task_get_complete (task); +} + +static gint +sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + g_autoptr (GDateTime) dt1 = NULL; + g_autoptr (GDateTime) dt2 = NULL; + GtdTask *task1; + GtdTask *task2; + GDate dates[2]; + gint result; + + task1 = (GtdTask*) a; + task2 = (GtdTask*) b; + + dt1 = gtd_task_get_due_date (task1); + dt2 = gtd_task_get_due_date (task2); + + g_date_clear (dates, 2); + + g_date_set_dmy (&dates[0], + g_date_time_get_day_of_month (dt1), + g_date_time_get_month (dt1), + g_date_time_get_year (dt1)); + + g_date_set_dmy (&dates[1], + g_date_time_get_day_of_month (dt2), + g_date_time_get_month (dt2), + g_date_time_get_year (dt2)); + + result = g_date_days_between (&dates[1], &dates[0]); + + if (result != 0) + return result; + + return gtd_task_compare (task1, task2); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdPanelToday *self) +{ + if (self->number_of_tasks == g_list_model_get_n_items (model)) + return; + + self->number_of_tasks = g_list_model_get_n_items (model); + g_object_notify (G_OBJECT (self), "subtitle"); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdPanelToday *self) +{ + g_autoptr (GDateTime) now = NULL; + GtkFilter *filter; + + now = g_date_time_new_now_local (); + gtd_task_list_view_set_default_date (self->view, now); + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + + +/* + * GtdPanel iface + */ + +static const gchar* +gtd_panel_today_get_panel_name (GtdPanel *panel) +{ + return GTD_PANEL_TODAY_NAME; +} + +static const gchar* +gtd_panel_today_get_panel_title (GtdPanel *panel) +{ + return _("Today"); +} + +static GList* +gtd_panel_today_get_header_widgets (GtdPanel *panel) +{ + return NULL; +} + +static const GMenu* +gtd_panel_today_get_menu (GtdPanel *panel) +{ + return NULL; +} + +static GIcon* +gtd_panel_today_get_icon (GtdPanel *panel) +{ + return g_object_ref (GTD_PANEL_TODAY (panel)->icon); +} + +static guint32 +gtd_panel_today_get_priority (GtdPanel *panel) +{ + return GTD_PANEL_TODAY_PRIORITY; +} + +static gchar* +gtd_panel_today_get_subtitle (GtdPanel *panel) +{ + GtdPanelToday *self = GTD_PANEL_TODAY (panel); + + return g_strdup_printf ("%d", self->number_of_tasks); +} + +static void +gtd_panel_iface_init (GtdPanelInterface *iface) +{ + iface->get_panel_name = gtd_panel_today_get_panel_name; + iface->get_panel_title = gtd_panel_today_get_panel_title; + iface->get_header_widgets = gtd_panel_today_get_header_widgets; + iface->get_menu = gtd_panel_today_get_menu; + iface->get_icon = gtd_panel_today_get_icon; + iface->get_priority = gtd_panel_today_get_priority; + iface->get_subtitle = gtd_panel_today_get_subtitle; +} + +static void +gtd_panel_today_finalize (GObject *object) +{ + GtdPanelToday *self = (GtdPanelToday *)object; + + g_clear_object (&self->css_provider); + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + g_clear_object (&self->incomplete_model); + g_clear_object (&self->sort_model); + + G_OBJECT_CLASS (gtd_panel_today_parent_class)->finalize (object); +} + +static void +gtd_panel_today_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtdPanelToday *self = GTD_PANEL_TODAY (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + + case PROP_MENU: + g_value_set_object (value, NULL); + break; + + case PROP_NAME: + g_value_set_string (value, GTD_PANEL_TODAY_NAME); + break; + + case PROP_PRIORITY: + g_value_set_uint (value, GTD_PANEL_TODAY_PRIORITY); + break; + + case PROP_SUBTITLE: + g_value_take_string (value, gtd_panel_get_subtitle (GTD_PANEL (self))); + break; + + case PROP_TITLE: + g_value_set_string (value, gtd_panel_get_panel_title (GTD_PANEL (self))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtd_panel_today_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +gtd_panel_today_class_init (GtdPanelTodayClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_panel_today_finalize; + object_class->get_property = gtd_panel_today_get_property; + object_class->set_property = gtd_panel_today_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_MENU, "menu"); + g_object_class_override_property (object_class, PROP_NAME, "name"); + g_object_class_override_property (object_class, PROP_PRIORITY, "priority"); + g_object_class_override_property (object_class, PROP_SUBTITLE, "subtitle"); + g_object_class_override_property (object_class, PROP_TITLE, "title"); +} + +static void +gtd_panel_today_init (GtdPanelToday *self) +{ + g_autoptr (GDateTime) now = NULL; + GtdManager *manager; + GtkCustomFilter *incomplete_filter; + GtkCustomFilter *filter; + GtkCustomSorter *sorter; + + manager = gtd_manager_get_default (); + + self->icon = g_themed_icon_new ("view-tasks-today-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + sorter = gtk_custom_sorter_new (sort_func, self, NULL); + self->sort_model = gtk_sort_list_model_new (G_LIST_MODEL (self->filter_model), + GTK_SORTER (sorter)); + + incomplete_filter = gtk_custom_filter_new (filter_complete_func, self, NULL); + self->incomplete_model = gtk_filter_list_model_new (G_LIST_MODEL (self->sort_model), + GTK_FILTER (incomplete_filter)); + + /* Connect to GtdManager::list-* signals to update the title */ + manager = gtd_manager_get_default (); + now = g_date_time_new_now_local (); + + /* The main view */ + self->view = GTD_TASK_LIST_VIEW (gtd_task_list_view_new ()); + gtd_task_list_view_set_model (self->view, G_LIST_MODEL (self->sort_model)); + gtd_task_list_view_set_show_list_name (self->view, TRUE); + gtd_task_list_view_set_show_due_date (self->view, FALSE); + gtd_task_list_view_set_default_date (self->view, now); + + gtk_widget_set_hexpand (GTK_WIDGET (self->view), TRUE); + gtk_widget_set_vexpand (GTK_WIDGET (self->view), TRUE); + gtk_box_append (GTK_BOX (self), GTK_WIDGET (self->view)); + + gtd_task_list_view_set_header_func (self->view, header_func, self); + + g_signal_connect_object (self->incomplete_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); + + load_css_provider (self); +} + +GtkWidget* +gtd_panel_today_new (void) +{ + return g_object_new (GTD_TYPE_PANEL_TODAY, NULL); +} diff --git a/src/plugins/today-panel/gtd-panel-today.h b/src/plugins/today-panel/gtd-panel-today.h new file mode 100644 index 0000000..135e67a --- /dev/null +++ b/src/plugins/today-panel/gtd-panel-today.h @@ -0,0 +1,35 @@ +/* gtd-panel-today.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef GTD_PANEL_TODAY_H +#define GTD_PANEL_TODAY_H + +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_PANEL_TODAY (gtd_panel_today_get_type()) + +G_DECLARE_FINAL_TYPE (GtdPanelToday, gtd_panel_today, GTD, PANEL_TODAY, GtkBox) + +GtkWidget* gtd_panel_today_new (void); + +G_END_DECLS + +#endif /* GTD_PANEL_TODAY_H */ diff --git a/src/plugins/today-panel/gtd-today-omni-area-addin.c b/src/plugins/today-panel/gtd-today-omni-area-addin.c new file mode 100644 index 0000000..d92979d --- /dev/null +++ b/src/plugins/today-panel/gtd-today-omni-area-addin.c @@ -0,0 +1,304 @@ +/* gtd-today-omni-area-addin.c + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "gtd-today-omni-area-addin.h" + +#include "endeavour.h" +#include "config.h" + +#include <glib/gi18n.h> + +#define MESSAGE_ID "today-counter-message-id" + +struct _GtdTodayOmniAreaAddin +{ + GObject parent; + + GIcon *icon; + GtkFilterListModel *filter_model; + + GtdOmniArea *omni_area; + guint number_of_tasks; + + gboolean had_tasks; + gboolean finished_tasks; + + guint idle_update_message_timeout_id; +}; + +static void gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtdTodayOmniAreaAddin, gtd_today_omni_area_addin, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GTD_TYPE_OMNI_AREA_ADDIN, gtd_omni_area_addin_iface_init)) + +const gchar *end_messages[] = +{ + N_("No more tasks left"), + N_("Nothing else to do here"), + N_("You made it!"), + N_("Looks like there’s nothing else left here") +}; + +static gboolean +can_show_omni_area_message (GtdTodayOmniAreaAddin *self) +{ + GtdWorkspace *current_workspace; + GtdWindow *window; + + window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (self->omni_area))); + current_workspace = gtd_window_get_current_workspace (window); + + if (current_workspace && + g_str_equal (gtd_workspace_get_id (current_workspace), "task-lists")) + { + return TRUE; + } + + return FALSE; +} + +static void +update_omni_area_message (GtdTodayOmniAreaAddin *self) +{ + + g_autofree gchar *message = NULL; + + g_assert (self->omni_area != NULL); + + if (!can_show_omni_area_message (self)) + { + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + return; + } + + if (self->number_of_tasks > 0) + { + message = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, + "%d task for today", + "%d tasks for today", + self->number_of_tasks), + self->number_of_tasks); + } + else + { + if (self->finished_tasks) + { + gint message_index = g_random_int_range (0, G_N_ELEMENTS (end_messages)); + + message = g_strdup (gettext (end_messages[message_index])); + } + else + { + message = g_strdup (_("No tasks scheduled for today")); + } + } + + gtd_omni_area_withdraw_message (self->omni_area, MESSAGE_ID); + gtd_omni_area_push_message (self->omni_area, MESSAGE_ID, message, self->icon); +} + +static gboolean +is_today (GDateTime *today, + GDateTime *dt) +{ + if (!dt) + return FALSE; + + if (g_date_time_get_year (dt) == g_date_time_get_year (today) && + g_date_time_get_day_of_year (dt) == g_date_time_get_day_of_year (today)) + { + return TRUE; + } + + return FALSE; +} + + +/* + * Callbacks + */ + +static gboolean +idle_update_omni_area_message_cb (gpointer user_data) +{ + GtdTodayOmniAreaAddin *self = GTD_TODAY_OMNI_AREA_ADDIN (user_data); + + update_omni_area_message (self); + + self->idle_update_message_timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +static gboolean +filter_func (gpointer item, + gpointer user_data) +{ + g_autoptr (GDateTime) task_dt = NULL; + g_autoptr (GDateTime) now = NULL; + GtdTask *task; + + task = (GtdTask*) item; + + if (gtd_task_get_complete (task)) + return FALSE; + + now = g_date_time_new_now_local (); + task_dt = gtd_task_get_due_date (task); + + return is_today (now, task_dt); +} + +static void +on_clock_day_changed_cb (GtdClock *clock, + GtdTodayOmniAreaAddin *self) +{ + GtkFilter *filter; + + self->had_tasks = FALSE; + self->finished_tasks = FALSE; + + filter = gtk_filter_list_model_get_filter (self->filter_model); + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); +} + +static void +on_model_items_changed_cb (GListModel *model, + guint position, + guint n_removed, + guint n_added, + GtdTodayOmniAreaAddin *self) +{ + guint number_of_tasks = g_list_model_get_n_items (model); + + if (self->number_of_tasks == number_of_tasks) + return; + + self->number_of_tasks = number_of_tasks; + + if (number_of_tasks != 0) + self->had_tasks = number_of_tasks != 0; + + self->finished_tasks = self->had_tasks && number_of_tasks == 0; + + g_clear_handle_id (&self->idle_update_message_timeout_id, g_source_remove); + self->idle_update_message_timeout_id = g_timeout_add_seconds (2, idle_update_omni_area_message_cb, self); +} + +static void +on_window_current_workspace_changed_cb (GtdWindow *window, + GParamSpec *pspec, + GtdTodayOmniAreaAddin *self) +{ + update_omni_area_message (self); +} + + +/* + * GtdOmniAreaAddin iface + */ + +static void +gtd_today_omni_area_addin_omni_area_addin_load (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdTodayOmniAreaAddin *self; + GtdWindow *window; + + self = GTD_TODAY_OMNI_AREA_ADDIN (addin); + window = GTD_WINDOW (gtk_widget_get_root (GTK_WIDGET (omni_area))); + + g_signal_connect_object (window, + "notify::current-workspace", + G_CALLBACK (on_window_current_workspace_changed_cb), + self, + 0); + + self->omni_area = omni_area; + update_omni_area_message (self); +} + +static void +gtd_today_omni_area_addin_omni_area_addin_unload (GtdOmniAreaAddin *addin, + GtdOmniArea *omni_area) +{ + GtdTodayOmniAreaAddin *self = GTD_TODAY_OMNI_AREA_ADDIN (addin); + + gtd_omni_area_withdraw_message (omni_area, MESSAGE_ID); + self->omni_area = NULL; +} + +static void +gtd_omni_area_addin_iface_init (GtdOmniAreaAddinInterface *iface) +{ + iface->load = gtd_today_omni_area_addin_omni_area_addin_load; + iface->unload = gtd_today_omni_area_addin_omni_area_addin_unload; +} + +/* + * GObject overrides + */ + +static void +gtd_today_omni_area_addin_finalize (GObject *object) +{ + GtdTodayOmniAreaAddin *self = (GtdTodayOmniAreaAddin *)object; + + g_clear_handle_id (&self->idle_update_message_timeout_id, g_source_remove); + g_clear_object (&self->icon); + g_clear_object (&self->filter_model); + + G_OBJECT_CLASS (gtd_today_omni_area_addin_parent_class)->finalize (object); +} + +static void +gtd_today_omni_area_addin_class_init (GtdTodayOmniAreaAddinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gtd_today_omni_area_addin_finalize; +} + +static void +gtd_today_omni_area_addin_init (GtdTodayOmniAreaAddin *self) +{ + GtkCustomFilter *filter; + GtdManager *manager; + + manager = gtd_manager_get_default (); + + self->icon = g_themed_icon_new ("view-tasks-today-symbolic"); + + filter = gtk_custom_filter_new (filter_func, self, NULL); + self->filter_model = gtk_filter_list_model_new (gtd_manager_get_tasks_model (manager), + GTK_FILTER (filter)); + + g_signal_connect_object (self->filter_model, + "items-changed", + G_CALLBACK (on_model_items_changed_cb), + self, + 0); + + g_signal_connect_object (gtd_manager_get_clock (manager), + "day-changed", + G_CALLBACK (on_clock_day_changed_cb), + self, + 0); +} diff --git a/src/plugins/today-panel/gtd-today-omni-area-addin.h b/src/plugins/today-panel/gtd-today-omni-area-addin.h new file mode 100644 index 0000000..e0fc04a --- /dev/null +++ b/src/plugins/today-panel/gtd-today-omni-area-addin.h @@ -0,0 +1,30 @@ +/* gtd-today-omni-area-addin.h + * + * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GTD_TYPE_TODAY_OMNI_AREA_ADDIN (gtd_today_omni_area_addin_get_type()) +G_DECLARE_FINAL_TYPE (GtdTodayOmniAreaAddin, gtd_today_omni_area_addin, GTD, TODAY_OMNI_AREA_ADDIN, GObject) + +G_END_DECLS diff --git a/src/plugins/today-panel/meson.build b/src/plugins/today-panel/meson.build new file mode 100644 index 0000000..baf173b --- /dev/null +++ b/src/plugins/today-panel/meson.build @@ -0,0 +1,14 @@ +plugins_ldflags += ['-Wl,--undefined=today_panel_plugin_register_types'] + +plugins_sources += files( + 'gtd-panel-today.c', + 'gtd-today-omni-area-addin.c', + 'today-panel-plugin.c' +) + + +plugins_sources += gnome.compile_resources( + 'today-panel-resources', + 'today-panel.gresource.xml', + c_name: 'today_panel_plugin', +) diff --git a/src/plugins/today-panel/theme/Adwaita.css b/src/plugins/today-panel/theme/Adwaita.css new file mode 100644 index 0000000..93b4213 --- /dev/null +++ b/src/plugins/today-panel/theme/Adwaita.css @@ -0,0 +1,11 @@ +label.date-scheduled { + color: @theme_selected_bg_color; + font-size: 1.4rem; + font-weight: bold; +} + +label.date-overdue { + color: #ee2222; + font-size: 1.4rem; + font-weight: bold; +} diff --git a/src/plugins/today-panel/today-panel-plugin.c b/src/plugins/today-panel/today-panel-plugin.c new file mode 100644 index 0000000..9dbc6ce --- /dev/null +++ b/src/plugins/today-panel/today-panel-plugin.c @@ -0,0 +1,34 @@ +/* gtd-plugin-today-panel.c + * + * Copyright (C) 2016-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include <endeavour.h> + +#include "gtd-panel-today.h" +#include "gtd-today-omni-area-addin.h" + +G_MODULE_EXPORT void +today_panel_plugin_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + GTD_TYPE_PANEL, + GTD_TYPE_PANEL_TODAY); + + peas_object_module_register_extension_type (module, + GTD_TYPE_OMNI_AREA_ADDIN, + GTD_TYPE_TODAY_OMNI_AREA_ADDIN); +} diff --git a/src/plugins/today-panel/today-panel.gresource.xml b/src/plugins/today-panel/today-panel.gresource.xml new file mode 100644 index 0000000..1b84499 --- /dev/null +++ b/src/plugins/today-panel/today-panel.gresource.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/todo/plugins/today-panel"> + <file>today-panel.plugin</file> + <file>theme/Adwaita.css</file> + </gresource> +</gresources> diff --git a/src/plugins/today-panel/today-panel.plugin b/src/plugins/today-panel/today-panel.plugin new file mode 100644 index 0000000..89640ff --- /dev/null +++ b/src/plugins/today-panel/today-panel.plugin @@ -0,0 +1,14 @@ +[Plugin] +Name = Today tasks +Module = today-panel +Description = A panel to show tasks scheduled for today +Version = @VERSION@ +Authors = Georges Basile Stavracas Neto <gbsneto@gnome.org> +Copyright = Copyleft © The Endeavour maintainers +Website = https://wiki.gnome.org/Apps/Todo +Builtin = true +Hidden = true +License = GPL +Loader = C +Embedded = today_panel_plugin_register_types +Depends = |
