summaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/all-tasks-panel/all-tasks-panel-plugin.c34
-rw-r--r--src/plugins/all-tasks-panel/all-tasks-panel.gresource.xml6
-rw-r--r--src/plugins/all-tasks-panel/all-tasks-panel.plugin13
-rw-r--r--src/plugins/all-tasks-panel/gtd-all-tasks-panel.c491
-rw-r--r--src/plugins/all-tasks-panel/gtd-all-tasks-panel.h34
-rw-r--r--src/plugins/all-tasks-panel/meson.build12
-rw-r--r--src/plugins/eds/e-source-endeavour.c128
-rw-r--r--src/plugins/eds/e-source-endeavour.h35
-rw-r--r--src/plugins/eds/eds-plugin.c30
-rw-r--r--src/plugins/eds/eds.gresource.xml6
-rw-r--r--src/plugins/eds/eds.plugin14
-rw-r--r--src/plugins/eds/gtd-eds-autoptr.h27
-rw-r--r--src/plugins/eds/gtd-eds.h32
-rw-r--r--src/plugins/eds/gtd-plugin-eds.c323
-rw-r--r--src/plugins/eds/gtd-plugin-eds.h28
-rw-r--r--src/plugins/eds/gtd-provider-eds.c1157
-rw-r--r--src/plugins/eds/gtd-provider-eds.h61
-rw-r--r--src/plugins/eds/gtd-provider-goa.c262
-rw-r--r--src/plugins/eds/gtd-provider-goa.h43
-rw-r--r--src/plugins/eds/gtd-provider-local.c150
-rw-r--r--src/plugins/eds/gtd-provider-local.h37
-rw-r--r--src/plugins/eds/gtd-task-eds.c650
-rw-r--r--src/plugins/eds/gtd-task-eds.h46
-rw-r--r--src/plugins/eds/gtd-task-list-eds.c875
-rw-r--r--src/plugins/eds/gtd-task-list-eds.h53
-rw-r--r--src/plugins/eds/meson.build27
-rw-r--r--src/plugins/inbox-panel/gtd-inbox-panel.c275
-rw-r--r--src/plugins/inbox-panel/gtd-inbox-panel.h32
-rw-r--r--src/plugins/inbox-panel/inbox-panel-plugin.c32
-rw-r--r--src/plugins/inbox-panel/inbox-panel.gresource.xml6
-rw-r--r--src/plugins/inbox-panel/inbox-panel.plugin14
-rw-r--r--src/plugins/inbox-panel/meson.build12
-rw-r--r--src/plugins/meson.build36
-rw-r--r--src/plugins/next-week-panel/gtd-next-week-panel.c573
-rw-r--r--src/plugins/next-week-panel/gtd-next-week-panel.h34
-rw-r--r--src/plugins/next-week-panel/meson.build12
-rw-r--r--src/plugins/next-week-panel/next-week-panel-plugin.c33
-rw-r--r--src/plugins/next-week-panel/next-week-panel.gresource.xml7
-rw-r--r--src/plugins/next-week-panel/next-week-panel.plugin13
-rw-r--r--src/plugins/next-week-panel/theme/Adwaita.css11
-rw-r--r--src/plugins/peace/gtd-peace-omni-area-addin.c209
-rw-r--r--src/plugins/peace/gtd-peace-omni-area-addin.h30
-rw-r--r--src/plugins/peace/meson.build12
-rw-r--r--src/plugins/peace/peace-plugin.c31
-rw-r--r--src/plugins/peace/peace.gresource.xml6
-rw-r--r--src/plugins/peace/peace.plugin12
-rw-r--r--src/plugins/scheduled-panel/gtd-panel-scheduled.c518
-rw-r--r--src/plugins/scheduled-panel/gtd-panel-scheduled.h35
-rw-r--r--src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.c153
-rw-r--r--src/plugins/scheduled-panel/gtd-plugin-scheduled-panel.h37
-rw-r--r--src/plugins/scheduled-panel/meson.build6
-rw-r--r--src/plugins/scheduled-panel/scheduled-panel.gresource.xml7
-rw-r--r--src/plugins/scheduled-panel/scheduled-panel.plugin13
-rw-r--r--src/plugins/scheduled-panel/theme/Adwaita.css11
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-list-row.c333
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-list-row.h37
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-list-row.ui41
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-panel-row.c180
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-panel-row.h37
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-panel-row.ui34
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-provider-row.c300
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-provider-row.h39
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar-provider-row.ui74
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar.c929
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar.h44
-rw-r--r--src/plugins/task-lists-workspace/gtd-sidebar.ui144
-rw-r--r--src/plugins/task-lists-workspace/gtd-task-list-panel.c636
-rw-r--r--src/plugins/task-lists-workspace/gtd-task-list-panel.h40
-rw-r--r--src/plugins/task-lists-workspace/gtd-task-list-panel.ui113
-rw-r--r--src/plugins/task-lists-workspace/gtd-task-lists-workspace.c711
-rw-r--r--src/plugins/task-lists-workspace/gtd-task-lists-workspace.h35
-rw-r--r--src/plugins/task-lists-workspace/gtd-task-lists-workspace.ui164
-rw-r--r--src/plugins/task-lists-workspace/meson.build20
-rw-r--r--src/plugins/task-lists-workspace/task-lists-workspace-plugin.c31
-rw-r--r--src/plugins/task-lists-workspace/task-lists-workspace.gresource.xml12
-rw-r--r--src/plugins/task-lists-workspace/task-lists-workspace.plugin14
-rw-r--r--src/plugins/today-panel/gtd-panel-today.c480
-rw-r--r--src/plugins/today-panel/gtd-panel-today.h35
-rw-r--r--src/plugins/today-panel/gtd-today-omni-area-addin.c304
-rw-r--r--src/plugins/today-panel/gtd-today-omni-area-addin.h30
-rw-r--r--src/plugins/today-panel/meson.build14
-rw-r--r--src/plugins/today-panel/theme/Adwaita.css11
-rw-r--r--src/plugins/today-panel/today-panel-plugin.c34
-rw-r--r--src/plugins/today-panel/today-panel.gresource.xml7
-rw-r--r--src/plugins/today-panel/today-panel.plugin14
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) ? &note : 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 (&current_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, &current_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">&lt;Control&gt;Page_Down</property>
+ <property name="action">action(sidebar.move-down)</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkShortcut">
+ <property name="trigger">&lt;Alt&gt;Down</property>
+ <property name="action">action(sidebar.move-down)</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkShortcut">
+ <property name="trigger">&lt;Control&gt;Page_Up</property>
+ <property name="action">action(sidebar.move-up)</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkShortcut">
+ <property name="trigger">&lt;Alt&gt;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 =