summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMatthew Fennell <matthew@fennell.dev>2025-12-27 12:40:20 +0000
committerMatthew Fennell <matthew@fennell.dev>2025-12-27 12:40:20 +0000
commit5d8e439bc597159e3c9f0a8b65c0ae869dead3a8 (patch)
treeed28aefed8add0da1c55c08fdf80b23c4346e0dc /src
Import Upstream version 43.0upstream/latest
Diffstat (limited to 'src')
-rw-r--r--src/animation/gtd-animatable.c204
-rw-r--r--src/animation/gtd-animatable.h85
-rw-r--r--src/animation/gtd-animation-enums.h173
-rw-r--r--src/animation/gtd-animation-utils.c168
-rw-r--r--src/animation/gtd-animation-utils.h65
-rw-r--r--src/animation/gtd-easing.c474
-rw-r--r--src/animation/gtd-easing.h141
-rw-r--r--src/animation/gtd-interval.c1134
-rw-r--r--src/animation/gtd-interval.h116
-rw-r--r--src/animation/gtd-keyframe-transition.c716
-rw-r--r--src/animation/gtd-keyframe-transition.h83
-rw-r--r--src/animation/gtd-property-transition.c359
-rw-r--r--src/animation/gtd-property-transition.h55
-rw-r--r--src/animation/gtd-timeline.c1547
-rw-r--r--src/animation/gtd-timeline.h150
-rw-r--r--src/animation/gtd-transition.c655
-rw-r--r--src/animation/gtd-transition.h85
-rw-r--r--src/core/gtd-activatable.c129
-rw-r--r--src/core/gtd-activatable.h52
-rw-r--r--src/core/gtd-clock.c292
-rw-r--r--src/core/gtd-clock.h33
-rw-r--r--src/core/gtd-log.c103
-rw-r--r--src/core/gtd-log.h27
-rw-r--r--src/core/gtd-manager-protected.h29
-rw-r--r--src/core/gtd-manager.c933
-rw-r--r--src/core/gtd-manager.h86
-rw-r--r--src/core/gtd-notification.c441
-rw-r--r--src/core/gtd-notification.h68
-rw-r--r--src/core/gtd-object.c313
-rw-r--r--src/core/gtd-object.h54
-rw-r--r--src/core/gtd-plugin-manager.c323
-rw-r--r--src/core/gtd-plugin-manager.h45
-rw-r--r--src/core/gtd-provider.c681
-rw-r--r--src/core/gtd-provider.h208
-rw-r--r--src/core/gtd-task-list.c1161
-rw-r--r--src/core/gtd-task-list.h118
-rw-r--r--src/core/gtd-task.c993
-rw-r--r--src/core/gtd-task.h122
-rw-r--r--src/endeavour.h56
-rw-r--r--src/gtd-debug.h.in229
-rw-r--r--src/gtd-enum-types.c.template39
-rw-r--r--src/gtd-enum-types.h.template24
-rw-r--r--src/gtd-types.h57
-rw-r--r--src/gtd-utils-private.h29
-rw-r--r--src/gtd-utils.c158
-rw-r--r--src/gtd-utils.h35
-rw-r--r--src/gtd-vcs.h.in29
-rw-r--r--src/gui/assets/all-done.svg1
-rw-r--r--src/gui/gtd-application.c331
-rw-r--r--src/gui/gtd-application.h36
-rw-r--r--src/gui/gtd-bin-layout.c112
-rw-r--r--src/gui/gtd-bin-layout.h32
-rw-r--r--src/gui/gtd-color-button.c273
-rw-r--r--src/gui/gtd-color-button.h38
-rw-r--r--src/gui/gtd-edit-pane.c602
-rw-r--r--src/gui/gtd-edit-pane.h47
-rw-r--r--src/gui/gtd-edit-pane.ui201
-rw-r--r--src/gui/gtd-initial-setup-window.c247
-rw-r--r--src/gui/gtd-initial-setup-window.h37
-rw-r--r--src/gui/gtd-initial-setup-window.ui82
-rw-r--r--src/gui/gtd-markdown-renderer.c357
-rw-r--r--src/gui/gtd-markdown-renderer.h35
-rw-r--r--src/gui/gtd-max-size-layout.c474
-rw-r--r--src/gui/gtd-max-size-layout.h52
-rw-r--r--src/gui/gtd-menu-button.c1056
-rw-r--r--src/gui/gtd-menu-button.h104
-rw-r--r--src/gui/gtd-new-task-row.c374
-rw-r--r--src/gui/gtd-new-task-row.h43
-rw-r--r--src/gui/gtd-new-task-row.ui31
-rw-r--r--src/gui/gtd-omni-area-addin.c69
-rw-r--r--src/gui/gtd-omni-area-addin.h49
-rw-r--r--src/gui/gtd-omni-area.c256
-rw-r--r--src/gui/gtd-omni-area.h41
-rw-r--r--src/gui/gtd-omni-area.ui85
-rw-r--r--src/gui/gtd-panel.c313
-rw-r--r--src/gui/gtd-panel.h77
-rw-r--r--src/gui/gtd-provider-popover.c245
-rw-r--r--src/gui/gtd-provider-popover.h34
-rw-r--r--src/gui/gtd-provider-popover.ui164
-rw-r--r--src/gui/gtd-provider-row.c240
-rw-r--r--src/gui/gtd-provider-row.h44
-rw-r--r--src/gui/gtd-provider-row.ui71
-rw-r--r--src/gui/gtd-provider-selector.c695
-rw-r--r--src/gui/gtd-provider-selector.h50
-rw-r--r--src/gui/gtd-provider-selector.ui109
-rw-r--r--src/gui/gtd-star-widget.c193
-rw-r--r--src/gui/gtd-star-widget.h37
-rw-r--r--src/gui/gtd-task-list-popover.c269
-rw-r--r--src/gui/gtd-task-list-popover.h34
-rw-r--r--src/gui/gtd-task-list-popover.ui39
-rw-r--r--src/gui/gtd-task-list-view.c1220
-rw-r--r--src/gui/gtd-task-list-view.h73
-rw-r--r--src/gui/gtd-task-list-view.ui97
-rw-r--r--src/gui/gtd-task-row.c839
-rw-r--r--src/gui/gtd-task-row.h54
-rw-r--r--src/gui/gtd-task-row.ui127
-rw-r--r--src/gui/gtd-widget.c1668
-rw-r--r--src/gui/gtd-widget.h110
-rw-r--r--src/gui/gtd-window.c413
-rw-r--r--src/gui/gtd-window.h43
-rw-r--r--src/gui/gtd-window.ui33
-rw-r--r--src/gui/gtd-workspace.c158
-rw-r--r--src/gui/gtd-workspace.h61
-rw-r--r--src/gui/gui.gresource.xml25
-rw-r--r--src/gui/menus.ui21
-rw-r--r--src/gui/meson.build6
-rw-r--r--src/gui/shortcuts-dialog.ui72
-rw-r--r--src/main.c44
-rw-r--r--src/meson.build339
-rw-r--r--src/models/gtd-list-model-filter.c569
-rw-r--r--src/models/gtd-list-model-filter.h44
-rw-r--r--src/models/gtd-list-model-sort.c500
-rw-r--r--src/models/gtd-list-model-sort.h46
-rw-r--r--src/models/gtd-list-store.c567
-rw-r--r--src/models/gtd-list-store.h66
-rw-r--r--src/models/gtd-task-list-view-model.c216
-rw-r--r--src/models/gtd-task-list-view-model.h43
-rw-r--r--src/models/gtd-task-model-private.h29
-rw-r--r--src/models/gtd-task-model.c216
-rw-r--r--src/models/gtd-task-model.h33
-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
-rw-r--r--src/themes/_omniarea.css13
-rw-r--r--src/themes/_taskrow.css38
-rw-r--r--src/themes/_widgets.css27
-rw-r--r--src/themes/style.css58
-rw-r--r--src/todo.gresource.xml9
210 files changed, 40274 insertions, 0 deletions
diff --git a/src/animation/gtd-animatable.c b/src/animation/gtd-animatable.c
new file mode 100644
index 0000000..b068b31
--- /dev/null
+++ b/src/animation/gtd-animatable.c
@@ -0,0 +1,204 @@
+/* gtd-animatable.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
+ */
+
+
+/**
+ * SECTION:gtd-animatable
+ * @short_description: Interface for animatable classes
+ *
+ * #GtdAnimatable is an interface that allows a #GObject class
+ * to control how an widget will animate a property.
+ *
+ * Each #GtdAnimatable should implement the
+ * #GtdAnimatableInterface.interpolate_property() virtual function of the
+ * interface to compute the animation state between two values of an interval
+ * depending on a progress fwidget, expressed as a floating point value.
+ */
+
+#include "gtd-animatable.h"
+
+#include "gtd-debug.h"
+#include "gtd-interval.h"
+
+G_DEFINE_INTERFACE (GtdAnimatable, gtd_animatable, G_TYPE_OBJECT);
+
+static void
+gtd_animatable_default_init (GtdAnimatableInterface *iface)
+{
+}
+
+/**
+ * gtd_animatable_find_property:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the animatable property to find
+ *
+ * Finds the #GParamSpec for @property_name
+ *
+ * Return value: (transfer none) (nullable): The #GParamSpec for the given property
+ */
+GParamSpec *
+gtd_animatable_find_property (GtdAnimatable *animatable,
+ const gchar *property_name)
+{
+ GtdAnimatableInterface *iface;
+
+ g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), NULL);
+ g_return_val_if_fail (property_name != NULL, NULL);
+
+ GTD_TRACE_MSG ("[animation] Looking for property '%s'", property_name);
+
+ iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+ if (iface->find_property != NULL)
+ return iface->find_property (animatable, property_name);
+
+ return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable),
+ property_name);
+}
+
+/**
+ * gtd_animatable_get_initial_state:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the animatable property to retrieve
+ * @value: a #GValue initialized to the type of the property to retrieve
+ *
+ * Retrieves the current state of @property_name and sets @value with it
+ */
+void
+gtd_animatable_get_initial_state (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GValue *value)
+{
+ GtdAnimatableInterface *iface;
+
+ g_return_if_fail (GTD_IS_ANIMATABLE (animatable));
+ g_return_if_fail (property_name != NULL);
+
+ GTD_TRACE_MSG ("[animation] Getting initial state of '%s'", property_name);
+
+ iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+ if (iface->get_initial_state != NULL)
+ iface->get_initial_state (animatable, property_name, value);
+ else
+ g_object_get_property (G_OBJECT (animatable), property_name, value);
+}
+
+/**
+ * gtd_animatable_set_final_state:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the animatable property to set
+ * @value: the value of the animatable property to set
+ *
+ * Sets the current state of @property_name to @value
+ */
+void
+gtd_animatable_set_final_state (GtdAnimatable *animatable,
+ const gchar *property_name,
+ const GValue *value)
+{
+ GtdAnimatableInterface *iface;
+
+ g_return_if_fail (GTD_IS_ANIMATABLE (animatable));
+ g_return_if_fail (property_name != NULL);
+
+ GTD_TRACE_MSG ("[animation] Setting state of property '%s'", property_name);
+
+ iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+ if (iface->set_final_state != NULL)
+ iface->set_final_state (animatable, property_name, value);
+ else
+ g_object_set_property (G_OBJECT (animatable), property_name, value);
+}
+
+/**
+ * gtd_animatable_interpolate_value:
+ * @animatable: a #GtdAnimatable
+ * @property_name: the name of the property to interpolate
+ * @interval: a #GtdInterval with the animation range
+ * @progress: the progress to use to interpolate between the
+ * initial and final values of the @interval
+ * @value: (out): return location for an initialized #GValue
+ * using the same type of the @interval
+ *
+ * Asks a #GtdAnimatable implementation to interpolate a
+ * a named property between the initial and final values of
+ * a #GtdInterval, using @progress as the interpolation
+ * value, and store the result inside @value.
+ *
+ * This function should be used for every property animation
+ * involving #GtdAnimatable<!-- -->s.
+ *
+ * This function replaces gtd_animatable_animate_property().
+ *
+ * Return value: %TRUE if the interpolation was successful,
+ * and %FALSE otherwise
+ */
+gboolean
+gtd_animatable_interpolate_value (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GtdInterval *interval,
+ gdouble progress,
+ GValue *value)
+{
+ GtdAnimatableInterface *iface;
+
+ g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), FALSE);
+ g_return_val_if_fail (property_name != NULL, FALSE);
+ g_return_val_if_fail (GTD_IS_INTERVAL (interval), FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+
+ GTD_TRACE_MSG ("[animation] Interpolating '%s' (progress: %.3f)",
+ property_name,
+ progress);
+
+ iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+ if (iface->interpolate_value != NULL)
+ {
+ return iface->interpolate_value (animatable, property_name,
+ interval,
+ progress,
+ value);
+ }
+ else
+ {
+ return gtd_interval_compute_value (interval, progress, value);
+ }
+}
+
+/**
+ * gtd_animatable_get_widget:
+ * @animatable: a #GtdAnimatable
+ *
+ * Get animated widget.
+ *
+ * Return value: (transfer none): a #GtdWidget
+ */
+GtdWidget *
+gtd_animatable_get_widget (GtdAnimatable *animatable)
+{
+ GtdAnimatableInterface *iface;
+
+ g_return_val_if_fail (GTD_IS_ANIMATABLE (animatable), NULL);
+
+ iface = GTD_ANIMATABLE_GET_IFACE (animatable);
+
+ g_return_val_if_fail (iface->get_widget, NULL);
+
+ return iface->get_widget (animatable);
+}
diff --git a/src/animation/gtd-animatable.h b/src/animation/gtd-animatable.h
new file mode 100644
index 0000000..3edb07d
--- /dev/null
+++ b/src/animation/gtd-animatable.h
@@ -0,0 +1,85 @@
+/* gtd-animatable.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 "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_ANIMATABLE (gtd_animatable_get_type ())
+G_DECLARE_INTERFACE (GtdAnimatable, gtd_animatable, GTD, ANIMATABLE, GObject)
+
+/**
+ * GtdAnimatableInterface:
+ * @find_property: virtual function for retrieving the #GParamSpec of
+ * an animatable property
+ * @get_initial_state: virtual function for retrieving the initial
+ * state of an animatable property
+ * @set_final_state: virtual function for setting the state of an
+ * animatable property
+ * @interpolate_value: virtual function for interpolating the progress
+ * of a property
+ * @get_widget: virtual function for getting associated #GtdWidget
+ *
+ * Since: 1.0
+ */
+struct _GtdAnimatableInterface
+{
+ /*< private >*/
+ GTypeInterface parent_iface;
+
+ /*< public >*/
+ GParamSpec *(* find_property) (GtdAnimatable *animatable,
+ const gchar *property_name);
+ void (* get_initial_state) (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GValue *value);
+ void (* set_final_state) (GtdAnimatable *animatable,
+ const gchar *property_name,
+ const GValue *value);
+ gboolean (* interpolate_value) (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GtdInterval *interval,
+ gdouble progress,
+ GValue *value);
+ GtdWidget * (* get_widget) (GtdAnimatable *animatable);
+};
+
+GParamSpec *gtd_animatable_find_property (GtdAnimatable *animatable,
+ const gchar *property_name);
+
+void gtd_animatable_get_initial_state (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GValue *value);
+
+void gtd_animatable_set_final_state (GtdAnimatable *animatable,
+ const gchar *property_name,
+ const GValue *value);
+
+gboolean gtd_animatable_interpolate_value (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GtdInterval *interval,
+ gdouble progress,
+ GValue *value);
+
+GtdWidget * gtd_animatable_get_widget (GtdAnimatable *animatable);
+
+G_END_DECLS
diff --git a/src/animation/gtd-animation-enums.h b/src/animation/gtd-animation-enums.h
new file mode 100644
index 0000000..ed26f8e
--- /dev/null
+++ b/src/animation/gtd-animation-enums.h
@@ -0,0 +1,173 @@
+/* gtd-animation-enums.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>
+
+G_BEGIN_DECLS
+
+
+/**
+ * GtdEaseMode:
+ * @GTD_CUSTOM_MODE: custom progress function
+ * @GTD_EASE_LINEAR: linear tweening
+ * @GTD_EASE_IN_QUAD: quadratic tweening
+ * @GTD_EASE_OUT_QUAD: quadratic tweening, inverse of
+ * %GTD_EASE_IN_QUAD
+ * @GTD_EASE_IN_OUT_QUAD: quadratic tweening, combininig
+ * %GTD_EASE_IN_QUAD and %GTD_EASE_OUT_QUAD
+ * @GTD_EASE_IN_CUBIC: cubic tweening
+ * @GTD_EASE_OUT_CUBIC: cubic tweening, invers of
+ * %GTD_EASE_IN_CUBIC
+ * @GTD_EASE_IN_OUT_CUBIC: cubic tweening, combining
+ * %GTD_EASE_IN_CUBIC and %GTD_EASE_OUT_CUBIC
+ * @GTD_EASE_IN_QUART: quartic tweening
+ * @GTD_EASE_OUT_QUART: quartic tweening, inverse of
+ * %GTD_EASE_IN_QUART
+ * @GTD_EASE_IN_OUT_QUART: quartic tweening, combining
+ * %GTD_EASE_IN_QUART and %GTD_EASE_OUT_QUART
+ * @GTD_EASE_IN_QUINT: quintic tweening
+ * @GTD_EASE_OUT_QUINT: quintic tweening, inverse of
+ * %GTD_EASE_IN_QUINT
+ * @GTD_EASE_IN_OUT_QUINT: fifth power tweening, combining
+ * %GTD_EASE_IN_QUINT and %GTD_EASE_OUT_QUINT
+ * @GTD_EASE_IN_SINE: sinusoidal tweening
+ * @GTD_EASE_OUT_SINE: sinusoidal tweening, inverse of
+ * %GTD_EASE_IN_SINE
+ * @GTD_EASE_IN_OUT_SINE: sine wave tweening, combining
+ * %GTD_EASE_IN_SINE and %GTD_EASE_OUT_SINE
+ * @GTD_EASE_IN_EXPO: exponential tweening
+ * @GTD_EASE_OUT_EXPO: exponential tweening, inverse of
+ * %GTD_EASE_IN_EXPO
+ * @GTD_EASE_IN_OUT_EXPO: exponential tweening, combining
+ * %GTD_EASE_IN_EXPO and %GTD_EASE_OUT_EXPO
+ * @GTD_EASE_IN_CIRC: circular tweening
+ * @GTD_EASE_OUT_CIRC: circular tweening, inverse of
+ * %GTD_EASE_IN_CIRC
+ * @GTD_EASE_IN_OUT_CIRC: circular tweening, combining
+ * %GTD_EASE_IN_CIRC and %GTD_EASE_OUT_CIRC
+ * @GTD_EASE_IN_ELASTIC: elastic tweening, with offshoot on start
+ * @GTD_EASE_OUT_ELASTIC: elastic tweening, with offshoot on end
+ * @GTD_EASE_IN_OUT_ELASTIC: elastic tweening with offshoot on both ends
+ * @GTD_EASE_IN_BACK: overshooting cubic tweening, with
+ * backtracking on start
+ * @GTD_EASE_OUT_BACK: overshooting cubic tweening, with
+ * backtracking on end
+ * @GTD_EASE_IN_OUT_BACK: overshooting cubic tweening, with
+ * backtracking on both ends
+ * @GTD_EASE_IN_BOUNCE: exponentially decaying parabolic (bounce)
+ * tweening, with bounce on start
+ * @GTD_EASE_OUT_BOUNCE: exponentially decaying parabolic (bounce)
+ * tweening, with bounce on end
+ * @GTD_EASE_IN_OUT_BOUNCE: exponentially decaying parabolic (bounce)
+ * tweening, with bounce on both ends
+ * @GTD_ANIMATION_LAST: last animation mode, used as a guard for
+ * registered global alpha functions
+ *
+ * The animation modes used by #ClutterAnimatable. This
+ * enumeration can be expanded in later versions of Clutter.
+ *
+ * <figure id="easing-modes">
+ * <title>Easing modes provided by Clutter</title>
+ * <graphic fileref="easing-modes.png" format="PNG"/>
+ * </figure>
+ *
+ * Every global alpha function registered using clutter_alpha_register_func()
+ * or clutter_alpha_register_closure() will have a logical id greater than
+ * %GTD_ANIMATION_LAST.
+ *
+ * Since: 1.0
+ */
+typedef enum
+{
+ GTD_CUSTOM_MODE = 0,
+
+ /* linear */
+ GTD_EASE_LINEAR,
+
+ /* quadratic */
+ GTD_EASE_IN_QUAD,
+ GTD_EASE_OUT_QUAD,
+ GTD_EASE_IN_OUT_QUAD,
+
+ /* cubic */
+ GTD_EASE_IN_CUBIC,
+ GTD_EASE_OUT_CUBIC,
+ GTD_EASE_IN_OUT_CUBIC,
+
+ /* quartic */
+ GTD_EASE_IN_QUART,
+ GTD_EASE_OUT_QUART,
+ GTD_EASE_IN_OUT_QUART,
+
+ /* quintic */
+ GTD_EASE_IN_QUINT,
+ GTD_EASE_OUT_QUINT,
+ GTD_EASE_IN_OUT_QUINT,
+
+ /* sinusoidal */
+ GTD_EASE_IN_SINE,
+ GTD_EASE_OUT_SINE,
+ GTD_EASE_IN_OUT_SINE,
+
+ /* exponential */
+ GTD_EASE_IN_EXPO,
+ GTD_EASE_OUT_EXPO,
+ GTD_EASE_IN_OUT_EXPO,
+
+ /* circular */
+ GTD_EASE_IN_CIRC,
+ GTD_EASE_OUT_CIRC,
+ GTD_EASE_IN_OUT_CIRC,
+
+ /* elastic */
+ GTD_EASE_IN_ELASTIC,
+ GTD_EASE_OUT_ELASTIC,
+ GTD_EASE_IN_OUT_ELASTIC,
+
+ /* overshooting cubic */
+ GTD_EASE_IN_BACK,
+ GTD_EASE_OUT_BACK,
+ GTD_EASE_IN_OUT_BACK,
+
+ /* exponentially decaying parabolic */
+ GTD_EASE_IN_BOUNCE,
+ GTD_EASE_OUT_BOUNCE,
+ GTD_EASE_IN_OUT_BOUNCE,
+
+ /* guard, before registered alpha functions */
+ GTD_EASE_LAST
+} GtdEaseMode;
+
+/**
+ * GtdTimelineDirection:
+ * @GTD_TIMELINE_FORWARD: forward direction for a timeline
+ * @GTD_TIMELINE_BACKWARD: backward direction for a timeline
+ *
+ * The direction of a #GtdTimeline
+ */
+typedef enum
+{
+ GTD_TIMELINE_FORWARD,
+ GTD_TIMELINE_BACKWARD
+} GtdTimelineDirection;
+
+G_END_DECLS
diff --git a/src/animation/gtd-animation-utils.c b/src/animation/gtd-animation-utils.c
new file mode 100644
index 0000000..7ab2198
--- /dev/null
+++ b/src/animation/gtd-animation-utils.c
@@ -0,0 +1,168 @@
+/* gtd-animation-utils.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-animation-utils.h"
+
+
+typedef struct
+{
+ GType value_type;
+ GtdProgressFunc func;
+} ProgressData;
+
+G_LOCK_DEFINE_STATIC (progress_funcs);
+static GHashTable *progress_funcs = NULL;
+
+gboolean
+gtd_has_progress_function (GType gtype)
+{
+ const char *type_name = g_type_name (gtype);
+
+ if (progress_funcs == NULL)
+ return FALSE;
+
+ return g_hash_table_lookup (progress_funcs, type_name) != NULL;
+}
+
+gboolean
+gtd_run_progress_function (GType gtype,
+ const GValue *initial,
+ const GValue *final,
+ gdouble progress,
+ GValue *retval)
+{
+ ProgressData *pdata;
+ gboolean res;
+
+ G_LOCK (progress_funcs);
+
+ if (G_UNLIKELY (!progress_funcs))
+ {
+ res = FALSE;
+ goto out;
+ }
+
+ pdata = g_hash_table_lookup (progress_funcs, g_type_name (gtype));
+ if (G_UNLIKELY (!pdata))
+ {
+ res = FALSE;
+ goto out;
+ }
+
+ res = pdata->func (initial, final, progress, retval);
+
+out:
+ G_UNLOCK (progress_funcs);
+
+ return res;
+}
+
+static void
+progress_data_destroy (gpointer data)
+{
+ g_free (data);
+}
+
+/**
+ * gtd_interval_register_progress_func: (skip)
+ * @value_type: a #GType
+ * @func: a #GtdProgressFunc, or %NULL to unset a previously
+ * set progress function
+ *
+ * Sets the progress function for a given @value_type, like:
+ *
+ * |[
+ * gtd_interval_register_progress_func (MY_TYPE_FOO,
+ * my_foo_progress);
+ * ]|
+ *
+ * Whenever a #GtdInterval instance using the default
+ * #GtdInterval::compute_value implementation is set as an
+ * interval between two #GValue of type @value_type, it will call
+ * @func to establish the value depending on the given progress,
+ * for instance:
+ *
+ * |[
+ * static gboolean
+ * my_int_progress (const GValue *a,
+ * const GValue *b,
+ * gdouble progress,
+ * GValue *retval)
+ * {
+ * gint ia = g_value_get_int (a);
+ * gint ib = g_value_get_int (b);
+ * gint res = factor * (ib - ia) + ia;
+ *
+ * g_value_set_int (retval, res);
+ *
+ * return TRUE;
+ * }
+ *
+ * gtd_interval_register_progress_func (G_TYPE_INT, my_int_progress);
+ * ]|
+ *
+ * To unset a previously set progress function of a #GType, pass %NULL
+ * for @func.
+ *
+ * Since: 1.0
+ */
+void
+gtd_interval_register_progress_func (GType value_type,
+ GtdProgressFunc func)
+{
+ ProgressData *progress_func;
+ const char *type_name;
+
+ g_return_if_fail (value_type != G_TYPE_INVALID);
+
+ type_name = g_type_name (value_type);
+
+ G_LOCK (progress_funcs);
+
+ if (G_UNLIKELY (!progress_funcs))
+ progress_funcs = g_hash_table_new_full (NULL, NULL, NULL, progress_data_destroy);
+
+ progress_func = g_hash_table_lookup (progress_funcs, type_name);
+
+ if (G_UNLIKELY (progress_func))
+ {
+ if (func == NULL)
+ {
+ g_hash_table_remove (progress_funcs, type_name);
+ g_free (progress_func);
+ }
+ else
+ {
+ progress_func->func = func;
+ }
+ }
+ else
+ {
+ progress_func = g_new0 (ProgressData, 1);
+ progress_func->value_type = value_type;
+ progress_func->func = func;
+
+ g_hash_table_replace (progress_funcs,
+ (gpointer) type_name,
+ progress_func);
+ }
+
+ G_UNLOCK (progress_funcs);
+}
diff --git a/src/animation/gtd-animation-utils.h b/src/animation/gtd-animation-utils.h
new file mode 100644
index 0000000..b41bc12
--- /dev/null
+++ b/src/animation/gtd-animation-utils.h
@@ -0,0 +1,65 @@
+/* gtd-animation-utils.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
+
+
+/**
+ * GtdProgressFunc:
+ * @a: the initial value of an interval
+ * @b: the final value of an interval
+ * @progress: the progress factor, between 0 and 1
+ * @retval: the value used to store the progress
+ *
+ * Prototype of the progress function used to compute the value
+ * between the two ends @a and @b of an interval depending on
+ * the value of @progress.
+ *
+ * The #GValue in @retval is already initialized with the same
+ * type as @a and @b.
+ *
+ * This function will be called by #GtdInterval if the
+ * type of the values of the interval was registered using
+ * gtd_interval_register_progress_func().
+ *
+ * Return value: %TRUE if the function successfully computed
+ * the value and stored it inside @retval
+ */
+typedef gboolean (* GtdProgressFunc) (const GValue *a,
+ const GValue *b,
+ gdouble progress,
+ GValue *retval);
+
+void gtd_interval_register_progress_func (GType value_type,
+ GtdProgressFunc func);
+
+
+gboolean gtd_has_progress_function (GType gtype);
+gboolean gtd_run_progress_function (GType gtype,
+ const GValue *initial,
+ const GValue *final,
+ gdouble progress,
+ GValue *retval);
+
+G_END_DECLS
diff --git a/src/animation/gtd-easing.c b/src/animation/gtd-easing.c
new file mode 100644
index 0000000..c674f91
--- /dev/null
+++ b/src/animation/gtd-easing.c
@@ -0,0 +1,474 @@
+/* gtd-easing.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-easing.h"
+
+#include <math.h>
+
+gdouble
+gtd_linear (gdouble t,
+ gdouble d)
+{
+ return t / d;
+}
+
+gdouble
+gtd_ease_in_quad (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ return p * p;
+}
+
+gdouble
+gtd_ease_out_quad (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ return -1.0 * p * (p - 2);
+}
+
+gdouble
+gtd_ease_in_out_quad (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / (d / 2);
+
+ if (p < 1)
+ return 0.5 * p * p;
+
+ p -= 1;
+
+ return -0.5 * (p * (p - 2) - 1);
+}
+
+gdouble
+gtd_ease_in_cubic (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ return p * p * p;
+}
+
+gdouble
+gtd_ease_out_cubic (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d - 1;
+
+ return p * p * p + 1;
+}
+
+gdouble
+gtd_ease_in_out_cubic (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / (d / 2);
+
+ if (p < 1)
+ return 0.5 * p * p * p;
+
+ p -= 2;
+
+ return 0.5 * (p * p * p + 2);
+}
+
+gdouble
+gtd_ease_in_quart (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ return p * p * p * p;
+}
+
+gdouble
+gtd_ease_out_quart (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d - 1;
+
+ return -1.0 * (p * p * p * p - 1);
+}
+
+gdouble
+gtd_ease_in_out_quart (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / (d / 2);
+
+ if (p < 1)
+ return 0.5 * p * p * p * p;
+
+ p -= 2;
+
+ return -0.5 * (p * p * p * p - 2);
+}
+
+gdouble
+gtd_ease_in_quint (gdouble t,
+ gdouble d)
+ {
+ gdouble p = t / d;
+
+ return p * p * p * p * p;
+}
+
+gdouble
+gtd_ease_out_quint (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d - 1;
+
+ return p * p * p * p * p + 1;
+}
+
+gdouble
+gtd_ease_in_out_quint (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / (d / 2);
+
+ if (p < 1)
+ return 0.5 * p * p * p * p * p;
+
+ p -= 2;
+
+ return 0.5 * (p * p * p * p * p + 2);
+}
+
+gdouble
+gtd_ease_in_sine (gdouble t,
+ gdouble d)
+{
+ return -1.0 * cos (t / d * G_PI_2) + 1.0;
+}
+
+gdouble
+gtd_ease_out_sine (gdouble t,
+ gdouble d)
+{
+ return sin (t / d * G_PI_2);
+}
+
+gdouble
+gtd_ease_in_out_sine (gdouble t,
+ gdouble d)
+{
+ return -0.5 * (cos (G_PI * t / d) - 1);
+}
+
+gdouble
+gtd_ease_in_expo (gdouble t,
+ gdouble d)
+{
+ return (t == 0) ? 0.0 : pow (2, 10 * (t / d - 1));
+}
+
+gdouble
+gtd_ease_out_expo (gdouble t,
+ gdouble d)
+{
+ return (t == d) ? 1.0 : -pow (2, -10 * t / d) + 1;
+}
+
+gdouble
+gtd_ease_in_out_expo (gdouble t,
+ gdouble d)
+{
+ gdouble p;
+
+ if (t == 0)
+ return 0.0;
+
+ if (t == d)
+ return 1.0;
+
+ p = t / (d / 2);
+
+ if (p < 1)
+ return 0.5 * pow (2, 10 * (p - 1));
+
+ p -= 1;
+
+ return 0.5 * (-pow (2, -10 * p) + 2);
+}
+
+gdouble
+gtd_ease_in_circ (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ return -1.0 * (sqrt (1 - p * p) - 1);
+}
+
+gdouble
+gtd_ease_out_circ (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d - 1;
+
+ return sqrt (1 - p * p);
+}
+
+gdouble
+gtd_ease_in_out_circ (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / (d / 2);
+
+ if (p < 1)
+ return -0.5 * (sqrt (1 - p * p) - 1);
+
+ p -= 2;
+
+ return 0.5 * (sqrt (1 - p * p) + 1);
+}
+
+gdouble
+gtd_ease_in_elastic (gdouble t,
+ gdouble d)
+{
+ gdouble p = d * .3;
+ gdouble s = p / 4;
+ gdouble q = t / d;
+
+ if (q == 1)
+ return 1.0;
+
+ q -= 1;
+
+ return -(pow (2, 10 * q) * sin ((q * d - s) * (2 * G_PI) / p));
+}
+
+gdouble
+gtd_ease_out_elastic (gdouble t,
+ gdouble d)
+{
+ gdouble p = d * .3;
+ gdouble s = p / 4;
+ gdouble q = t / d;
+
+ if (q == 1)
+ return 1.0;
+
+ return pow (2, -10 * q) * sin ((q * d - s) * (2 * G_PI) / p) + 1.0;
+}
+
+gdouble
+gtd_ease_in_out_elastic (gdouble t,
+ gdouble d)
+{
+ gdouble p = d * (.3 * 1.5);
+ gdouble s = p / 4;
+ gdouble q = t / (d / 2);
+
+ if (q == 2)
+ return 1.0;
+
+ if (q < 1)
+ {
+ q -= 1;
+
+ return -.5 * (pow (2, 10 * q) * sin ((q * d - s) * (2 * G_PI) / p));
+ }
+ else
+ {
+ q -= 1;
+
+ return pow (2, -10 * q)
+ * sin ((q * d - s) * (2 * G_PI) / p)
+ * .5 + 1.0;
+ }
+}
+
+gdouble
+gtd_ease_in_back (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ return p * p * ((1.70158 + 1) * p - 1.70158);
+}
+
+gdouble
+gtd_ease_out_back (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d - 1;
+
+ return p * p * ((1.70158 + 1) * p + 1.70158) + 1;
+}
+
+gdouble
+gtd_ease_in_out_back (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / (d / 2);
+ gdouble s = 1.70158 * 1.525;
+
+ if (p < 1)
+ return 0.5 * (p * p * ((s + 1) * p - s));
+
+ p -= 2;
+
+ return 0.5 * (p * p * ((s + 1) * p + s) + 2);
+}
+
+static inline gdouble
+ease_out_bounce_internal (gdouble t,
+ gdouble d)
+{
+ gdouble p = t / d;
+
+ if (p < (1 / 2.75))
+ {
+ return 7.5625 * p * p;
+ }
+ else if (p < (2 / 2.75))
+ {
+ p -= (1.5 / 2.75);
+
+ return 7.5625 * p * p + .75;
+ }
+ else if (p < (2.5 / 2.75))
+ {
+ p -= (2.25 / 2.75);
+
+ return 7.5625 * p * p + .9375;
+ }
+ else
+ {
+ p -= (2.625 / 2.75);
+
+ return 7.5625 * p * p + .984375;
+ }
+}
+
+static inline gdouble
+ease_in_bounce_internal (gdouble t,
+ gdouble d)
+{
+ return 1.0 - ease_out_bounce_internal (d - t, d);
+}
+
+gdouble
+gtd_ease_in_bounce (gdouble t,
+ gdouble d)
+{
+ return ease_in_bounce_internal (t, d);
+}
+
+gdouble
+gtd_ease_out_bounce (gdouble t,
+ gdouble d)
+{
+ return ease_out_bounce_internal (t, d);
+}
+
+gdouble
+gtd_ease_in_out_bounce (gdouble t,
+ gdouble d)
+{
+ if (t < d / 2)
+ return ease_in_bounce_internal (t * 2, d) * 0.5;
+ else
+ return ease_out_bounce_internal (t * 2 - d, d) * 0.5 + 1.0 * 0.5;
+}
+
+/*< private >
+ * _gtd_animation_modes:
+ *
+ * A mapping of animation modes and easing functions.
+ */
+static const struct {
+ GtdEaseMode mode;
+ GtdEaseFunc func;
+ const char *name;
+} _gtd_animation_modes[] = {
+ { GTD_CUSTOM_MODE, NULL, "custom" },
+
+ { GTD_EASE_LINEAR, gtd_linear, "linear" },
+ { GTD_EASE_IN_QUAD, gtd_ease_in_quad, "easeInQuad" },
+ { GTD_EASE_OUT_QUAD, gtd_ease_out_quad, "easeOutQuad" },
+ { GTD_EASE_IN_OUT_QUAD, gtd_ease_in_out_quad, "easeInOutQuad" },
+ { GTD_EASE_IN_CUBIC, gtd_ease_in_cubic, "easeInCubic" },
+ { GTD_EASE_OUT_CUBIC, gtd_ease_out_cubic, "easeOutCubic" },
+ { GTD_EASE_IN_OUT_CUBIC, gtd_ease_in_out_cubic, "easeInOutCubic" },
+ { GTD_EASE_IN_QUART, gtd_ease_in_quart, "easeInQuart" },
+ { GTD_EASE_OUT_QUART, gtd_ease_out_quart, "easeOutQuart" },
+ { GTD_EASE_IN_OUT_QUART, gtd_ease_in_out_quart, "easeInOutQuart" },
+ { GTD_EASE_IN_QUINT, gtd_ease_in_quint, "easeInQuint" },
+ { GTD_EASE_OUT_QUINT, gtd_ease_out_quint, "easeOutQuint" },
+ { GTD_EASE_IN_OUT_QUINT, gtd_ease_in_out_quint, "easeInOutQuint" },
+ { GTD_EASE_IN_SINE, gtd_ease_in_sine, "easeInSine" },
+ { GTD_EASE_OUT_SINE, gtd_ease_out_sine, "easeOutSine" },
+ { GTD_EASE_IN_OUT_SINE, gtd_ease_in_out_sine, "easeInOutSine" },
+ { GTD_EASE_IN_EXPO, gtd_ease_in_expo, "easeInExpo" },
+ { GTD_EASE_OUT_EXPO, gtd_ease_out_expo, "easeOutExpo" },
+ { GTD_EASE_IN_OUT_EXPO, gtd_ease_in_out_expo, "easeInOutExpo" },
+ { GTD_EASE_IN_CIRC, gtd_ease_in_circ, "easeInCirc" },
+ { GTD_EASE_OUT_CIRC, gtd_ease_out_circ, "easeOutCirc" },
+ { GTD_EASE_IN_OUT_CIRC, gtd_ease_in_out_circ, "easeInOutCirc" },
+ { GTD_EASE_IN_ELASTIC, gtd_ease_in_elastic, "easeInElastic" },
+ { GTD_EASE_OUT_ELASTIC, gtd_ease_out_elastic, "easeOutElastic" },
+ { GTD_EASE_IN_OUT_ELASTIC, gtd_ease_in_out_elastic, "easeInOutElastic" },
+ { GTD_EASE_IN_BACK, gtd_ease_in_back, "easeInBack" },
+ { GTD_EASE_OUT_BACK, gtd_ease_out_back, "easeOutBack" },
+ { GTD_EASE_IN_OUT_BACK, gtd_ease_in_out_back, "easeInOutBack" },
+ { GTD_EASE_IN_BOUNCE, gtd_ease_in_bounce, "easeInBounce" },
+ { GTD_EASE_OUT_BOUNCE, gtd_ease_out_bounce, "easeOutBounce" },
+ { GTD_EASE_IN_OUT_BOUNCE, gtd_ease_in_out_bounce, "easeInOutBounce" },
+
+ { GTD_EASE_LAST, NULL, "sentinel" },
+};
+
+GtdEaseFunc
+gtd_get_easing_func_for_mode (GtdEaseMode mode)
+{
+ g_assert (_gtd_animation_modes[mode].mode == mode);
+ g_assert (_gtd_animation_modes[mode].func != NULL);
+
+ return _gtd_animation_modes[mode].func;
+}
+
+const char *
+gtd_get_easing_name_for_mode (GtdEaseMode mode)
+{
+ g_assert (_gtd_animation_modes[mode].mode == mode);
+ g_assert (_gtd_animation_modes[mode].func != NULL);
+
+ return _gtd_animation_modes[mode].name;
+}
+
+gdouble
+gtd_easing_for_mode (GtdEaseMode mode,
+ gdouble t,
+ gdouble d)
+{
+ g_assert (_gtd_animation_modes[mode].mode == mode);
+ g_assert (_gtd_animation_modes[mode].func != NULL);
+
+ return _gtd_animation_modes[mode].func (t, d);
+}
diff --git a/src/animation/gtd-easing.h b/src/animation/gtd-easing.h
new file mode 100644
index 0000000..b83bb7f
--- /dev/null
+++ b/src/animation/gtd-easing.h
@@ -0,0 +1,141 @@
+/* gtd-easing.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>
+
+#include "gtd-animation-enums.h"
+
+G_BEGIN_DECLS
+
+/**
+ * GtdEaseFunc:
+ * @t: elapsed time
+ * @d: total duration
+ *
+ * Internal type for the easing functions used by Gtd.
+ *
+ * Return value: the interpolated value, between -1.0 and 2.0
+ */
+typedef gdouble (* GtdEaseFunc) (gdouble t, gdouble d);
+
+GtdEaseFunc gtd_get_easing_func_for_mode (GtdEaseMode mode);
+
+const gchar* gtd_get_easing_name_for_mode (GtdEaseMode mode);
+
+gdouble gtd_easing_for_mode (GtdEaseMode mode,
+ gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_linear (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_quad (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_quad (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_quad (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_cubic (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_cubic (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_cubic (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_quart (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_quart (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_quart (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_quint (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_quint (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_quint (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_sine (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_sine (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_sine (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_expo (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_expo (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_expo (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_circ (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_circ (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_circ (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_elastic (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_elastic (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_elastic (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_back (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_back (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_back (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_bounce (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_out_bounce (gdouble t,
+ gdouble d);
+
+gdouble gtd_ease_in_out_bounce (gdouble t,
+ gdouble d);
+
+G_END_DECLS
diff --git a/src/animation/gtd-interval.c b/src/animation/gtd-interval.c
new file mode 100644
index 0000000..20efd82
--- /dev/null
+++ b/src/animation/gtd-interval.c
@@ -0,0 +1,1134 @@
+/* gtd-interval.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
+ */
+
+
+/**
+ * SECTION:clutter-interval
+ * @short_description: An object holding an interval of two values
+ *
+ * #GtdInterval is a simple object that can hold two values
+ * defining an interval. #GtdInterval can hold any value that
+ * can be enclosed inside a #GValue.
+ *
+ * Once a #GtdInterval for a specific #GType has been instantiated
+ * the #GtdInterval:value-type property cannot be changed anymore.
+ *
+ * #GtdInterval starts with a floating reference; this means that
+ * any object taking a reference on a #GtdInterval instance should
+ * also take ownership of the interval by using g_object_ref_sink().
+ *
+ * #GtdInterval can be subclassed to override the validation
+ * and value computation.
+ *
+ * #GtdInterval is available since Gtd 1.0
+ */
+
+#include "gtd-interval.h"
+
+#include "gtd-animation-utils.h"
+#include "gtd-easing.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gobject/gvaluecollector.h>
+
+enum
+{
+ PROP_0,
+ PROP_VALUE_TYPE,
+ PROP_INITIAL,
+ PROP_FINAL,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+enum
+{
+ INITIAL,
+ FINAL,
+ RESULT,
+ N_VALUES,
+};
+
+typedef struct
+{
+ GType value_type;
+
+ GValue *values;
+} GtdIntervalPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdInterval, gtd_interval, G_TYPE_INITIALLY_UNOWNED);
+
+
+static gboolean
+gtd_interval_real_validate (GtdInterval *self,
+ GParamSpec *pspec)
+{
+ GType pspec_gtype = G_PARAM_SPEC_VALUE_TYPE (pspec);
+
+ /* then check the fundamental types */
+ switch (G_TYPE_FUNDAMENTAL (pspec_gtype))
+ {
+ case G_TYPE_INT:
+ {
+ GParamSpecInt *pspec_int = G_PARAM_SPEC_INT (pspec);
+ gint a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_int->minimum && a <= pspec_int->maximum) &&
+ (b >= pspec_int->minimum && b <= pspec_int->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_INT64:
+ {
+ GParamSpecInt64 *pspec_int = G_PARAM_SPEC_INT64 (pspec);
+ gint64 a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_int->minimum && a <= pspec_int->maximum) &&
+ (b >= pspec_int->minimum && b <= pspec_int->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_UINT:
+ {
+ GParamSpecUInt *pspec_uint = G_PARAM_SPEC_UINT (pspec);
+ guint a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_uint->minimum && a <= pspec_uint->maximum) &&
+ (b >= pspec_uint->minimum && b <= pspec_uint->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_UINT64:
+ {
+ GParamSpecUInt64 *pspec_int = G_PARAM_SPEC_UINT64 (pspec);
+ guint64 a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_int->minimum && a <= pspec_int->maximum) &&
+ (b >= pspec_int->minimum && b <= pspec_int->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_CHAR:
+ {
+ GParamSpecChar *pspec_char = G_PARAM_SPEC_CHAR (pspec);
+ guchar a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_char->minimum && a <= pspec_char->maximum) &&
+ (b >= pspec_char->minimum && b <= pspec_char->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_UCHAR:
+ {
+ GParamSpecUChar *pspec_uchar = G_PARAM_SPEC_UCHAR (pspec);
+ guchar a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_uchar->minimum && a <= pspec_uchar->maximum) &&
+ (b >= pspec_uchar->minimum && b <= pspec_uchar->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_FLOAT:
+ {
+ GParamSpecFloat *pspec_flt = G_PARAM_SPEC_FLOAT (pspec);
+ float a, b;
+
+ a = b = 0.f;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_flt->minimum && a <= pspec_flt->maximum) &&
+ (b >= pspec_flt->minimum && b <= pspec_flt->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_DOUBLE:
+ {
+ GParamSpecDouble *pspec_flt = G_PARAM_SPEC_DOUBLE (pspec);
+ double a, b;
+
+ a = b = 0;
+ gtd_interval_get_interval (self, &a, &b);
+ if ((a >= pspec_flt->minimum && a <= pspec_flt->maximum) &&
+ (b >= pspec_flt->minimum && b <= pspec_flt->maximum))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ break;
+
+ case G_TYPE_BOOLEAN:
+ return TRUE;
+
+ default:
+ break;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+gtd_interval_real_compute_value (GtdInterval *self,
+ gdouble factor,
+ GValue *value)
+{
+ GValue *initial, *final;
+ GType value_type;
+ gboolean retval = FALSE;
+
+ initial = gtd_interval_peek_initial_value (self);
+ final = gtd_interval_peek_final_value (self);
+
+ value_type = gtd_interval_get_value_type (self);
+
+ if (gtd_has_progress_function (value_type))
+ {
+ retval = gtd_run_progress_function (value_type, initial, final, factor, value);
+ if (retval)
+ return TRUE;
+ }
+
+ switch (G_TYPE_FUNDAMENTAL (value_type))
+ {
+ case G_TYPE_INT:
+ {
+ gint ia, ib, res;
+
+ ia = g_value_get_int (initial);
+ ib = g_value_get_int (final);
+
+ res = (factor * (ib - ia)) + ia;
+
+ g_value_set_int (value, res);
+
+ retval = TRUE;
+ }
+ break;
+
+ case G_TYPE_CHAR:
+ {
+ gchar ia, ib, res;
+
+ ia = g_value_get_schar (initial);
+ ib = g_value_get_schar (final);
+
+ res = (factor * (ib - (gdouble) ia)) + ia;
+
+ g_value_set_schar (value, res);
+
+ retval = TRUE;
+ }
+ break;
+
+ case G_TYPE_UINT:
+ {
+ guint ia, ib, res;
+
+ ia = g_value_get_uint (initial);
+ ib = g_value_get_uint (final);
+
+ res = (factor * (ib - (gdouble) ia)) + ia;
+
+ g_value_set_uint (value, res);
+
+ retval = TRUE;
+ }
+ break;
+
+ case G_TYPE_UCHAR:
+ {
+ guchar ia, ib, res;
+
+ ia = g_value_get_uchar (initial);
+ ib = g_value_get_uchar (final);
+
+ res = (factor * (ib - (gdouble) ia)) + ia;
+
+ g_value_set_uchar (value, res);
+
+ retval = TRUE;
+ }
+ break;
+
+ case G_TYPE_FLOAT:
+ case G_TYPE_DOUBLE:
+ {
+ gdouble ia, ib, res;
+
+ if (value_type == G_TYPE_DOUBLE)
+ {
+ ia = g_value_get_double (initial);
+ ib = g_value_get_double (final);
+ }
+ else
+ {
+ ia = g_value_get_float (initial);
+ ib = g_value_get_float (final);
+ }
+
+ res = (factor * (ib - ia)) + ia;
+
+ if (value_type == G_TYPE_DOUBLE)
+ g_value_set_double (value, res);
+ else
+ g_value_set_float (value, res);
+
+ retval = TRUE;
+ }
+ break;
+
+ case G_TYPE_BOOLEAN:
+ if (factor > 0.5)
+ g_value_set_boolean (value, TRUE);
+ else
+ g_value_set_boolean (value, FALSE);
+
+ retval = TRUE;
+ break;
+
+ case G_TYPE_BOXED:
+ break;
+
+ default:
+ break;
+ }
+
+ /* We're trying to animate a property without knowing how to do that. Issue
+ * a warning with a hint to what could be done to fix that */
+ if (G_UNLIKELY (retval == FALSE))
+ {
+ g_warning ("%s: Could not compute progress between two %s. You can "
+ "register a progress function to instruct GtdInterval "
+ "how to deal with this GType",
+ G_STRLOC,
+ g_type_name (value_type));
+ }
+
+ return retval;
+}
+
+static void
+gtd_interval_finalize (GObject *object)
+{
+ GtdInterval *self = GTD_INTERVAL (object);
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+ if (G_IS_VALUE (&priv->values[INITIAL]))
+ g_value_unset (&priv->values[INITIAL]);
+
+ if (G_IS_VALUE (&priv->values[FINAL]))
+ g_value_unset (&priv->values[FINAL]);
+
+ if (G_IS_VALUE (&priv->values[RESULT]))
+ g_value_unset (&priv->values[RESULT]);
+
+ g_free (priv->values);
+
+ G_OBJECT_CLASS (gtd_interval_parent_class)->finalize (object);
+}
+
+static void
+gtd_interval_set_property (GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdInterval *self = GTD_INTERVAL (gobject);
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_VALUE_TYPE:
+ priv->value_type = g_value_get_gtype (value);
+ break;
+
+ case PROP_INITIAL:
+ if (g_value_get_boxed (value) != NULL)
+ gtd_interval_set_initial_value (self, g_value_get_boxed (value));
+ else if (G_IS_VALUE (&priv->values[INITIAL]))
+ g_value_unset (&priv->values[INITIAL]);
+ break;
+
+ case PROP_FINAL:
+ if (g_value_get_boxed (value) != NULL)
+ gtd_interval_set_final_value (self, g_value_get_boxed (value));
+ else if (G_IS_VALUE (&priv->values[FINAL]))
+ g_value_unset (&priv->values[FINAL]);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_interval_get_property (GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdIntervalPrivate *priv;
+
+ priv = gtd_interval_get_instance_private (GTD_INTERVAL (gobject));
+
+ switch (prop_id)
+ {
+ case PROP_VALUE_TYPE:
+ g_value_set_gtype (value, priv->value_type);
+ break;
+
+ case PROP_INITIAL:
+ if (G_IS_VALUE (&priv->values[INITIAL]))
+ g_value_set_boxed (value, &priv->values[INITIAL]);
+ break;
+
+ case PROP_FINAL:
+ if (G_IS_VALUE (&priv->values[FINAL]))
+ g_value_set_boxed (value, &priv->values[FINAL]);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_interval_class_init (GtdIntervalClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ klass->validate = gtd_interval_real_validate;
+ klass->compute_value = gtd_interval_real_compute_value;
+
+ gobject_class->set_property = gtd_interval_set_property,
+ gobject_class->get_property = gtd_interval_get_property;
+ gobject_class->finalize = gtd_interval_finalize;
+
+ /**
+ * GtdInterval:value-type:
+ *
+ * The type of the values in the interval.
+ */
+ obj_props[PROP_VALUE_TYPE] =
+ g_param_spec_gtype ("value-type",
+ "Value Type",
+ "The type of the values in the interval",
+ G_TYPE_NONE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdInterval:initial:
+ *
+ * The initial value of the interval.
+ */
+ obj_props[PROP_INITIAL] =
+ g_param_spec_boxed ("initial",
+ "Initial Value",
+ "Initial value of the interval",
+ G_TYPE_VALUE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdInterval:final:
+ *
+ * The final value of the interval.
+ */
+ obj_props[PROP_FINAL] =
+ g_param_spec_boxed ("final",
+ "Final Value",
+ "Final value of the interval",
+ G_TYPE_VALUE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
+
+static void
+gtd_interval_init (GtdInterval *self)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+ priv->value_type = G_TYPE_INVALID;
+ priv->values = g_malloc0 (sizeof (GValue) * N_VALUES);
+}
+
+static inline void
+gtd_interval_set_value_internal (GtdInterval *self,
+ gint index_,
+ const GValue *value)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ GType value_type;
+
+ g_assert (index_ >= INITIAL && index_ <= RESULT);
+
+ if (G_IS_VALUE (&priv->values[index_]))
+ g_value_unset (&priv->values[index_]);
+
+ g_value_init (&priv->values[index_], priv->value_type);
+
+ value_type = G_VALUE_TYPE (value);
+ if (value_type != priv->value_type ||
+ !g_type_is_a (value_type, priv->value_type))
+ {
+ if (g_value_type_compatible (value_type, priv->value_type))
+ {
+ g_value_copy (value, &priv->values[index_]);
+ return;
+ }
+
+ if (g_value_type_transformable (value_type, priv->value_type))
+ {
+ GValue transform = G_VALUE_INIT;
+
+ g_value_init (&transform, priv->value_type);
+
+ if (g_value_transform (value, &transform))
+ g_value_copy (&transform, &priv->values[index_]);
+ else
+ {
+ g_warning ("%s: Unable to convert a value of type '%s' into "
+ "the value type '%s' of the interval.",
+ G_STRLOC,
+ g_type_name (value_type),
+ g_type_name (priv->value_type));
+ }
+
+ g_value_unset (&transform);
+ }
+ }
+ else
+ g_value_copy (value, &priv->values[index_]);
+}
+
+static inline void
+gtd_interval_get_value_internal (GtdInterval *self,
+ gint index_,
+ GValue *value)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+
+ g_assert (index_ >= INITIAL && index_ <= RESULT);
+
+ g_value_copy (&priv->values[index_], value);
+}
+
+static gboolean
+gtd_interval_set_initial_internal (GtdInterval *self,
+ va_list *args)
+{;
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ GType gtype = priv->value_type;
+ GValue value = G_VALUE_INIT;
+ gchar *error;
+
+ /* initial value */
+ G_VALUE_COLLECT_INIT (&value, gtype, *args, 0, &error);
+
+ if (error)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+
+ /* we leak the value here as it might not be in a valid state
+ * given the error and calling g_value_unset() might lead to
+ * undefined behaviour
+ */
+ g_free (error);
+ return FALSE;
+ }
+
+ gtd_interval_set_value_internal (self, INITIAL, &value);
+ g_value_unset (&value);
+
+ return TRUE;
+}
+
+static gboolean
+gtd_interval_set_final_internal (GtdInterval *self,
+ va_list *args)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ GType gtype = priv->value_type;
+ GValue value = G_VALUE_INIT;
+ gchar *error;
+
+ /* initial value */
+ G_VALUE_COLLECT_INIT (&value, gtype, *args, 0, &error);
+
+ if (error)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+
+ /* we leak the value here as it might not be in a valid state
+ * given the error and calling g_value_unset() might lead to
+ * undefined behaviour
+ */
+ g_free (error);
+ return FALSE;
+ }
+
+ gtd_interval_set_value_internal (self, FINAL, &value);
+ g_value_unset (&value);
+
+ return TRUE;
+}
+
+static void
+gtd_interval_get_interval_valist (GtdInterval *self,
+ va_list var_args)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ GType gtype = priv->value_type;
+ GValue value = G_VALUE_INIT;
+ gchar *error;
+
+ /* initial value */
+ g_value_init (&value, gtype);
+ gtd_interval_get_initial_value (self, &value);
+ G_VALUE_LCOPY (&value, var_args, 0, &error);
+ if (error)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+ g_free (error);
+ g_value_unset (&value);
+ return;
+ }
+
+ g_value_unset (&value);
+
+ /* final value */
+ g_value_init (&value, gtype);
+ gtd_interval_get_final_value (self, &value);
+ G_VALUE_LCOPY (&value, var_args, 0, &error);
+ if (error)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+ g_free (error);
+ g_value_unset (&value);
+ return;
+ }
+
+ g_value_unset (&value);
+}
+
+/**
+ * gtd_interval_new:
+ * @gtype: the type of the values in the interval
+ * @...: the initial value and the final value of the interval
+ *
+ * Creates a new #GtdInterval holding values of type @gtype.
+ *
+ * This function avoids using a #GValue for the initial and final values
+ * of the interval:
+ *
+ * |[
+ * interval = gtd_interval_new (G_TYPE_FLOAT, 0.0, 1.0);
+ * interval = gtd_interval_new (G_TYPE_BOOLEAN, FALSE, TRUE);
+ * interval = gtd_interval_new (G_TYPE_INT, 0, 360);
+ * ]|
+ *
+ * Return value: the newly created #GtdInterval
+ */
+GtdInterval *
+gtd_interval_new (GType gtype,
+ ...)
+{
+ GtdInterval *retval;
+ va_list args;
+
+ g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL);
+
+ retval = g_object_new (GTD_TYPE_INTERVAL, "value-type", gtype, NULL);
+
+ va_start (args, gtype);
+
+ if (!gtd_interval_set_initial_internal (retval, &args))
+ goto out;
+
+ gtd_interval_set_final_internal (retval, &args);
+
+out:
+ va_end (args);
+
+ return retval;
+}
+
+/**
+ * gtd_interval_new_with_values:
+ * @gtype: the type of the values in the interval
+ * @initial: (allow-none): a #GValue holding the initial value of the interval
+ * @final: (allow-none): a #GValue holding the final value of the interval
+ *
+ * Creates a new #GtdInterval of type @gtype, between @initial
+ * and @final.
+ *
+ * This function is useful for language bindings.
+ *
+ * Return value: the newly created #GtdInterval
+ */
+GtdInterval *
+gtd_interval_new_with_values (GType gtype,
+ const GValue *initial,
+ const GValue *final)
+{
+ g_return_val_if_fail (gtype != G_TYPE_INVALID, NULL);
+ g_return_val_if_fail (initial == NULL || G_VALUE_TYPE (initial) == gtype, NULL);
+ g_return_val_if_fail (final == NULL || G_VALUE_TYPE (final) == gtype, NULL);
+
+ return g_object_new (GTD_TYPE_INTERVAL,
+ "value-type", gtype,
+ "initial", initial,
+ "final", final,
+ NULL);
+}
+
+/**
+ * gtd_interval_clone:
+ * @interval: a #GtdInterval
+ *
+ * Creates a copy of @interval.
+ *
+ * Return value: (transfer full): the newly created #GtdInterval
+ */
+GtdInterval *
+gtd_interval_clone (GtdInterval *self)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ GtdInterval *retval;
+ GType gtype;
+ GValue *tmp;
+
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+ g_return_val_if_fail (priv->value_type != G_TYPE_INVALID, NULL);
+
+ gtype = priv->value_type;
+ retval = g_object_new (GTD_TYPE_INTERVAL, "value-type", gtype, NULL);
+
+ tmp = gtd_interval_peek_initial_value (self);
+ gtd_interval_set_initial_value (retval, tmp);
+
+ tmp = gtd_interval_peek_final_value (self);
+ gtd_interval_set_final_value (retval, tmp);
+
+ return retval;
+}
+
+/**
+ * gtd_interval_get_value_type:
+ * @interval: a #GtdInterval
+ *
+ * Retrieves the #GType of the values inside @interval.
+ *
+ * Return value: the type of the value, or G_TYPE_INVALID
+ */
+GType
+gtd_interval_get_value_type (GtdInterval *self)
+{
+ GtdIntervalPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), G_TYPE_INVALID);
+
+ priv = gtd_interval_get_instance_private (self);
+ return priv->value_type;
+}
+
+/**
+ * gtd_interval_set_initial_value: (rename-to gtd_interval_set_initial)
+ * @interval: a #GtdInterval
+ * @value: a #GValue
+ *
+ * Sets the initial value of @interval to @value. The value is copied
+ * inside the #GtdInterval.
+ */
+void
+gtd_interval_set_initial_value (GtdInterval *self,
+ const GValue *value)
+{
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+ g_return_if_fail (value != NULL);
+
+ gtd_interval_set_value_internal (self, INITIAL, value);
+}
+
+/**
+ * gtd_interval_set_initial: (skip)
+ * @interval: a #GtdInterval
+ * @...: the initial value of the interval.
+ *
+ * Variadic arguments version of gtd_interval_set_initial_value().
+ *
+ * This function is meant as a convenience for the C API.
+ *
+ * Language bindings should use gtd_interval_set_initial_value()
+ * instead.
+ */
+void
+gtd_interval_set_initial (GtdInterval *self,
+ ...)
+{
+ va_list args;
+
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+
+ va_start (args, self);
+ gtd_interval_set_initial_internal (self, &args);
+ va_end (args);
+}
+
+/**
+ * gtd_interval_get_initial_value:
+ * @interval: a #GtdInterval
+ * @value: (out caller-allocates): a #GValue
+ *
+ * Retrieves the initial value of @interval and copies
+ * it into @value.
+ *
+ * The passed #GValue must be initialized to the value held by
+ * the #GtdInterval.
+ */
+void
+gtd_interval_get_initial_value (GtdInterval *self,
+ GValue *value)
+{
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+ g_return_if_fail (value != NULL);
+
+ gtd_interval_get_value_internal (self, INITIAL, value);
+}
+
+/**
+ * gtd_interval_peek_initial_value:
+ * @interval: a #GtdInterval
+ *
+ * Gets the pointer to the initial value of @interval
+ *
+ * Return value: (transfer none): the initial value of the interval.
+ * The value is owned by the #GtdInterval and it should not be
+ * modified or freed
+ */
+GValue *
+gtd_interval_peek_initial_value (GtdInterval *self)
+{
+ GtdIntervalPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+
+ priv = gtd_interval_get_instance_private (self);
+ return priv->values + INITIAL;
+}
+
+/**
+ * gtd_interval_set_final_value: (rename-to gtd_interval_set_final)
+ * @interval: a #GtdInterval
+ * @value: a #GValue
+ *
+ * Sets the final value of @interval to @value. The value is
+ * copied inside the #GtdInterval.
+ */
+void
+gtd_interval_set_final_value (GtdInterval *self,
+ const GValue *value)
+{
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+ g_return_if_fail (value != NULL);
+
+ gtd_interval_set_value_internal (self, FINAL, value);
+}
+
+/**
+ * gtd_interval_get_final_value:
+ * @interval: a #GtdInterval
+ * @value: (out caller-allocates): a #GValue
+ *
+ * Retrieves the final value of @interval and copies
+ * it into @value.
+ *
+ * The passed #GValue must be initialized to the value held by
+ * the #GtdInterval.
+ */
+void
+gtd_interval_get_final_value (GtdInterval *self,
+ GValue *value)
+{
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+ g_return_if_fail (value != NULL);
+
+ gtd_interval_get_value_internal (self, FINAL, value);
+}
+
+/**
+ * gtd_interval_set_final: (skip)
+ * @interval: a #GtdInterval
+ * @...: the final value of the interval
+ *
+ * Variadic arguments version of gtd_interval_set_final_value().
+ *
+ * This function is meant as a convenience for the C API.
+ *
+ * Language bindings should use gtd_interval_set_final_value() instead.
+ */
+void
+gtd_interval_set_final (GtdInterval *self,
+ ...)
+{
+ va_list args;
+
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+
+ va_start (args, self);
+ gtd_interval_set_final_internal (self, &args);
+ va_end (args);
+}
+
+/**
+ * gtd_interval_peek_final_value:
+ * @interval: a #GtdInterval
+ *
+ * Gets the pointer to the final value of @interval
+ *
+ * Return value: (transfer none): the final value of the interval.
+ * The value is owned by the #GtdInterval and it should not be
+ * modified or freed
+ */
+GValue *
+gtd_interval_peek_final_value (GtdInterval *self)
+{
+ GtdIntervalPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+
+ priv = gtd_interval_get_instance_private (self);
+ return priv->values + FINAL;
+}
+
+/**
+ * gtd_interval_set_interval:
+ * @interval: a #GtdInterval
+ * @...: the initial and final values of the interval
+ *
+ * Variable arguments wrapper for gtd_interval_set_initial_value()
+ * and gtd_interval_set_final_value() that avoids using the
+ * #GValue arguments:
+ *
+ * |[
+ * gtd_interval_set_interval (self, 0, 50);
+ * gtd_interval_set_interval (self, 1.0, 0.0);
+ * gtd_interval_set_interval (self, FALSE, TRUE);
+ * ]|
+ *
+ * This function is meant for the convenience of the C API; bindings
+ * should reimplement this function using the #GValue-based API.
+ */
+void
+gtd_interval_set_interval (GtdInterval *self,
+ ...)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ va_list args;
+
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+ g_return_if_fail (priv->value_type != G_TYPE_INVALID);
+
+ va_start (args, self);
+
+ if (!gtd_interval_set_initial_internal (self, &args))
+ goto out;
+
+ gtd_interval_set_final_internal (self, &args);
+
+out:
+ va_end (args);
+}
+
+/**
+ * gtd_interval_get_interval:
+ * @interval: a #GtdInterval
+ * @...: return locations for the initial and final values of
+ * the interval
+ *
+ * Variable arguments wrapper for gtd_interval_get_initial_value()
+ * and gtd_interval_get_final_value() that avoids using the
+ * #GValue arguments:
+ *
+ * |[
+ * gint a = 0, b = 0;
+ * gtd_interval_get_interval (self, &a, &b);
+ * ]|
+ *
+ * This function is meant for the convenience of the C API; bindings
+ * should reimplement this function using the #GValue-based API.
+ */
+void
+gtd_interval_get_interval (GtdInterval *self,
+ ...)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ va_list args;
+
+ g_return_if_fail (GTD_IS_INTERVAL (self));
+ g_return_if_fail (priv->value_type != G_TYPE_INVALID);
+
+ va_start (args, self);
+ gtd_interval_get_interval_valist (self, args);
+ va_end (args);
+}
+
+/**
+ * gtd_interval_validate:
+ * @interval: a #GtdInterval
+ * @pspec: a #GParamSpec
+ *
+ * Validates the initial and final values of @interval against
+ * a #GParamSpec.
+ *
+ * Return value: %TRUE if the #GtdInterval is valid, %FALSE otherwise
+ */
+gboolean
+gtd_interval_validate (GtdInterval *self,
+ GParamSpec *pspec)
+{
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE);
+ g_return_val_if_fail (G_IS_PARAM_SPEC (pspec), FALSE);
+
+ return GTD_INTERVAL_GET_CLASS (self)->validate (self, pspec);
+}
+
+/**
+ * gtd_interval_compute_value:
+ * @interval: a #GtdInterval
+ * @factor: the progress factor, between 0 and 1
+ * @value: (out caller-allocates): return location for an initialized #GValue
+ *
+ * Computes the value between the @interval boundaries given the
+ * progress @factor and copies it into @value.
+ *
+ * Return value: %TRUE if the operation was successful
+ */
+gboolean
+gtd_interval_compute_value (GtdInterval *self,
+ gdouble factor,
+ GValue *value)
+{
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE);
+ g_return_val_if_fail (value != NULL, FALSE);
+
+ return GTD_INTERVAL_GET_CLASS (self)->compute_value (self, factor, value);
+}
+
+/**
+ * gtd_interval_compute:
+ * @interval: a #GtdInterval
+ * @factor: the progress factor, between 0 and 1
+ *
+ * Computes the value between the @interval boundaries given the
+ * progress @factor
+ *
+ * Unlike gtd_interval_compute_value(), this function will
+ * return a const pointer to the computed value
+ *
+ * You should use this function if you immediately pass the computed
+ * value to another function that makes a copy of it, like
+ * g_object_set_property()
+ *
+ * Return value: (transfer none): a pointer to the computed value,
+ * or %NULL if the computation was not successfull
+ */
+const GValue *
+gtd_interval_compute (GtdInterval *self,
+ gdouble factor)
+{
+ GtdIntervalPrivate *priv = gtd_interval_get_instance_private (self);
+ GValue *value;
+ gboolean res;
+
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), NULL);
+
+ value = &(priv->values[RESULT]);
+
+ if (G_VALUE_TYPE (value) == G_TYPE_INVALID)
+ g_value_init (value, priv->value_type);
+
+ res = GTD_INTERVAL_GET_CLASS (self)->compute_value (self, factor, value);
+
+ if (res)
+ return priv->values + RESULT;
+
+ return NULL;
+}
+
+/**
+ * gtd_interval_is_valid:
+ * @interval: a #GtdInterval
+ *
+ * Checks if the @interval has a valid initial and final values.
+ *
+ * Return value: %TRUE if the #GtdInterval has an initial and
+ * final values, and %FALSE otherwise
+ */
+gboolean
+gtd_interval_is_valid (GtdInterval *self)
+{
+ GtdIntervalPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_INTERVAL (self), FALSE);
+
+ priv = gtd_interval_get_instance_private (self);
+
+ return G_IS_VALUE (&priv->values[INITIAL]) &&
+ G_IS_VALUE (&priv->values[FINAL]);
+}
diff --git a/src/animation/gtd-interval.h b/src/animation/gtd-interval.h
new file mode 100644
index 0000000..efcfb03
--- /dev/null
+++ b/src/animation/gtd-interval.h
@@ -0,0 +1,116 @@
+/* gtd-interval.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_INTERVAL (gtd_interval_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdInterval, gtd_interval, GTD, INTERVAL, GInitiallyUnowned)
+
+/**
+ * GtdIntervalClass:
+ * @validate: virtual function for validating an interval
+ * using a #GParamSpec
+ * @compute_value: virtual function for computing the value
+ * inside an interval using an adimensional factor between 0 and 1
+ *
+ * The #GtdIntervalClass contains only private data.
+ *
+ * Since: 1.0
+ */
+struct _GtdIntervalClass
+{
+ /*< private >*/
+ GInitiallyUnownedClass parent_class;
+
+ /*< public >*/
+ gboolean (* validate) (GtdInterval *self,
+ GParamSpec *pspec);
+ gboolean (* compute_value) (GtdInterval *self,
+ gdouble factor,
+ GValue *value);
+
+ /*< private >*/
+ /* padding for future expansion */
+ void (*_gtd_reserved1) (void);
+ void (*_gtd_reserved2) (void);
+ void (*_gtd_reserved3) (void);
+ void (*_gtd_reserved4) (void);
+ void (*_gtd_reserved5) (void);
+ void (*_gtd_reserved6) (void);
+};
+
+GtdInterval* gtd_interval_new (GType gtype,
+ ...);
+
+GtdInterval* gtd_interval_new_with_values (GType gtype,
+ const GValue *initial,
+ const GValue *final);
+
+
+GtdInterval* gtd_interval_clone (GtdInterval *self);
+
+GType gtd_interval_get_value_type (GtdInterval *self);
+
+void gtd_interval_set_initial (GtdInterval *self,
+ ...);
+
+void gtd_interval_set_initial_value (GtdInterval *self,
+ const GValue *value);
+
+void gtd_interval_get_initial_value (GtdInterval *self,
+ GValue *value);
+
+GValue* gtd_interval_peek_initial_value (GtdInterval *self);
+
+void gtd_interval_set_final (GtdInterval *self,
+ ...);
+
+void gtd_interval_set_final_value (GtdInterval *self,
+ const GValue *value);
+
+void gtd_interval_get_final_value (GtdInterval *self,
+ GValue *value);
+
+GValue* gtd_interval_peek_final_value (GtdInterval *self);
+
+void gtd_interval_set_interval (GtdInterval *self,
+ ...);
+
+void gtd_interval_get_interval (GtdInterval *self,
+ ...);
+
+gboolean gtd_interval_validate (GtdInterval *self,
+ GParamSpec *pspec);
+
+gboolean gtd_interval_compute_value (GtdInterval *self,
+ gdouble factor,
+ GValue *value);
+
+const GValue* gtd_interval_compute (GtdInterval *self,
+ gdouble factor);
+
+gboolean gtd_interval_is_valid (GtdInterval *self);
+
+
+G_END_DECLS
diff --git a/src/animation/gtd-keyframe-transition.c b/src/animation/gtd-keyframe-transition.c
new file mode 100644
index 0000000..a77fea2
--- /dev/null
+++ b/src/animation/gtd-keyframe-transition.c
@@ -0,0 +1,716 @@
+/* gtd-keyframe-transition.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
+ */
+
+/**
+ * SECTION:gtd-keyframe-transition
+ * @Title: GtdKeyframeTransition
+ * @Short_Description: Keyframe property transition
+ *
+ * #GtdKeyframeTransition allows animating a property by defining
+ * "key frames": values at a normalized position on the transition
+ * duration.
+ *
+ * The #GtdKeyframeTransition interpolates the value of the property
+ * to which it's bound across these key values.
+ *
+ * Setting up a #GtdKeyframeTransition means providing the times,
+ * values, and easing modes between these key frames, for instance:
+ *
+ * |[
+ * GtdTransition *keyframe;
+ *
+ * keyframe = gtd_keyframe_transition_new ("opacity");
+ * gtd_transition_set_from (keyframe, G_TYPE_UINT, 255);
+ * gtd_transition_set_to (keyframe, G_TYPE_UINT, 0);
+ * gtd_keyframe_transition_set (GTD_KEYFRAME_TRANSITION (keyframe),
+ * G_TYPE_UINT,
+ * 1, /&ast; number of key frames &ast;/
+ * 0.5, 128, GTD_EASE_IN_OUT_CUBIC);
+ * ]|
+ *
+ * The example above sets up a keyframe transition for the #GtdActor:opacity
+ * property of a #GtdActor; the transition starts and sets the value of the
+ * property to fully transparent; between the start of the transition and its mid
+ * point, it will animate the property to half opacity, using an easy in/easy out
+ * progress. Once the transition reaches the mid point, it will linearly fade the
+ * actor out until it reaches the end of the transition.
+ *
+ * The #GtdKeyframeTransition will add an implicit key frame between the last
+ * and the 1.0 value, to interpolate to the final value of the transition's
+ * interval.
+ */
+
+#include "gtd-keyframe-transition.h"
+
+#include "gtd-debug.h"
+#include "gtd-easing.h"
+#include "gtd-interval.h"
+#include "gtd-timeline.h"
+
+#include <math.h>
+#include <gobject/gvaluecollector.h>
+
+typedef struct _KeyFrame
+{
+ double key;
+
+ double start;
+ double end;
+
+ GtdEaseMode mode;
+
+ GtdInterval *interval;
+} KeyFrame;
+
+typedef struct
+{
+ GArray *frames;
+
+ gint current_frame;
+} GtdKeyframeTransitionPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdKeyframeTransition, gtd_keyframe_transition, GTD_TYPE_PROPERTY_TRANSITION)
+
+static void
+key_frame_free (gpointer data)
+{
+ if (data != NULL)
+ {
+ KeyFrame *key = data;
+
+ g_object_unref (key->interval);
+ }
+}
+
+static int
+sort_by_key (gconstpointer a,
+ gconstpointer b)
+{
+ const KeyFrame *k_a = a;
+ const KeyFrame *k_b = b;
+
+ if (fabs (k_a->key - k_b->key) < 0.0001)
+ return 0;
+
+ if (k_a->key > k_b->key)
+ return 1;
+
+ return -1;
+}
+
+static inline void
+gtd_keyframe_transition_sort_frames (GtdKeyframeTransition *self)
+{
+ GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+
+ if (priv->frames != NULL)
+ g_array_sort (priv->frames, sort_by_key);
+}
+
+static inline void
+gtd_keyframe_transition_init_frames (GtdKeyframeTransition *self,
+ gssize n_key_frames)
+{
+ GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+ guint i;
+
+ priv->frames = g_array_sized_new (FALSE, FALSE,
+ sizeof (KeyFrame),
+ n_key_frames);
+ g_array_set_clear_func (priv->frames, key_frame_free);
+
+ /* we add an implicit key frame that goes to 1.0, so that the
+ * user doesn't have to do that an can simply add key frames
+ * in between 0.0 and 1.0
+ */
+ for (i = 0; i < n_key_frames + 1; i++)
+ {
+ KeyFrame frame;
+
+ if (i == n_key_frames)
+ frame.key = 1.0;
+ else
+ frame.key = 0.0;
+
+ frame.mode = GTD_EASE_LINEAR;
+ frame.interval = NULL;
+
+ g_array_insert_val (priv->frames, i, frame);
+ }
+}
+
+static inline void
+gtd_keyframe_transition_update_frames (GtdKeyframeTransition *self)
+{
+ GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+ guint i;
+
+ if (priv->frames == NULL)
+ return;
+
+ for (i = 0; i < priv->frames->len; i++)
+ {
+ KeyFrame *cur_frame = &g_array_index (priv->frames, KeyFrame, i);
+ KeyFrame *prev_frame;
+
+ if (i > 0)
+ prev_frame = &g_array_index (priv->frames, KeyFrame, i - 1);
+ else
+ prev_frame = NULL;
+
+ if (prev_frame != NULL)
+ {
+ cur_frame->start = prev_frame->key;
+
+ if (prev_frame->interval != NULL)
+ {
+ const GValue *value;
+
+ value = gtd_interval_peek_final_value (prev_frame->interval);
+
+ if (cur_frame->interval != NULL)
+ gtd_interval_set_initial_value (cur_frame->interval, value);
+ else
+ {
+ cur_frame->interval =
+ gtd_interval_new_with_values (G_VALUE_TYPE (value), value, NULL);
+ }
+ }
+ }
+ else
+ cur_frame->start = 0.0;
+
+ cur_frame->end = cur_frame->key;
+ }
+}
+
+static void
+gtd_keyframe_transition_compute_value (GtdTransition *transition,
+ GtdAnimatable *animatable,
+ GtdInterval *interval,
+ gdouble progress)
+{
+ GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (transition);
+ GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+ GtdTimeline *timeline = GTD_TIMELINE (self);
+ GtdTransitionClass *parent_class;
+ GtdTimelineDirection direction;
+ GtdInterval *real_interval;
+ gdouble real_progress;
+ double t, d, p;
+ KeyFrame *cur_frame = NULL;
+
+ real_interval = interval;
+ real_progress = progress;
+
+ /* if we don't have any keyframe, we behave like our parent class */
+ if (priv->frames == NULL)
+ goto out;
+
+ direction = gtd_timeline_get_direction (timeline);
+
+ /* we need a normalized linear value */
+ t = gtd_timeline_get_elapsed_time (timeline);
+ d = gtd_timeline_get_duration (timeline);
+ p = t / d;
+
+ if (priv->current_frame < 0)
+ {
+ if (direction == GTD_TIMELINE_FORWARD)
+ priv->current_frame = 0;
+ else
+ priv->current_frame = priv->frames->len - 1;
+ }
+
+ cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame);
+
+ /* skip to the next key frame, depending on the direction of the timeline */
+ if (direction == GTD_TIMELINE_FORWARD)
+ {
+ if (p > cur_frame->end)
+ {
+ priv->current_frame = MIN (priv->current_frame + 1,
+ (gint) priv->frames->len - 1);
+ cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame);
+ }
+ }
+ else
+ {
+ if (p < cur_frame->start)
+ {
+ priv->current_frame = MAX (priv->current_frame - 1, 0);
+
+ cur_frame = &g_array_index (priv->frames, KeyFrame, priv->current_frame);
+ }
+ }
+
+ /* if we are at the boundaries of the transition, use the from and to
+ * value from the transition
+ */
+ if (priv->current_frame == 0)
+ {
+ const GValue *value;
+
+ value = gtd_interval_peek_initial_value (interval);
+ gtd_interval_set_initial_value (cur_frame->interval, value);
+ }
+ else if (priv->current_frame == (gint) priv->frames->len - 1)
+ {
+ const GValue *value;
+
+ cur_frame->mode = gtd_timeline_get_progress_mode (timeline);
+
+ value = gtd_interval_peek_final_value (interval);
+ gtd_interval_set_final_value (cur_frame->interval, value);
+ }
+
+ /* update the interval to be used to interpolate the property */
+ real_interval = cur_frame->interval;
+
+ /* normalize the progress and apply the easing mode */
+ real_progress = gtd_easing_for_mode (cur_frame->mode,
+ (p - cur_frame->start),
+ (cur_frame->end - cur_frame->start));
+
+#ifdef GTD_ENABLE_DEBUG
+ if (GTD_HAS_DEBUG (ANIMATION))
+ {
+ char *from, *to;
+ const GValue *value;
+
+ value = gtd_interval_peek_initial_value (cur_frame->interval);
+ from = g_strdup_value_contents (value);
+
+ value = gtd_interval_peek_final_value (cur_frame->interval);
+ to = g_strdup_value_contents (value);
+
+ GTD_TRACE_MSG ("[animation] cur_frame [%d] => { %g, %s, %s %s %s } - "
+ "progress: %g, sub-progress: %g\n",
+ priv->current_frame,
+ cur_frame->key,
+ gtd_get_easing_name_for_mode (cur_frame->mode),
+ from,
+ direction == GTD_TIMELINE_FORWARD ? "->" : "<-",
+ to,
+ p,
+ real_progress);
+
+ g_free (from);
+ g_free (to);
+ }
+#endif /* GTD_ENABLE_DEBUG */
+
+out:
+ parent_class =
+ GTD_TRANSITION_CLASS (gtd_keyframe_transition_parent_class);
+ parent_class->compute_value (transition, animatable, real_interval, real_progress);
+}
+
+static void
+gtd_keyframe_transition_started (GtdTimeline *timeline)
+{
+ GtdKeyframeTransition *self;
+ GtdKeyframeTransitionPrivate *priv;
+
+ self = GTD_KEYFRAME_TRANSITION (timeline);
+ priv = gtd_keyframe_transition_get_instance_private (self);
+
+ priv->current_frame = -1;
+
+ gtd_keyframe_transition_sort_frames (self);
+ gtd_keyframe_transition_update_frames (self);
+}
+
+static void
+gtd_keyframe_transition_completed (GtdTimeline *timeline)
+{
+ GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (timeline);
+ GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+
+ priv->current_frame = -1;
+}
+
+static void
+gtd_keyframe_transition_finalize (GObject *gobject)
+{
+ GtdKeyframeTransition *self = GTD_KEYFRAME_TRANSITION (gobject);
+ GtdKeyframeTransitionPrivate *priv = gtd_keyframe_transition_get_instance_private (self);
+
+ if (priv->frames != NULL)
+ g_array_unref (priv->frames);
+
+ G_OBJECT_CLASS (gtd_keyframe_transition_parent_class)->finalize (gobject);
+}
+
+static void
+gtd_keyframe_transition_class_init (GtdKeyframeTransitionClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtdTimelineClass *timeline_class = GTD_TIMELINE_CLASS (klass);
+ GtdTransitionClass *transition_class = GTD_TRANSITION_CLASS (klass);
+
+ gobject_class->finalize = gtd_keyframe_transition_finalize;
+
+ timeline_class->started = gtd_keyframe_transition_started;
+ timeline_class->completed = gtd_keyframe_transition_completed;
+
+ transition_class->compute_value = gtd_keyframe_transition_compute_value;
+}
+
+static void
+gtd_keyframe_transition_init (GtdKeyframeTransition *self)
+{
+}
+
+/**
+ * gtd_keyframe_transition_new:
+ * @property_name: the property to animate
+ *
+ * Creates a new #GtdKeyframeTransition for @property_name.
+ *
+ * Return value: (transfer full): the newly allocated
+ * #GtdKeyframeTransition instance. Use g_object_unref() when
+ * done to free its resources.
+ *
+ * Since: 1.12
+ */
+GtdTransition *
+gtd_keyframe_transition_new (const gchar *property_name)
+{
+ return g_object_new (GTD_TYPE_KEYFRAME_TRANSITION,
+ "property-name", property_name,
+ NULL);
+}
+
+/**
+ * gtd_keyframe_transition_set_key_frames:
+ * @transition: a #GtdKeyframeTransition
+ * @n_key_frames: the number of values
+ * @key_frames: (array length=n_key_frames): an array of keys between 0.0
+ * and 1.0, one for each key frame
+ *
+ * Sets the keys for each key frame inside @transition.
+ *
+ * If @transition does not hold any key frame, @n_key_frames key frames
+ * will be created; if @transition already has key frames, @key_frames must
+ * have at least as many elements as the number of key frames.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set_key_frames (GtdKeyframeTransition *self,
+ guint n_key_frames,
+ const gdouble *key_frames)
+{
+ GtdKeyframeTransitionPrivate *priv;
+ guint i;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+ g_return_if_fail (n_key_frames > 0);
+ g_return_if_fail (key_frames != NULL);
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+
+ if (priv->frames == NULL)
+ gtd_keyframe_transition_init_frames (self, n_key_frames);
+ else
+ g_return_if_fail (n_key_frames == priv->frames->len - 1);
+
+ for (i = 0; i < n_key_frames; i++)
+ {
+ KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+
+ frame->key = key_frames[i];
+ }
+}
+
+/**
+ * gtd_keyframe_transition_set_values:
+ * @transition: a #GtdKeyframeTransition
+ * @n_values: the number of values
+ * @values: (array length=n_values): an array of values, one for each
+ * key frame
+ *
+ * Sets the values for each key frame inside @transition.
+ *
+ * If @transition does not hold any key frame, @n_values key frames will
+ * be created; if @transition already has key frames, @values must have
+ * at least as many elements as the number of key frames.
+ *
+ * Since: 1.12
+ */
+void
+gtd_keyframe_transition_set_values (GtdKeyframeTransition *self,
+ guint n_values,
+ const GValue *values)
+{
+ GtdKeyframeTransitionPrivate *priv;
+ guint i;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+ g_return_if_fail (n_values > 0);
+ g_return_if_fail (values != NULL);
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+
+ if (priv->frames == NULL)
+ gtd_keyframe_transition_init_frames (self, n_values);
+ else
+ g_return_if_fail (n_values == priv->frames->len - 1);
+
+ for (i = 0; i < n_values; i++)
+ {
+ KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+
+ if (frame->interval)
+ gtd_interval_set_final_value (frame->interval, &values[i]);
+ else
+ frame->interval =
+ gtd_interval_new_with_values (G_VALUE_TYPE (&values[i]), NULL,
+ &values[i]);
+ }
+}
+
+/**
+ * gtd_keyframe_transition_set_modes:
+ * @transition: a #GtdKeyframeTransition
+ * @n_modes: the number of easing modes
+ * @modes: (array length=n_modes): an array of easing modes, one for
+ * each key frame
+ *
+ * Sets the easing modes for each key frame inside @transition.
+ *
+ * If @transition does not hold any key frame, @n_modes key frames will
+ * be created; if @transition already has key frames, @modes must have
+ * at least as many elements as the number of key frames.
+ */
+void
+gtd_keyframe_transition_set_modes (GtdKeyframeTransition *self,
+ guint n_modes,
+ const GtdEaseMode *modes)
+{
+ GtdKeyframeTransitionPrivate *priv;
+ guint i;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+ g_return_if_fail (n_modes > 0);
+ g_return_if_fail (modes != NULL);
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+
+ if (priv->frames == NULL)
+ gtd_keyframe_transition_init_frames (self, n_modes);
+ else
+ g_return_if_fail (n_modes == priv->frames->len - 1);
+
+ for (i = 0; i < n_modes; i++)
+ {
+ KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+
+ frame->mode = modes[i];
+ }
+}
+
+/**
+ * gtd_keyframe_transition_set: (skip)
+ * @transition: a #GtdKeyframeTransition
+ * @gtype: the type of the values to use for the key frames
+ * @n_key_frames: the number of key frames between the initial
+ * and final values
+ * @...: a list of tuples, containing the key frame index, the value
+ * at the key frame, and the animation mode
+ *
+ * Sets the key frames of the @transition.
+ *
+ * This variadic arguments function is a convenience for C developers;
+ * language bindings should use gtd_keyframe_transition_set_key_frames(),
+ * gtd_keyframe_transition_set_modes(), and
+ * gtd_keyframe_transition_set_values() instead.
+ */
+void
+gtd_keyframe_transition_set (GtdKeyframeTransition *self,
+ GType gtype,
+ guint n_key_frames,
+ ...)
+{
+ GtdKeyframeTransitionPrivate *priv;
+ va_list args;
+ guint i;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+ g_return_if_fail (gtype != G_TYPE_INVALID);
+ g_return_if_fail (n_key_frames > 0);
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+
+ if (priv->frames == NULL)
+ gtd_keyframe_transition_init_frames (self, n_key_frames);
+ else
+ g_return_if_fail (n_key_frames == priv->frames->len - 1);
+
+ va_start (args, n_key_frames);
+
+ for (i = 0; i < n_key_frames; i++)
+ {
+ KeyFrame *frame = &g_array_index (priv->frames, KeyFrame, i);
+ GValue value = G_VALUE_INIT;
+ char *error = NULL;
+
+ frame->key = va_arg (args, gdouble);
+
+ G_VALUE_COLLECT_INIT (&value, gtype, args, 0, &error);
+ if (error != NULL)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+ g_free (error);
+ break;
+ }
+
+ frame->mode = va_arg (args, GtdEaseMode);
+
+ g_clear_object (&frame->interval);
+ frame->interval = gtd_interval_new_with_values (gtype, NULL, &value);
+
+ g_value_unset (&value);
+ }
+
+ va_end (args);
+}
+
+/**
+ * gtd_keyframe_transition_clear:
+ * @transition: a #GtdKeyframeTransition
+ *
+ * Removes all key frames from @transition.
+ */
+void
+gtd_keyframe_transition_clear (GtdKeyframeTransition *self)
+{
+ GtdKeyframeTransitionPrivate *priv;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+ if (priv->frames != NULL)
+ {
+ g_array_unref (priv->frames);
+ priv->frames = NULL;
+ }
+}
+
+/**
+ * gtd_keyframe_transition_get_n_key_frames:
+ * @transition: a #GtdKeyframeTransition
+ *
+ * Retrieves the number of key frames inside @transition.
+ *
+ * Return value: the number of key frames
+ */
+guint
+gtd_keyframe_transition_get_n_key_frames (GtdKeyframeTransition *self)
+{
+ GtdKeyframeTransitionPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_KEYFRAME_TRANSITION (self), 0);
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+ if (priv->frames == NULL)
+ return 0;
+
+ return priv->frames->len - 1;
+}
+
+/**
+ * gtd_keyframe_transition_set_key_frame:
+ * @transition: a #GtdKeyframeTransition
+ * @index_: the index of the key frame
+ * @key: the key of the key frame
+ * @mode: the easing mode of the key frame
+ * @value: a #GValue containing the value of the key frame
+ *
+ * Sets the details of the key frame at @index_ inside @transition.
+ *
+ * The @transition must already have a key frame at @index_, and @index_
+ * must be smaller than the number of key frames inside @transition.
+ */
+void
+gtd_keyframe_transition_set_key_frame (GtdKeyframeTransition *self,
+ guint index_,
+ double key,
+ GtdEaseMode mode,
+ const GValue *value)
+{
+ GtdKeyframeTransitionPrivate *priv;
+ KeyFrame *frame;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+
+ g_return_if_fail (priv->frames != NULL);
+ g_return_if_fail (index_ < priv->frames->len - 1);
+
+ frame = &g_array_index (priv->frames, KeyFrame, index_);
+ frame->key = key;
+ frame->mode = mode;
+ gtd_interval_set_final_value (frame->interval, value);
+}
+
+/**
+ * gtd_keyframe_transition_get_key_frame:
+ * @transition: a #GtdKeyframeTransition
+ * @index_: the index of the key frame
+ * @key: (out) (allow-none): return location for the key, or %NULL
+ * @mode: (out) (allow-none): return location for the easing mode, or %NULL
+ * @value: (out caller-allocates): a #GValue initialized with the type of
+ * the values
+ *
+ * Retrieves the details of the key frame at @index_ inside @transition.
+ *
+ * The @transition must already have key frames set, and @index_ must be
+ * smaller than the number of key frames.
+ */
+void
+gtd_keyframe_transition_get_key_frame (GtdKeyframeTransition *self,
+ guint index_,
+ double *key,
+ GtdEaseMode *mode,
+ GValue *value)
+{
+ GtdKeyframeTransitionPrivate *priv;
+ const KeyFrame *frame;
+
+ g_return_if_fail (GTD_IS_KEYFRAME_TRANSITION (self));
+
+ priv = gtd_keyframe_transition_get_instance_private (self);
+ g_return_if_fail (priv->frames != NULL);
+ g_return_if_fail (index_ < priv->frames->len - 1);
+
+ frame = &g_array_index (priv->frames, KeyFrame, index_);
+
+ if (key != NULL)
+ *key = frame->key;
+
+ if (mode != NULL)
+ *mode = frame->mode;
+
+ if (value != NULL)
+ gtd_interval_get_final_value (frame->interval, value);
+}
diff --git a/src/animation/gtd-keyframe-transition.h b/src/animation/gtd-keyframe-transition.h
new file mode 100644
index 0000000..d3d5024
--- /dev/null
+++ b/src/animation/gtd-keyframe-transition.h
@@ -0,0 +1,83 @@
+/* gtd-keyframe-transition.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 "gtd-property-transition.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_KEYFRAME_TRANSITION (gtd_keyframe_transition_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdKeyframeTransition, gtd_keyframe_transition, GTD, KEYFRAME_TRANSITION, GtdPropertyTransition)
+
+/**
+ * GtdKeyframeTransitionClass:
+ *
+ * The `GtdKeyframeTransitionClass` structure contains only
+ * private data.
+ *
+ * Since: 1.12
+ */
+struct _GtdKeyframeTransitionClass
+{
+ /*< private >*/
+ GtdPropertyTransitionClass parent_class;
+
+ gpointer _padding[8];
+};
+
+
+GtdTransition* gtd_keyframe_transition_new (const gchar *property_name);
+
+
+void gtd_keyframe_transition_set_key_frames (GtdKeyframeTransition *transition,
+ guint n_key_frames,
+ const gdouble *key_frames);
+
+void gtd_keyframe_transition_set_values (GtdKeyframeTransition *transition,
+ guint n_values,
+ const GValue *values);
+
+void gtd_keyframe_transition_set_modes (GtdKeyframeTransition *transition,
+ guint n_modes,
+ const GtdEaseMode *modes);
+
+void gtd_keyframe_transition_set (GtdKeyframeTransition *transition,
+ GType gtype,
+ guint n_key_frames,
+ ...);
+
+void gtd_keyframe_transition_set_key_frame (GtdKeyframeTransition *transition,
+ guint index_,
+ double key,
+ GtdEaseMode mode,
+ const GValue *value);
+
+void gtd_keyframe_transition_get_key_frame (GtdKeyframeTransition *transition,
+ guint index_,
+ double *key,
+ GtdEaseMode *mode,
+ GValue *value);
+
+guint gtd_keyframe_transition_get_n_key_frames (GtdKeyframeTransition *transition);
+
+void gtd_keyframe_transition_clear (GtdKeyframeTransition *transition);
+
+G_END_DECLS
diff --git a/src/animation/gtd-property-transition.c b/src/animation/gtd-property-transition.c
new file mode 100644
index 0000000..f0ceb88
--- /dev/null
+++ b/src/animation/gtd-property-transition.c
@@ -0,0 +1,359 @@
+/* gtd-property-transition.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
+ */
+
+
+/**
+ * SECTION:gtd-property-transition
+ * @Title: GtdPropertyTransition
+ * @Short_Description: Property transitions
+ *
+ * #GtdPropertyTransition is a specialized #GtdTransition that
+ * can be used to tween a property of a #GtdAnimatable instance.
+ */
+
+#include "gtd-property-transition.h"
+
+#include "gtd-animatable.h"
+#include "gtd-debug.h"
+#include "gtd-interval.h"
+#include "gtd-transition.h"
+
+typedef struct
+{
+ gchar *property_name;
+
+ GParamSpec *pspec;
+} GtdPropertyTransitionPrivate;
+
+enum
+{
+ PROP_0,
+ PROP_PROPERTY_NAME,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdPropertyTransition, gtd_property_transition, GTD_TYPE_TRANSITION)
+
+static inline void
+gtd_property_transition_ensure_interval (GtdPropertyTransition *self,
+ GtdAnimatable *animatable,
+ GtdInterval *interval)
+{
+ GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+ GValue *value_p;
+
+ if (gtd_interval_is_valid (interval))
+ return;
+
+ /* if no initial value has been set, use the current value */
+ value_p = gtd_interval_peek_initial_value (interval);
+ if (!G_IS_VALUE (value_p))
+ {
+ g_value_init (value_p, gtd_interval_get_value_type (interval));
+ gtd_animatable_get_initial_state (animatable,
+ priv->property_name,
+ value_p);
+ }
+
+ /* if no final value has been set, use the current value */
+ value_p = gtd_interval_peek_final_value (interval);
+ if (!G_IS_VALUE (value_p))
+ {
+ g_value_init (value_p, gtd_interval_get_value_type (interval));
+ gtd_animatable_get_initial_state (animatable,
+ priv->property_name,
+ value_p);
+ }
+}
+
+static void
+gtd_property_transition_attached (GtdTransition *transition,
+ GtdAnimatable *animatable)
+{
+ GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition);
+ GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+ GtdInterval *interval;
+
+ if (priv->property_name == NULL)
+ return;
+
+ priv->pspec =
+ gtd_animatable_find_property (animatable, priv->property_name);
+
+ if (priv->pspec == NULL)
+ return;
+
+ interval = gtd_transition_get_interval (transition);
+ if (interval == NULL)
+ return;
+
+ gtd_property_transition_ensure_interval (self, animatable, interval);
+}
+
+static void
+gtd_property_transition_detached (GtdTransition *transition,
+ GtdAnimatable *animatable)
+{
+ GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition);
+ GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+
+ priv->pspec = NULL;
+}
+
+static void
+gtd_property_transition_compute_value (GtdTransition *transition,
+ GtdAnimatable *animatable,
+ GtdInterval *interval,
+ gdouble progress)
+{
+ GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (transition);
+ GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+ GValue value = G_VALUE_INIT;
+ GType p_type, i_type;
+ gboolean res;
+
+ /* if we have a GParamSpec we also have an animatable instance */
+ if (priv->pspec == NULL)
+ return;
+
+ gtd_property_transition_ensure_interval (self, animatable, interval);
+
+ p_type = G_PARAM_SPEC_VALUE_TYPE (priv->pspec);
+ i_type = gtd_interval_get_value_type (interval);
+
+ g_value_init (&value, i_type);
+
+ res = gtd_animatable_interpolate_value (animatable,
+ priv->property_name,
+ interval,
+ progress,
+ &value);
+
+ if (res)
+ {
+ if (i_type != p_type || g_type_is_a (i_type, p_type))
+ {
+ if (g_value_type_transformable (i_type, p_type))
+ {
+ GValue transform = G_VALUE_INIT;
+
+ g_value_init (&transform, p_type);
+
+ if (g_value_transform (&value, &transform))
+ {
+ gtd_animatable_set_final_state (animatable,
+ priv->property_name,
+ &transform);
+ }
+ else
+ g_warning ("%s: Unable to convert a value of type '%s' from "
+ "the value type '%s' of the interval.",
+ G_STRLOC,
+ g_type_name (p_type),
+ g_type_name (i_type));
+
+ g_value_unset (&transform);
+ }
+ }
+ else
+ gtd_animatable_set_final_state (animatable,
+ priv->property_name,
+ &value);
+ }
+
+ g_value_unset (&value);
+}
+
+static void
+gtd_property_transition_set_property (GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject);
+
+ switch (prop_id)
+ {
+ case PROP_PROPERTY_NAME:
+ gtd_property_transition_set_property_name (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ }
+}
+
+static void
+gtd_property_transition_get_property (GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject);
+ GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_PROPERTY_NAME:
+ g_value_set_string (value, priv->property_name);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ }
+}
+
+static void
+gtd_property_transition_finalize (GObject *gobject)
+{
+ GtdPropertyTransition *self = GTD_PROPERTY_TRANSITION (gobject);
+ GtdPropertyTransitionPrivate *priv = gtd_property_transition_get_instance_private (self);
+
+ g_free (priv->property_name);
+
+ G_OBJECT_CLASS (gtd_property_transition_parent_class)->finalize (gobject);
+}
+
+static void
+gtd_property_transition_class_init (GtdPropertyTransitionClass *klass)
+{
+ GtdTransitionClass *transition_class = GTD_TRANSITION_CLASS (klass);
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ transition_class->attached = gtd_property_transition_attached;
+ transition_class->detached = gtd_property_transition_detached;
+ transition_class->compute_value = gtd_property_transition_compute_value;
+
+ gobject_class->set_property = gtd_property_transition_set_property;
+ gobject_class->get_property = gtd_property_transition_get_property;
+ gobject_class->finalize = gtd_property_transition_finalize;
+
+ /**
+ * GtdPropertyTransition:property-name:
+ *
+ * The name of the property of a #GtdAnimatable to animate.
+ */
+ obj_props[PROP_PROPERTY_NAME] =
+ g_param_spec_string ("property-name",
+ "Property Name",
+ "The name of the property to animate",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
+
+static void
+gtd_property_transition_init (GtdPropertyTransition *self)
+{
+}
+
+/**
+ * gtd_property_transition_new_for_actor:
+ * @actor: a #GtdActor
+ * @property_name: (allow-none): a property of @animatable, or %NULL
+ *
+ * Creates a new #GtdPropertyTransition.
+ *
+ * Return value: (transfer full): the newly created #GtdPropertyTransition.
+ * Use g_object_unref() when done
+ */
+GtdTransition *
+gtd_property_transition_new_for_actor (GtdWidget *widget,
+ const char *property_name)
+{
+ return g_object_new (GTD_TYPE_PROPERTY_TRANSITION,
+ "widget", widget,
+ "property-name", property_name,
+ NULL);
+}
+
+/**
+ * gtd_property_transition_new:
+ * @property_name: (allow-none): a property of @animatable, or %NULL
+ *
+ * Creates a new #GtdPropertyTransition.
+ *
+ * Return value: (transfer full): the newly created #GtdPropertyTransition.
+ * Use g_object_unref() when done
+ */
+GtdTransition *
+gtd_property_transition_new (const char *property_name)
+{
+ return g_object_new (GTD_TYPE_PROPERTY_TRANSITION,
+ "property-name", property_name,
+ NULL);
+}
+
+/**
+ * gtd_property_transition_set_property_name:
+ * @transition: a #GtdPropertyTransition
+ * @property_name: (allow-none): a property name
+ *
+ * Sets the #GtdPropertyTransition:property-name property of @transition.
+ */
+void
+gtd_property_transition_set_property_name (GtdPropertyTransition *self,
+ const gchar *property_name)
+{
+ GtdPropertyTransitionPrivate *priv;
+ GtdAnimatable *animatable;
+
+ g_return_if_fail (GTD_IS_PROPERTY_TRANSITION (self));
+
+ priv = gtd_property_transition_get_instance_private (self);
+
+ if (g_strcmp0 (priv->property_name, property_name) == 0)
+ return;
+
+ g_free (priv->property_name);
+ priv->property_name = g_strdup (property_name);
+ priv->pspec = NULL;
+
+ animatable = gtd_transition_get_animatable (GTD_TRANSITION (self));
+ if (animatable)
+ priv->pspec = gtd_animatable_find_property (animatable, priv->property_name);
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROPERTY_NAME]);
+}
+
+/**
+ * gtd_property_transition_get_property_name:
+ * @transition: a #GtdPropertyTransition
+ *
+ * Retrieves the value of the #GtdPropertyTransition:property-name
+ * property.
+ *
+ * Return value: the name of the property being animated, or %NULL if
+ * none is set. The returned string is owned by the @transition and
+ * it should not be freed.
+ */
+const char *
+gtd_property_transition_get_property_name (GtdPropertyTransition *self)
+{
+ GtdPropertyTransitionPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_PROPERTY_TRANSITION (self), NULL);
+
+ priv = gtd_property_transition_get_instance_private (self);
+ return priv->property_name;
+}
diff --git a/src/animation/gtd-property-transition.h b/src/animation/gtd-property-transition.h
new file mode 100644
index 0000000..3b38dda
--- /dev/null
+++ b/src/animation/gtd-property-transition.h
@@ -0,0 +1,55 @@
+/* gtd-property-transition.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 "gtd-transition.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PROPERTY_TRANSITION (gtd_property_transition_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdPropertyTransition, gtd_property_transition, GTD, PROPERTY_TRANSITION, GtdTransition)
+
+/**
+ * GtdPropertyTransitionClass:
+ *
+ * The #GtdPropertyTransitionClass structure
+ * contains private data.
+ */
+struct _GtdPropertyTransitionClass
+{
+ /*< private >*/
+ GtdTransitionClass parent_class;
+
+ gpointer _padding[8];
+};
+
+GtdTransition * gtd_property_transition_new_for_widget (GtdWidget *widget,
+ const char *property_name);
+
+GtdTransition * gtd_property_transition_new (const char *property_name);
+
+void gtd_property_transition_set_property_name (GtdPropertyTransition *transition,
+ const char *property_name);
+
+const char * gtd_property_transition_get_property_name (GtdPropertyTransition *transition);
+
+G_END_DECLS
diff --git a/src/animation/gtd-timeline.c b/src/animation/gtd-timeline.c
new file mode 100644
index 0000000..da16fd9
--- /dev/null
+++ b/src/animation/gtd-timeline.c
@@ -0,0 +1,1547 @@
+/*
+ * Gtd.
+ *
+ * An OpenGL based 'interactive canvas' library.
+ *
+ * Authored By Matthew Allum <mallum@openedhand.com>
+ *
+ * Copyright (C) 2006 OpenedHand
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gtd-timeline
+ * @short_description: A class for time-based events
+ *
+ * #GtdTimeline is a base class for managing time-based event that cause
+ * GTK to redraw, such as animations.
+ *
+ * Each #GtdTimeline instance has a duration: once a timeline has been
+ * started, using gtd_timeline_start(), it will emit a signal that can
+ * be used to update the state of the widgets.
+ *
+ * It is important to note that #GtdTimeline is not a generic API for
+ * calling closures after an interval; each Timeline is tied into the master
+ * clock used to drive the frame cycle. If you need to schedule a closure
+ * after an interval, see gtd_threads_add_timeout() instead.
+ *
+ * Users of #GtdTimeline should connect to the #GtdTimeline::new-frame
+ * signal, which is emitted each time a timeline is advanced during the maste
+ * clock iteration. The #GtdTimeline::new-frame signal provides the time
+ * elapsed since the beginning of the timeline, in milliseconds. A normalized
+ * progress value can be obtained by calling gtd_timeline_get_progress().
+ * By using gtd_timeline_get_delta() it is possible to obtain the wallclock
+ * time elapsed since the last emission of the #GtdTimeline::new-frame
+ * signal.
+ *
+ * Initial state can be set up by using the #GtdTimeline::started signal,
+ * while final state can be set up by using the #GtdTimeline::stopped
+ * signal. The #GtdTimeline guarantees the emission of at least a single
+ * #GtdTimeline::new-frame signal, as well as the emission of the
+ * #GtdTimeline::completed signal every time the #GtdTimeline reaches
+ * its #GtdTimeline:duration.
+ *
+ * It is possible to connect to specific points in the timeline progress by
+ * adding markers using gtd_timeline_add_marker_at_time() and connecting
+ * to the #GtdTimeline::marker-reached signal.
+ *
+ * Timelines can be made to loop once they reach the end of their duration, by
+ * using gtd_timeline_set_repeat_count(); a looping timeline will still
+ * emit the #GtdTimeline::completed signal once it reaches the end of its
+ * duration at each repeat. If you want to be notified of the end of the last
+ * repeat, use the #GtdTimeline::stopped signal.
+ *
+ * Timelines have a #GtdTimeline:direction: the default direction is
+ * %GTD_TIMELINE_FORWARD, and goes from 0 to the duration; it is possible
+ * to change the direction to %GTD_TIMELINE_BACKWARD, and have the timeline
+ * go from the duration to 0. The direction can be automatically reversed
+ * when reaching completion by using the #GtdTimeline:auto-reverse property.
+ *
+ * Timelines are used in the Gtd animation framework by classes like
+ * #GtdTransition.
+ */
+
+#define G_LOG_DOMAIN "GtdTimeline"
+
+#include "gtd-timeline.h"
+
+#include "gtd-debug.h"
+#include "endeavour.h"
+
+typedef struct
+{
+ GtdTimelineDirection direction;
+
+ GtdWidget *widget;
+ guint frame_tick_id;
+
+ gint64 duration_us;
+ gint64 delay_us;
+
+ /* The current amount of elapsed time */
+ gint64 elapsed_time_us;
+
+ /* The elapsed time since the last frame was fired */
+ gint64 delta_us;
+
+ /* Time we last advanced the elapsed time and showed a frame */
+ gint64 last_frame_time_us;
+
+ gint64 start_us;
+
+ /* How many times the timeline should repeat */
+ gint repeat_count;
+
+ /* The number of times the timeline has repeated */
+ gint current_repeat;
+
+ GtdTimelineProgressFunc progress_func;
+ gpointer progress_data;
+ GDestroyNotify progress_notify;
+ GtdEaseMode progress_mode;
+
+ guint is_playing : 1;
+
+ /* If we've just started playing and haven't yet gotten
+ * a tick from the master clock
+ */
+ guint auto_reverse : 1;
+} GtdTimelinePrivate;
+
+enum
+{
+ PROP_0,
+
+ PROP_AUTO_REVERSE,
+ PROP_DELAY,
+ PROP_DURATION,
+ PROP_DIRECTION,
+ PROP_REPEAT_COUNT,
+ PROP_PROGRESS_MODE,
+ PROP_WIDGET,
+
+ PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+enum
+{
+ NEW_FRAME,
+ STARTED,
+ PAUSED,
+ COMPLETED,
+ STOPPED,
+
+ LAST_SIGNAL
+};
+
+static guint timeline_signals[LAST_SIGNAL] = { 0, };
+
+static void set_is_playing (GtdTimeline *self,
+ gboolean is_playing);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdTimeline, gtd_timeline, G_TYPE_OBJECT)
+
+static inline gint64
+us_to_ms (gint64 ms)
+{
+ return ms / 1000;
+}
+
+static inline gint64
+ms_to_us (gint64 us)
+{
+ return us * 1000;
+}
+
+static inline gboolean
+is_waiting_for_delay (GtdTimeline *self,
+ gint64 frame_time_us)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ return priv->start_us + priv->delay_us > frame_time_us;
+}
+
+static void
+emit_frame_signal (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ gint64 elapsed_time_ms = us_to_ms (priv->elapsed_time_us);
+
+ GTD_TRACE_MSG ("Emitting ::new-frame signal on timeline[%p]", self);
+
+ g_signal_emit (self, timeline_signals[NEW_FRAME], 0, elapsed_time_ms);
+}
+
+static gboolean
+is_complete (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return (priv->direction == GTD_TIMELINE_FORWARD
+ ? priv->elapsed_time_us >= priv->duration_us
+ : priv->elapsed_time_us <= 0);
+}
+
+static gboolean
+maybe_loop_timeline (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ GtdTimelineDirection saved_direction = priv->direction;
+ gint64 overflow_us = priv->elapsed_time_us;
+ gint64 end_us;
+
+ /* Update the current elapsed time in case the signal handlers
+ * want to take a peek. If we clamp elapsed time, then we need
+ * to correpondingly reduce elapsed_time_delta to reflect the correct
+ * range of times */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us = priv->duration_us;
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ priv->elapsed_time_us = 0;
+
+ end_us = priv->elapsed_time_us;
+
+ /* Emit the signal */
+ emit_frame_signal (self);
+
+ /* Did the signal handler modify the elapsed time? */
+ if (priv->elapsed_time_us != end_us)
+ return TRUE;
+
+ /* Note: If the new-frame signal handler paused the timeline
+ * on the last frame we will still go ahead and send the
+ * completed signal */
+ GTD_TRACE_MSG ("Timeline [%p] completed (cur: %ldµs, tot: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->delta_us);
+
+ if (priv->is_playing &&
+ (priv->repeat_count == 0 ||
+ priv->repeat_count == priv->current_repeat))
+ {
+ /* We stop the timeline now, so that the completed signal handler
+ * may choose to re-start the timeline
+ */
+ set_is_playing (self, FALSE);
+
+ g_signal_emit (self, timeline_signals[COMPLETED], 0);
+ g_signal_emit (self, timeline_signals[STOPPED], 0, TRUE);
+ }
+ else
+ {
+ g_signal_emit (self, timeline_signals[COMPLETED], 0);
+ }
+
+ priv->current_repeat += 1;
+
+ if (priv->auto_reverse)
+ {
+ /* :auto-reverse changes the direction of the timeline */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->direction = GTD_TIMELINE_BACKWARD;
+ else
+ priv->direction = GTD_TIMELINE_FORWARD;
+
+ g_object_notify_by_pspec (G_OBJECT (self),
+ obj_props[PROP_DIRECTION]);
+ }
+
+ /*
+ * Again check to see if the user has manually played with
+ * the elapsed time, before we finally stop or loop the timeline,
+ * except changing time from 0 -> duration (or vice-versa)
+ * since these are considered equivalent
+ */
+ if (priv->elapsed_time_us != end_us &&
+ !((priv->elapsed_time_us == 0 && end_us == priv->duration_us) ||
+ (priv->elapsed_time_us == priv->duration_us && end_us == 0)))
+ {
+ return TRUE;
+ }
+
+ if (priv->repeat_count == 0)
+ {
+ gtd_timeline_rewind (self);
+ return FALSE;
+ }
+
+ /* Try and interpolate smoothly around a loop */
+ if (saved_direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us = overflow_us - priv->duration_us;
+ else
+ priv->elapsed_time_us = priv->duration_us + overflow_us;
+
+ /* Or if the direction changed, we try and bounce */
+ if (priv->direction != saved_direction)
+ priv->elapsed_time_us = priv->duration_us - priv->elapsed_time_us;
+
+ return TRUE;
+}
+
+static gboolean
+tick_timeline (GtdTimeline *self,
+ gint64 tick_time_us)
+{
+ GtdTimelinePrivate *priv;
+ gboolean should_continue;
+ gboolean complete;
+ gint64 elapsed_us;
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ GTD_TRACE_MSG ("Timeline [%p] ticked (elapsed_time: %ldµs, delta_us: %ldµs, "
+ "last_frame_time: %ldµs, tick_time: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->delta_us,
+ priv->last_frame_time_us,
+ tick_time_us);
+
+ /* Check the is_playing variable before performing the timeline tick.
+ * This is necessary, as if a timeline is stopped in response to a
+ * frame clock generated signal of a different timeline, this code can
+ * still be reached.
+ */
+ if (!priv->is_playing)
+ return FALSE;
+
+ elapsed_us = tick_time_us - priv->last_frame_time_us;
+ priv->last_frame_time_us = tick_time_us;
+
+ if (is_waiting_for_delay (self, tick_time_us))
+ {
+ GTD_TRACE_MSG ("- waiting for delay");
+ return G_SOURCE_CONTINUE;
+ }
+
+ /* if the clock rolled back between ticks we need to
+ * account for it; the best course of action, since the
+ * clock roll back can happen by any arbitrary amount
+ * of milliseconds, is to drop a frame here
+ */
+ if (elapsed_us <= 0)
+ return TRUE;
+
+ priv->delta_us = elapsed_us;
+
+ GTD_TRACE_MSG ("Timeline [%p] activated (elapsed time: %ldµs, "
+ "duration: %ldµs, delta_us: %ldµs)",
+ self,
+ priv->elapsed_time_us,
+ priv->duration_us,
+ priv->delta_us);
+
+ g_object_ref (self);
+
+ /* Advance time */
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ priv->elapsed_time_us += priv->delta_us;
+ else
+ priv->elapsed_time_us -= priv->delta_us;
+
+ complete = is_complete (self);
+ should_continue = !complete ? priv->is_playing : maybe_loop_timeline (self);
+
+ /* If we have not reached the end of the timeline */
+ if (!complete)
+ emit_frame_signal (self);
+
+ g_object_unref (self);
+
+ return should_continue;
+}
+
+static gboolean
+frame_tick_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ GtdTimeline *self = GTD_TIMELINE (user_data);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+ gint64 frame_time_us;
+
+ frame_time_us = gdk_frame_clock_get_frame_time (frame_clock);
+
+ if (tick_timeline (self, frame_time_us))
+ return G_SOURCE_CONTINUE;
+
+ priv->frame_tick_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+add_tick_callback (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_assert (!(priv->frame_tick_id > 0 && !priv->widget));
+
+ if (priv->frame_tick_id == 0)
+ {
+ priv->frame_tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (priv->widget),
+ frame_tick_cb,
+ self,
+ NULL);
+ }
+}
+
+static void
+remove_tick_callback (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_assert (!(priv->frame_tick_id > 0 && !priv->widget));
+
+ if (priv->frame_tick_id > 0)
+ {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (priv->widget), priv->frame_tick_id);
+ priv->frame_tick_id = 0;
+ }
+}
+
+static void
+set_is_playing (GtdTimeline *self,
+ gboolean is_playing)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ is_playing = !!is_playing;
+
+ if (is_playing == priv->is_playing)
+ return;
+
+ priv->is_playing = is_playing;
+
+ if (priv->is_playing)
+ {
+ priv->start_us = g_get_monotonic_time ();
+ priv->last_frame_time_us = priv->start_us;
+ priv->current_repeat = 0;
+
+ add_tick_callback (self);
+ }
+ else
+ {
+ remove_tick_callback (self);
+ }
+}
+
+static gdouble
+timeline_progress_func (GtdTimeline *self,
+ gdouble elapsed,
+ gdouble duration,
+ gpointer user_data)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return gtd_easing_for_mode (priv->progress_mode, elapsed, duration);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_timeline_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+
+ switch (prop_id)
+ {
+ case PROP_DELAY:
+ gtd_timeline_set_delay (self, g_value_get_uint (value));
+ break;
+
+ case PROP_DURATION:
+ gtd_timeline_set_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_DIRECTION:
+ gtd_timeline_set_direction (self, g_value_get_enum (value));
+ break;
+
+ case PROP_AUTO_REVERSE:
+ gtd_timeline_set_auto_reverse (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_REPEAT_COUNT:
+ gtd_timeline_set_repeat_count (self, g_value_get_int (value));
+ break;
+
+ case PROP_PROGRESS_MODE:
+ gtd_timeline_set_progress_mode (self, g_value_get_enum (value));
+ break;
+
+ case PROP_WIDGET:
+ gtd_timeline_set_widget (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_timeline_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_DELAY:
+ g_value_set_uint (value, us_to_ms (priv->delay_us));
+ break;
+
+ case PROP_DURATION:
+ g_value_set_uint (value, gtd_timeline_get_duration (self));
+ break;
+
+ case PROP_DIRECTION:
+ g_value_set_enum (value, priv->direction);
+ break;
+
+ case PROP_AUTO_REVERSE:
+ g_value_set_boolean (value, priv->auto_reverse);
+ break;
+
+ case PROP_REPEAT_COUNT:
+ g_value_set_int (value, priv->repeat_count);
+ break;
+
+ case PROP_PROGRESS_MODE:
+ g_value_set_enum (value, priv->progress_mode);
+ break;
+
+ case PROP_WIDGET:
+ g_value_set_object (value, priv->widget);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_timeline_dispose (GObject *object)
+{
+ GtdTimeline *self = GTD_TIMELINE (object);
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->progress_notify != NULL)
+ {
+ priv->progress_notify (priv->progress_data);
+ priv->progress_func = NULL;
+ priv->progress_data = NULL;
+ priv->progress_notify = NULL;
+ }
+
+ G_OBJECT_CLASS (gtd_timeline_parent_class)->dispose (object);
+}
+
+static void
+gtd_timeline_class_init (GtdTimelineClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ /**
+ * GtdTimeline::widget:
+ *
+ * The widget the timeline is associated with. This will determine what frame
+ * clock will drive it.
+ */
+ obj_props[PROP_WIDGET] =
+ g_param_spec_object ("widget",
+ "Widget",
+ "Associated GtdWidget",
+ GTD_TYPE_WIDGET,
+ G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ /**
+ * GtdTimeline:delay:
+ *
+ * A delay, in milliseconds, that should be observed by the
+ * timeline before actually starting.
+ */
+ obj_props[PROP_DELAY] =
+ g_param_spec_uint ("delay",
+ "Delay",
+ "Delay before start",
+ 0, G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:duration:
+ *
+ * Duration of the timeline in milliseconds.
+ */
+ obj_props[PROP_DURATION] =
+ g_param_spec_uint ("duration",
+ "Duration",
+ "Duration of the timeline in milliseconds",
+ 0, G_MAXUINT,
+ 1000,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:direction:GIT
+ *
+ * The direction of the timeline, either %GTD_TIMELINE_FORWARD or
+ * %GTD_TIMELINE_BACKWARD.
+ */
+ obj_props[PROP_DIRECTION] =
+ g_param_spec_enum ("direction",
+ "Direction",
+ "Direction of the timeline",
+ GTD_TYPE_TIMELINE_DIRECTION,
+ GTD_TIMELINE_FORWARD,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:auto-reverse:
+ *
+ * If the direction of the timeline should be automatically reversed
+ * when reaching the end.
+ */
+ obj_props[PROP_AUTO_REVERSE] =
+ g_param_spec_boolean ("auto-reverse",
+ "Auto Reverse",
+ "Whether the direction should be reversed when reaching the end",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:repeat-count:
+ *
+ * Defines how many times the timeline should repeat.
+ *
+ * If the repeat count is 0, the timeline does not repeat.
+ *
+ * If the repeat count is set to -1, the timeline will repeat until it is
+ * stopped.
+ */
+ obj_props[PROP_REPEAT_COUNT] =
+ g_param_spec_int ("repeat-count",
+ "Repeat Count",
+ "How many times the timeline should repeat",
+ -1, G_MAXINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTimeline:progress-mode:
+ *
+ * Controls the way a #GtdTimeline computes the normalized progress.
+ */
+ obj_props[PROP_PROGRESS_MODE] =
+ g_param_spec_enum ("progress-mode",
+ "Progress Mode",
+ "How the timeline should compute the progress",
+ GTD_TYPE_EASE_MODE,
+ GTD_EASE_LINEAR,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ object_class->dispose = gtd_timeline_dispose;
+ object_class->set_property = gtd_timeline_set_property;
+ object_class->get_property = gtd_timeline_get_property;
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+
+ /**
+ * GtdTimeline::new-frame:
+ * @timeline: the timeline which received the signal
+ * @msecs: the elapsed time between 0 and duration
+ *
+ * The ::new-frame signal is emitted for each timeline running
+ * timeline before a new frame is drawn to give animations a chance
+ * to update the scene.
+ */
+ timeline_signals[NEW_FRAME] =
+ g_signal_new ("new-frame",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, new_frame),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1, G_TYPE_INT);
+ /**
+ * GtdTimeline::completed:
+ * @timeline: the #GtdTimeline which received the signal
+ *
+ * The #GtdTimeline::completed signal is emitted when the timeline's
+ * elapsed time reaches the value of the #GtdTimeline:duration
+ * property.
+ *
+ * This signal will be emitted even if the #GtdTimeline is set to be
+ * repeating.
+ *
+ * If you want to get notification on whether the #GtdTimeline has
+ * been stopped or has finished its run, including its eventual repeats,
+ * you should use the #GtdTimeline::stopped signal instead.
+ */
+ timeline_signals[COMPLETED] =
+ g_signal_new ("completed",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, completed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+ /**
+ * GtdTimeline::started:
+ * @timeline: the #GtdTimeline which received the signal
+ *
+ * The ::started signal is emitted when the timeline starts its run.
+ * This might be as soon as gtd_timeline_start() is invoked or
+ * after the delay set in the GtdTimeline:delay property has
+ * expired.
+ */
+ timeline_signals[STARTED] =
+ g_signal_new ("started",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, started),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+ /**
+ * GtdTimeline::paused:
+ * @timeline: the #GtdTimeline which received the signal
+ *
+ * The ::paused signal is emitted when gtd_timeline_pause() is invoked.
+ */
+ timeline_signals[PAUSED] =
+ g_signal_new ("paused",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, paused),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ /**
+ * GtdTimeline::stopped:
+ * @timeline: the #GtdTimeline that emitted the signal
+ * @is_finished: %TRUE if the signal was emitted at the end of the
+ * timeline.
+ *
+ * The #GtdTimeline::stopped signal is emitted when the timeline
+ * has been stopped, either because gtd_timeline_stop() has been
+ * called, or because it has been exhausted.
+ *
+ * This is different from the #GtdTimeline::completed signal,
+ * which gets emitted after every repeat finishes.
+ *
+ * If the #GtdTimeline has is marked as infinitely repeating,
+ * this signal will never be emitted.
+ */
+ timeline_signals[STOPPED] =
+ g_signal_new ("stopped",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTimelineClass, stopped),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_BOOLEAN);
+}
+
+static void
+gtd_timeline_init (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ priv->progress_mode = GTD_EASE_LINEAR;
+ priv->progress_func = timeline_progress_func;
+}
+
+/**
+ * gtd_timeline_new:
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration_ms milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ * g_object_unref() when done using it
+ */
+GtdTimeline *
+gtd_timeline_new (guint duration_ms)
+{
+ return g_object_new (GTD_TYPE_TIMELINE,
+ "duration", duration_ms,
+ NULL);
+}
+
+/**
+ * gtd_timeline_new_for_widget:
+ * @widget: The #GtdWidget the timeline is associated with
+ * @duration_ms: Duration of the timeline in milliseconds
+ *
+ * Creates a new #GtdTimeline with a duration of @duration milli seconds.
+ *
+ * Return value: the newly created #GtdTimeline instance. Use
+ * g_object_unref() when done using it
+ */
+GtdTimeline *
+gtd_timeline_new_for_widget (GtdWidget *widget,
+ guint duration_ms)
+{
+ return g_object_new (GTD_TYPE_TIMELINE,
+ "duration", duration_ms,
+ "widget", widget,
+ NULL);
+}
+
+/**
+ * gtd_timeline_set_widget:
+ * @timeline: a #GtdTimeline
+ * @widget: a #GtdWidget
+ *
+ * Set the widget the timeline is associated with.
+ */
+void
+gtd_timeline_set_widget (GtdTimeline *self,
+ GtdWidget *widget)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ if (priv->widget)
+ {
+ remove_tick_callback (self);
+ priv->widget = NULL;
+ }
+
+ priv->widget = widget;
+
+ if (priv->is_playing)
+ add_tick_callback (self);
+}
+
+/**
+ * gtd_timeline_start:
+ * @timeline: A #GtdTimeline
+ *
+ * Starts the #GtdTimeline playing.
+ **/
+void
+gtd_timeline_start (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->is_playing)
+ return;
+
+ if (priv->duration_us == 0)
+ return;
+
+ priv->delta_us = 0;
+ set_is_playing (self, TRUE);
+
+ g_signal_emit (self, timeline_signals[STARTED], 0);
+}
+
+/**
+ * gtd_timeline_pause:
+ * @timeline: A #GtdTimeline
+ *
+ * Pauses the #GtdTimeline on current frame
+ **/
+void
+gtd_timeline_pause (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (!priv->is_playing)
+ return;
+
+ priv->delta_us = 0;
+ set_is_playing (self, FALSE);
+
+ g_signal_emit (self, timeline_signals[PAUSED], 0);
+}
+
+/**
+ * gtd_timeline_stop:
+ * @timeline: A #GtdTimeline
+ *
+ * Stops the #GtdTimeline and moves to frame 0
+ **/
+void
+gtd_timeline_stop (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+ gboolean was_playing;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ /* we check the is_playing here because pause() will return immediately
+ * if the timeline wasn't playing, so we don't know if it was actually
+ * stopped, and yet we still don't want to emit a ::stopped signal if
+ * the timeline was not playing in the first place.
+ */
+ was_playing = priv->is_playing;
+
+ gtd_timeline_pause (self);
+ gtd_timeline_rewind (self);
+
+ if (was_playing)
+ g_signal_emit (self, timeline_signals[STOPPED], 0, FALSE);
+}
+
+/**
+ * gtd_timeline_rewind:
+ * @timeline: A #GtdTimeline
+ *
+ * Rewinds #GtdTimeline to the first frame if its direction is
+ * %GTD_TIMELINE_FORWARD and the last frame if it is
+ * %GTD_TIMELINE_BACKWARD.
+ */
+void
+gtd_timeline_rewind (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ gtd_timeline_advance (self, 0);
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ gtd_timeline_advance (self, us_to_ms (priv->duration_us));
+}
+
+/**
+ * gtd_timeline_skip:
+ * @timeline: A #GtdTimeline
+ * @msecs: Amount of time to skip
+ *
+ * Advance timeline by the requested time in milliseconds
+ */
+void
+gtd_timeline_skip (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->direction == GTD_TIMELINE_FORWARD)
+ {
+ priv->elapsed_time_us += us;
+
+ if (priv->elapsed_time_us > priv->duration_us)
+ priv->elapsed_time_us = 1;
+ }
+ else if (priv->direction == GTD_TIMELINE_BACKWARD)
+ {
+ priv->elapsed_time_us -= us;
+
+ if (priv->elapsed_time_us < 1)
+ priv->elapsed_time_us = priv->duration_us - 1;
+ }
+
+ priv->delta_us = 0;
+}
+
+/**
+ * gtd_timeline_advance:
+ * @timeline: A #GtdTimeline
+ * @msecs: Time to advance to
+ *
+ * Advance timeline to the requested point. The point is given as a
+ * time in milliseconds since the timeline started.
+ *
+ * The @timeline will not emit the #GtdTimeline::new-frame
+ * signal for the given time. The first ::new-frame signal after the call to
+ * gtd_timeline_advance() will be emit the skipped markers.
+ */
+void
+gtd_timeline_advance (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ priv->elapsed_time_us = MIN (us, priv->duration_us);
+}
+
+/**
+ * gtd_timeline_get_elapsed_time:
+ * @timeline: A #GtdTimeline
+ *
+ * Request the current time position of the timeline.
+ *
+ * Return value: current elapsed time in milliseconds.
+ */
+guint
+gtd_timeline_get_elapsed_time (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return us_to_ms (priv->elapsed_time_us);
+}
+
+/**
+ * gtd_timeline_is_playing:
+ * @timeline: A #GtdTimeline
+ *
+ * Queries state of a #GtdTimeline.
+ *
+ * Return value: %TRUE if timeline is currently playing
+ */
+gboolean
+gtd_timeline_is_playing (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->is_playing;
+}
+
+/**
+ * gtd_timeline_get_delay:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the delay set using gtd_timeline_set_delay().
+ *
+ * Return value: the delay in milliseconds.
+ */
+guint
+gtd_timeline_get_delay (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return us_to_ms (priv->delay_us);
+}
+
+/**
+ * gtd_timeline_set_delay:
+ * @timeline: a #GtdTimeline
+ * @msecs: delay in milliseconds
+ *
+ * Sets the delay, in milliseconds, before @timeline should start.
+ */
+void
+gtd_timeline_set_delay (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->delay_us != us)
+ {
+ priv->delay_us = us;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DELAY]);
+ }
+}
+
+/**
+ * gtd_timeline_get_duration:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the duration of a #GtdTimeline in milliseconds.
+ * See gtd_timeline_set_duration().
+ *
+ * Return value: the duration of the timeline, in milliseconds.
+ */
+guint
+gtd_timeline_get_duration (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ return us_to_ms (priv->duration_us);
+}
+
+/**
+ * gtd_timeline_set_duration:
+ * @timeline: a #GtdTimeline
+ * @msecs: duration of the timeline in milliseconds
+ *
+ * Sets the duration of the timeline, in milliseconds. The speed
+ * of the timeline depends on the GtdTimeline:fps setting.
+ */
+void
+gtd_timeline_set_duration (GtdTimeline *self,
+ guint msecs)
+{
+ GtdTimelinePrivate *priv;
+ gint64 us;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (msecs > 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ us = ms_to_us (msecs);
+
+ if (priv->duration_us != us)
+ {
+ priv->duration_us = us;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DURATION]);
+ }
+}
+
+/**
+ * gtd_timeline_get_progress:
+ * @timeline: a #GtdTimeline
+ *
+ * The position of the timeline in a normalized [-1, 2] interval.
+ *
+ * The return value of this function is determined by the progress
+ * mode set using gtd_timeline_set_progress_mode(), or by the
+ * progress function set using gtd_timeline_set_progress_func().
+ *
+ * Return value: the normalized current position in the timeline.
+ */
+gdouble
+gtd_timeline_get_progress (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0.0);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ return priv->progress_func (self,
+ (gdouble) priv->elapsed_time_us,
+ (gdouble) priv->duration_us,
+ priv->progress_data);
+}
+
+/**
+ * gtd_timeline_get_direction:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the direction of the timeline set with
+ * gtd_timeline_set_direction().
+ *
+ * Return value: the direction of the timeline
+ */
+GtdTimelineDirection
+gtd_timeline_get_direction (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_TIMELINE_FORWARD);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->direction;
+}
+
+/**
+ * gtd_timeline_set_direction:
+ * @timeline: a #GtdTimeline
+ * @direction: the direction of the timeline
+ *
+ * Sets the direction of @timeline, either %GTD_TIMELINE_FORWARD or
+ * %GTD_TIMELINE_BACKWARD.
+ */
+void
+gtd_timeline_set_direction (GtdTimeline *self,
+ GtdTimelineDirection direction)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->direction != direction)
+ {
+ priv->direction = direction;
+
+ if (priv->elapsed_time_us == 0)
+ priv->elapsed_time_us = priv->duration_us;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_DIRECTION]);
+ }
+}
+
+/**
+ * gtd_timeline_get_delta:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the amount of time elapsed since the last
+ * GtdTimeline::new-frame signal.
+ *
+ * This function is only useful inside handlers for the ::new-frame
+ * signal, and its behaviour is undefined if the timeline is not
+ * playing.
+ *
+ * Return value: the amount of time in milliseconds elapsed since the
+ * last frame
+ */
+guint
+gtd_timeline_get_delta (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ if (!gtd_timeline_is_playing (self))
+ return 0;
+
+ priv = gtd_timeline_get_instance_private (self);
+ return us_to_ms (priv->delta_us);
+}
+
+/**
+ * gtd_timeline_set_auto_reverse:
+ * @timeline: a #GtdTimeline
+ * @reverse: %TRUE if the @timeline should reverse the direction
+ *
+ * Sets whether @timeline should reverse the direction after the
+ * emission of the #GtdTimeline::completed signal.
+ *
+ * Setting the #GtdTimeline:auto-reverse property to %TRUE is the
+ * equivalent of connecting a callback to the #GtdTimeline::completed
+ * signal and changing the direction of the timeline from that callback;
+ * for instance, this code:
+ *
+ * |[
+ * static void
+ * reverse_timeline (GtdTimeline *self)
+ * {
+ * GtdTimelineDirection dir = gtd_timeline_get_direction (self);
+ *
+ * if (dir == GTD_TIMELINE_FORWARD)
+ * dir = GTD_TIMELINE_BACKWARD;
+ * else
+ * dir = GTD_TIMELINE_FORWARD;
+ *
+ * gtd_timeline_set_direction (self, dir);
+ * }
+ * ...
+ * timeline = gtd_timeline_new (1000);
+ * gtd_timeline_set_repeat_count (self, -1);
+ * g_signal_connect (self, "completed",
+ * G_CALLBACK (reverse_timeline),
+ * NULL);
+ * ]|
+ *
+ * can be effectively replaced by:
+ *
+ * |[
+ * timeline = gtd_timeline_new (1000);
+ * gtd_timeline_set_repeat_count (self, -1);
+ * gtd_timeline_set_auto_reverse (self);
+ * ]|
+ */
+void
+gtd_timeline_set_auto_reverse (GtdTimeline *self,
+ gboolean reverse)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ reverse = !!reverse;
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->auto_reverse != reverse)
+ {
+ priv->auto_reverse = reverse;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_AUTO_REVERSE]);
+ }
+}
+
+/**
+ * gtd_timeline_get_auto_reverse:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the value set by gtd_timeline_set_auto_reverse().
+ *
+ * Return value: %TRUE if the timeline should automatically reverse, and
+ * %FALSE otherwise
+ */
+gboolean
+gtd_timeline_get_auto_reverse (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), FALSE);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->auto_reverse;
+}
+
+/**
+ * gtd_timeline_set_repeat_count:
+ * @timeline: a #GtdTimeline
+ * @count: the number of times the timeline should repeat
+ *
+ * Sets the number of times the @timeline should repeat.
+ *
+ * If @count is 0, the timeline never repeats.
+ *
+ * If @count is -1, the timeline will always repeat until
+ * it's stopped.
+ */
+void
+gtd_timeline_set_repeat_count (GtdTimeline *self,
+ gint count)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (count >= -1);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->repeat_count != count)
+ {
+ priv->repeat_count = count;
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_REPEAT_COUNT]);
+ }
+}
+
+/**
+ * gtd_timeline_get_repeat_count:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the number set using gtd_timeline_set_repeat_count().
+ *
+ * Return value: the number of repeats
+ */
+gint
+gtd_timeline_get_repeat_count (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->repeat_count;
+}
+
+/**
+ * gtd_timeline_set_progress_func:
+ * @timeline: a #GtdTimeline
+ * @func: (scope notified) (allow-none): a progress function, or %NULL
+ * @data: (closure): data to pass to @func
+ * @notify: a function to be called when the progress function is removed
+ * or the timeline is disposed
+ *
+ * Sets a custom progress function for @timeline. The progress function will
+ * be called by gtd_timeline_get_progress() and will be used to compute
+ * the progress value based on the elapsed time and the total duration of the
+ * timeline.
+ *
+ * If @func is not %NULL, the #GtdTimeline:progress-mode property will
+ * be set to %GTD_CUSTOM_MODE.
+ *
+ * If @func is %NULL, any previously set progress function will be unset, and
+ * the #GtdTimeline:progress-mode property will be set to %GTD_EASE_LINEAR.
+ */
+void
+gtd_timeline_set_progress_func (GtdTimeline *self,
+ GtdTimelineProgressFunc func,
+ gpointer data,
+ GDestroyNotify notify)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->progress_notify != NULL)
+ priv->progress_notify (priv->progress_data);
+
+ priv->progress_func = func;
+ priv->progress_data = data;
+ priv->progress_notify = notify;
+
+ if (priv->progress_func != NULL)
+ priv->progress_mode = GTD_CUSTOM_MODE;
+ else
+ priv->progress_mode = GTD_EASE_LINEAR;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]);
+}
+
+/**
+ * gtd_timeline_set_progress_mode:
+ * @timeline: a #GtdTimeline
+ * @mode: the progress mode, as a #GtdEaseMode
+ *
+ * Sets the progress function using a value from the #GtdEaseMode
+ * enumeration. The @mode cannot be %GTD_CUSTOM_MODE or bigger than
+ * %GTD_ANIMATION_LAST.
+ */
+void
+gtd_timeline_set_progress_mode (GtdTimeline *self,
+ GtdEaseMode mode)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_if_fail (GTD_IS_TIMELINE (self));
+ g_return_if_fail (mode < GTD_EASE_LAST);
+ g_return_if_fail (mode != GTD_CUSTOM_MODE);
+
+ priv = gtd_timeline_get_instance_private (self);
+
+ if (priv->progress_mode == mode)
+ return;
+
+ if (priv->progress_notify != NULL)
+ priv->progress_notify (priv->progress_data);
+
+ priv->progress_mode = mode;
+ priv->progress_func = timeline_progress_func;
+ priv->progress_data = NULL;
+ priv->progress_notify = NULL;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_PROGRESS_MODE]);
+}
+
+/**
+ * gtd_timeline_get_progress_mode:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the progress mode set using gtd_timeline_set_progress_mode()
+ * or gtd_timeline_set_progress_func().
+ *
+ * Return value: a #GtdEaseMode
+ */
+GtdEaseMode
+gtd_timeline_get_progress_mode (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), GTD_EASE_LINEAR);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->progress_mode;
+}
+
+/**
+ * gtd_timeline_get_duration_hint:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the full duration of the @timeline, taking into account the
+ * current value of the #GtdTimeline:repeat-count property.
+ *
+ * If the #GtdTimeline:repeat-count property is set to -1, this function
+ * will return %G_MAXINT64.
+ *
+ * The returned value is to be considered a hint, and it's only valid
+ * as long as the @timeline hasn't been changed.
+ *
+ * Return value: the full duration of the #GtdTimeline
+ */
+gint64
+gtd_timeline_get_duration_hint (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+ gint64 duration_ms;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ duration_ms = us_to_ms (priv->duration_us);
+
+ if (priv->repeat_count == 0)
+ return duration_ms;
+ else if (priv->repeat_count < 0)
+ return G_MAXINT64;
+ else
+ return priv->repeat_count * duration_ms;
+}
+
+/**
+ * gtd_timeline_get_current_repeat:
+ * @timeline: a #GtdTimeline
+ *
+ * Retrieves the current repeat for a timeline.
+ *
+ * Repeats start at 0.
+ *
+ * Return value: the current repeat
+ */
+gint
+gtd_timeline_get_current_repeat (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TIMELINE (self), 0);
+
+ priv = gtd_timeline_get_instance_private (self);
+ return priv->current_repeat;
+}
+
+/**
+ * gtd_timeline_get_widget:
+ * @timeline: a #GtdTimeline
+ *
+ * Get the widget the timeline is associated with.
+ *
+ * Returns: (transfer none): the associated #GtdWidget
+ */
+GtdWidget *
+gtd_timeline_get_widget (GtdTimeline *self)
+{
+ GtdTimelinePrivate *priv = gtd_timeline_get_instance_private (self);
+
+ return priv->widget;
+}
diff --git a/src/animation/gtd-timeline.h b/src/animation/gtd-timeline.h
new file mode 100644
index 0000000..a8d6023
--- /dev/null
+++ b/src/animation/gtd-timeline.h
@@ -0,0 +1,150 @@
+/* gtd-timeline.h
+ *
+ * Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * Heavily inspired by Clutter, authored By Matthew Allum <mallum@openedhand.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>
+#include <gtk/gtk.h>
+
+#include "gtd-easing.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TIMELINE (gtd_timeline_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdTimeline, gtd_timeline, GTD, TIMELINE, GObject)
+
+/**
+ * GtdTimelineProgressFunc:
+ * @timeline: a #GtdTimeline
+ * @elapsed: the elapsed time, in milliseconds
+ * @total: the total duration of the timeline, in milliseconds,
+ * @user_data: data passed to the function
+ *
+ * A function for defining a custom progress.
+ *
+ * Return value: the progress, as a floating point value between -1.0 and 2.0.
+ */
+typedef gdouble (* GtdTimelineProgressFunc) (GtdTimeline *timeline,
+ gdouble elapsed,
+ gdouble total,
+ gpointer user_data);
+
+/**
+ * GtdTimelineClass:
+ * @started: class handler for the #GtdTimeline::started signal
+ * @completed: class handler for the #GtdTimeline::completed signal
+ * @paused: class handler for the #GtdTimeline::paused signal
+ * @new_frame: class handler for the #GtdTimeline::new-frame signal
+ * @stopped: class handler for the #GtdTimeline::stopped signal
+ *
+ * The #GtdTimelineClass structure contains only private data
+ */
+struct _GtdTimelineClass
+{
+ /*< private >*/
+ GObjectClass parent_class;
+
+ /*< public >*/
+ void (*started) (GtdTimeline *timeline);
+ void (*completed) (GtdTimeline *timeline);
+ void (*paused) (GtdTimeline *timeline);
+
+ void (*new_frame) (GtdTimeline *timeline,
+ gint msecs);
+
+ void (*stopped) (GtdTimeline *timeline,
+ gboolean is_finished);
+};
+
+GtdTimeline* gtd_timeline_new_for_widget (GtdWidget *widget,
+ guint duration_ms);
+
+GtdWidget* gtd_timeline_get_widget (GtdTimeline *timeline);
+
+void gtd_timeline_set_widget (GtdTimeline *timeline,
+ GtdWidget *widget);
+
+guint gtd_timeline_get_duration (GtdTimeline *timeline);
+
+void gtd_timeline_set_duration (GtdTimeline *timeline,
+ guint msecs);
+
+GtdTimelineDirection gtd_timeline_get_direction (GtdTimeline *timeline);
+
+void gtd_timeline_set_direction (GtdTimeline *timeline,
+ GtdTimelineDirection direction);
+
+void gtd_timeline_start (GtdTimeline *timeline);
+
+void gtd_timeline_pause (GtdTimeline *timeline);
+
+void gtd_timeline_stop (GtdTimeline *timeline);
+
+void gtd_timeline_set_auto_reverse (GtdTimeline *timeline,
+ gboolean reverse);
+
+gboolean gtd_timeline_get_auto_reverse (GtdTimeline *timeline);
+
+void gtd_timeline_set_repeat_count (GtdTimeline *timeline,
+ gint count);
+
+gint gtd_timeline_get_repeat_count (GtdTimeline *timeline);
+
+void gtd_timeline_rewind (GtdTimeline *timeline);
+
+void gtd_timeline_skip (GtdTimeline *timeline,
+ guint msecs);
+
+void gtd_timeline_advance (GtdTimeline *timeline,
+ guint msecs);
+
+guint gtd_timeline_get_elapsed_time (GtdTimeline *timeline);
+
+gdouble gtd_timeline_get_progress (GtdTimeline *timeline);
+
+gboolean gtd_timeline_is_playing (GtdTimeline *timeline);
+
+void gtd_timeline_set_delay (GtdTimeline *timeline,
+ guint msecs);
+
+guint gtd_timeline_get_delay (GtdTimeline *timeline);
+
+guint gtd_timeline_get_delta (GtdTimeline *timeline);
+
+void gtd_timeline_set_progress_func (GtdTimeline *timeline,
+ GtdTimelineProgressFunc func,
+ gpointer data,
+ GDestroyNotify notify);
+
+void gtd_timeline_set_progress_mode (GtdTimeline *timeline,
+ GtdEaseMode mode);
+
+GtdEaseMode gtd_timeline_get_progress_mode (GtdTimeline *timeline);
+
+gint64 gtd_timeline_get_duration_hint (GtdTimeline *timeline);
+gint gtd_timeline_get_current_repeat (GtdTimeline *timeline);
+
+G_END_DECLS
+
+
+G_END_DECLS
diff --git a/src/animation/gtd-transition.c b/src/animation/gtd-transition.c
new file mode 100644
index 0000000..0ca1d3f
--- /dev/null
+++ b/src/animation/gtd-transition.c
@@ -0,0 +1,655 @@
+/* gtd-transition.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
+ */
+
+
+/**
+ * SECTION:gtd-transition
+ * @Title: GtdTransition
+ * @Short_Description: Transition between two values
+ *
+ * #GtdTransition is an abstract subclass of #GtdTimeline that
+ * computes the interpolation between two values, stored by a #GtdInterval.
+ */
+
+#include "gtd-transition.h"
+
+#include "gtd-animatable.h"
+#include "gtd-debug.h"
+#include "gtd-interval.h"
+#include "gtd-timeline.h"
+
+#include <gobject/gvaluecollector.h>
+
+typedef struct
+{
+ GtdInterval *interval;
+ GtdAnimatable *animatable;
+
+ guint remove_on_complete : 1;
+} GtdTransitionPrivate;
+
+enum
+{
+ PROP_0,
+ PROP_INTERVAL,
+ PROP_ANIMATABLE,
+ PROP_REMOVE_ON_COMPLETE,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+static GQuark quark_animatable_set = 0;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtdTransition, gtd_transition, GTD_TYPE_TIMELINE)
+
+static void
+gtd_transition_attach (GtdTransition *self,
+ GtdAnimatable *animatable)
+{
+ GTD_TRANSITION_GET_CLASS (self)->attached (self, animatable);
+}
+
+static void
+gtd_transition_detach (GtdTransition *self,
+ GtdAnimatable *animatable)
+{
+ GTD_TRANSITION_GET_CLASS (self)->detached (self, animatable);
+}
+
+static void
+gtd_transition_real_compute_value (GtdTransition *self,
+ GtdAnimatable *animatable,
+ GtdInterval *interval,
+ gdouble progress)
+{
+}
+
+static void
+gtd_transition_real_attached (GtdTransition *self,
+ GtdAnimatable *animatable)
+{
+}
+
+static void
+gtd_transition_real_detached (GtdTransition *self,
+ GtdAnimatable *animatable)
+{
+}
+
+static void
+gtd_transition_new_frame (GtdTimeline *timeline,
+ gint elapsed)
+{
+ GtdTransition *self = GTD_TRANSITION (timeline);
+ GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+ gdouble progress;
+
+ if (!priv->interval || !priv->animatable)
+ return;
+
+ progress = gtd_timeline_get_progress (timeline);
+
+ GTD_TRANSITION_GET_CLASS (timeline)->compute_value (self,
+ priv->animatable,
+ priv->interval,
+ progress);
+}
+
+static void
+gtd_transition_stopped (GtdTimeline *timeline,
+ gboolean is_finished)
+{
+ GtdTransition *self = GTD_TRANSITION (timeline);
+ GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+ if (is_finished &&
+ priv->animatable != NULL &&
+ priv->remove_on_complete)
+ {
+ gtd_transition_detach (GTD_TRANSITION (timeline),
+ priv->animatable);
+ g_clear_object (&priv->animatable);
+ }
+}
+
+static void
+gtd_transition_set_property (GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTransition *self = GTD_TRANSITION (gobject);
+
+ switch (prop_id)
+ {
+ case PROP_INTERVAL:
+ gtd_transition_set_interval (self, g_value_get_object (value));
+ break;
+
+ case PROP_ANIMATABLE:
+ gtd_transition_set_animatable (self, g_value_get_object (value));
+ break;
+
+ case PROP_REMOVE_ON_COMPLETE:
+ gtd_transition_set_remove_on_complete (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_transition_get_property (GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTransition *self = GTD_TRANSITION (gobject);
+ GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_INTERVAL:
+ g_value_set_object (value, priv->interval);
+ break;
+
+ case PROP_ANIMATABLE:
+ g_value_set_object (value, priv->animatable);
+ break;
+
+ case PROP_REMOVE_ON_COMPLETE:
+ g_value_set_boolean (value, priv->remove_on_complete);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gtd_transition_dispose (GObject *gobject)
+{
+ GtdTransition *self = GTD_TRANSITION (gobject);
+ GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+ if (priv->animatable != NULL)
+ gtd_transition_detach (GTD_TRANSITION (gobject),
+ priv->animatable);
+
+ g_clear_object (&priv->interval);
+ g_clear_object (&priv->animatable);
+
+ G_OBJECT_CLASS (gtd_transition_parent_class)->dispose (gobject);
+}
+
+static void
+gtd_transition_class_init (GtdTransitionClass *klass)
+{
+ GtdTimelineClass *timeline_class = GTD_TIMELINE_CLASS (klass);
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ quark_animatable_set =
+ g_quark_from_static_string ("-gtd-transition-animatable-set");
+
+ klass->compute_value = gtd_transition_real_compute_value;
+ klass->attached = gtd_transition_real_attached;
+ klass->detached = gtd_transition_real_detached;
+
+ timeline_class->new_frame = gtd_transition_new_frame;
+ timeline_class->stopped = gtd_transition_stopped;
+
+ gobject_class->set_property = gtd_transition_set_property;
+ gobject_class->get_property = gtd_transition_get_property;
+ gobject_class->dispose = gtd_transition_dispose;
+
+ /**
+ * GtdTransition:interval:
+ *
+ * The #GtdInterval used to describe the initial and final states
+ * of the transition.
+ */
+ obj_props[PROP_INTERVAL] =
+ g_param_spec_object ("interval",
+ "Interval",
+ "The interval of values to transition",
+ GTD_TYPE_INTERVAL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTransition:animatable:
+ *
+ * The #GtdAnimatable instance currently being animated.
+ */
+ obj_props[PROP_ANIMATABLE] =
+ g_param_spec_object ("animatable",
+ "Animatable",
+ "The animatable object",
+ GTD_TYPE_ANIMATABLE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdTransition:remove-on-complete:
+ *
+ * Whether the #GtdTransition should be automatically detached
+ * from the #GtdTransition:animatable instance whenever the
+ * #GtdTimeline::stopped signal is emitted.
+ *
+ * The #GtdTransition:remove-on-complete property takes into
+ * account the value of the #GtdTimeline:repeat-count property,
+ * and it only detaches the transition if the transition is not
+ * repeating.
+ */
+ obj_props[PROP_REMOVE_ON_COMPLETE] =
+ g_param_spec_boolean ("remove-on-complete",
+ "Remove on Complete",
+ "Detach the transition when completed",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
+
+static void
+gtd_transition_init (GtdTransition *self)
+{
+}
+
+/**
+ * gtd_transition_set_interval:
+ * @transition: a #GtdTransition
+ * @interval: (allow-none): a #GtdInterval, or %NULL
+ *
+ * Sets the #GtdTransition:interval property using @interval.
+ *
+ * The @transition will acquire a reference on the @interval, sinking
+ * the floating flag on it if necessary.
+ */
+void
+gtd_transition_set_interval (GtdTransition *self,
+ GtdInterval *interval)
+{
+ GtdTransitionPrivate *priv;
+
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+ g_return_if_fail (interval == NULL || GTD_IS_INTERVAL (interval));
+
+ priv = gtd_transition_get_instance_private (self);
+
+ if (priv->interval == interval)
+ return;
+
+ g_clear_object (&priv->interval);
+
+ if (interval != NULL)
+ priv->interval = g_object_ref_sink (interval);
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_INTERVAL]);
+}
+
+/**
+ * gtd_transition_get_interval:
+ * @transition: a #GtdTransition
+ *
+ * Retrieves the interval set using gtd_transition_set_interval()
+ *
+ * Return value: (transfer none): a #GtdInterval, or %NULL; the returned
+ * interval is owned by the #GtdTransition and it should not be freed
+ * directly
+ */
+GtdInterval *
+gtd_transition_get_interval (GtdTransition *self)
+{
+ GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_TRANSITION (self), NULL);
+
+ priv = gtd_transition_get_instance_private (self);
+ return priv->interval;
+}
+
+/**
+ * gtd_transition_set_animatable:
+ * @transition: a #GtdTransition
+ * @animatable: (allow-none): a #GtdAnimatable, or %NULL
+ *
+ * Sets the #GtdTransition:animatable property.
+ *
+ * The @transition will acquire a reference to the @animatable instance,
+ * and will call the #GtdTransitionClass.attached() virtual function.
+ *
+ * If an existing #GtdAnimatable is attached to @self, the
+ * reference will be released, and the #GtdTransitionClass.detached()
+ * virtual function will be called.
+ */
+void
+gtd_transition_set_animatable (GtdTransition *self,
+ GtdAnimatable *animatable)
+{
+ GtdTransitionPrivate *priv;
+ GtdWidget *widget;
+
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+ g_return_if_fail (animatable == NULL || GTD_IS_ANIMATABLE (animatable));
+
+ priv = gtd_transition_get_instance_private (self);
+
+ if (priv->animatable == animatable)
+ return;
+
+ if (priv->animatable != NULL)
+ gtd_transition_detach (self, priv->animatable);
+
+ g_clear_object (&priv->animatable);
+
+ if (animatable != NULL)
+ {
+ priv->animatable = g_object_ref (animatable);
+ gtd_transition_attach (self, priv->animatable);
+ }
+
+ widget = gtd_animatable_get_widget (animatable);
+ gtd_timeline_set_widget (GTD_TIMELINE (self), widget);
+}
+
+/**
+ * gtd_transition_get_animatable:
+ * @transition: a #GtdTransition
+ *
+ * Retrieves the #GtdAnimatable set using gtd_transition_set_animatable().
+ *
+ * Return value: (transfer none): a #GtdAnimatable, or %NULL; the returned
+ * animatable is owned by the #GtdTransition, and it should not be freed
+ * directly.
+ */
+GtdAnimatable *
+gtd_transition_get_animatable (GtdTransition *self)
+{
+ GtdTransitionPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TRANSITION (self), NULL);
+
+ priv = gtd_transition_get_instance_private (self);
+ return priv->animatable;
+}
+
+/**
+ * gtd_transition_set_remove_on_complete:
+ * @transition: a #GtdTransition
+ * @remove_complete: whether to detach @transition when complete
+ *
+ * Sets whether @transition should be detached from the #GtdAnimatable
+ * set using gtd_transition_set_animatable() when the
+ * #GtdTimeline::completed signal is emitted.
+ */
+void
+gtd_transition_set_remove_on_complete (GtdTransition *self,
+ gboolean remove_complete)
+{
+ GtdTransitionPrivate *priv;
+
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+
+ priv = gtd_transition_get_instance_private (self);
+ remove_complete = !!remove_complete;
+
+ if (priv->remove_on_complete == remove_complete)
+ return;
+
+ priv->remove_on_complete = remove_complete;
+
+ g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_REMOVE_ON_COMPLETE]);
+}
+
+/**
+ * gtd_transition_get_remove_on_complete:
+ * @transition: a #GtdTransition
+ *
+ * Retrieves the value of the #GtdTransition:remove-on-complete property.
+ *
+ * Return value: %TRUE if the @transition should be detached when complete,
+ * and %FALSE otherwise
+ */
+gboolean
+gtd_transition_get_remove_on_complete (GtdTransition *self)
+{
+ GtdTransitionPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TRANSITION (self), FALSE);
+
+ priv = gtd_transition_get_instance_private (self);
+ return priv->remove_on_complete;
+}
+
+typedef void (* IntervalSetFunc) (GtdInterval *interval,
+ const GValue *value);
+
+static inline void
+gtd_transition_set_value (GtdTransition *self,
+ IntervalSetFunc interval_set_func,
+ const GValue *value)
+{
+ GtdTransitionPrivate *priv = gtd_transition_get_instance_private (self);
+ GType interval_type;
+
+ if (priv->interval == NULL)
+ {
+ priv->interval = gtd_interval_new_with_values (G_VALUE_TYPE (value), NULL, NULL);
+ g_object_ref_sink (priv->interval);
+ }
+
+ interval_type = gtd_interval_get_value_type (priv->interval);
+
+ if (!g_type_is_a (G_VALUE_TYPE (value), interval_type))
+ {
+ if (g_value_type_compatible (G_VALUE_TYPE (value), interval_type))
+ {
+ interval_set_func (priv->interval, value);
+ return;
+ }
+
+ if (g_value_type_transformable (G_VALUE_TYPE (value), interval_type))
+ {
+ GValue transform = G_VALUE_INIT;
+
+ g_value_init (&transform, interval_type);
+ if (g_value_transform (value, &transform))
+ interval_set_func (priv->interval, &transform);
+ else
+ {
+ g_warning ("%s: Unable to convert a value of type '%s' into "
+ "the value type '%s' of the interval used by the "
+ "transition.",
+ G_STRLOC,
+ g_type_name (G_VALUE_TYPE (value)),
+ g_type_name (interval_type));
+ }
+
+ g_value_unset (&transform);
+ }
+ }
+ else
+ {
+ interval_set_func (priv->interval, value);
+ }
+}
+
+/**
+ * gtd_transition_set_from_value: (rename-to gtd_transition_set_from)
+ * @transition: a #GtdTransition
+ * @value: a #GValue with the initial value of the transition
+ *
+ * Sets the initial value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * This function will copy the contents of @value, so it is
+ * safe to call g_value_unset() after it returns.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This function is meant to be used by language bindings.
+ */
+void
+gtd_transition_set_from_value (GtdTransition *self,
+ const GValue *value)
+{
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+ g_return_if_fail (G_IS_VALUE (value));
+
+ gtd_transition_set_value (self, gtd_interval_set_initial_value, value);
+}
+
+/**
+ * gtd_transition_set_to_value: (rename-to gtd_transition_set_to)
+ * @transition: a #GtdTransition
+ * @value: a #GValue with the final value of the transition
+ *
+ * Sets the final value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * This function will copy the contents of @value, so it is
+ * safe to call g_value_unset() after it returns.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This function is meant to be used by language bindings.
+ */
+void
+gtd_transition_set_to_value (GtdTransition *self,
+ const GValue *value)
+{
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+ g_return_if_fail (G_IS_VALUE (value));
+
+ gtd_transition_set_value (self,
+ gtd_interval_set_final_value,
+ value);
+}
+
+/**
+ * gtd_transition_set_from: (skip)
+ * @transition: a #GtdTransition
+ * @value_type: the type of the value to set
+ * @...: the initial value
+ *
+ * Sets the initial value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This is a convenience function for the C API; language bindings
+ * should use gtd_transition_set_from_value() instead.
+ */
+void
+gtd_transition_set_from (GtdTransition *self,
+ GType value_type,
+ ...)
+{
+ GValue value = G_VALUE_INIT;
+ gchar *error = NULL;
+ va_list args;
+
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+ g_return_if_fail (value_type != G_TYPE_INVALID);
+
+ va_start (args, value_type);
+
+ G_VALUE_COLLECT_INIT (&value, value_type, args, 0, &error);
+
+ va_end (args);
+
+ if (error != NULL)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+ g_free (error);
+ return;
+ }
+
+ gtd_transition_set_value (self, gtd_interval_set_initial_value, &value);
+
+ g_value_unset (&value);
+}
+
+/**
+ * gtd_transition_set_to: (skip)
+ * @transition: a #GtdTransition
+ * @value_type: the type of the value to set
+ * @...: the final value
+ *
+ * Sets the final value of the transition.
+ *
+ * This is a convenience function that will either create the
+ * #GtdInterval used by @self, or will update it if
+ * the #GtdTransition:interval is already set.
+ *
+ * If @transition already has a #GtdTransition:interval set,
+ * then @value must hold the same type, or a transformable type,
+ * as the interval's #GtdInterval:value-type property.
+ *
+ * This is a convenience function for the C API; language bindings
+ * should use gtd_transition_set_to_value() instead.
+ */
+void
+gtd_transition_set_to (GtdTransition *self,
+ GType value_type,
+ ...)
+{
+ GValue value = G_VALUE_INIT;
+ gchar *error = NULL;
+ va_list args;
+
+ g_return_if_fail (GTD_IS_TRANSITION (self));
+ g_return_if_fail (value_type != G_TYPE_INVALID);
+
+ va_start (args, value_type);
+
+ G_VALUE_COLLECT_INIT (&value, value_type, args, 0, &error);
+
+ va_end (args);
+
+ if (error != NULL)
+ {
+ g_warning ("%s: %s", G_STRLOC, error);
+ g_free (error);
+ return;
+ }
+
+ gtd_transition_set_value (self, gtd_interval_set_final_value, &value);
+
+ g_value_unset (&value);
+}
diff --git a/src/animation/gtd-transition.h b/src/animation/gtd-transition.h
new file mode 100644
index 0000000..32e20fa
--- /dev/null
+++ b/src/animation/gtd-transition.h
@@ -0,0 +1,85 @@
+/* gtd-transition.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 "gtd-timeline.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TRANSITION (gtd_transition_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdTransition, gtd_transition, GTD, TRANSITION, GtdTimeline)
+
+/**
+ * GtdTransitionClass:
+ * @attached: virtual function; called when a transition is attached to
+ * a #GtdAnimatable instance
+ * @detached: virtual function; called when a transition is detached from
+ * a #GtdAnimatable instance
+ * @compute_value: virtual function; called each frame to compute and apply
+ * the interpolation of the interval
+ *
+ * The #GtdTransitionClass structure contains
+ * private data.
+ *
+ * Since: 1.10
+ */
+struct _GtdTransitionClass
+{
+ /*< private >*/
+ GtdTimelineClass parent_class;
+
+ /*< public >*/
+ void (* attached) (GtdTransition *transition,
+ GtdAnimatable *animatable);
+ void (* detached) (GtdTransition *transition,
+ GtdAnimatable *animatable);
+
+ void (* compute_value) (GtdTransition *transition,
+ GtdAnimatable *animatable,
+ GtdInterval *interval,
+ gdouble progress);
+
+ /*< private >*/
+ gpointer _padding[8];
+};
+
+void gtd_transition_set_interval (GtdTransition *transition,
+ GtdInterval *interval);
+GtdInterval * gtd_transition_get_interval (GtdTransition *transition);
+void gtd_transition_set_from_value (GtdTransition *transition,
+ const GValue *value);
+void gtd_transition_set_to_value (GtdTransition *transition,
+ const GValue *value);
+void gtd_transition_set_from (GtdTransition *transition,
+ GType value_type,
+ ...);
+void gtd_transition_set_to (GtdTransition *transition,
+ GType value_type,
+ ...);
+
+void gtd_transition_set_animatable (GtdTransition *transition,
+ GtdAnimatable *animatable);
+GtdAnimatable * gtd_transition_get_animatable (GtdTransition *transition);
+void gtd_transition_set_remove_on_complete (GtdTransition *transition,
+ gboolean remove_complete);
+gboolean gtd_transition_get_remove_on_complete (GtdTransition *transition);
+
+G_END_DECLS
diff --git a/src/core/gtd-activatable.c b/src/core/gtd-activatable.c
new file mode 100644
index 0000000..fcfd60e
--- /dev/null
+++ b/src/core/gtd-activatable.c
@@ -0,0 +1,129 @@
+/* gtd-activatable.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 "GtdActivatable"
+
+#include "gtd-activatable.h"
+#include "gtd-panel.h"
+#include "gtd-provider.h"
+
+/**
+ * SECTION:gtd-activatable
+ * @short_description:entry point for plugins
+ * @title:GtdActivatable
+ * @stability:Unstable
+ *
+ * The #GtdActivatable interface is the interface plugins must
+ * implement in order to be seen by Endeavour.
+ *
+ * When plugins are loaded, the gtd_activatable_activate() vfunc
+ * is called. Use this vfunc to load anything that depends on
+ * Endeavour.
+ *
+ * When plugins are unloaded, the gtd_activatable_deactivate() vfunc
+ * if called. Ideally, the implementation should undo everything that
+ * was done on gtd_activatable_activate().
+ *
+ * A plugin implementation may expose one or more #GtdProvider instances,
+ * which are the data sources of Endeavour. See the 'eds' plugin for
+ * a reference on how to expose one ('local') and multiple (Online Accounts)
+ * providers.
+ *
+ * Plugins may also expose one or more #GtdPanel implementations.
+ *
+ * Optionally, a plugin may expose a preferences panel. See gtd_activatable_get_preferences_panel().
+ */
+
+G_DEFINE_INTERFACE (GtdActivatable, gtd_activatable, G_TYPE_OBJECT)
+
+static void
+gtd_activatable_default_init (GtdActivatableInterface *iface)
+{
+ /**
+ * GtdActivatable::preferences-panel:
+ *
+ * The preferences panel of the plugin, or %NULL.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_object ("preferences-panel",
+ "Preferences panel",
+ "The preferences panel of the plugins",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READABLE));
+}
+
+/**
+ * gtd_activatable_activate:
+ * @activatable: a #GtdActivatable
+ *
+ * Activates the extension. This is the starting point where
+ * the implementation does everything it needs to do. Avoid
+ * doing it earlier than this call.
+ *
+ * This function is called after the extension is loaded and
+ * the signals are connected. If you want to do anything before
+ * that, the _init function should be used instead.
+ */
+void
+gtd_activatable_activate (GtdActivatable *activatable)
+{
+ g_return_if_fail (GTD_IS_ACTIVATABLE (activatable));
+
+ if (GTD_ACTIVATABLE_GET_IFACE (activatable)->activate)
+ GTD_ACTIVATABLE_GET_IFACE (activatable)->activate (activatable);
+}
+
+/**
+ * gtd_activatable_deactivate:
+ * @activatable: a #GtdActivatable
+ *
+ * Deactivates the extension. Here, the extension should remove
+ * all providers and panels it set.
+ *
+ * This function is called before the extension is removed. At
+ * this point, the plugin manager already removed all providers
+ * and widgets this extension exported. If you want to do anything
+ * after the extension is removed, use GObject::finalize instead.
+ */
+void
+gtd_activatable_deactivate (GtdActivatable *activatable)
+{
+ g_return_if_fail (GTD_IS_ACTIVATABLE (activatable));
+
+ if (GTD_ACTIVATABLE_GET_IFACE (activatable)->deactivate)
+ GTD_ACTIVATABLE_GET_IFACE (activatable)->deactivate (activatable);
+}
+
+/**
+ * gtd_activatable_get_preferences_panel:
+ * @activatable: a #GtdActivatable
+ *
+ * Retrieve the preferences panel of @activatable if any.
+ *
+ * Returns: (transfer none)(nullable): a #GtkWidget, or %NULL
+ */
+GtkWidget*
+gtd_activatable_get_preferences_panel (GtdActivatable *activatable)
+{
+ g_return_val_if_fail (GTD_IS_ACTIVATABLE (activatable), NULL);
+
+ if (GTD_ACTIVATABLE_GET_IFACE (activatable)->get_preferences_panel)
+ return GTD_ACTIVATABLE_GET_IFACE (activatable)->get_preferences_panel (activatable);
+
+ return NULL;
+}
diff --git a/src/core/gtd-activatable.h b/src/core/gtd-activatable.h
new file mode 100644
index 0000000..2e26375
--- /dev/null
+++ b/src/core/gtd-activatable.h
@@ -0,0 +1,52 @@
+/* gtd-activatable.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_ACTIVATABLE_H
+#define GTD_ACTIVATABLE_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gtk/gtk.h>
+#include <libpeas/peas.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_ACTIVATABLE (gtd_activatable_get_type ())
+
+G_DECLARE_INTERFACE (GtdActivatable, gtd_activatable, GTD, ACTIVATABLE, GObject)
+
+struct _GtdActivatableInterface
+{
+ GTypeInterface parent;
+
+ void (*activate) (GtdActivatable *activatable);
+
+ void (*deactivate) (GtdActivatable *activatable);
+
+ GtkWidget* (*get_preferences_panel) (GtdActivatable *activatable);
+};
+
+void gtd_activatable_activate (GtdActivatable *activatable);
+
+void gtd_activatable_deactivate (GtdActivatable *activatable);
+
+GtkWidget* gtd_activatable_get_preferences_panel (GtdActivatable *activatable);
+
+G_END_DECLS
+
+#endif /* GTD_ACTIVATABLE_H */
diff --git a/src/core/gtd-clock.c b/src/core/gtd-clock.c
new file mode 100644
index 0000000..68263c6
--- /dev/null
+++ b/src/core/gtd-clock.c
@@ -0,0 +1,292 @@
+/* gtd-clock.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 "GtdClock"
+
+#include "gtd-clock.h"
+#include "gtd-debug.h"
+
+#include <gio/gio.h>
+
+struct _GtdClock
+{
+ GtdObject parent;
+
+ guint timeout_id;
+
+ GDateTime *current;
+
+ GDBusProxy *logind;
+ GCancellable *cancellable;
+};
+
+static gboolean timeout_cb (gpointer user_data);
+
+G_DEFINE_TYPE (GtdClock, gtd_clock, GTD_TYPE_OBJECT)
+
+enum
+{
+ DAY_CHANGED,
+ HOUR_CHANGED,
+ MINUTE_CHANGED,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+update_current_date (GtdClock *self)
+{
+ g_autoptr (GDateTime) now = NULL;
+ gboolean minute_changed;
+ gboolean hour_changed;
+ gboolean day_changed;
+
+ GTD_ENTRY;
+
+ now = g_date_time_new_now_local ();
+
+ day_changed = g_date_time_get_year (now) != g_date_time_get_year (self->current) ||
+ g_date_time_get_day_of_year (now) != g_date_time_get_day_of_year (self->current);
+ hour_changed = day_changed || g_date_time_get_hour (now) != g_date_time_get_hour (self->current);
+ minute_changed = hour_changed || g_date_time_get_minute (now) != g_date_time_get_minute (self->current);
+
+ if (day_changed)
+ g_signal_emit (self, signals[DAY_CHANGED], 0);
+
+ if (hour_changed)
+ g_signal_emit (self, signals[HOUR_CHANGED], 0);
+
+ if (minute_changed)
+ g_signal_emit (self, signals[MINUTE_CHANGED], 0);
+
+ GTD_TRACE_MSG ("Ticking clock");
+
+ g_clear_pointer (&self->current, g_date_time_unref);
+ self->current = g_date_time_ref (now);
+
+ GTD_EXIT;
+}
+
+static void
+schedule_update (GtdClock *self)
+{
+ g_autoptr (GDateTime) now = NULL;
+ guint seconds_between;
+
+ /* Remove the previous timeout if we came from resume */
+ if (self->timeout_id > 0)
+ {
+ g_source_remove (self->timeout_id);
+ self->timeout_id = 0;
+ }
+
+ now = g_date_time_new_now_local ();
+
+ seconds_between = 60 - g_date_time_get_second (now);
+
+ self->timeout_id = g_timeout_add_seconds (seconds_between, timeout_cb, self);
+}
+
+/*
+ * Callbacks
+ */
+
+static void
+logind_signal_received_cb (GDBusProxy *logind,
+ const gchar *sender,
+ const gchar *signal,
+ GVariant *params,
+ GtdClock *self)
+{
+ GVariant *child;
+ gboolean resuming;
+
+ if (!g_str_equal (signal, "PrepareForSleep"))
+ return;
+
+ child = g_variant_get_child_value (params, 0);
+ resuming = !g_variant_get_boolean (child);
+
+ /* Only emit :update when resuming */
+ if (resuming)
+ {
+ /* Reschedule the daily timeout */
+ update_current_date (self);
+ schedule_update (self);
+ }
+
+ g_clear_pointer (&child, g_variant_unref);
+}
+
+static void
+login_proxy_acquired_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+ GtdClock *self;
+
+ self = GTD_CLOCK (user_data);
+
+ gtd_object_pop_loading (GTD_OBJECT (self));
+
+ self->logind = g_dbus_proxy_new_for_bus_finish (res, &error);
+
+ if (error)
+ {
+ g_warning ("Error acquiring org.freedesktop.login1: %s", error->message);
+ return;
+ }
+
+ g_signal_connect (self->logind,
+ "g-signal",
+ G_CALLBACK (logind_signal_received_cb),
+ self);
+}
+
+static gboolean
+timeout_cb (gpointer user_data)
+{
+ GtdClock *self = user_data;
+
+ self->timeout_id = 0;
+
+ update_current_date (self);
+ schedule_update (self);
+
+ return G_SOURCE_REMOVE;
+}
+
+
+/*
+ * GObject overrides
+ */
+static void
+gtd_clock_finalize (GObject *object)
+{
+ GtdClock *self = (GtdClock *)object;
+
+ g_cancellable_cancel (self->cancellable);
+
+ if (self->timeout_id > 0)
+ {
+ g_source_remove (self->timeout_id);
+ self->timeout_id = 0;
+ }
+
+ g_clear_pointer (&self->current, g_date_time_unref);
+
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->logind);
+
+ G_OBJECT_CLASS (gtd_clock_parent_class)->finalize (object);
+}
+
+static void
+gtd_clock_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+}
+
+static void
+gtd_clock_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_clock_class_init (GtdClockClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_clock_finalize;
+ object_class->get_property = gtd_clock_get_property;
+ object_class->set_property = gtd_clock_set_property;
+
+ /**
+ * GtdClock:day-changed:
+ *
+ * Emited when the day changes.
+ */
+ signals[DAY_CHANGED] = g_signal_new ("day-changed",
+ GTD_TYPE_CLOCK,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+ /**
+ * GtdClock:hour-changed:
+ *
+ * Emited when the current hour changes.
+ */
+ signals[HOUR_CHANGED] = g_signal_new ("hour-changed",
+ GTD_TYPE_CLOCK,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+ /**
+ * GtdClock:minute-changed:
+ *
+ * Emited when the current minute changes.
+ */
+ signals[MINUTE_CHANGED] = g_signal_new ("minute-changed",
+ GTD_TYPE_CLOCK,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static void
+gtd_clock_init (GtdClock *self)
+{
+ gtd_object_push_loading (GTD_OBJECT (self));
+
+ self->current = g_date_time_new_now_local ();
+ self->cancellable = g_cancellable_new ();
+
+ g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.login1",
+ "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager",
+ self->cancellable,
+ login_proxy_acquired_cb,
+ self);
+
+ schedule_update (self);
+}
+
+GtdClock*
+gtd_clock_new (void)
+{
+ return g_object_new (GTD_TYPE_CLOCK, NULL);
+}
diff --git a/src/core/gtd-clock.h b/src/core/gtd-clock.h
new file mode 100644
index 0000000..ab09b0b
--- /dev/null
+++ b/src/core/gtd-clock.h
@@ -0,0 +1,33 @@
+/* gtd-clock.h
+ *
+ * Copyright (C) 2017 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-object.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_CLOCK (gtd_clock_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdClock, gtd_clock, GTD, CLOCK, GtdObject)
+
+GtdClock* gtd_clock_new (void);
+
+G_END_DECLS
diff --git a/src/core/gtd-log.c b/src/core/gtd-log.c
new file mode 100644
index 0000000..fc2645d
--- /dev/null
+++ b/src/core/gtd-log.c
@@ -0,0 +1,103 @@
+/* gtd-log.c
+ *
+ * Copyright (C) 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/>.
+ */
+
+#include "gtd-debug.h"
+#include "gtd-log.h"
+
+#include <unistd.h>
+#include <glib.h>
+
+G_LOCK_DEFINE_STATIC (channel_lock);
+
+GIOChannel *standard_channel = NULL;
+
+static const gchar* ignored_domains[] =
+{
+ "GdkPixbuf",
+ NULL
+};
+
+static const gchar *
+log_level_str (GLogLevelFlags log_level)
+{
+ switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+ {
+ case G_LOG_LEVEL_ERROR: return " \033[1;31mERROR\033[0m";
+ case G_LOG_LEVEL_CRITICAL: return "\033[1;35mCRITICAL\033[0m";
+ case G_LOG_LEVEL_WARNING: return " \033[1;33mWARNING\033[0m";
+ case G_LOG_LEVEL_MESSAGE: return " \033[1;34mMESSAGE\033[0m";
+ case G_LOG_LEVEL_INFO: return " \033[1;32mINFO\033[0m";
+ case G_LOG_LEVEL_DEBUG: return " \033[1;32mDEBUG\033[0m";
+ case GTD_LOG_LEVEL_TRACE: return " \033[1;36mTRACE\033[0m";
+
+ default:
+ return " UNKNOWN";
+ }
+}
+
+static void
+gtd_log_handler (const gchar *domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer user_data)
+{
+ g_autoptr (GDateTime) now = NULL;
+ g_autofree gchar *buffer = NULL;
+ g_autofree gchar *ftime = NULL;
+ const gchar *level;
+ gint microsecond;
+
+ /* Skip ignored log domains */
+ if (domain && g_strv_contains (ignored_domains, domain))
+ return;
+
+ level = log_level_str (log_level);
+ now = g_date_time_new_now_local ();
+ ftime = g_date_time_format (now, "%H:%M:%S");
+ microsecond = g_date_time_get_microsecond (now);
+ buffer = g_strdup_printf ("%s.%4.4d %28s: %s: %s\n",
+ ftime,
+ microsecond,
+ domain,
+ level,
+ message);
+
+ /* Safely write to the channel */
+ G_LOCK (channel_lock);
+
+ g_io_channel_write_chars (standard_channel, buffer, -1, NULL, NULL);
+ g_io_channel_flush (standard_channel, NULL);
+
+ G_UNLOCK (channel_lock);
+}
+
+void
+gtd_log_init (void)
+{
+ static gsize initialized = FALSE;
+
+ if (g_once_init_enter (&initialized))
+ {
+ standard_channel = g_io_channel_unix_new (STDOUT_FILENO);
+
+ g_log_set_default_handler (gtd_log_handler, NULL);
+
+ g_once_init_leave (&initialized, TRUE);
+ }
+}
+
diff --git a/src/core/gtd-log.h b/src/core/gtd-log.h
new file mode 100644
index 0000000..0ad53a8
--- /dev/null
+++ b/src/core/gtd-log.h
@@ -0,0 +1,27 @@
+/* gtd-log.h
+ *
+ * Copyright (C) 2017 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 <glib-object.h>
+
+G_BEGIN_DECLS
+
+void gtd_log_init (void);
+
+G_END_DECLS
diff --git a/src/core/gtd-manager-protected.h b/src/core/gtd-manager-protected.h
new file mode 100644
index 0000000..142419f
--- /dev/null
+++ b/src/core/gtd-manager-protected.h
@@ -0,0 +1,29 @@
+/* gtd-manager-protected.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-types.h"
+
+G_BEGIN_DECLS
+
+void gtd_manager_load_plugins (GtdManager *manager);
+
+GtdPluginManager* gtd_manager_get_plugin_manager (GtdManager *manager);
+
+G_END_DECLS
diff --git a/src/core/gtd-manager.c b/src/core/gtd-manager.c
new file mode 100644
index 0000000..b499b76
--- /dev/null
+++ b/src/core/gtd-manager.c
@@ -0,0 +1,933 @@
+/* gtd-manager.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 "GtdManager"
+
+#include "models/gtd-list-store.h"
+#include "models/gtd-task-model-private.h"
+#include "gtd-clock.h"
+#include "gtd-debug.h"
+#include "gtd-manager.h"
+#include "gtd-manager-protected.h"
+#include "gtd-notification.h"
+#include "gtd-panel.h"
+#include "gtd-plugin-manager.h"
+#include "gtd-provider.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-utils.h"
+#include "gtd-workspace.h"
+
+#include <glib/gi18n.h>
+
+/**
+ * SECTION:gtd-manager
+ * @short_description:bridge between plugins and Endeavour
+ * @title:GtdManager
+ * @stability:Unstable
+ * @see_also:#GtdNotification,#GtdActivatable
+ *
+ * The #GtdManager object is a singleton object that exposes all the data
+ * inside the plugin to Endeavour, and vice-versa. From here, plugins have
+ * access to all the tasklists, tasks and panels of the other plugins.
+ *
+ * Objects can use gtd_manager_emit_error_message() to send errors to
+ * Endeavour. This will create a #GtdNotification internally.
+ */
+
+struct _GtdManager
+{
+ GtdObject parent;
+
+ GSettings *settings;
+ GtdPluginManager *plugin_manager;
+
+ GListModel *inbox_model;
+ GListModel *lists_model;
+ GListModel *providers_model;
+ GListModel *tasks_model;
+ GListModel *unarchived_tasks_model;
+
+ GList *providers;
+ GtdProvider *default_provider;
+ GtdClock *clock;
+
+ GCancellable *cancellable;
+};
+
+G_DEFINE_TYPE (GtdManager, gtd_manager, GTD_TYPE_OBJECT)
+
+/* Singleton instance */
+static GtdManager *gtd_manager_instance = NULL;
+
+enum
+{
+ LIST_ADDED,
+ LIST_CHANGED,
+ LIST_REMOVED,
+ SHOW_ERROR_MESSAGE,
+ SHOW_NOTIFICATION,
+ PROVIDER_ADDED,
+ PROVIDER_REMOVED,
+ NUM_SIGNALS
+};
+
+enum
+{
+ PROP_0,
+ PROP_DEFAULT_PROVIDER,
+ PROP_CLOCK,
+ PROP_PLUGIN_MANAGER,
+ LAST_PROP
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+check_provider_is_default (GtdManager *self,
+ GtdProvider *provider)
+{
+ g_autofree gchar *default_provider = NULL;
+
+ default_provider = g_settings_get_string (self->settings, "default-provider");
+
+ if (g_strcmp0 (default_provider, gtd_provider_get_id (provider)) == 0)
+ gtd_manager_set_default_provider (self, provider);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static gboolean
+filter_archived_lists_func (gpointer item,
+ gpointer user_data)
+{
+ GtdTaskList *list;
+ GtdTask *task;
+
+ task = (GtdTask*) item;
+ list = gtd_task_get_list (task);
+
+ return !gtd_task_list_get_archived (list);
+}
+
+static gboolean
+filter_inbox_cb (gpointer item,
+ gpointer user_data)
+{
+ GtdProvider *provider = gtd_task_list_get_provider (item);
+
+ return gtd_provider_get_inbox (provider) == item;
+}
+
+static gint
+compare_lists_cb (GtdTaskList *list_a,
+ GtdTaskList *list_b,
+ gpointer user_data)
+{
+ gint result;
+
+ /* 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));
+}
+
+static void
+on_task_list_modified_cb (GtdTaskList *list,
+ GtdTask *task,
+ GtdManager *self)
+{
+ GTD_ENTRY;
+ g_signal_emit (self, signals[LIST_CHANGED], 0, list);
+ GTD_EXIT;
+}
+
+static void
+on_list_added_cb (GtdProvider *provider,
+ GtdTaskList *list,
+ GtdManager *self)
+{
+ GTD_ENTRY;
+
+ gtd_list_store_insert_sorted (GTD_LIST_STORE (self->lists_model),
+ list,
+ (GCompareDataFunc) compare_lists_cb,
+ self);
+
+ g_signal_connect (list,
+ "task-added",
+ G_CALLBACK (on_task_list_modified_cb),
+ self);
+
+ g_signal_connect (list,
+ "task-updated",
+ G_CALLBACK (on_task_list_modified_cb),
+ self);
+
+ g_signal_connect (list,
+ "task-removed",
+ G_CALLBACK (on_task_list_modified_cb),
+ self);
+
+ g_signal_emit (self, signals[LIST_ADDED], 0, list);
+
+ GTD_EXIT;
+}
+
+static void
+on_list_changed_cb (GtdProvider *provider,
+ GtdTaskList *list,
+ GtdManager *self)
+{
+ GtkFilter *filter;
+
+ GTD_ENTRY;
+
+ gtd_list_store_sort (GTD_LIST_STORE (self->lists_model),
+ (GCompareDataFunc) compare_lists_cb,
+ self);
+
+ filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->unarchived_tasks_model));
+ gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT);
+
+ g_signal_emit (self, signals[LIST_CHANGED], 0, list);
+
+ GTD_EXIT;
+}
+
+static void
+on_list_removed_cb (GtdProvider *provider,
+ GtdTaskList *list,
+ GtdManager *self)
+{
+ GTD_ENTRY;
+
+ if (!list)
+ GTD_RETURN ();
+
+ gtd_list_store_remove (GTD_LIST_STORE (self->lists_model), list);
+
+ g_signal_handlers_disconnect_by_func (list,
+ on_task_list_modified_cb,
+ self);
+
+ g_signal_emit (self, signals[LIST_REMOVED], 0, list);
+
+ GTD_EXIT;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_manager_finalize (GObject *object)
+{
+ GtdManager *self = (GtdManager *)object;
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->plugin_manager);
+ g_clear_object (&self->settings);
+ g_clear_object (&self->clock);
+ g_clear_object (&self->unarchived_tasks_model);
+ g_clear_object (&self->lists_model);
+ g_clear_object (&self->inbox_model);
+
+ G_OBJECT_CLASS (gtd_manager_parent_class)->finalize (object);
+}
+
+static void
+gtd_manager_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdManager *self = (GtdManager *) object;
+
+ switch (prop_id)
+ {
+ case PROP_DEFAULT_PROVIDER:
+ g_value_set_object (value, self->default_provider);
+ break;
+
+ case PROP_CLOCK:
+ g_value_set_object (value, self->clock);
+ break;
+
+ case PROP_PLUGIN_MANAGER:
+ g_value_set_object (value, self->plugin_manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_manager_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdManager *self = (GtdManager *) object;
+
+ switch (prop_id)
+ {
+ case PROP_DEFAULT_PROVIDER:
+ if (g_set_object (&self->default_provider, g_value_get_object (value)))
+ g_object_notify (object, "default-provider");
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_manager_class_init (GtdManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_manager_finalize;
+ object_class->get_property = gtd_manager_get_property;
+ object_class->set_property = gtd_manager_set_property;
+
+ /**
+ * GtdManager::default-provider:
+ *
+ * The default provider.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_DEFAULT_PROVIDER,
+ g_param_spec_object ("default-provider",
+ "The default provider of the application",
+ "The default provider of the application",
+ GTD_TYPE_PROVIDER,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdManager::clock:
+ *
+ * The underlying clock of Endeavour.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_CLOCK,
+ g_param_spec_object ("clock",
+ "The clock",
+ "The clock of the application",
+ GTD_TYPE_CLOCK,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdManager::plugin-manager:
+ *
+ * The plugin manager.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_PLUGIN_MANAGER,
+ g_param_spec_object ("plugin-manager",
+ "The plugin manager",
+ "The plugin manager of the application",
+ GTD_TYPE_PLUGIN_MANAGER,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdManager::list-added:
+ * @manager: a #GtdManager
+ * @list: a #GtdTaskList
+ *
+ * The ::list-added signal is emmited after a #GtdTaskList
+ * is connected.
+ */
+ signals[LIST_ADDED] = g_signal_new ("list-added",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK_LIST);
+
+ /**
+ * GtdManager::list-changed:
+ * @manager: a #GtdManager
+ * @list: a #GtdTaskList
+ *
+ * The ::list-changed signal is emmited after a #GtdTaskList
+ * has any of it's properties changed.
+ */
+ signals[LIST_CHANGED] = g_signal_new ("list-changed",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK_LIST);
+
+ /**
+ * GtdManager::list-removed:
+ * @manager: a #GtdManager
+ * @list: a #GtdTaskList
+ *
+ * The ::list-removed signal is emmited after a #GtdTaskList
+ * is disconnected.
+ */
+ signals[LIST_REMOVED] = g_signal_new ("list-removed",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK_LIST);
+
+ /**
+ * GtdManager::show-error-message:
+ * @manager: a #GtdManager
+ * @primary_text: the primary message
+ * @secondary_text: the detailed explanation of the error or the text to the notification button.
+ * @action : optionally action of type GtdNotificationActionFunc ignored if it's null.
+ * @user_data : user data passed to the action.
+ *
+ * Notifies about errors, and sends the error message for widgets
+ * to display.
+ */
+ signals[SHOW_ERROR_MESSAGE] = g_signal_new ("show-error-message",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 4,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_POINTER,
+ G_TYPE_POINTER);
+
+ /**
+ * GtdManager::show-notification:
+ * @manager: a #GtdManager
+ * @notification: the #GtdNotification
+ *
+ * Sends a notification.
+ */
+ signals[SHOW_NOTIFICATION] = g_signal_new ("show-notification",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_NOTIFICATION);
+
+ /**
+ * GtdManager::provider-added:
+ * @manager: a #GtdManager
+ * @provider: a #GtdProvider
+ *
+ * The ::provider-added signal is emmited after a #GtdProvider
+ * is added.
+ */
+ signals[PROVIDER_ADDED] = g_signal_new ("provider-added",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_PROVIDER);
+
+ /**
+ * GtdManager::provider-removed:
+ * @manager: a #GtdManager
+ * @provider: a #GtdProvider
+ *
+ * The ::provider-removed signal is emmited after a #GtdProvider
+ * is removed from the list.
+ */
+ signals[PROVIDER_REMOVED] = g_signal_new ("provider-removed",
+ GTD_TYPE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_PROVIDER);
+}
+
+
+static void
+gtd_manager_init (GtdManager *self)
+{
+ GtkCustomFilter *archived_lists_filter;
+ GtkCustomFilter *inbox_filter;
+
+ inbox_filter = gtk_custom_filter_new (filter_inbox_cb, self, NULL);
+ archived_lists_filter = gtk_custom_filter_new (filter_archived_lists_func, self, NULL);
+
+ self->settings = g_settings_new ("org.gnome.todo");
+ self->plugin_manager = gtd_plugin_manager_new ();
+ self->clock = gtd_clock_new ();
+ self->cancellable = g_cancellable_new ();
+ self->lists_model = (GListModel*) gtd_list_store_new (GTD_TYPE_TASK_LIST);
+ self->inbox_model = (GListModel*) gtk_filter_list_model_new (self->lists_model,
+ GTK_FILTER (inbox_filter));
+ self->tasks_model = (GListModel*) _gtd_task_model_new (self);
+ self->unarchived_tasks_model = (GListModel*) gtk_filter_list_model_new (self->tasks_model,
+ GTK_FILTER (archived_lists_filter));
+ self->providers_model = (GListModel*) gtd_list_store_new (GTD_TYPE_PROVIDER);
+}
+
+/**
+ * gtd_manager_get_default:
+ *
+ * Retrieves the singleton #GtdManager instance. You should always
+ * use this function instead of @gtd_manager_new.
+ *
+ * Returns: (transfer none): the singleton #GtdManager instance.
+ */
+GtdManager*
+gtd_manager_get_default (void)
+{
+ if (!gtd_manager_instance)
+ gtd_manager_instance = gtd_manager_new ();
+
+ return gtd_manager_instance;
+}
+
+GtdManager*
+gtd_manager_new (void)
+{
+ return g_object_new (GTD_TYPE_MANAGER, NULL);
+}
+
+/**
+ * gtd_manager_get_providers:
+ * @manager: a #GtdManager
+ *
+ * Retrieves the list of available #GtdProvider.
+ *
+ * Returns: (transfer container) (element-type Gtd.Provider): a newly allocated #GList of
+ * #GtdStorage. Free with @g_list_free after use.
+ */
+GList*
+gtd_manager_get_providers (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return g_list_copy (self->providers);
+}
+
+/**
+ * gtd_manager_add_provider:
+ * @self: a #GtdManager
+ * @provider: a #GtdProvider
+ *
+ * Adds @provider to the list of providers.
+ */
+void
+gtd_manager_add_provider (GtdManager *self,
+ GtdProvider *provider)
+{
+ g_autoptr (GList) lists = NULL;
+ GList *l;
+
+ g_return_if_fail (GTD_IS_MANAGER (self));
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+
+ GTD_ENTRY;
+
+ self->providers = g_list_prepend (self->providers, provider);
+ gtd_list_store_append (GTD_LIST_STORE (self->providers_model), provider);
+
+ /* Add lists */
+ lists = gtd_provider_get_task_lists (provider);
+
+ for (l = lists; l != NULL; l = l->next)
+ on_list_added_cb (provider, l->data, self);
+
+ g_object_connect (provider,
+ "signal::list-added", G_CALLBACK (on_list_added_cb), self,
+ "signal::list-changed", G_CALLBACK (on_list_changed_cb), self,
+ "signal::list-removed", G_CALLBACK (on_list_removed_cb), self,
+ NULL);
+
+ /* If we just added the default provider, update the property */
+ check_provider_is_default (self, provider);
+
+ g_signal_emit (self, signals[PROVIDER_ADDED], 0, provider);
+
+ GTD_EXIT;
+}
+
+/**
+ * gtd_manager_remove_provider:
+ * @self: a #GtdManager
+ * @provider: a #GtdProvider
+ *
+ * Removes @provider from the list of providers.
+ */
+void
+gtd_manager_remove_provider (GtdManager *self,
+ GtdProvider *provider)
+{
+ g_autoptr (GList) lists = NULL;
+ GList *l;
+
+ g_return_if_fail (GTD_IS_MANAGER (self));
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+
+ GTD_ENTRY;
+
+ self->providers = g_list_remove (self->providers, provider);
+ gtd_list_store_remove (GTD_LIST_STORE (self->providers_model), provider);
+
+ /* Remove lists */
+ lists = gtd_provider_get_task_lists (provider);
+
+ for (l = lists; l != NULL; l = l->next)
+ on_list_removed_cb (provider, l->data, self);
+
+ g_signal_handlers_disconnect_by_func (provider, on_list_added_cb, self);
+ g_signal_handlers_disconnect_by_func (provider, on_list_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (provider, on_list_removed_cb, self);
+
+ g_signal_emit (self, signals[PROVIDER_REMOVED], 0, provider);
+
+ GTD_EXIT;
+}
+
+
+/**
+ * gtd_manager_get_default_provider:
+ * @manager: a #GtdManager
+ *
+ * Retrieves the default provider location. Default is "local".
+ *
+ * Returns: (transfer none): the default provider.
+ */
+GtdProvider*
+gtd_manager_get_default_provider (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->default_provider;
+}
+
+/**
+ * gtd_manager_set_default_provider:
+ * @manager: a #GtdManager
+ * @provider: (nullable): the default provider.
+ *
+ * Sets the provider.
+ */
+void
+gtd_manager_set_default_provider (GtdManager *self,
+ GtdProvider *provider)
+{
+ g_return_if_fail (GTD_IS_MANAGER (self));
+
+ if (!g_set_object (&self->default_provider, provider))
+ return;
+
+ g_settings_set_string (self->settings,
+ "default-provider",
+ provider ? gtd_provider_get_id (provider) : "local");
+
+ g_object_notify (G_OBJECT (self), "default-provider");
+}
+
+/**
+ * gtd_manager_get_inbox:
+ * @self: a #GtdManager
+ *
+ * Retrieves the local inbox.
+ *
+ * Returns: (transfer none)(nullable): a #GtdTaskList
+ */
+GtdTaskList*
+gtd_manager_get_inbox (GtdManager *self)
+{
+ GList *l;
+
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ for (l = self->providers; l; l = l->next)
+ {
+ if (g_str_equal (gtd_provider_get_id (l->data), "local"))
+ return gtd_provider_get_inbox (l->data);
+ }
+
+ return NULL;
+}
+
+/**
+ * gtd_manager_get_settings:
+ * @manager: a #GtdManager
+ *
+ * Retrieves the internal #GSettings from @manager.
+ *
+ * Returns: (transfer none): the internal #GSettings of @manager
+ */
+GSettings*
+gtd_manager_get_settings (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->settings;
+}
+
+/**
+ * gtd_manager_get_is_first_run:
+ * @manager: a #GtdManager
+ *
+ * Retrieves the 'first-run' setting.
+ *
+ * Returns: %TRUE if Endeavour was never run before, %FALSE otherwise.
+ */
+gboolean
+gtd_manager_get_is_first_run (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), FALSE);
+
+ return g_settings_get_boolean (self->settings, "first-run");
+}
+
+/**
+ * gtd_manager_set_is_first_run:
+ * @manager: a #GtdManager
+ * @is_first_run: %TRUE to make it first run, %FALSE otherwise.
+ *
+ * Sets the 'first-run' setting.
+ */
+void
+gtd_manager_set_is_first_run (GtdManager *self,
+ gboolean is_first_run)
+{
+ g_return_if_fail (GTD_IS_MANAGER (self));
+
+ g_settings_set_boolean (self->settings, "first-run", is_first_run);
+}
+
+/**
+ * gtd_manager_emit_error_message:
+ * @self: a #GtdManager
+ * @title: (nullable): the title of the error
+ * @description: (nullable): detailed description of the error
+ * @function: (scope call)(nullable): function to be called when the notification is dismissed
+ * @user_data: user data
+ *
+ * Reports an error.
+ */
+void
+gtd_manager_emit_error_message (GtdManager *self,
+ const gchar *title,
+ const gchar *description,
+ GtdErrorActionFunc function,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_MANAGER (self));
+
+ g_signal_emit (self,
+ signals[SHOW_ERROR_MESSAGE],
+ 0,
+ title,
+ description,
+ function,
+ user_data);
+}
+
+/**
+ * gtd_manager_send_notification:
+ * @self: a #GtdManager
+ * @notification: a #GtdNotification
+ *
+ * Sends a notification to the notification system.
+ */
+void
+gtd_manager_send_notification (GtdManager *self,
+ GtdNotification *notification)
+{
+ g_return_if_fail (GTD_IS_MANAGER (self));
+
+ g_signal_emit (self, signals[SHOW_NOTIFICATION], 0, notification);
+}
+
+/**
+ * gtd_manager_get_clock:
+ * @self: a #GtdManager
+ *
+ * Retrieves the #GtdClock from @self. You can use the
+ * clock to know when your code should be updated.
+ *
+ * Returns: (transfer none): a #GtdClock
+ */
+GtdClock*
+gtd_manager_get_clock (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->clock;
+}
+
+/**
+ * gtd_manager_get_task_lists_model:
+ * @self: a #GtdManager
+ *
+ * Retrieves the #GListModel containing #GtdTaskLists from
+ * @self. You can use the this model to bind to GtkListBox
+ * or other widgets.
+ *
+ * The model is sorted.
+ *
+ * Returns: (transfer none): a #GListModel
+ */
+GListModel*
+gtd_manager_get_task_lists_model (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->lists_model;
+}
+
+
+/**
+ * gtd_manager_get_all_tasks_model:
+ * @self: a #GtdManager
+ *
+ * Retrieves the #GListModel containing #GtdTasks from
+ * @self. You can use the this model to bind to GtkListBox
+ * or other widgets.
+ *
+ * The model is sorted.
+ *
+ * Returns: (transfer none): a #GListModel
+ */
+GListModel*
+gtd_manager_get_all_tasks_model (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->tasks_model;
+}
+
+/**
+ * gtd_manager_get_tasks_model:
+ * @self: a #GtdManager
+ *
+ * Retrieves the #GListModel containing #GtdTasks from
+ * @self. You can use the this model to bind to GtkListBox
+ * or other widgets.
+ *
+ * The model returned by this function is filtered to only
+ * contain tasks from unarchived lists. If you need all tasks,
+ * see gtd_manager_get_all_tasks_model().
+ *
+ * The model is sorted.
+ *
+ * Returns: (transfer none): a #GListModel
+ */
+GListModel*
+gtd_manager_get_tasks_model (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->unarchived_tasks_model;
+}
+
+/**
+ * gtd_manager_get_inbox_model:
+ * @self: a #GtdManager
+ *
+ * Retrieves the #GListModel containing #GtdTaskLists that are
+ * inbox.
+ *
+ * Returns: (transfer none): a #GListModel
+ */
+GListModel*
+gtd_manager_get_inbox_model (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->inbox_model;
+}
+
+/**
+ * gtd_manager_get_providers_model:
+ * @self: a #GtdManager
+ *
+ * Retrieves the #GListModel containing #GtdProviders.
+ *
+ * Returns: (transfer none): a #GListModel
+ */
+GListModel*
+gtd_manager_get_providers_model (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->providers_model;
+}
+
+void
+gtd_manager_load_plugins (GtdManager *self)
+{
+ GTD_ENTRY;
+
+ gtd_plugin_manager_load_plugins (self->plugin_manager);
+
+ GTD_EXIT;
+}
+
+GtdPluginManager*
+gtd_manager_get_plugin_manager (GtdManager *self)
+{
+ g_return_val_if_fail (GTD_IS_MANAGER (self), NULL);
+
+ return self->plugin_manager;
+}
diff --git a/src/core/gtd-manager.h b/src/core/gtd-manager.h
new file mode 100644
index 0000000..68d661b
--- /dev/null
+++ b/src/core/gtd-manager.h
@@ -0,0 +1,86 @@
+/* gtd-manager.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 <gio/gio.h>
+
+#include "gtd-object.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_MANAGER (gtd_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdManager, gtd_manager, GTD, MANAGER, GtdObject)
+
+/**
+ * GtdErrorActionFunc:
+ */
+typedef void (*GtdErrorActionFunc) (GtdNotification *notification,
+ gpointer user_data);
+
+
+GtdManager* gtd_manager_new (void);
+
+GtdManager* gtd_manager_get_default (void);
+
+GList* gtd_manager_get_providers (GtdManager *manager);
+
+void gtd_manager_add_provider (GtdManager *self,
+ GtdProvider *provider);
+
+void gtd_manager_remove_provider (GtdManager *self,
+ GtdProvider *provider);
+
+GtdProvider* gtd_manager_get_default_provider (GtdManager *manager);
+
+void gtd_manager_set_default_provider (GtdManager *manager,
+ GtdProvider *provider);
+
+GtdTaskList* gtd_manager_get_inbox (GtdManager *self);
+
+GSettings* gtd_manager_get_settings (GtdManager *manager);
+
+gboolean gtd_manager_get_is_first_run (GtdManager *manager);
+
+void gtd_manager_set_is_first_run (GtdManager *manager,
+ gboolean is_first_run);
+
+void gtd_manager_emit_error_message (GtdManager *self,
+ const gchar *title,
+ const gchar *description,
+ GtdErrorActionFunc function,
+ gpointer user_data);
+
+void gtd_manager_send_notification (GtdManager *self,
+ GtdNotification *notification);
+
+GtdClock* gtd_manager_get_clock (GtdManager *self);
+
+GListModel* gtd_manager_get_task_lists_model (GtdManager *self);
+
+GListModel* gtd_manager_get_all_tasks_model (GtdManager *self);
+
+GListModel* gtd_manager_get_tasks_model (GtdManager *self);
+
+GListModel* gtd_manager_get_inbox_model (GtdManager *self);
+
+GListModel* gtd_manager_get_providers_model (GtdManager *self);
+
+G_END_DECLS
diff --git a/src/core/gtd-notification.c b/src/core/gtd-notification.c
new file mode 100644
index 0000000..eee3e46
--- /dev/null
+++ b/src/core/gtd-notification.c
@@ -0,0 +1,441 @@
+/* gtd-notification.c
+ *
+ * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ * Copyright (C) 2022 Jamie Murphy <hello@itsjamie.dev>
+ *
+ * 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 "GtdNotification"
+
+#include "gtd-notification.h"
+#include "gtd-object.h"
+
+#include <glib/gi18n.h>
+
+/**
+ * SECTION: gtd-notification
+ * @short_description: An auxiliary class around #AdwToast
+ * @title: GtdNotification
+ * @stability: Stable
+ *
+ * The #GtdNotification represents a notification shown at the top of
+ * the window. It is an auxiliary class around #AdwToast, and is used only
+ * to store toast data and callbacks.
+ *
+ * A notification may have a dismissal action that is called when the toast
+ * is dismissed, whether by user interaction or a timeout
+ *
+ * Optionally, the notification may have a secondary action
+ * (see gtd_notification_set_secondary_action()), shown as a button.
+ *
+ * A user should not create a UI for a #GtdNotification and should instead
+ * pass it to a #GtdManager, via gtd_manager_send_notification()
+ *
+ * Example:
+ * |[
+ * GtdNotification *notification;
+ *
+ * notification = gtd_notification_new ("Something happened!");
+ *
+ * gtd_notification_set_dismissal_action (notification,
+ * called_when_notification_is_dismissed,
+ * self);
+ *
+ * gtd_notification_set_secondary_action (notification,
+ * "Details",
+ * show_details,
+ * self);
+ * [...]
+ * ]|
+ */
+
+typedef struct
+{
+ gchar *text;
+
+ GtdNotificationActionFunc dismissal_action;
+ gboolean has_dismissal_action;
+ gpointer dismissal_action_data;
+
+ GtdNotificationActionFunc secondary_action;
+ gboolean has_secondary_action;
+ gpointer secondary_action_data;
+ gchar *secondary_action_name;
+} GtdNotificationPrivate;
+
+struct _GtdNotification
+{
+ GtdObject parent;
+
+ /*< private >*/
+ GtdNotificationPrivate *priv;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdNotification, gtd_notification, GTD_TYPE_OBJECT)
+
+enum
+{
+ PROP_0,
+ PROP_HAS_DISMISSAL_ACTION,
+ PROP_HAS_SECONDARY_ACTION,
+ PROP_SECONDARY_ACTION_NAME,
+ PROP_TEXT,
+ LAST_PROP
+};
+
+enum
+{
+ EXECUTED,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+static void
+gtd_notification_finalize (GObject *object)
+{
+ GtdNotification *self = (GtdNotification *)object;
+ GtdNotificationPrivate *priv = gtd_notification_get_instance_private (self);
+
+ g_clear_pointer (&priv->secondary_action_name, g_free);
+ g_clear_pointer (&priv->text, g_free);
+
+ G_OBJECT_CLASS (gtd_notification_parent_class)->finalize (object);
+}
+
+static void
+gtd_notification_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdNotification *self = GTD_NOTIFICATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_HAS_DISMISSAL_ACTION:
+ g_value_set_boolean (value, self->priv->has_dismissal_action);
+ break;
+
+ case PROP_HAS_SECONDARY_ACTION:
+ g_value_set_boolean (value, self->priv->has_secondary_action);
+ break;
+
+ case PROP_SECONDARY_ACTION_NAME:
+ g_value_set_string (value, self->priv->secondary_action_name ? self->priv->secondary_action_name : "");
+ break;
+
+ case PROP_TEXT:
+ g_value_set_string (value, gtd_notification_get_text (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_notification_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdNotification *self = GTD_NOTIFICATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_SECONDARY_ACTION_NAME:
+ gtd_notification_set_secondary_action (self,
+ g_value_get_string (value),
+ self->priv->secondary_action,
+ self->priv->secondary_action_data);
+ break;
+
+ case PROP_TEXT:
+ gtd_notification_set_text (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_notification_class_init (GtdNotificationClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_notification_finalize;
+ object_class->get_property = gtd_notification_get_property;
+ object_class->set_property = gtd_notification_set_property;
+
+ /**
+ * GtdNotification::has-dismissal-action:
+ *
+ * @TRUE if the notification has a dismissal action or @FALSE otherwise.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_HAS_DISMISSAL_ACTION,
+ g_param_spec_boolean ("has-dismissal-action",
+ "Whether the notification has a dismissal action",
+ "Whether the notification has the dismissal action.",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY));
+
+ /**
+ * GtdNotification::has-secondary-action:
+ *
+ * @TRUE if the notification has a secondary action or @FALSE otherwise. The
+ * secondary action is triggered only by user explicit input.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_HAS_SECONDARY_ACTION,
+ g_param_spec_boolean ("has-secondary-action",
+ "Whether the notification has a secondary action",
+ "Whether the notification has the secondary action, activated by the user",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY));
+
+ /**
+ * GtdNotification::secondary-action-name:
+ *
+ * The main text of the notification, usually a markuped text.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_SECONDARY_ACTION_NAME,
+ g_param_spec_string ("secondary-action-name",
+ "Text of the secondary action button",
+ "The text of the secondary action button",
+ "",
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdNotification::text:
+ *
+ * The main text of the notification, usually a markuped text.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_TEXT,
+ g_param_spec_string ("text",
+ "Notification message",
+ "The main message of the notification",
+ "",
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdNotification::executed:
+ *
+ * The ::executed signal is emitted after the primary or secondary
+ * #GtdNotification action is executed.
+ */
+ signals[EXECUTED] = g_signal_new ("executed",
+ GTD_TYPE_NOTIFICATION,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static void
+gtd_notification_init (GtdNotification *self)
+{
+ self->priv = gtd_notification_get_instance_private (self);
+ self->priv->secondary_action_name = NULL;
+ self->priv->text = NULL;
+}
+
+/**
+ * gtd_notification_new:
+ * @text: (nullable): text of the notification
+ *
+ * Creates a new notification with @text.
+ *
+ * Returns: (transfer full): a new #GtdNotification
+ */
+GtdNotification*
+gtd_notification_new (const gchar *text)
+{
+ return g_object_new (GTD_TYPE_NOTIFICATION,
+ "text", text,
+ NULL);
+}
+
+/**
+ * gtd_notification_set_dismissal_action:
+ * @notification: a #GtdNotification
+ * @func: (closure user_data) (scope call) (nullable): the dismissal action function
+ * @user_data: data passed to @func
+ *
+ * Sets the dismissal action of @notification
+ */
+void
+gtd_notification_set_dismissal_action (GtdNotification *notification,
+ GtdNotificationActionFunc func,
+ gpointer user_data)
+{
+ GtdNotificationPrivate *priv;
+ gboolean has_action;
+
+ g_return_if_fail (GTD_IS_NOTIFICATION (notification));
+
+ priv = notification->priv;
+ has_action = (func != NULL);
+
+ if (has_action != priv->has_dismissal_action)
+ {
+ priv->has_dismissal_action = has_action;
+
+ priv->dismissal_action = has_action ? func : NULL;
+ priv->dismissal_action_data = has_action ? user_data : NULL;
+
+ g_object_notify (G_OBJECT (notification), "has-dismissal-action");
+ }
+}
+
+/**
+ * gtd_notification_set_secondary_action:
+ * @notification: a #GtdNotification
+ * @name: the name of the secondary action
+ * @func: (closure user_data) (scope call) (nullable): the secondary action function
+ * @user_data: data passed to @func
+ *
+ * Sets the secondary action of @notification, which is triggered
+ * only on user explicit input.
+ */
+void
+gtd_notification_set_secondary_action (GtdNotification *notification,
+ const gchar *name,
+ GtdNotificationActionFunc func,
+ gpointer user_data)
+{
+ GtdNotificationPrivate *priv;
+ gboolean has_action;
+
+ g_return_if_fail (GTD_IS_NOTIFICATION (notification));
+
+ priv = notification->priv;
+ has_action = (func != NULL);
+
+ if (has_action != priv->has_secondary_action)
+ {
+ priv->has_secondary_action = has_action;
+
+ priv->secondary_action = has_action ? func : NULL;
+ priv->secondary_action_data = has_action ? user_data : NULL;
+
+ if (priv->secondary_action_name != name)
+ {
+ g_clear_pointer (&priv->secondary_action_name, g_free);
+ priv->secondary_action_name = g_strdup (name);
+
+ g_object_notify (G_OBJECT (notification), "secondary-action-name");
+ }
+
+ g_object_notify (G_OBJECT (notification), "has-secondary-action");
+ }
+}
+
+/**
+ * gtd_notification_get_text:
+ * @notification: a #GtdNotification
+ *
+ * Gets the text of @notification.
+ *
+ * Returns: (transfer none): the text of @notification.
+ */
+const gchar*
+gtd_notification_get_text (GtdNotification *notification)
+{
+ g_return_val_if_fail (GTD_IS_NOTIFICATION (notification), NULL);
+
+ return notification->priv->text ? notification->priv->text : "";
+}
+
+/**
+ * gtd_notification_set_text:
+ * @notification: a #GtdNotification
+ * @text: the user-visible text of @notification
+ *
+ * Sets the text of @notification to @text.
+ */
+void
+gtd_notification_set_text (GtdNotification *notification,
+ const gchar *text)
+{
+ GtdNotificationPrivate *priv;
+
+ g_return_if_fail (GTD_IS_NOTIFICATION (notification));
+
+ priv = notification->priv;
+
+ if (g_strcmp0 (priv->text, text) != 0)
+ {
+ g_clear_pointer (&priv->text, g_free);
+ priv->text = g_strdup (text);
+
+ g_object_notify (G_OBJECT (notification), "text");
+ }
+}
+
+/**
+ * gtd_notification_execute_dismissal_action:
+ * @notification: a #GtdNotification
+ *
+ * Executes the dismissal action of @notification if any.
+ */
+void
+gtd_notification_execute_dismissal_action (GtdNotification *notification)
+{
+ GtdNotificationPrivate *priv;
+
+ g_return_if_fail (GTD_IS_NOTIFICATION (notification));
+
+ priv = notification->priv;
+
+ if (priv->dismissal_action)
+ priv->dismissal_action (notification, priv->dismissal_action_data);
+
+ g_signal_emit (notification, signals[EXECUTED], 0);
+}
+
+/**
+ * gtd_notification_execute_secondary_action:
+ * @notification: a #GtdNotification
+ *
+ * Executes the secondary action of @notification if any.
+ */
+void
+gtd_notification_execute_secondary_action (GtdNotification *notification)
+{
+ GtdNotificationPrivate *priv;
+
+ g_return_if_fail (GTD_IS_NOTIFICATION (notification));
+
+ priv = notification->priv;
+
+ if (priv->secondary_action)
+ {
+ priv->secondary_action (notification, priv->secondary_action_data);
+
+ g_signal_emit (notification, signals[EXECUTED], 0);
+ }
+}
diff --git a/src/core/gtd-notification.h b/src/core/gtd-notification.h
new file mode 100644
index 0000000..fd420ad
--- /dev/null
+++ b/src/core/gtd-notification.h
@@ -0,0 +1,68 @@
+/* gtd-notification.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_NOTIFICATION_H
+#define GTD_NOTIFICATION_H
+
+#include "gtd-object.h"
+#include "gtd-types.h"
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_NOTIFICATION (gtd_notification_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdNotification, gtd_notification, GTD, NOTIFICATION, GtdObject)
+
+/**
+ * GtdNotificationActionFunc:
+ * @notification: the #GtdNotification running the function
+ * @user_data: (closure): user data
+ *
+ * Will be called when the dismissal or secondary action of @notification
+ * is executed.
+ */
+typedef void (*GtdNotificationActionFunc) (GtdNotification *notification,
+ gpointer user_data);
+
+
+GtdNotification* gtd_notification_new (const gchar *text);
+
+void gtd_notification_execute_dismissal_action (GtdNotification *notification);
+
+void gtd_notification_execute_secondary_action (GtdNotification *notification);
+
+void gtd_notification_set_dismissal_action (GtdNotification *notification,
+ GtdNotificationActionFunc func,
+ gpointer user_data);
+
+void gtd_notification_set_secondary_action (GtdNotification *notification,
+ const gchar *name,
+ GtdNotificationActionFunc func,
+ gpointer user_data);
+
+const gchar* gtd_notification_get_text (GtdNotification *notification);
+
+void gtd_notification_set_text (GtdNotification *notification,
+ const gchar *text);
+
+G_END_DECLS
+
+#endif /* GTD_NOTIFICATION_H */
diff --git a/src/core/gtd-object.c b/src/core/gtd-object.c
new file mode 100644
index 0000000..c1157fc
--- /dev/null
+++ b/src/core/gtd-object.c
@@ -0,0 +1,313 @@
+/* gtd-object.c
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "GtdObject"
+
+#include "gtd-object.h"
+
+#include <glib/gi18n.h>
+
+/**
+ * SECTION:gtd-object
+ * @Short_description: base class for loadable and uniquely identifiable objects
+ * @Title: GtdObject
+ *
+ * #GtdObject is the base class of many object in Endeavour, and it useful for
+ * when a given object is loadable and/or uniquely identifiable. Some examples of
+ * it are #GtdTask, #GtdTaskList and #GtdNotification.
+ *
+ */
+
+typedef struct
+{
+ guint64 loading;
+ gchar *uid;
+} GtdObjectPrivate;
+
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdObject, gtd_object, G_TYPE_OBJECT)
+
+
+enum
+{
+ PROP_0,
+ PROP_LOADING,
+ PROP_UID,
+ N_PROPS
+};
+
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+static const gchar*
+gtd_object_real_get_uid (GtdObject *object)
+{
+ GtdObjectPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_OBJECT (object), NULL);
+
+ priv = gtd_object_get_instance_private (object);
+
+ return priv->uid;
+}
+
+static void
+gtd_object_real_set_uid (GtdObject *object,
+ const gchar *uid)
+{
+ GtdObjectPrivate *priv;
+
+ g_assert (GTD_IS_OBJECT (object));
+
+ priv = gtd_object_get_instance_private (object);
+
+ if (g_strcmp0 (priv->uid, uid) == 0)
+ return;
+
+ g_clear_pointer (&priv->uid, g_free);
+ priv->uid = g_strdup (uid);
+
+ g_object_notify_by_pspec (G_OBJECT (object), properties[PROP_UID]);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_object_finalize (GObject *object)
+{
+ GtdObject *self = GTD_OBJECT (object);
+ GtdObjectPrivate *priv = gtd_object_get_instance_private (self);
+
+ g_clear_pointer (&priv->uid, g_free);
+
+ G_OBJECT_CLASS (gtd_object_parent_class)->finalize (object);
+}
+
+static void
+gtd_object_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdObject *self = GTD_OBJECT (object);
+ GtdObjectPrivate *priv = gtd_object_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_LOADING:
+ g_value_set_boolean (value, priv->loading > 0);
+ break;
+
+ case PROP_UID:
+ g_value_set_string (value, GTD_OBJECT_GET_CLASS (self)->get_uid (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_object_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdObject *self = GTD_OBJECT (object);
+
+ switch (prop_id)
+ {
+ case PROP_UID:
+ GTD_OBJECT_GET_CLASS (self)->set_uid (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_object_class_init (GtdObjectClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ klass->get_uid = gtd_object_real_get_uid;
+ klass->set_uid = gtd_object_real_set_uid;
+
+ object_class->finalize = gtd_object_finalize;
+ object_class->get_property = gtd_object_get_property;
+ object_class->set_property = gtd_object_set_property;
+
+ /**
+ * GtdObject::uid:
+ *
+ * The unique identified of the object, set by the backend.
+ */
+ properties[PROP_UID] = g_param_spec_string ("uid",
+ "Unique identifier of the object",
+ "The unique identifier of the object, defined by the backend",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdObject::loading:
+ *
+ * Whether the object is loading or not.
+ */
+ properties[PROP_LOADING] = g_param_spec_boolean ("loading",
+ "Loading state of the object",
+ "Whether the object is loading or not",
+ TRUE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtd_object_init (GtdObject *self)
+{
+}
+
+/**
+ * gtd_object_new:
+ * @uid: unique identifier of the object
+ *
+ * Creates a new #GtdObject object.
+ *
+ * Returns: (transfer full): a new #GtdObject
+ */
+GtdObject*
+gtd_object_new (const gchar *uid)
+{
+ return g_object_new (GTD_TYPE_OBJECT, "uid", uid, NULL);
+}
+
+/**
+ * gtd_object_get_uid:
+ * @object: a #GtdObject
+ *
+ * Retrieves the internal unique identifier of @object.
+ *
+ * Returns: (transfer none): the unique identifier of @object. Do
+ * not free after usage.
+ */
+const gchar*
+gtd_object_get_uid (GtdObject *object)
+{
+ GtdObjectClass *class;
+
+ g_return_val_if_fail (GTD_IS_OBJECT (object), NULL);
+
+ class = GTD_OBJECT_GET_CLASS (object);
+
+ g_assert (class);
+ return class->get_uid (object);
+}
+
+/**
+ * gtd_object_set_uid:
+ * @object: a #GtdObject
+ * @uid: the unique identifier of @object
+ *
+ * Sets the unique identifier of @object to @uid. Only
+ * a #GtdBackend should do it.
+ */
+void
+gtd_object_set_uid (GtdObject *object,
+ const gchar *uid)
+{
+ GtdObjectClass *class;
+
+ g_return_if_fail (GTD_IS_OBJECT (object));
+
+ class = GTD_OBJECT_GET_CLASS (object);
+
+ g_assert (class);
+ class->set_uid (object, uid);
+}
+
+/**
+ * gtd_object_get_loading:
+ * @object: a #GtdObject
+ *
+ * Whether @object is loading or not.
+ *
+ * Returns: %TRUE if @object is loading, %FALSE otherwise.
+ */
+gboolean
+gtd_object_get_loading (GtdObject *object)
+{
+ GtdObjectPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_OBJECT (object), FALSE);
+
+ priv = gtd_object_get_instance_private (object);
+
+ return priv->loading > 0;
+}
+
+/**
+ * gtd_object_push_loading:
+ * @object: a #GtdObject
+ *
+ * Increases the loading counter of @object by one. The object is marked
+ * as loading while the loading counter is greater than zero.
+ */
+void
+gtd_object_push_loading (GtdObject *object)
+{
+ GtdObjectPrivate *priv;
+
+ g_return_if_fail (GTD_IS_OBJECT (object));
+
+ priv = gtd_object_get_instance_private (object);
+
+ priv->loading++;
+
+ if (priv->loading == 1)
+ g_object_notify_by_pspec (G_OBJECT (object), properties[PROP_LOADING]);
+}
+
+/**
+ * gtd_object_pop_loading:
+ * @object: a #GtdObject
+ *
+ * Decreases the loading counter of @object by one. The object is marked
+ * as loading while the loading counter is greater than zero.
+ *
+ * It is a programming error to pop more times then push the loading the
+ * counter.
+ */
+void
+gtd_object_pop_loading (GtdObject *object)
+{
+ GtdObjectPrivate *priv;
+
+ g_return_if_fail (GTD_IS_OBJECT (object));
+
+ priv = gtd_object_get_instance_private (object);
+
+ priv->loading--;
+
+ if (priv->loading == 0)
+ g_object_notify_by_pspec (G_OBJECT (object), properties[PROP_LOADING]);
+}
diff --git a/src/core/gtd-object.h b/src/core/gtd-object.h
new file mode 100644
index 0000000..c0033d3
--- /dev/null
+++ b/src/core/gtd-object.h
@@ -0,0 +1,54 @@
+/* gtd-object.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/>.
+ */
+
+#pragma once
+
+#include "gtd-types.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_OBJECT (gtd_object_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (GtdObject, gtd_object, GTD, OBJECT, GObject)
+
+struct _GtdObjectClass
+{
+ GObjectClass parent;
+
+ /* public */
+ const gchar* (* get_uid) (GtdObject *object);
+ void (* set_uid) (GtdObject *object,
+ const gchar *uid);
+};
+
+GtdObject* gtd_object_new (const gchar *uid);
+
+const gchar* gtd_object_get_uid (GtdObject *object);
+
+void gtd_object_set_uid (GtdObject *object,
+ const gchar *uid);
+
+gboolean gtd_object_get_loading (GtdObject *object);
+
+void gtd_object_push_loading (GtdObject *object);
+
+void gtd_object_pop_loading (GtdObject *object);
+
+G_END_DECLS
diff --git a/src/core/gtd-plugin-manager.c b/src/core/gtd-plugin-manager.c
new file mode 100644
index 0000000..e159916
--- /dev/null
+++ b/src/core/gtd-plugin-manager.c
@@ -0,0 +1,323 @@
+/* gtd-plugin-manager.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 "GtdPluginManager"
+
+#include "gtd-activatable.h"
+#include "gtd-manager.h"
+#include "gtd-panel.h"
+#include "gtd-provider.h"
+#include "gtd-plugin-manager.h"
+
+#include <libpeas/peas.h>
+
+struct _GtdPluginManager
+{
+ GtdObject parent;
+
+ GHashTable *info_to_extension;
+};
+
+G_DEFINE_TYPE (GtdPluginManager, gtd_plugin_manager, GTD_TYPE_OBJECT)
+
+enum
+{
+ PLUGIN_LOADED,
+ PLUGIN_UNLOADED,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+static const gchar * const default_plugins[] = {
+ "all-tasks-panel",
+ "eds",
+ "task-lists-workspace",
+ "today-panel",
+ "inbox-panel",
+ "next-week-panel",
+ "peace",
+};
+
+static gboolean
+gtd_str_equal0 (gconstpointer a,
+ gconstpointer b)
+{
+ if (a == b)
+ return TRUE;
+ else if (!a || !b)
+ return FALSE;
+ else
+ return g_str_equal (a, b);
+}
+
+static gchar**
+get_loaded_extensions (const gchar **extensions)
+{
+ g_autoptr (GPtrArray) loaded_plugins = NULL;
+ gsize i;
+
+ loaded_plugins = g_ptr_array_new ();
+
+ for (i = 0; extensions && extensions[i]; i++)
+ g_ptr_array_add (loaded_plugins, g_strdup (extensions[i]));
+
+ for (i = 0; i < G_N_ELEMENTS (default_plugins); i++)
+ {
+ if (g_ptr_array_find_with_equal_func (loaded_plugins,
+ default_plugins[i],
+ gtd_str_equal0,
+ NULL))
+ {
+ continue;
+ }
+
+ g_ptr_array_add (loaded_plugins, g_strdup (default_plugins[i]));
+ }
+
+ g_ptr_array_add (loaded_plugins, NULL);
+
+ return (gchar**) g_ptr_array_free (g_steal_pointer (&loaded_plugins), FALSE);
+}
+
+static gboolean
+from_gsetting_to_property_func (GValue *value,
+ GVariant *variant,
+ gpointer user_data)
+{
+ g_autofree const gchar **extensions = NULL;
+ g_autofree gchar **loaded_extensions = NULL;
+
+ extensions = g_variant_get_strv (variant, NULL);
+ loaded_extensions = get_loaded_extensions (extensions);
+
+ g_value_take_boxed (value, g_steal_pointer (&loaded_extensions));
+
+ return TRUE;
+}
+
+static GVariant*
+from_property_to_gsetting_func (const GValue *value,
+ const GVariantType *expected_type,
+ gpointer user_data)
+{
+ g_autofree gchar **loaded_extensions = NULL;
+ const gchar **extensions = NULL;
+
+ extensions = g_value_get_boxed (value);
+ loaded_extensions = get_loaded_extensions (extensions);
+
+ return g_variant_new_strv ((const gchar * const *)loaded_extensions, -1);
+}
+
+static void
+on_plugin_unloaded_cb (PeasEngine *engine,
+ PeasPluginInfo *info,
+ GtdPluginManager *self)
+{
+ GtdActivatable *activatable;
+
+ activatable = g_hash_table_lookup (self->info_to_extension, info);
+
+ if (!activatable)
+ return;
+
+ /* Deactivates the extension */
+ gtd_activatable_deactivate (activatable);
+
+ /* Emit the signal */
+ g_signal_emit (self, signals[PLUGIN_UNLOADED], 0, info, activatable);
+
+ g_hash_table_remove (self->info_to_extension, info);
+
+ /* Destroy the extension */
+ g_clear_object (&activatable);
+}
+
+static void
+on_plugin_loaded_cb (PeasEngine *engine,
+ PeasPluginInfo *info,
+ GtdPluginManager *self)
+{
+ if (peas_engine_provides_extension (engine, info, GTD_TYPE_ACTIVATABLE))
+ {
+ GtdActivatable *activatable;
+ PeasExtension *extension;
+
+ /*
+ * Actually create the plugin object,
+ * which should load all the providers.
+ */
+ extension = peas_engine_create_extension (engine,
+ info,
+ GTD_TYPE_ACTIVATABLE,
+ NULL);
+
+ /* All extensions shall be GtdActivatable impls */
+ activatable = GTD_ACTIVATABLE (extension);
+
+ g_hash_table_insert (self->info_to_extension,
+ info,
+ extension);
+
+ /* Activate extension */
+ gtd_activatable_activate (activatable);
+
+ /* Emit the signal */
+ g_signal_emit (self, signals[PLUGIN_LOADED], 0, info, extension);
+ }
+}
+
+static void
+setup_engine (GtdPluginManager *self)
+{
+ PeasEngine *engine;
+ gchar *plugin_dir;
+
+ engine = peas_engine_get_default ();
+
+ /* Enable Python3 plugins */
+ peas_engine_enable_loader (engine, "python3");
+
+ /* Let Peas search for plugins in the specified directory */
+ plugin_dir = g_build_filename (PACKAGE_LIB_DIR,
+ "plugins",
+ NULL);
+
+ peas_engine_add_search_path (engine,
+ plugin_dir,
+ NULL);
+
+ g_free (plugin_dir);
+
+ /* User-installed plugins shall be detected too */
+ plugin_dir = g_build_filename (g_get_home_dir (),
+ ".local",
+ "lib",
+ "endeavour",
+ "plugins",
+ NULL);
+
+ peas_engine_add_search_path (engine, plugin_dir, NULL);
+ peas_engine_prepend_search_path (engine,
+ "resource:///org/gnome/todo/plugins",
+ "resource:///org/gnome/todo/plugins");
+
+ g_free (plugin_dir);
+
+ /* Hear about loaded plugins */
+ g_signal_connect_after (engine, "load-plugin", G_CALLBACK (on_plugin_loaded_cb), self);
+ g_signal_connect (engine, "unload-plugin",G_CALLBACK (on_plugin_unloaded_cb), self);
+}
+
+static void
+gtd_plugin_manager_finalize (GObject *object)
+{
+ GtdPluginManager *self = (GtdPluginManager *)object;
+
+ g_clear_pointer (&self->info_to_extension, g_hash_table_destroy);
+
+ G_OBJECT_CLASS (gtd_plugin_manager_parent_class)->finalize (object);
+}
+
+static void
+gtd_plugin_manager_class_init (GtdPluginManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_plugin_manager_finalize;
+
+ signals[PLUGIN_LOADED] = g_signal_new ("plugin-loaded",
+ GTD_TYPE_PLUGIN_MANAGER,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 2,
+ PEAS_TYPE_PLUGIN_INFO,
+ GTD_TYPE_ACTIVATABLE);
+
+ signals[PLUGIN_UNLOADED] = g_signal_new ("plugin-unloaded",
+ GTD_TYPE_PLUGIN_MANAGER,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 2,
+ PEAS_TYPE_PLUGIN_INFO,
+ GTD_TYPE_ACTIVATABLE);
+}
+
+static void
+gtd_plugin_manager_init (GtdPluginManager *self)
+{
+ self->info_to_extension = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+ gtd_object_push_loading (GTD_OBJECT (self));
+
+ setup_engine (self);
+
+ gtd_object_pop_loading (GTD_OBJECT (self));
+}
+
+GtdPluginManager*
+gtd_plugin_manager_new (void)
+{
+ return g_object_new (GTD_TYPE_PLUGIN_MANAGER, NULL);
+}
+
+void
+gtd_plugin_manager_load_plugins (GtdPluginManager *self)
+{
+ PeasEngine *engine;
+ GSettings *settings;
+
+ engine = peas_engine_get_default ();
+ settings = gtd_manager_get_settings (gtd_manager_get_default ());
+
+ g_settings_bind_with_mapping (settings,
+ "active-extensions",
+ engine,
+ "loaded-plugins",
+ G_SETTINGS_BIND_DEFAULT,
+ from_gsetting_to_property_func,
+ from_property_to_gsetting_func,
+ self,
+ NULL);
+}
+
+GtdActivatable*
+gtd_plugin_manager_get_plugin (GtdPluginManager *self,
+ PeasPluginInfo *info)
+{
+ g_return_val_if_fail (GTD_IS_PLUGIN_MANAGER (self), NULL);
+
+ return g_hash_table_lookup (self->info_to_extension, info);
+}
+
+GList*
+gtd_plugin_manager_get_loaded_plugins (GtdPluginManager *self)
+{
+ g_return_val_if_fail (GTD_IS_PLUGIN_MANAGER (self), NULL);
+
+ return g_hash_table_get_values (self->info_to_extension);
+}
diff --git a/src/core/gtd-plugin-manager.h b/src/core/gtd-plugin-manager.h
new file mode 100644
index 0000000..7bef2c6
--- /dev/null
+++ b/src/core/gtd-plugin-manager.h
@@ -0,0 +1,45 @@
+/* gtd-plugin-manager.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_PLUGIN_MANAGER_H
+#define GTD_PLUGIN_MANAGER_H
+
+#include <glib-object.h>
+#include <libpeas/peas.h>
+
+#include "gtd-object.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PLUGIN_MANAGER (gtd_plugin_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdPluginManager, gtd_plugin_manager, GTD, PLUGIN_MANAGER, GtdObject)
+
+GtdPluginManager* gtd_plugin_manager_new (void);
+
+void gtd_plugin_manager_load_plugins (GtdPluginManager *self);
+
+GtdActivatable* gtd_plugin_manager_get_plugin (GtdPluginManager *self,
+ PeasPluginInfo *info);
+
+GList* gtd_plugin_manager_get_loaded_plugins (GtdPluginManager *self);
+
+G_END_DECLS
+
+#endif /* GTD_PLUGIN_MANAGER_H */
diff --git a/src/core/gtd-provider.c b/src/core/gtd-provider.c
new file mode 100644
index 0000000..1d69a80
--- /dev/null
+++ b/src/core/gtd-provider.c
@@ -0,0 +1,681 @@
+/* gtd-provider.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 "GtdProvider"
+
+#include "gtd-provider.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-utils.h"
+
+/**
+ * SECTION:gtd-provider
+ * @short_description:data sources for Endeavour
+ * @title: GtdProvider
+ * @stability:Unstable
+ *
+ * The #GtdProvider is the interface that Endeavour uses to
+ * connect to data sources. It must provide ways to create, update
+ * and remove tasks and tasklists.
+ *
+ * A provider implementation must also expose which is the default
+ * tasklist among the tasklists it manages.
+ */
+
+G_DEFINE_INTERFACE (GtdProvider, gtd_provider, GTD_TYPE_OBJECT)
+
+enum
+{
+ LIST_ADDED,
+ LIST_CHANGED,
+ LIST_REMOVED,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+
+static void
+gtd_provider_default_init (GtdProviderInterface *iface)
+{
+ /**
+ * GtdProvider::enabled:
+ *
+ * Whether the #GtdProvider is enabled.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_boolean ("enabled",
+ "Identifier of the provider",
+ "The identifier of the provider",
+ FALSE,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdProvider::icon:
+ *
+ * The icon of the #GtdProvider, e.g. the account icon
+ * of a GNOME Online Accounts' account.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_object ("icon",
+ "Icon of the provider",
+ "The icon of the provider",
+ G_TYPE_ICON,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdProvider::id:
+ *
+ * The unique identifier of the #GtdProvider.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("id",
+ "Identifier of the provider",
+ "The identifier of the provider",
+ NULL,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdProvider::name:
+ *
+ * The user-visible name of the #GtdProvider.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("name",
+ "Name of the provider",
+ "The user-visible name of the provider",
+ NULL,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdProvider::provider-type:
+ *
+ * The type of the #GtdProvider.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("provider-type",
+ "Type of the provider",
+ "The type of the provider",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdProvider::description:
+ *
+ * The description of the #GtdProvider, e.g. the account user
+ * of a GNOME Online Accounts' account.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("description",
+ "Description of the provider",
+ "The description of the provider",
+ NULL,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdProvider::list-added:
+ * @provider: a #GtdProvider
+ * @list: a #GtdTaskList
+ *
+ * The ::list-added signal is emmited after a #GtdTaskList
+ * is connected.
+ */
+ signals[LIST_ADDED] = g_signal_new ("list-added",
+ GTD_TYPE_PROVIDER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK_LIST);
+
+ /**
+ * GtdProvider::list-changed:
+ * @provider: a #GtdProvider
+ * @list: a #GtdTaskList
+ *
+ * The ::list-changed signal is emmited after a #GtdTaskList
+ * has any of it's properties changed.
+ */
+ signals[LIST_CHANGED] = g_signal_new ("list-changed",
+ GTD_TYPE_PROVIDER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK_LIST);
+
+ /**
+ * GtdProvider::list-removed:
+ * @provider: a #GtdProvider
+ * @list: a #GtdTaskList
+ *
+ * The ::list-removed signal is emmited after a #GtdTaskList
+ * is disconnected.
+ */
+ signals[LIST_REMOVED] = g_signal_new ("list-removed",
+ GTD_TYPE_PROVIDER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK_LIST);
+}
+
+/**
+ * gtd_provider_get_id:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves the identifier of @provider.
+ *
+ * Returns: (transfer none): the id of @provider
+ */
+const gchar*
+gtd_provider_get_id (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_id, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_id (provider);
+}
+
+/**
+ * gtd_provider_get_name:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves the user-visible name of @provider.
+ *
+ * Returns: (transfer none): the name of @provider
+ */
+const gchar*
+gtd_provider_get_name (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_name, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_name (provider);
+}
+
+/**
+ * gtd_provider_get_provider_type:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves the type of the @provider. This should return the
+ * same value, regardless of the account name.
+ *
+ * For example: "todoist", "todo-txt" or "google"
+ *
+ * Returns: (transfer none): the type of the @provider
+ */
+const gchar*
+gtd_provider_get_provider_type (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_name, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_provider_type (provider);
+}
+
+/**
+ * gtd_provider_get_description:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves the description of @provider.
+ *
+ * Returns: (transfer none): the description of @provider
+ */
+const gchar*
+gtd_provider_get_description (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_description, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_description (provider);
+}
+
+/**
+ * gtd_provider_get_enabled:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves whether @provider is enabled or not. A disabled
+ * provider cannot be selected to be default nor be selected
+ * to add tasks to it.
+ *
+ * Returns: %TRUE if provider is enabled, %FALSE otherwise.
+ */
+gboolean
+gtd_provider_get_enabled (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_enabled, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_enabled (provider);
+}
+
+/**
+ * gtd_provider_refresh:
+ * @provider: a #GtdProvider
+ *
+ * Asks the provider to refresh. Online providers may want to
+ * synchronize tasks and tasklists, credentials, etc, when this
+ * is called.
+ *
+ * This is an optional feature. Providers that do not implement
+ * the "refresh" vfunc will be ignored.
+ */
+void
+gtd_provider_refresh (GtdProvider *provider)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+
+ if (GTD_PROVIDER_GET_IFACE (provider)->refresh)
+ GTD_PROVIDER_GET_IFACE (provider)->refresh (provider);
+}
+
+/**
+ * gtd_provider_get_icon:
+ * @provider: a #GtdProvider
+ *
+ * The icon of @provider.
+ *
+ * Returns: (transfer none): a #GIcon
+ */
+GIcon*
+gtd_provider_get_icon (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_icon, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_icon (provider);
+}
+
+/**
+ * gtd_provider_create_task:
+ * @provider: a #GtdProvider
+ * @list: a #GtdTaskLast
+ * @title: The task title
+ * @due_date: (nullable): a #GDateTime
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (scope async): a callback
+ * @user_data: (closure): user data for @callback
+ *
+ * Creates the given task in @provider.
+ */
+void
+gtd_provider_create_task (GtdProvider *provider,
+ GtdTaskList *list,
+ const gchar *title,
+ GDateTime *due_date,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+ g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->create_task);
+
+ GTD_PROVIDER_GET_IFACE (provider)->create_task (provider,
+ list,
+ title,
+ due_date,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtd_provider_create_task_finish:
+ * @self: a #GtdProvider
+ * @result: a #GAsyncResult
+ * @error: (out)(nullable): return location for a #GError
+ *
+ * Finishes creating the task.
+ *
+ * Returns: (transfer none)(nullable): a #GtdTask
+ */
+GtdTask*
+gtd_provider_create_task_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->create_task_finish, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (self)->create_task_finish (self, result, error);
+}
+
+/**
+ * gtd_provider_update_task:
+ * @provider: a #GtdProvider
+ * @task: a #GtdTask
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (scope async): a callback
+ * @user_data: (closure): user data for @callback
+ *
+ * Updates the given task in @provider.
+ */
+void
+gtd_provider_update_task (GtdProvider *provider,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+ g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->update_task);
+
+ GTD_PROVIDER_GET_IFACE (provider)->update_task (provider,
+ task,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtd_provider_update_task_finish:
+ * @self: a #GtdProvider
+ * @result: a #GAsyncResult
+ * @error: (out)(nullable): return location for a #GError
+ *
+ * Finishes updating the task list.
+ *
+ * Returns: %TRUE if task list was successfully updated, %FALSE otherwise
+ */
+gboolean
+gtd_provider_update_task_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->update_task_finish, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (self)->update_task_finish (self, result, error);
+}
+
+/**
+ * gtd_provider_remove_task:
+ * @provider: a #GtdProvider
+ * @task: a #GtdTask
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (scope async): a callback
+ * @user_data: (closure): user data for @callback
+ *
+ * Removes the given task from @provider.
+ */
+void
+gtd_provider_remove_task (GtdProvider *provider,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+ g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->remove_task);
+
+ GTD_PROVIDER_GET_IFACE (provider)->remove_task (provider,
+ task,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtd_provider_remove_task_finish:
+ * @self: a #GtdProvider
+ * @result: a #GAsyncResult
+ * @error: (out)(nullable): return location for a #GError
+ *
+ * Finishes removing the task.
+ *
+ * Returns: %TRUE if task was successfully removed, %FALSE otherwise
+ */
+gboolean
+gtd_provider_remove_task_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->remove_task_finish, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (self)->remove_task_finish (self, result, error);
+}
+
+/**
+ * gtd_provider_create_task_list:
+ * @provider: a #GtdProvider
+ * @name: (nullable): the name of the new task list
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (scope async): a callback
+ * @user_data: (closure): user data for @callback
+ *
+ * Creates the given list in @provider.
+ */
+void
+gtd_provider_create_task_list (GtdProvider *provider,
+ const gchar *name,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+ g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->create_task_list);
+
+ GTD_PROVIDER_GET_IFACE (provider)->create_task_list (provider,
+ name,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtd_provider_create_task_list_finish:
+ * @self: a #GtdProvider
+ * @result: a #GAsyncResult
+ * @error: (out)(nullable): return location for a #GError
+ *
+ * Finishes creating the task list. The provider will emit the
+ * GtdProvider:list-added signal after creating the task list.
+ *
+ * Returns: %TRUE if task list was successfully created, %FALSE otherwise
+ */
+gboolean
+gtd_provider_create_task_list_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->create_task_list_finish, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (self)->create_task_list_finish (self, result, error);
+}
+
+/**
+ * gtd_provider_update_task_list:
+ * @provider: a #GtdProvider
+ * @list: a #GtdTaskList
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (scope async): a callback
+ * @user_data: (closure): user data for @callback
+ *
+ * Updates the given list in @provider.
+ */
+void
+gtd_provider_update_task_list (GtdProvider *provider,
+ GtdTaskList *list,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+ g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->update_task_list);
+
+ GTD_PROVIDER_GET_IFACE (provider)->update_task_list (provider,
+ list,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtd_provider_update_task_list_finish:
+ * @self: a #GtdProvider
+ * @result: a #GAsyncResult
+ * @error: (out)(nullable): return location for a #GError
+ *
+ * Finishes updating the task list. The provider will emit the
+ * GtdProvider:list-updated signal after updating the task list.
+ *
+ * Returns: %TRUE if task list was successfully updated, %FALSE otherwise
+ */
+gboolean
+gtd_provider_update_task_list_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->update_task_list_finish, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (self)->update_task_list_finish (self, result, error);
+}
+
+/**
+ * gtd_provider_remove_task_list:
+ * @provider: a #GtdProvider
+ * @list: a #GtdTaskList
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (scope async): a callback
+ * @user_data: (closure): user data for @callback
+ *
+ * Removes the given list from @provider.
+ */
+void
+gtd_provider_remove_task_list (GtdProvider *provider,
+ GtdTaskList *list,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+ g_return_if_fail (GTD_PROVIDER_GET_IFACE (provider)->remove_task_list);
+
+ GTD_PROVIDER_GET_IFACE (provider)->remove_task_list (provider,
+ list,
+ cancellable,
+ callback,
+ user_data);
+}
+
+/**
+ * gtd_provider_remove_task_list_finish:
+ * @self: a #GtdProvider
+ * @result: a #GAsyncResult
+ * @error: (out)(nullable): return location for a #GError
+ *
+ * Finishes removing the task list. The provider will emit the
+ * GtdProvider:list-removed signal after removing the task list.
+ *
+ * Returns: %TRUE if task list was successfully removed, %FALSE otherwise
+ */
+gboolean
+gtd_provider_remove_task_list_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (self)->remove_task_list_finish, FALSE);
+
+ return GTD_PROVIDER_GET_IFACE (self)->remove_task_list_finish (self, result, error);
+}
+
+/**
+ * gtd_provider_get_task_lists:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves the tasklists that this provider contains.
+ *
+ * Returns: (transfer container) (element-type Gtd.TaskList): the list of tasks, or %NULL
+ */
+GList*
+gtd_provider_get_task_lists (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_task_lists, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_task_lists (provider);
+}
+
+/**
+ * gtd_provider_get_inbox:
+ * @provider: a #GtdProvider
+ *
+ * Retrieves the inbox of @provider.
+ *
+ * Returns: (transfer none)(nullable): a #GtdTaskList
+ */
+GtdTaskList*
+gtd_provider_get_inbox (GtdProvider *provider)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER (provider), NULL);
+ g_return_val_if_fail (GTD_PROVIDER_GET_IFACE (provider)->get_inbox, NULL);
+
+ return GTD_PROVIDER_GET_IFACE (provider)->get_inbox (provider);
+}
+
+/**
+ * gtd_provider_compare:
+ * @a: a #GtdProvider
+ * @b: a #GtdProvider
+ *
+ * Compares @a and @b. The sorting criteria is internal and
+ * may change.
+ *
+ * Returns: -1 if @a comes before @b, 1 for the oposite, and
+ * 0 if they're equal
+ */
+gint
+gtd_provider_compare (GtdProvider *a,
+ GtdProvider *b)
+{
+ gint result;
+
+ g_return_val_if_fail (GTD_IS_PROVIDER (a), 0);
+ g_return_val_if_fail (GTD_IS_PROVIDER (b), 0);
+
+ if (a == b)
+ return 0;
+
+ result = gtd_collate_compare_strings (gtd_provider_get_name (a), gtd_provider_get_name (b));
+
+ if (result != 0)
+ return result;
+
+ return gtd_collate_compare_strings (gtd_provider_get_description (a), gtd_provider_get_description (b));
+}
diff --git a/src/core/gtd-provider.h b/src/core/gtd-provider.h
new file mode 100644
index 0000000..0b8e097
--- /dev/null
+++ b/src/core/gtd-provider.h
@@ -0,0 +1,208 @@
+/* gtd-storage.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_H
+#define GTD_PROVIDER_H
+
+#include "gtd-object.h"
+#include "gtd-types.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PROVIDER (gtd_provider_get_type ())
+
+G_DECLARE_INTERFACE (GtdProvider, gtd_provider, GTD, PROVIDER, GtdObject)
+
+struct _GtdProviderInterface
+{
+ GTypeInterface parent;
+
+ /* Information */
+ const gchar* (*get_id) (GtdProvider *provider);
+
+ const gchar* (*get_name) (GtdProvider *provider);
+
+ const gchar* (*get_provider_type) (GtdProvider *provider);
+
+ const gchar* (*get_description) (GtdProvider *provider);
+
+ gboolean (*get_enabled) (GtdProvider *provider);
+
+ void (*refresh) (GtdProvider *provider);
+
+ /* Customs */
+ GIcon* (*get_icon) (GtdProvider *provider);
+
+ /* Tasks */
+ void (*create_task) (GtdProvider *provider,
+ GtdTaskList *list,
+ const gchar *title,
+ GDateTime *due_date,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ GtdTask* (*create_task_finish) (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ void (*update_task) (GtdProvider *provider,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ gboolean (*update_task_finish) (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ void (*remove_task) (GtdProvider *provider,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ gboolean (*remove_task_finish) (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ /* Task lists */
+ void (*create_task_list) (GtdProvider *provider,
+ const gchar *name,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ gboolean (*create_task_list_finish) (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ void (*update_task_list) (GtdProvider *provider,
+ GtdTaskList *list,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ gboolean (*update_task_list_finish) (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ void (*remove_task_list) (GtdProvider *provider,
+ GtdTaskList *list,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+ gboolean (*remove_task_list_finish) (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ GList* (*get_task_lists) (GtdProvider *provider);
+
+ GtdTaskList* (*get_inbox) (GtdProvider *provider);
+};
+
+const gchar* gtd_provider_get_id (GtdProvider *provider);
+
+const gchar* gtd_provider_get_name (GtdProvider *provider);
+
+const gchar* gtd_provider_get_provider_type (GtdProvider *provider);
+
+const gchar* gtd_provider_get_description (GtdProvider *provider);
+
+gboolean gtd_provider_get_enabled (GtdProvider *provider);
+
+void gtd_provider_refresh (GtdProvider *provider);
+
+GIcon* gtd_provider_get_icon (GtdProvider *provider);
+
+void gtd_provider_create_task (GtdProvider *provider,
+ GtdTaskList *list,
+ const gchar *title,
+ GDateTime *due_date,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GtdTask* gtd_provider_create_task_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+void gtd_provider_update_task (GtdProvider *provider,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtd_provider_update_task_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+void gtd_provider_remove_task (GtdProvider *provider,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtd_provider_remove_task_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+void gtd_provider_create_task_list (GtdProvider *provider,
+ const gchar *name,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtd_provider_create_task_list_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+void gtd_provider_update_task_list (GtdProvider *provider,
+ GtdTaskList *list,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtd_provider_update_task_list_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+void gtd_provider_remove_task_list (GtdProvider *provider,
+ GtdTaskList *list,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean gtd_provider_remove_task_list_finish (GtdProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+GList* gtd_provider_get_task_lists (GtdProvider *provider);
+
+GtdTaskList* gtd_provider_get_inbox (GtdProvider *provider);
+
+gint gtd_provider_compare (GtdProvider *a,
+ GtdProvider *b);
+
+G_END_DECLS
+
+#endif /* GTD_PROVIDER_H */
diff --git a/src/core/gtd-task-list.c b/src/core/gtd-task-list.c
new file mode 100644
index 0000000..2e71a9a
--- /dev/null
+++ b/src/core/gtd-task-list.c
@@ -0,0 +1,1161 @@
+/* gtd-task-list.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 "GtdTaskList"
+
+#include "gtd-debug.h"
+#include "gtd-provider.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+
+#include <glib/gi18n.h>
+
+/**
+ * SECTION:gtd-task-list
+ * @short_description:a list of tasks
+ * @title:GtdTaskList
+ * @stability:Unstable
+ * @see_also:#GtdTask
+ *
+ * A #GtdTaskList represents a task list, and contains a list of tasks, a
+ * color, a name and the provider who generated it.
+ *
+ * Only a #GtdProvider can create a #GtdTaskList. Equally, a #GtdTaskList
+ * is only valid when associated with a #GtdProvider.
+ *
+ * It implements #GListModel, and can be used as the model for #GtkListBox.
+ */
+
+typedef struct
+{
+ GtdTask *task;
+ GTask *gtask;
+} ImportingTaskData;
+
+typedef struct
+{
+ GtdProvider *provider;
+ GdkRGBA *color;
+
+ GHashTable *task_to_uid;
+ GHashTable *tasks;
+ GSequence *sorted_tasks;
+ guint n_tasks;
+
+ guint freeze_counter;
+
+ gchar *name;
+ gboolean removable;
+ gboolean archived;
+} GtdTaskListPrivate;
+
+
+static gint compare_tasks_cb (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data);
+
+static void task_changed_cb (GtdTask *task,
+ GParamSpec *pspec,
+ GtdTaskList *self);
+
+static void g_list_model_iface_init (GListModelInterface *iface);
+
+
+G_DEFINE_TYPE_WITH_CODE (GtdTaskList, gtd_task_list, GTD_TYPE_OBJECT,
+ G_ADD_PRIVATE (GtdTaskList)
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init))
+
+enum
+{
+ TASK_ADDED,
+ TASK_REMOVED,
+ TASK_UPDATED,
+ NUM_SIGNALS
+};
+
+enum
+{
+ PROP_0,
+ PROP_ARCHIVED,
+ PROP_COLOR,
+ PROP_IS_REMOVABLE,
+ PROP_NAME,
+ PROP_PROVIDER,
+ N_PROPS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary functions
+ */
+
+static void
+update_task_uid (GtdTaskList *self,
+ GtdTask *task)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+ const gchar *old_uid;
+ gchar *new_uid;
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ g_debug ("Updating uid of task '%s'", gtd_task_get_title (task));
+
+ new_uid = g_strdup (gtd_object_get_uid (GTD_OBJECT (task)));
+
+ old_uid = g_hash_table_lookup (priv->task_to_uid, task);
+ iter = g_hash_table_lookup (priv->tasks, old_uid);
+
+ g_assert (g_sequence_get (iter) == task);
+
+ g_hash_table_remove (priv->tasks, old_uid);
+
+ g_hash_table_insert (priv->task_to_uid, task, new_uid);
+ g_hash_table_insert (priv->tasks, new_uid, iter);
+}
+
+static guint
+add_task (GtdTaskList *self,
+ GtdTask *task)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+ gchar *uid;
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ uid = g_strdup (gtd_object_get_uid (GTD_OBJECT (task)));
+ iter = g_sequence_insert_sorted (priv->sorted_tasks,
+ g_object_ref (task),
+ compare_tasks_cb,
+ NULL);
+
+ g_hash_table_insert (priv->task_to_uid, task, uid);
+ g_hash_table_insert (priv->tasks, uid, iter);
+
+ g_signal_connect (task, "notify", G_CALLBACK (task_changed_cb), self);
+
+ priv->n_tasks++;
+
+ g_signal_emit (self, signals[TASK_ADDED], 0, task);
+
+ return g_sequence_iter_get_position (iter);
+}
+
+static guint
+remove_task (GtdTaskList *self,
+ GtdTask *task)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+ const gchar *uid;
+ guint position;
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ g_signal_handlers_disconnect_by_func (task, task_changed_cb, self);
+
+ uid = gtd_object_get_uid (GTD_OBJECT (task));
+ iter = g_hash_table_lookup (priv->tasks, uid);
+ position = g_sequence_iter_get_position (iter);
+
+ g_hash_table_remove (priv->task_to_uid, task);
+ g_hash_table_remove (priv->tasks, uid);
+
+ g_sequence_remove (iter);
+
+ priv->n_tasks--;
+
+ g_signal_emit (self, signals[TASK_REMOVED], 0, task);
+
+ return position;
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_import_old_task_removed_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GtdTask *new_task;
+ GTask *gtask;
+ g_autoptr (GError) error = NULL;
+
+ gtask = user_data;
+
+ gtd_provider_remove_task_finish (GTD_PROVIDER (object), result, &error);
+
+ if (error)
+ {
+ g_warning ("Error removing task: %s", error->message);
+ return;
+ }
+
+ new_task = g_task_get_task_data (gtask);
+ g_task_return_pointer (gtask, g_object_ref (new_task), g_object_unref);
+}
+
+static void
+on_task_updated_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+
+ gtd_provider_update_task_finish (GTD_PROVIDER (object), result, &error);
+
+ if (error)
+ {
+ g_warning ("Error updating task: %s", error->message);
+ return;
+ }
+}
+
+static void
+on_import_new_task_added_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GtdTask) new_task = NULL;
+ g_autoptr (GError) error = NULL;
+ GTask *gtask;
+ GtdProvider *provider;
+ GtdTask *importing_task;
+ GtdTaskList *list;
+ ImportingTaskData *data;
+
+ GTD_ENTRY;
+
+ data = user_data;
+ gtask = data->gtask;
+ importing_task = data->task;
+
+ new_task = gtd_provider_create_task_finish (GTD_PROVIDER (object), result, &error);
+ g_task_set_task_data (gtask, g_object_ref (new_task), g_object_unref);
+ list = gtd_task_get_list (new_task);
+
+ if (error)
+ {
+ g_warning ("Error creating task: %s", error->message);
+ GTD_RETURN ();
+ }
+
+ gtd_task_set_complete (new_task, gtd_task_get_complete (importing_task));
+ gtd_task_set_description (new_task, gtd_task_get_description (importing_task));
+ gtd_task_set_due_date (new_task, gtd_task_get_due_date (importing_task));
+ gtd_task_set_important (new_task, gtd_task_get_important (importing_task));
+ gtd_task_set_title (new_task, gtd_task_get_title (importing_task));
+
+ gtd_task_list_update_task (list, new_task);
+
+ provider = gtd_task_get_provider (importing_task);
+
+ gtd_provider_remove_task (provider,
+ importing_task,
+ NULL,
+ on_import_old_task_removed_cb,
+ gtask);
+
+ g_free (data);
+
+ GTD_EXIT;
+}
+
+static gint
+compare_tasks_cb (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ return gtd_task_compare ((GtdTask*) a, (GtdTask*) b);
+}
+
+static void
+task_changed_cb (GtdTask *task,
+ GParamSpec *pspec,
+ GtdTaskList *self)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+ guint old_position;
+ guint new_position;
+
+ GTD_ENTRY;
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ if (g_strcmp0 (g_param_spec_get_name (pspec), "loading") == 0)
+ GTD_RETURN ();
+
+ if (g_strcmp0 (g_param_spec_get_name (pspec), "uid") == 0)
+ {
+ update_task_uid (self, task);
+ GTD_RETURN ();
+ }
+
+ /* Don't update when the list is frozen */
+ if (priv->freeze_counter > 0)
+ GTD_RETURN ();
+
+ iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task)));
+
+ old_position = g_sequence_iter_get_position (iter);
+ g_sequence_sort_changed (iter, compare_tasks_cb, NULL);
+ new_position = g_sequence_iter_get_position (iter);
+
+ if (old_position != new_position)
+ {
+ GTD_TRACE_MSG ("Old position: %u, New position: %u", old_position, new_position);
+
+ g_list_model_items_changed (G_LIST_MODEL (self), old_position, 1, 0);
+ g_list_model_items_changed (G_LIST_MODEL (self), new_position, 0, 1);
+ }
+
+ GTD_EXIT;
+}
+
+
+/*
+ * GListModel iface
+ */
+
+static GType
+gtd_list_model_get_type (GListModel *model)
+{
+ return GTD_TYPE_TASK;
+}
+
+static guint
+gtd_list_model_get_n_items (GListModel *model)
+{
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (GTD_TASK_LIST (model));
+ return priv->n_tasks;
+}
+
+static gpointer
+gtd_list_model_get_item (GListModel *model,
+ guint i)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+ GtdTask *task;
+
+ priv = gtd_task_list_get_instance_private (GTD_TASK_LIST (model));
+ iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, i);
+ task = g_sequence_get (iter);
+
+ return g_object_ref (task);
+}
+
+static void
+g_list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtd_list_model_get_type;
+ iface->get_n_items = gtd_list_model_get_n_items;
+ iface->get_item = gtd_list_model_get_item;
+}
+
+
+/*
+ * GtdTaskList overrides
+ */
+
+static gboolean
+gtd_task_list_real_get_archived (GtdTaskList *self)
+{
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+
+ return priv->archived;
+}
+
+static void
+gtd_task_list_real_set_archived (GtdTaskList *self,
+ gboolean archived)
+{
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+
+ priv->archived = archived;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_list_finalize (GObject *object)
+{
+ GtdTaskList *self = (GtdTaskList*) object;
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+
+ g_clear_object (&priv->provider);
+
+ g_clear_pointer (&priv->color, gdk_rgba_free);
+ g_clear_pointer (&priv->name, g_free);
+ g_clear_pointer (&priv->sorted_tasks, g_sequence_free);
+ g_clear_pointer (&priv->tasks, g_hash_table_destroy);
+ g_clear_pointer (&priv->task_to_uid, g_hash_table_destroy);
+
+ G_OBJECT_CLASS (gtd_task_list_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_list_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskList *self = GTD_TASK_LIST (object);
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_ARCHIVED:
+ g_value_set_boolean (value, gtd_task_list_get_archived (self));
+ break;
+
+ case PROP_COLOR:
+ {
+ GdkRGBA *color = gtd_task_list_get_color (self);
+ g_value_set_boxed (value, color);
+ gdk_rgba_free (color);
+ break;
+ }
+
+ case PROP_IS_REMOVABLE:
+ g_value_set_boolean (value, gtd_task_list_is_removable (self));
+ break;
+
+ case PROP_NAME:
+ g_value_set_string (value, priv->name);
+ break;
+
+ case PROP_PROVIDER:
+ g_value_set_object (value, priv->provider);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_list_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskList *self = GTD_TASK_LIST (object);
+
+ switch (prop_id)
+ {
+ case PROP_ARCHIVED:
+ gtd_task_list_set_archived (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_COLOR:
+ gtd_task_list_set_color (self, g_value_get_boxed (value));
+ break;
+
+ case PROP_IS_REMOVABLE:
+ gtd_task_list_set_is_removable (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_NAME:
+ gtd_task_list_set_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_PROVIDER:
+ gtd_task_list_set_provider (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_list_class_init (GtdTaskListClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_task_list_finalize;
+ object_class->get_property = gtd_task_list_get_property;
+ object_class->set_property = gtd_task_list_set_property;
+
+ klass->get_archived = gtd_task_list_real_get_archived;
+ klass->set_archived = gtd_task_list_real_set_archived;
+
+ /**
+ * GtdTaskList::archived:
+ *
+ * Whether the task list is archived or not.
+ */
+ properties[PROP_ARCHIVED] = g_param_spec_boolean ("archived",
+ "Whether the list is archived",
+ "Whether the list is archived or not",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GtdTaskList::color:
+ *
+ * The color of the list.
+ */
+ properties[PROP_COLOR] = g_param_spec_boxed ("color",
+ "Color of the list",
+ "The color of the list",
+ GDK_TYPE_RGBA,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GtdTaskList::is-removable:
+ *
+ * Whether the task list can be removed from the system.
+ */
+ properties[PROP_IS_REMOVABLE] = g_param_spec_boolean ("is-removable",
+ "Whether the task list is removable",
+ "Whether the task list can be removed from the system",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GtdTaskList::name:
+ *
+ * The display name of the list.
+ */
+ properties[PROP_NAME] = g_param_spec_string ("name",
+ "Name of the list",
+ "The name of the list",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GtdTaskList::provider:
+ *
+ * The data provider of the list.
+ */
+ properties[PROP_PROVIDER] = g_param_spec_object ("provider",
+ "Provider of the list",
+ "The provider that handles the list",
+ GTD_TYPE_PROVIDER,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * GtdTaskList::task-added:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * The ::task-added signal is emitted after a #GtdTask
+ * is added to the list.
+ */
+ signals[TASK_ADDED] = g_signal_new ("task-added",
+ GTD_TYPE_TASK_LIST,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTaskListClass, task_added),
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK);
+
+ /**
+ * GtdTaskList::task-removed:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * The ::task-removed signal is emitted after a #GtdTask
+ * is removed from the list.
+ */
+ signals[TASK_REMOVED] = g_signal_new ("task-removed",
+ GTD_TYPE_TASK_LIST,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTaskListClass, task_removed),
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK);
+
+ /**
+ * GtdTaskList::task-updated:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * The ::task-updated signal is emitted after a #GtdTask
+ * in the list is updated.
+ */
+ signals[TASK_UPDATED] = g_signal_new ("task-updated",
+ GTD_TYPE_TASK_LIST,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GtdTaskListClass, task_updated),
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK);
+}
+
+static void
+gtd_task_list_init (GtdTaskList *self)
+{
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+
+ priv->task_to_uid = g_hash_table_new (g_str_hash, g_str_equal);
+ priv->tasks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ priv->sorted_tasks = g_sequence_new (g_object_unref);
+}
+
+/**
+ * gtd_task_list_new:
+ * @provider: (nullable): a #GtdProvider
+ *
+ * Creates a new list.
+ *
+ * Returns: (transfer full): the new #GtdTaskList
+ */
+GtdTaskList *
+gtd_task_list_new (GtdProvider *provider)
+{
+ return g_object_new (GTD_TYPE_TASK_LIST,
+ "provider", provider,
+ NULL);
+}
+
+/**
+ * gtd_task_list_get_color:
+ * @list: a #GtdTaskList
+ *
+ * Retrieves the color of %list. It is guarantee that it always returns a
+ * color, given a valid #GtdTaskList.
+ *
+ * Returns: (transfer full): the color of %list. Free with %gdk_rgba_free after use.
+ */
+GdkRGBA*
+gtd_task_list_get_color (GtdTaskList *list)
+{
+ GtdTaskListPrivate *priv;
+ GdkRGBA rgba;
+
+ g_return_val_if_fail (GTD_IS_TASK_LIST (list), NULL);
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ if (!priv->color)
+ {
+ gdk_rgba_parse (&rgba, "#ffffff");
+ priv->color = gdk_rgba_copy (&rgba);
+ }
+
+ return gdk_rgba_copy (priv->color);
+}
+
+/**
+ * gtd_task_list_set_color:
+ * @list: a #GtdTaskList
+ * #color: a #GdkRGBA
+ *
+ * sets the color of @list.
+ */
+void
+gtd_task_list_set_color (GtdTaskList *list,
+ const GdkRGBA *color)
+{
+ GtdTaskListPrivate *priv;
+ GdkRGBA *current_color;
+
+ g_return_if_fail (GTD_IS_TASK_LIST (list));
+
+ priv = gtd_task_list_get_instance_private (list);
+ current_color = gtd_task_list_get_color (list);
+
+ if (!gdk_rgba_equal (current_color, color))
+ {
+ g_clear_pointer (&priv->color, gdk_rgba_free);
+ priv->color = gdk_rgba_copy (color);
+
+ g_object_notify (G_OBJECT (list), "color");
+ }
+
+ gdk_rgba_free (current_color);
+}
+
+/**
+ * gtd_task_list_get_name:
+ * @list: a #GtdTaskList
+ *
+ * Retrieves the user-visible name of @list, or %NULL.
+ *
+ * Returns: (transfer none): the internal name of @list. Do not free
+ * after use.
+ */
+const gchar*
+gtd_task_list_get_name (GtdTaskList *list)
+{
+ GtdTaskListPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TASK_LIST (list), NULL);
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ return priv->name;
+}
+
+/**
+ * gtd_task_list_set_name:
+ * @list: a #GtdTaskList
+ * @name: (nullable): the name of @list
+ *
+ * Sets the @list name to @name.
+ */
+void
+gtd_task_list_set_name (GtdTaskList *list,
+ const gchar *name)
+{
+ GtdTaskListPrivate *priv;
+
+ g_assert (GTD_IS_TASK_LIST (list));
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ if (g_strcmp0 (priv->name, name) != 0)
+ {
+ g_free (priv->name);
+ priv->name = g_strdup (name);
+
+ g_object_notify (G_OBJECT (list), "name");
+ }
+}
+
+/**
+ * gtd_task_list_get_provider:
+ * @list: a #GtdTaskList
+ *
+ * Retrieves the #GtdProvider who owns this list.
+ *
+ * Returns: (transfer none): a #GtdProvider
+ */
+GtdProvider*
+gtd_task_list_get_provider (GtdTaskList *list)
+{
+ GtdTaskListPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TASK_LIST (list), NULL);
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ return priv->provider;
+}
+
+/**
+ * gtd_task_list_set_provider:
+ * @self: a #GtdTaskList
+ * @provider: (nullable): a #GtdProvider, or %NULL
+ *
+ * Sets the provider of this tasklist.
+ */
+void
+gtd_task_list_set_provider (GtdTaskList *self,
+ GtdProvider *provider)
+{
+ GtdTaskListPrivate *priv;
+
+ g_assert (GTD_IS_TASK_LIST (self));
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ if (g_set_object (&priv->provider, provider))
+ g_object_notify (G_OBJECT (self), "provider");
+}
+
+/**
+ * gtd_task_list_add_task:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * Adds @task to @list.
+ */
+void
+gtd_task_list_add_task (GtdTaskList *self,
+ GtdTask *task)
+{
+ guint position;
+
+ g_assert (GTD_IS_TASK_LIST (self));
+ g_assert (GTD_IS_TASK (task));
+ g_assert (!gtd_task_list_contains (self, task));
+
+ position = add_task (self, task);
+ g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+}
+
+/**
+ * gtd_task_list_update_task:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * Updates @task at @list.
+ */
+void
+gtd_task_list_update_task (GtdTaskList *self,
+ GtdTask *task)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+
+ g_return_if_fail (GTD_IS_TASK_LIST (self));
+ g_return_if_fail (GTD_IS_TASK (task));
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ g_return_if_fail (gtd_task_list_contains (self, task));
+
+ iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task)));
+
+ g_list_model_items_changed (G_LIST_MODEL (self),
+ g_sequence_iter_get_position (iter),
+ 1,
+ 1);
+
+ g_signal_emit (self, signals[TASK_UPDATED], 0, task);
+}
+
+/**
+ * gtd_task_list_remove_task:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * Removes @task from @list if it's inside the list.
+ */
+void
+gtd_task_list_remove_task (GtdTaskList *list,
+ GtdTask *task)
+{
+ guint position;
+
+ g_assert (GTD_IS_TASK_LIST (list));
+ g_assert (GTD_IS_TASK (task));
+ g_assert (gtd_task_list_contains (list, task));
+
+ position = remove_task (list, task);
+ g_list_model_items_changed (G_LIST_MODEL (list), position, 1, 0);
+}
+
+/**
+ * gtd_task_list_contains:
+ * @list: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * Checks if @task is inside @list.
+ *
+ * Returns: %TRUE if @list contains @task, %FALSE otherwise
+ */
+gboolean
+gtd_task_list_contains (GtdTaskList *list,
+ GtdTask *task)
+{
+ GtdTaskListPrivate *priv;
+
+ g_assert (GTD_IS_TASK_LIST (list));
+ g_assert (GTD_IS_TASK (task));
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ return g_hash_table_contains (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task)));
+}
+
+/**
+ * gtd_task_list_get_is_removable:
+ * @list: a #GtdTaskList
+ *
+ * Retrieves whether @list can be removed or not.
+ *
+ * Returns: %TRUE if the @list can be removed, %FALSE otherwise
+ */
+gboolean
+gtd_task_list_is_removable (GtdTaskList *list)
+{
+ GtdTaskListPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TASK_LIST (list), FALSE);
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ return priv->removable;
+}
+
+/**
+ * gtd_task_list_set_is_removable:
+ * @list: a #GtdTaskList
+ * @is_removable: %TRUE if @list can be deleted, %FALSE otherwise
+ *
+ * Sets whether @list can be deleted or not.
+ */
+void
+gtd_task_list_set_is_removable (GtdTaskList *list,
+ gboolean is_removable)
+{
+ GtdTaskListPrivate *priv;
+
+ g_return_if_fail (GTD_IS_TASK_LIST (list));
+
+ priv = gtd_task_list_get_instance_private (list);
+
+ if (priv->removable != is_removable)
+ {
+ priv->removable = is_removable;
+
+ g_object_notify (G_OBJECT (list), "is-removable");
+ }
+}
+
+/**
+ * gtd_task_list_get_task_by_id:
+ * @self: a #GtdTaskList
+ * @id: the id of the task
+ *
+ * Retrieves a task from @self with the given @id.
+ *
+ * Returns: (transfer none)(nullable): a #GtdTask, or %NULL
+ */
+GtdTask*
+gtd_task_list_get_task_by_id (GtdTaskList *self,
+ const gchar *id)
+{
+ GtdTaskListPrivate *priv;
+ GSequenceIter *iter;
+
+ g_return_val_if_fail (GTD_IS_TASK_LIST (self), NULL);
+
+ priv = gtd_task_list_get_instance_private (self);
+ iter = g_hash_table_lookup (priv->tasks, id);
+
+ if (!iter)
+ return NULL;
+
+ return g_sequence_get (iter);
+}
+
+/**
+ * gtd_task_list_move_task_to_position:
+ * @self: a #GtdTaskList
+ * @task: a #GtdTask
+ * @new_position: the new position of @task inside @self
+ *
+ * Moves @task to @new_position, and repositions the elements
+ * in between as well.
+ *
+ * @task must belong to @self.
+ */
+void
+gtd_task_list_move_task_to_position (GtdTaskList *self,
+ GtdTask *task,
+ guint new_position)
+{
+
+ GtdTaskListPrivate *priv = gtd_task_list_get_instance_private (self);
+ GSequenceIter *new_position_iter;
+ GSequenceIter *iter;
+ guint old_position;
+ guint length;
+ guint start;
+ guint i;
+
+
+ g_return_if_fail (GTD_IS_TASK_LIST (self));
+ g_return_if_fail (GTD_IS_TASK (task));
+ g_return_if_fail (gtd_task_list_contains (self, task));
+ g_return_if_fail (g_list_model_get_n_items (G_LIST_MODEL (self)) >= new_position);
+
+ iter = g_hash_table_lookup (priv->tasks, gtd_object_get_uid (GTD_OBJECT (task)));
+ old_position = g_sequence_iter_get_position (iter);
+
+ if (old_position == new_position)
+ return;
+
+ GTD_TRACE_MSG ("Moving task '%s' (%s) from %u to %u",
+ gtd_task_get_title (task),
+ gtd_object_get_uid (GTD_OBJECT (task)),
+ old_position,
+ new_position);
+
+ /* Update the GSequence */
+ new_position_iter = new_position < old_position ?
+ g_sequence_get_iter_at_pos (priv->sorted_tasks, new_position) :
+ g_sequence_get_iter_at_pos (priv->sorted_tasks, new_position + 1);
+ g_sequence_move (iter, new_position_iter);
+
+ /* Update the 'position' property of all tasks in between */
+ priv->freeze_counter++;
+
+ length = ABS ((gint) new_position - (gint64) old_position) + 1;
+ start = MIN (old_position, new_position);
+ iter = g_sequence_get_iter_at_pos (priv->sorted_tasks, start);
+
+ for (i = 0; i < length; i++)
+ {
+ GtdTask *aux = g_sequence_get (iter);
+
+ g_signal_handlers_block_by_func (aux, task_changed_cb, self);
+ gtd_task_set_position (aux, start + i);
+ g_signal_handlers_unblock_by_func (aux, task_changed_cb, self);
+
+ gtd_provider_update_task (priv->provider, aux, NULL, on_task_updated_cb, self);
+
+ iter = g_sequence_iter_next (iter);
+ }
+
+ g_list_model_items_changed (G_LIST_MODEL (self), old_position, 1, 0);
+ g_list_model_items_changed (G_LIST_MODEL (self), new_position, 0, 1);
+
+ priv->freeze_counter--;
+}
+
+/**
+ * gtd_task_list_get_archived:
+ * @self: a #GtdTaskList
+ *
+ * Retrieves whether @self is archived or not. Archived task lists
+ * are hidden by default, and new tasks cannot be added.
+ *
+ * Returns: %TRUE if @self is archived, %FALSE otherwise.
+ */
+gboolean
+gtd_task_list_get_archived (GtdTaskList *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK_LIST (self), FALSE);
+
+ return GTD_TASK_LIST_GET_CLASS (self)->get_archived (self);
+}
+
+/**
+ * gtd_task_list_set_archived:
+ * @self: a #GtdTaskList
+ * @archived: whether @self is archived or not
+ *
+ * Sets the "archive" property of @self to @archived.
+ */
+void
+gtd_task_list_set_archived (GtdTaskList *self,
+ gboolean archived)
+{
+ gboolean was_archived;
+
+ g_return_if_fail (GTD_IS_TASK_LIST (self));
+
+ was_archived = gtd_task_list_get_archived (self);
+
+ if (archived == was_archived)
+ return;
+
+ GTD_TASK_LIST_GET_CLASS (self)->set_archived (self, archived);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ARCHIVED]);
+}
+
+/**
+ * gtd_task_list_import_task:
+ * @self: a #GtdTaskList
+ * @task: a #GtdTask
+ *
+ * Imports task into @self
+ */
+void
+gtd_task_list_import_task (GtdTaskList *self,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr (GTask) gtask = NULL;
+ GtdProvider *provider;
+ ImportingTaskData *data;
+
+ g_return_if_fail (GTD_IS_TASK_LIST (self));
+
+ gtask = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (gtask, gtd_task_list_import_task);
+
+ provider = gtd_task_get_provider (task);
+
+ data = g_new0 (ImportingTaskData, 1);
+ data->gtask = g_object_ref (gtask);
+ data->task = g_object_ref (task);
+
+ gtd_provider_create_task (provider,
+ self,
+ gtd_task_get_title (task),
+ gtd_task_get_due_date (task),
+ NULL,
+ on_import_new_task_added_cb,
+ data);
+}
+
+/**
+ * gtd_task_list_import_task_finish:
+ * @self: a #GtdTaskList
+ * @result: a #GAsyncResult
+ * @error: a #GError
+ *
+ * Imports task into @self
+ *
+ * Returns: (transfer full): a #GTask
+ */
+GTask *
+gtd_task_list_import_task_finish (GtdTaskList *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, self), NULL);
+ g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gtd_task_list_import_task, NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+
+/**
+ * gtd_task_list_is_inbox:
+ * @self: a #GtdTaskList
+ *
+ * Retrieves whether @self is the inbox task list of its provider.
+ *
+ * Returns: %TRUE if @self is the inbox of it's provider, %FALSE otherwise.
+ */
+gboolean
+gtd_task_list_is_inbox (GtdTaskList *self)
+{
+ GtdTaskListPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TASK_LIST (self), FALSE);
+
+ priv = gtd_task_list_get_instance_private (self);
+
+ return self == gtd_provider_get_inbox (priv->provider);
+}
diff --git a/src/core/gtd-task-list.h b/src/core/gtd-task-list.h
new file mode 100644
index 0000000..6f06490
--- /dev/null
+++ b/src/core/gtd-task-list.h
@@ -0,0 +1,118 @@
+/* gtd-task-list.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_H
+#define GTD_TASK_LIST_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+#include <glib.h>
+
+#include "gtd-object.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TASK_LIST (gtd_task_list_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (GtdTaskList, gtd_task_list, GTD, TASK_LIST, GtdObject)
+
+struct _GtdTaskListClass
+{
+ GtdObjectClass parent;
+
+ /* Vfuncs */
+ gboolean (*get_archived) (GtdTaskList *self);
+
+ void (*set_archived) (GtdTaskList *self,
+ gboolean archived);
+
+ /* Signal methods */
+ void (*task_added) (GtdTaskList *list,
+ GtdTask *task);
+
+ void (*task_updated) (GtdTaskList *list,
+ GtdTask *task);
+
+ void (*task_removed) (GtdTaskList *list,
+ GtdTask *task);
+
+ gpointer padding[10];
+};
+
+GtdTaskList* gtd_task_list_new (GtdProvider *provider);
+
+GdkRGBA* gtd_task_list_get_color (GtdTaskList *list);
+
+void gtd_task_list_set_color (GtdTaskList *list,
+ const GdkRGBA *color);
+
+gboolean gtd_task_list_is_removable (GtdTaskList *list);
+
+void gtd_task_list_set_is_removable (GtdTaskList *list,
+ gboolean is_removable);
+
+const gchar* gtd_task_list_get_name (GtdTaskList *list);
+
+void gtd_task_list_set_name (GtdTaskList *list,
+ const gchar *name);
+
+GtdProvider* gtd_task_list_get_provider (GtdTaskList *list);
+
+void gtd_task_list_set_provider (GtdTaskList *self,
+ GtdProvider *provider);
+
+void gtd_task_list_add_task (GtdTaskList *list,
+ GtdTask *task);
+
+void gtd_task_list_update_task (GtdTaskList *list,
+ GtdTask *task);
+
+void gtd_task_list_remove_task (GtdTaskList *list,
+ GtdTask *task);
+
+gboolean gtd_task_list_contains (GtdTaskList *list,
+ GtdTask *task);
+
+GtdTask* gtd_task_list_get_task_by_id (GtdTaskList *self,
+ const gchar *id);
+
+void gtd_task_list_move_task_to_position (GtdTaskList *self,
+ GtdTask *task,
+ guint new_position);
+
+gboolean gtd_task_list_get_archived (GtdTaskList *self);
+
+void gtd_task_list_set_archived (GtdTaskList *self,
+ gboolean archived);
+
+void gtd_task_list_import_task (GtdTaskList *self,
+ GtdTask *task,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GTask * gtd_task_list_import_task_finish (GtdTaskList *self,
+ GAsyncResult *result,
+ GError **error);
+
+gboolean gtd_task_list_is_inbox (GtdTaskList *self);
+
+G_END_DECLS
+
+#endif /* GTD_TASK_LIST_H */
diff --git a/src/core/gtd-task.c b/src/core/gtd-task.c
new file mode 100644
index 0000000..6951dec
--- /dev/null
+++ b/src/core/gtd-task.c
@@ -0,0 +1,993 @@
+/* gtd-task.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 "GtdTask"
+
+#include "gtd-debug.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+
+#include <glib/gi18n.h>
+
+/**
+ * SECTION:gtd-task
+ * @short_description: a task
+ * @title:GtdTask
+ * @stability:Unstable
+ * @see_also:#GtdTaskList
+ *
+ * A #GtdTask is an object that represents a task. All #GtdTasks
+ * must be inside a #GtdTaskList.
+ */
+
+typedef struct
+{
+ gchar *description;
+ GtdTaskList *list;
+
+ GDateTime *creation_date;
+ GDateTime *completion_date;
+ GDateTime *due_date;
+
+ gchar *title;
+
+ gint32 priority;
+ gint64 position;
+ gboolean complete;
+ gboolean important;
+} GtdTaskPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdTask, gtd_task, GTD_TYPE_OBJECT)
+
+enum
+{
+ PROP_0,
+ PROP_COMPLETE,
+ PROP_DESCRIPTION,
+ PROP_CREATION_DATE,
+ PROP_DUE_DATE,
+ PROP_IMPORTANT,
+ PROP_LIST,
+ PROP_POSITION,
+ PROP_TITLE,
+ LAST_PROP
+};
+
+static void
+task_list_weak_notified (gpointer data,
+ GObject *where_the_object_was)
+{
+ GtdTask *task = GTD_TASK (data);
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (task);
+ priv->list = NULL;
+}
+
+/*
+ * GtdTask default implementations
+ */
+
+static GDateTime*
+gtd_task_real_get_completion_date (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->completion_date ? g_date_time_ref (priv->completion_date) : NULL;
+}
+
+static void
+gtd_task_real_set_completion_date (GtdTask *self,
+ GDateTime *dt)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ g_clear_pointer (&priv->completion_date, g_date_time_unref);
+ priv->completion_date = dt ? g_date_time_ref (dt) : NULL;
+}
+
+static gboolean
+gtd_task_real_get_complete (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->complete;
+}
+
+static void
+gtd_task_real_set_complete (GtdTask *self,
+ gboolean complete)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+ GDateTime *dt;
+
+ dt = complete ? g_date_time_new_now_local () : NULL;
+ gtd_task_real_set_completion_date (self, dt);
+
+ priv->complete = complete;
+}
+
+static GDateTime*
+gtd_task_real_get_creation_date (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->creation_date ? g_date_time_ref (priv->creation_date) : NULL;
+}
+
+static void
+gtd_task_real_set_creation_date (GtdTask *self,
+ GDateTime *dt)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ g_clear_pointer (&priv->creation_date, g_date_time_unref);
+ priv->creation_date = dt ? g_date_time_ref (dt) : NULL;
+}
+
+static const gchar*
+gtd_task_real_get_description (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->description ? priv->description : "";
+}
+
+static void
+gtd_task_real_set_description (GtdTask *self,
+ const gchar *description)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ g_clear_pointer (&priv->description, g_free);
+ priv->description = g_strdup (description);
+}
+
+static GDateTime*
+gtd_task_real_get_due_date (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->due_date ? g_date_time_ref (priv->due_date) : NULL;
+}
+
+static void
+gtd_task_real_set_due_date (GtdTask *self,
+ GDateTime *due_date)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ g_clear_pointer (&priv->due_date, g_date_time_unref);
+
+ if (due_date)
+ priv->due_date = g_date_time_ref (due_date);
+}
+
+static gboolean
+gtd_task_real_get_important (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->important;
+}
+
+static void
+gtd_task_real_set_important (GtdTask *self,
+ gboolean important)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ if (priv->important == important)
+ return;
+
+ priv->important = important;
+}
+
+static gint64
+gtd_task_real_get_position (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->position;
+}
+
+static void
+gtd_task_real_set_position (GtdTask *self,
+ gint64 position)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ priv->position = position;
+}
+
+static const gchar*
+gtd_task_real_get_title (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ return priv->title;
+}
+
+static void
+gtd_task_real_set_title (GtdTask *self,
+ const gchar *title)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ g_clear_pointer (&priv->title, g_free);
+ priv->title = title ? g_strdup (title) : NULL;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_finalize (GObject *object)
+{
+ GtdTask *self = (GtdTask*) object;
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ if (priv->list)
+ g_object_weak_unref (G_OBJECT (priv->list), task_list_weak_notified, self);
+
+ priv->list = NULL;
+ g_free (priv->description);
+
+ G_OBJECT_CLASS (gtd_task_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTask *self = GTD_TASK (object);
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+ GDateTime *date;
+
+ switch (prop_id)
+ {
+ case PROP_COMPLETE:
+ g_value_set_boolean (value, gtd_task_get_complete (self));
+ break;
+
+ case PROP_CREATION_DATE:
+ g_value_set_boxed (value, gtd_task_get_creation_date (self));
+ break;
+
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, gtd_task_get_description (self));
+ break;
+
+ case PROP_DUE_DATE:
+ date = gtd_task_get_due_date (self);
+ g_value_set_boxed (value, date);
+ g_clear_pointer (&date, g_date_time_unref);
+ break;
+
+ case PROP_IMPORTANT:
+ g_value_set_boolean (value, gtd_task_get_important (self));
+ break;
+
+ case PROP_LIST:
+ g_value_set_object (value, priv->list);
+ break;
+
+ case PROP_POSITION:
+ g_value_set_int64 (value, gtd_task_get_position (self));
+ break;
+
+ case PROP_TITLE:
+ g_value_set_string (value, gtd_task_get_title (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTask *self = GTD_TASK (object);
+
+ switch (prop_id)
+ {
+ case PROP_COMPLETE:
+ gtd_task_set_complete (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_CREATION_DATE:
+ gtd_task_set_creation_date (self, g_value_get_boxed (value));
+ break;
+
+ case PROP_DESCRIPTION:
+ gtd_task_set_description (self, g_value_get_string (value));
+ break;
+
+ case PROP_DUE_DATE:
+ gtd_task_set_due_date (self, g_value_get_boxed (value));
+ break;
+
+ case PROP_IMPORTANT:
+ gtd_task_set_important (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_LIST:
+ gtd_task_set_list (self, g_value_get_object (value));
+ break;
+
+ case PROP_POSITION:
+ gtd_task_set_position (self, g_value_get_int64 (value));
+ break;
+
+ case PROP_TITLE:
+ gtd_task_set_title (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_class_init (GtdTaskClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ klass->get_complete = gtd_task_real_get_complete;
+ klass->set_complete = gtd_task_real_set_complete;
+ klass->get_creation_date = gtd_task_real_get_creation_date;
+ klass->set_creation_date = gtd_task_real_set_creation_date;
+ klass->get_completion_date = gtd_task_real_get_completion_date;
+ klass->set_completion_date = gtd_task_real_set_completion_date;
+ klass->get_description = gtd_task_real_get_description;
+ klass->set_description = gtd_task_real_set_description;
+ klass->get_due_date = gtd_task_real_get_due_date;
+ klass->set_due_date = gtd_task_real_set_due_date;
+ klass->get_important = gtd_task_real_get_important;
+ klass->set_important = gtd_task_real_set_important;
+ klass->get_position = gtd_task_real_get_position;
+ klass->set_position = gtd_task_real_set_position;
+ klass->get_title = gtd_task_real_get_title;
+ klass->set_title = gtd_task_real_set_title;
+
+ object_class->finalize = gtd_task_finalize;
+ object_class->get_property = gtd_task_get_property;
+ object_class->set_property = gtd_task_set_property;
+
+ /**
+ * GtdTask::complete:
+ *
+ * @TRUE if the task is marked as complete or @FALSE otherwise. Usually
+ * represented by a checkbox at user interfaces.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_COMPLETE,
+ g_param_spec_boolean ("complete",
+ "Whether the task is completed or not",
+ "Whether the task is marked as completed by the user",
+ FALSE,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdTask::creation-date:
+ *
+ * The @GDateTime that represents the time in which the task was created.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_CREATION_DATE,
+ g_param_spec_boxed ("creation-date",
+ "Creation date of the task",
+ "The day the task was created.",
+ G_TYPE_DATE_TIME,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdTask::description:
+ *
+ * Description of the task.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_DESCRIPTION,
+ g_param_spec_string ("description",
+ "Description of the task",
+ "Optional string describing the task",
+ NULL,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdTask::due-date:
+ *
+ * The @GDateTime that represents the time in which the task should
+ * be completed before.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_DUE_DATE,
+ g_param_spec_boxed ("due-date",
+ "End date of the task",
+ "The day the task is supposed to be completed",
+ G_TYPE_DATE_TIME,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdTask::important:
+ *
+ * @TRUE if the task is important, @FALSE otherwise.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_IMPORTANT,
+ g_param_spec_boolean ("important",
+ "Whether the task is important or not",
+ "Whether the task is important or not",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdTask::list:
+ *
+ * The @GtdTaskList that contains this task.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_LIST,
+ g_param_spec_object ("list",
+ "List of the task",
+ "The list that owns this task",
+ GTD_TYPE_TASK_LIST,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdTask::position:
+ *
+ * Position of the task, -1 if not set.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_POSITION,
+ g_param_spec_int64 ("position",
+ "Position of the task",
+ "The position of the task. -1 means no position, and tasks will be sorted alphabetically.",
+ -1,
+ G_MAXINT64,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdTask::title:
+ *
+ * The title of the task, usually the task name.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_TITLE,
+ g_param_spec_string ("title",
+ "Title of the task",
+ "The title of the task",
+ NULL,
+ G_PARAM_READWRITE));
+}
+
+static void
+gtd_task_init (GtdTask *self)
+{
+ GtdTaskPrivate *priv = gtd_task_get_instance_private (self);
+
+ priv->position = -1;
+}
+
+/**
+ * gtd_task_new:
+ *
+ * Creates a new #GtdTask
+ *
+ * Returns: (transfer full): a #GtdTask
+ */
+GtdTask *
+gtd_task_new (void)
+{
+ return g_object_new (GTD_TYPE_TASK, NULL);
+}
+
+/**
+ * gtd_task_get_complete:
+ * @self: a #GtdTask
+ *
+ * Retrieves whether the task is complete or not.
+ *
+ * Returns: %TRUE if the task is complete, %FALSE otherwise
+ */
+gboolean
+gtd_task_get_complete (GtdTask *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK (self), FALSE);
+
+ return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (self))->get_complete (self);
+}
+
+/**
+ * gtd_task_set_complete:
+ * @self: a #GtdTask
+ * @complete: the new value
+ *
+ * Updates the complete state of @task.
+ */
+void
+gtd_task_set_complete (GtdTask *task,
+ gboolean complete)
+{
+ g_return_if_fail (GTD_IS_TASK (task));
+
+ if (gtd_task_get_complete (task) == complete)
+ return;
+
+ GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_complete (task, complete);
+
+ g_object_notify (G_OBJECT (task), "complete");
+}
+
+/**
+ * gtd_task_get_creation_date:
+ * @self: a #GtdTask
+ *
+ * Returns the #GDateTime that represents the task's creation date.
+ * The value is referenced for thread safety. Returns %NULL if
+ * no date is set.
+ *
+ * Returns: (transfer full): the internal #GDateTime referenced
+ * for thread safety, or %NULL. Unreference it after use.
+ */
+GDateTime*
+gtd_task_get_creation_date (GtdTask *task)
+{
+ g_return_val_if_fail (GTD_IS_TASK (task), NULL);
+
+ return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_creation_date (task);
+}
+
+/**
+ * gtd_task_set_creation_date:
+ * @self: a #GtdTask
+ *
+ * Sets the creation date of @task.
+ */
+void
+gtd_task_set_creation_date (GtdTask *task,
+ GDateTime *dt)
+{
+ g_return_if_fail (GTD_IS_TASK (task));
+
+ if (gtd_task_get_creation_date (task) == dt)
+ return;
+
+ GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_creation_date (task, dt);
+
+ g_object_notify (G_OBJECT (task), "complete");
+}
+
+/**
+ * gtd_task_get_completion_date:
+ * @self: a #GtdTask
+ *
+ * Returns the #GDateTime that represents the task's completion date.
+ * Returns %NULL if no date is set.
+ *
+ * Returns: (transfer full)(nullable): the internal #GDateTime or %NULL.
+ * Unreference it after use.
+ */
+GDateTime*
+gtd_task_get_completion_date (GtdTask *task)
+{
+ g_return_val_if_fail (GTD_IS_TASK (task), NULL);
+
+ return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_completion_date (task);
+}
+
+/**
+ * gtd_task_get_description:
+ * @self: a #GtdTask
+ *
+ * Retrieves the description of the task.
+ *
+ * Returns: (transfer none): the description of @task
+ */
+const gchar*
+gtd_task_get_description (GtdTask *task)
+{
+ g_return_val_if_fail (GTD_IS_TASK (task), NULL);
+
+ return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_description (task);
+}
+
+/**
+ * gtd_task_set_description:
+ * @self: a #GtdTask
+ * @description: (nullable): the new description, or %NULL
+ *
+ * Updates the description of @task. The string is not stripped off of
+ * spaces to preserve user data.
+ */
+void
+gtd_task_set_description (GtdTask *task,
+ const gchar *description)
+{
+ GtdTaskPrivate *priv;
+
+ g_assert (GTD_IS_TASK (task));
+ g_assert (g_utf8_validate (description, -1, NULL));
+
+ priv = gtd_task_get_instance_private (task);
+
+ if (g_strcmp0 (priv->description, description) == 0)
+ return;
+
+ GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_description (task, description);
+
+ g_object_notify (G_OBJECT (task), "description");
+}
+
+/**
+ * gtd_task_get_due_date:
+ * @self: a #GtdTask
+ *
+ * Returns the #GDateTime that represents the task's due date.
+ * The value is referenced for thread safety. Returns %NULL if
+ * no date is set.
+ *
+ * Returns: (transfer full) (nullable): the internal #GDateTime referenced
+ * for thread safety, or %NULL. Unreference it after use.
+ */
+GDateTime*
+gtd_task_get_due_date (GtdTask *task)
+{
+ g_return_val_if_fail (GTD_IS_TASK (task), NULL);
+
+ return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_due_date (task);
+}
+
+/**
+ * gtd_task_set_due_date:
+ * @self: a #GtdTask
+ * @dt: (nullable): a #GDateTime
+ *
+ * Updates the internal @GtdTask::due-date property.
+ */
+void
+gtd_task_set_due_date (GtdTask *task,
+ GDateTime *dt)
+{
+ g_autoptr (GDateTime) current_dt = NULL;
+
+ g_assert (GTD_IS_TASK (task));
+
+ current_dt = gtd_task_get_due_date (task);
+
+ /* Don't do anything if the date is equal */
+ if (current_dt == dt || (current_dt && dt && g_date_time_equal (current_dt, dt)))
+ return;
+
+ GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_due_date (task, dt);
+
+ g_object_notify (G_OBJECT (task), "due-date");
+}
+
+/**
+ * gtd_task_get_important:
+ * @self: a #GtdTask
+ *
+ * Retrieves whether @self is @important or not.
+ *
+ * Returns: %TRUE if @self is important, %FALSE otherwise
+ */
+gboolean
+gtd_task_get_important (GtdTask *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK (self), FALSE);
+
+ return GTD_TASK_GET_CLASS (self)->get_important (self);
+}
+
+/**
+ * gtd_task_set_important:
+ * @self: a #GtdTask
+ * @important: whether @self is important or not
+ *
+ * Sets whether @self is @important or not.
+ */
+void
+gtd_task_set_important (GtdTask *self,
+ gboolean important)
+{
+ g_return_if_fail (GTD_IS_TASK (self));
+
+ important = !!important;
+
+ GTD_TASK_GET_CLASS (self)->set_important (self, important);
+ g_object_notify (G_OBJECT (self), "important");
+}
+
+/**
+ * gtd_task_get_list:
+ *
+ * Returns a weak reference to the #GtdTaskList that
+ * owns the given @task.
+ *
+ * Returns: (transfer none): a weak reference to the
+ * #GtdTaskList that owns @task. Do not free after
+ * usage.
+ */
+GtdTaskList*
+gtd_task_get_list (GtdTask *task)
+{
+ GtdTaskPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TASK (task), NULL);
+
+ priv = gtd_task_get_instance_private (task);
+
+ return priv->list;
+}
+
+/**
+ * gtd_task_set_list:
+ * @self: a #GtdTask
+ * @list: (nullable): a #GtdTaskList
+ *
+ * Sets the parent #GtdTaskList of @task.
+ */
+void
+gtd_task_set_list (GtdTask *task,
+ GtdTaskList *list)
+{
+ GtdTaskPrivate *priv;
+
+ g_assert (GTD_IS_TASK (task));
+ g_assert (GTD_IS_TASK_LIST (list));
+
+ priv = gtd_task_get_instance_private (task);
+
+ if (priv->list == list)
+ return;
+
+ if (priv->list)
+ g_object_weak_unref (G_OBJECT (priv->list), task_list_weak_notified, task);
+
+ priv->list = list;
+ g_object_weak_ref (G_OBJECT (priv->list), task_list_weak_notified, task);
+ g_object_notify (G_OBJECT (task), "list");
+}
+
+/**
+ * gtd_task_get_position:
+ * @self: a #GtdTask
+ *
+ * Returns the position of @task inside the parent #GtdTaskList,
+ * or -1 if not set.
+ *
+ * Returns: the position of the task, or -1
+ */
+gint64
+gtd_task_get_position (GtdTask *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK (self), -1);
+
+ return GTD_TASK_CLASS (G_OBJECT_GET_CLASS (self))->get_position (self);
+}
+
+/**
+ * gtd_task_set_position:
+ * @self: a #GtdTask
+ * @position: the priority of @task, or -1
+ *
+ * Sets the @task position inside the parent #GtdTaskList. It
+ * is up to the interface to handle two or more #GtdTask with
+ * the same position value.
+ */
+void
+gtd_task_set_position (GtdTask *self,
+ gint64 position)
+{
+ g_return_if_fail (GTD_IS_TASK (self));
+
+ if (gtd_task_get_position (self) == position)
+ return;
+
+ GTD_TASK_CLASS (G_OBJECT_GET_CLASS (self))->set_position (self, position);
+
+ g_object_notify (G_OBJECT (self), "position");
+}
+
+/**
+ * gtd_task_get_title:
+ * @self: a #GtdTask
+ *
+ * Retrieves the title of the task, or %NULL.
+ *
+ * Returns: (transfer none): the title of @task, or %NULL
+ */
+const gchar*
+gtd_task_get_title (GtdTask *task)
+{
+ const gchar *title;
+
+ g_return_val_if_fail (GTD_IS_TASK (task), NULL);
+
+ title = GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->get_title (task);
+
+ return title ? title : "";
+}
+
+/**
+ * gtd_task_set_title:
+ * @self: a #GtdTask
+ * @title: (nullable): the new title, or %NULL
+ *
+ * Updates the title of @task. The string is stripped off of
+ * leading spaces.
+ */
+void
+gtd_task_set_title (GtdTask *task,
+ const gchar *title)
+{
+ const gchar *current_title;
+
+ g_return_if_fail (GTD_IS_TASK (task));
+ g_return_if_fail (g_utf8_validate (title, -1, NULL));
+
+ current_title = gtd_task_get_title (task);
+
+ if (g_strcmp0 (current_title, title) == 0)
+ return;
+
+ GTD_TASK_CLASS (G_OBJECT_GET_CLASS (task))->set_title (task, title);
+
+ g_object_notify (G_OBJECT (task), "title");
+}
+
+/**
+ * gtd_task_compare:
+ * @t1: (nullable): a #GtdTask
+ * @t2: (nullable): a #GtdTask
+ *
+ * Compare @t1 and @t2.
+ *
+ * Returns: %-1 if @t1 comes before @t2, %1 for the opposite, %0 if they're equal
+ */
+gint
+gtd_task_compare (GtdTask *t1,
+ GtdTask *t2)
+{
+ GDateTime *dt1;
+ GDateTime *dt2;
+ gchar *txt1;
+ gchar *txt2;
+ gint retval;
+
+ if (!t1 && !t2)
+ return 0;
+ if (!t1)
+ return 1;
+ if (!t2)
+ return -1;
+
+ /*
+ * The custom position overrides any comparison we can make. To keep compatibility,
+ * for now, we only compare by position if both tasks have a custom position set.
+ */
+ if (gtd_task_get_position (t1) != -1 && gtd_task_get_position (t2) != -1)
+ {
+ retval = gtd_task_get_position (t1) - gtd_task_get_position (t2);
+
+ if (retval != 0)
+ return retval;
+ }
+
+ /* Compare by due date */
+ dt1 = gtd_task_get_due_date (t1);
+ dt2 = gtd_task_get_due_date (t2);
+
+ if (!dt1 && !dt2)
+ retval = 0;
+ else if (!dt1)
+ retval = 1;
+ else if (!dt2)
+ retval = -1;
+ else
+ retval = g_date_time_compare (dt1, dt2);
+
+ if (dt1)
+ g_date_time_unref (dt1);
+ if (dt2)
+ g_date_time_unref (dt2);
+
+ if (retval != 0)
+ return retval;
+
+ /* Compare by creation date */
+ dt1 = gtd_task_get_creation_date (t1);
+ dt2 = gtd_task_get_creation_date (t2);
+
+ 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;
+
+ /* If they're equal up to now, compare by title */
+ txt1 = txt2 = NULL;
+
+ txt1 = g_utf8_casefold (gtd_task_get_title (t1), -1);
+ txt2 = g_utf8_casefold (gtd_task_get_title (t2), -1);
+
+ retval = g_strcmp0 (txt1, txt2);
+
+ g_free (txt1);
+ g_free (txt2);
+
+ return retval;
+}
+
+/**
+ * gtd_task_get_provider:
+ * @self: a #GtdTaskList
+ *
+ * Utility function to retrieve the data provider that backs this
+ * task. Notice that this is exactly the same as writing:
+ *
+ * |[<!-- language="C" -->
+ * GtdTaskList *list;
+ * GtdProvider *provider;
+ *
+ * list = gtd_task_get_list (task);
+ * provider = gtd_task_list_get_provider (list);
+ * ]|
+ *
+ * Returns: (transfer none)(nullable): the #GtdProvider of this task's list.
+ */
+GtdProvider*
+gtd_task_get_provider (GtdTask *self)
+{
+ GtdTaskPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_TASK (self), NULL);
+
+ priv = gtd_task_get_instance_private (self);
+
+ if (priv->list)
+ return gtd_task_list_get_provider (priv->list);
+
+ return NULL;
+}
diff --git a/src/core/gtd-task.h b/src/core/gtd-task.h
new file mode 100644
index 0000000..0c2b5aa
--- /dev/null
+++ b/src/core/gtd-task.h
@@ -0,0 +1,122 @@
+/* gtd-task.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_H
+#define GTD_TASK_H
+
+#include "gtd-object.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TASK (gtd_task_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (GtdTask, gtd_task, GTD, TASK, GtdObject)
+
+struct _GtdTaskClass
+{
+ GtdObjectClass parent;
+
+ gboolean (*get_complete) (GtdTask *self);
+ void (*set_complete) (GtdTask *self,
+ gboolean complete);
+
+ GDateTime* (*get_creation_date) (GtdTask *self);
+ void (*set_creation_date) (GtdTask *self,
+ GDateTime *dt);
+
+ GDateTime* (*get_completion_date) (GtdTask *self);
+ void (*set_completion_date) (GtdTask *self,
+ GDateTime *dt);
+
+ const gchar* (*get_description) (GtdTask *self);
+ void (*set_description) (GtdTask *self,
+ const gchar *description);
+
+ GDateTime* (*get_due_date) (GtdTask *self);
+ void (*set_due_date) (GtdTask *self,
+ GDateTime *dt);
+
+ gboolean (*get_important) (GtdTask *self);
+ void (*set_important) (GtdTask *self,
+ gboolean important);
+
+ gint64 (*get_position) (GtdTask *self);
+ void (*set_position) (GtdTask *self,
+ gint64 position);
+
+ const gchar* (*get_title) (GtdTask *self);
+ void (*set_title) (GtdTask *self,
+ const gchar *title);
+
+ gpointer padding[8];
+};
+
+GtdTask* gtd_task_new (void);
+
+gboolean gtd_task_get_complete (GtdTask *self);
+
+void gtd_task_set_complete (GtdTask *self,
+ gboolean complete);
+
+GDateTime* gtd_task_get_creation_date (GtdTask *self);
+
+void gtd_task_set_creation_date (GtdTask *self,
+ GDateTime *dt);
+
+GDateTime* gtd_task_get_completion_date (GtdTask *self);
+
+const gchar* gtd_task_get_description (GtdTask *self);
+
+void gtd_task_set_description (GtdTask *self,
+ const gchar *description);
+
+GDateTime* gtd_task_get_due_date (GtdTask *self);
+
+void gtd_task_set_due_date (GtdTask *self,
+ GDateTime *dt);
+
+gboolean gtd_task_get_important (GtdTask *self);
+
+void gtd_task_set_important (GtdTask *self,
+ gboolean important);
+
+GtdTaskList* gtd_task_get_list (GtdTask *self);
+
+void gtd_task_set_list (GtdTask *self,
+ GtdTaskList *list);
+
+gint64 gtd_task_get_position (GtdTask *self);
+
+void gtd_task_set_position (GtdTask *self,
+ gint64 position);
+
+const gchar* gtd_task_get_title (GtdTask *self);
+
+void gtd_task_set_title (GtdTask *self,
+ const gchar *title);
+
+gint gtd_task_compare (GtdTask *t1,
+ GtdTask *t2);
+
+GtdProvider* gtd_task_get_provider (GtdTask *self);
+
+G_END_DECLS
+
+#endif /* GTD_TASK_H */
diff --git a/src/endeavour.h b/src/endeavour.h
new file mode 100644
index 0000000..aa93aa9
--- /dev/null
+++ b/src/endeavour.h
@@ -0,0 +1,56 @@
+/* endeavour.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 ENDEAVOUR_H
+#define ENDEAVOUR_H
+
+#include <libpeas/peas.h>
+
+#include "gtd-enum-types.h"
+
+#include "gtd-activatable.h"
+#include "gtd-bin-layout.h"
+#include "gtd-easing.h"
+#include "gtd-keyframe-transition.h"
+#include "gtd-list-model-filter.h"
+#include "gtd-list-model-sort.h"
+#include "gtd-list-store.h"
+#include "gtd-manager.h"
+#include "gtd-max-size-layout.h"
+#include "gtd-menu-button.h"
+#include "gtd-notification.h"
+#include "gtd-object.h"
+#include "gtd-omni-area.h"
+#include "gtd-omni-area-addin.h"
+#include "gtd-panel.h"
+#include "gtd-property-transition.h"
+#include "gtd-provider.h"
+#include "gtd-provider-popover.h"
+#include "gtd-star-widget.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-task-list-view.h"
+#include "gtd-timeline.h"
+#include "gtd-transition.h"
+#include "gtd-types.h"
+#include "gtd-utils.h"
+#include "gtd-widget.h"
+#include "gtd-window.h"
+#include "gtd-workspace.h"
+
+#endif /* ENDEAVOUR_H */
diff --git a/src/gtd-debug.h.in b/src/gtd-debug.h.in
new file mode 100644
index 0000000..ba5bcf8
--- /dev/null
+++ b/src/gtd-debug.h.in
@@ -0,0 +1,229 @@
+/* gtd-debug.h.in
+ *
+ * Copyright (C) 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/>.
+ */
+
+#pragma once
+
+#include <glib.h>
+
+/**
+ * SECTION:gtd-debug
+ * @short_description: Debugging macros
+ * @title:Debugging
+ * @stability:stable
+ *
+ * Macros used for tracing and debugging code. These
+ * are only valid when Endeavour is compiled with tracing
+ * support (pass `--enable-tracing` to the configure
+ * script to do that).
+ */
+
+G_BEGIN_DECLS
+
+#ifndef GTD_ENABLE_TRACE
+# define GTD_ENABLE_TRACE @ENABLE_TRACING@
+#endif
+#if GTD_ENABLE_TRACE != 1
+# undef GTD_ENABLE_TRACE
+#endif
+
+/**
+ * GTD_LOG_LEVEL_TRACE: (skip)
+ */
+#ifndef GTD_LOG_LEVEL_TRACE
+# define GTD_LOG_LEVEL_TRACE ((GLogLevelFlags)(1 << G_LOG_LEVEL_USER_SHIFT))
+#endif
+
+#ifdef GTD_ENABLE_TRACE
+
+/**
+ * GTD_TRACE_MSG:
+ * @fmt: printf-like format of the message
+ * @...: arguments for @fmt
+ *
+ * Prints a trace message.
+ */
+# define GTD_TRACE_MSG(fmt, ...) \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " MSG: %s():%d: " fmt, \
+ G_STRFUNC, __LINE__, ##__VA_ARGS__)
+
+/**
+ * GTD_PROBE:
+ *
+ * Prints a probing message. Put this macro in the code when
+ * you want to check the program reaches a certain section
+ * of code.
+ */
+# define GTD_PROBE \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, "PROBE: %s():%d", \
+ G_STRFUNC, __LINE__)
+
+/**
+ * GTD_TODO:
+ * @_msg: the message to print
+ *
+ * Prints a TODO message.
+ */
+# define GTD_TODO(_msg) \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " TODO: %s():%d: %s", \
+ G_STRFUNC, __LINE__, _msg)
+
+/**
+ * GTD_ENTRY:
+ *
+ * Prints an entry message. This shouldn't be used in
+ * critical functions. Place this at the beggining of
+ * the function, before any assertion.
+ */
+# define GTD_ENTRY \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, "ENTRY: %s():%d", \
+ G_STRFUNC, __LINE__)
+
+/**
+ * GTD_EXIT:
+ *
+ * Prints an exit message. This shouldn't be used in
+ * critical functions. Place this at the end of
+ * the function, after any relevant code. If the
+ * function returns something, use GTD_RETURN()
+ * instead.
+ */
+# define GTD_EXIT \
+ G_STMT_START { \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " EXIT: %s():%d", \
+ G_STRFUNC, __LINE__); \
+ return; \
+ } G_STMT_END
+
+/**
+ * GTD_GOTO:
+ * @_l: goto tag
+ *
+ * Logs a goto jump.
+ */
+# define GTD_GOTO(_l) \
+ G_STMT_START { \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " GOTO: %s():%d ("#_l")",\
+ G_STRFUNC, __LINE__); \
+ goto _l; \
+ } G_STMT_END
+
+/**
+ * GTD_RETURN:
+ * @_r: the return value.
+ *
+ * Prints an exit message, and returns @_r. See #GTD_EXIT.
+ */
+# define GTD_RETURN(_r) \
+ G_STMT_START { \
+ g_log(G_LOG_DOMAIN, GTD_LOG_LEVEL_TRACE, " EXIT: %s():%d ", \
+ G_STRFUNC, __LINE__); \
+ return _r; \
+ } G_STMT_END
+
+#else
+
+/**
+ * GTD_TODO:
+ * @_msg: the message to print
+ *
+ * Prints a TODO message.
+ */
+# define GTD_TODO(_msg)
+
+/**
+ * GTD_PROBE:
+ *
+ * Prints a probing message.
+ */
+# define GTD_PROBE
+
+/**
+ * GTD_TRACE_MSG:
+ * @fmt: printf-like format of the message
+ * @...: arguments for @fmt
+ *
+ * Prints a trace message.
+ */
+# define GTD_TRACE_MSG(fmt, ...)
+
+/**
+ * GTD_ENTRY:
+ *
+ * Prints a probing message. This shouldn't be used in
+ * critical functions. Place this at the beggining of
+ * the function, before any assertion.
+ */
+# define GTD_ENTRY
+
+/**
+ * GTD_GOTO:
+ * @_l: goto tag
+ *
+ * Logs a goto jump.
+ */
+# define GTD_GOTO(_l) goto _l
+
+/**
+ * GTD_EXIT:
+ *
+ * Prints an exit message. This shouldn't be used in
+ * critical functions. Place this at the end of
+ * the function, after any relevant code. If the
+ * function returns somethin, use GTD_RETURN()
+ * instead.
+ */
+# define GTD_EXIT return
+
+/**
+ * GTD_RETURN:
+ * @_r: the return value.
+ *
+ * Prints an exit message, and returns @_r. See #GTD_EXIT.
+ */
+# define GTD_RETURN(_r) return _r
+#endif
+
+/**
+ * _GTD_BUG: (skip)
+ */
+#define _GTD_BUG(Component, Description, File, Line, Func, ...) \
+ G_STMT_START { \
+ g_printerr ("-----------------------------------------------------------------\n"); \
+ g_printerr ("You've found a bug in Endeavour or one of its dependent libraries.\n"); \
+ g_printerr ("Please help us help you by filing a bug report at:\n"); \
+ g_printerr ("\n"); \
+ g_printerr ("@BUGREPORT_URL@&component=%s\n", Component); \
+ g_printerr ("\n"); \
+ g_printerr ("%s:%d in function %s()\n", File, Line, Func); \
+ g_printerr ("\n"); \
+ g_printerr (Description"\n", ##__VA_ARGS__); \
+ g_printerr ("-----------------------------------------------------------------\n"); \
+ } G_STMT_END
+
+/**
+ * GTD_BUG:
+ * @Component: the component
+ * @Description: the description
+ * @...: extra arguments
+ *
+ * Logs a bug-friendly message.
+ */
+#define GTD_BUG(Component, Description, ...) \
+ _GTD_BUG(Component, Description, __FILE__, __LINE__, G_STRFUNC, ##__VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/gtd-enum-types.c.template b/src/gtd-enum-types.c.template
new file mode 100644
index 0000000..ed83a71
--- /dev/null
+++ b/src/gtd-enum-types.c.template
@@ -0,0 +1,39 @@
+/*** BEGIN file-header ***/
+#include "gtd-enum-types.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+#include "@filename@"
+
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+ static GType the_type = 0;
+
+ if (the_type == 0)
+ {
+ static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+ { @VALUENAME@,
+ "@VALUENAME@",
+ "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+ { 0, NULL, NULL }
+ };
+ the_type = g_@type@_register_static (
+ g_intern_static_string ("@EnumName@"),
+ values);
+ }
+ return the_type;
+}
+
+/*** END value-tail ***/
diff --git a/src/gtd-enum-types.h.template b/src/gtd-enum-types.h.template
new file mode 100644
index 0000000..f3d5156
--- /dev/null
+++ b/src/gtd-enum-types.h.template
@@ -0,0 +1,24 @@
+/*** BEGIN file-header ***/
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* Enumerations from "@filename@" */
+
+/*** END file-production ***/
+
+/*** BEGIN enumeration-production ***/
+#define GTD_TYPE_@ENUMSHORT@ (@enum_name@_get_type())
+GType @enum_name@_get_type (void) G_GNUC_CONST;
+
+/*** END enumeration-production ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+
+/*** END file-tail ***/
diff --git a/src/gtd-types.h b/src/gtd-types.h
new file mode 100644
index 0000000..a240cc9
--- /dev/null
+++ b/src/gtd-types.h
@@ -0,0 +1,57 @@
+/* gtd-types.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_TYPES_H
+#define GTD_TYPES_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GtdActivatable GtdActivatable;
+typedef struct _GtdAnimatable GtdAnimatable;
+typedef struct _GtdApplication GtdApplication;
+typedef struct _GtdClock GtdClock;
+typedef struct _GtdDoneButton GtdDoneButton;
+typedef struct _GtdInterval GtdInterval;
+typedef struct _GtdInitialSetupWindow GtdInitialSetupWindow;
+typedef struct _GtdListView GtdListView;
+typedef struct _GtdManager GtdManager;
+typedef struct _GtdMarkdownRenderer GtdMarkdownRenderer;
+typedef struct _GtdNotification GtdNotification;
+typedef struct _GtdObject GtdObject;
+typedef struct _GtdOmniArea GtdOmniArea;
+typedef struct _GtdPanel GtdPanel;
+typedef struct _GtdPluginManager GtdPluginManager;
+typedef struct _GtdProvider GtdProvider;
+typedef struct _GtdStorage GtdStorage;
+typedef struct _GtdStoragePopover GtdStoragePopover;
+typedef struct _GtdStorageRow GtdStorageRow;
+typedef struct _GtdStorageSelector GtdStorageSelector;
+typedef struct _GtdTask GtdTask;
+typedef struct _GtdTaskList GtdTaskList;
+typedef struct _GtdTaskListItem GtdTaskListItem;
+typedef struct _GtdTaskRow GtdTaskRow;
+typedef struct _GtdTransition GtdTransition;
+typedef struct _GtdWidget GtdWidget;
+typedef struct _GtdWindow GtdWindow;
+typedef struct _GtdWorkspace GtdWorkspace;
+
+G_END_DECLS
+
+#endif /* GTD_TYPES_H */
diff --git a/src/gtd-utils-private.h b/src/gtd-utils-private.h
new file mode 100644
index 0000000..f668307
--- /dev/null
+++ b/src/gtd-utils-private.h
@@ -0,0 +1,29 @@
+/* gtd-utils-private.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 <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+void gtd_ensure_types (void);
+
+G_END_DECLS
diff --git a/src/gtd-utils.c b/src/gtd-utils.c
new file mode 100644
index 0000000..fd14cfd
--- /dev/null
+++ b/src/gtd-utils.c
@@ -0,0 +1,158 @@
+/* gtd-utils.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-bin-layout.h"
+#include "gtd-max-size-layout.h"
+#include "gtd-task.h"
+#include "gtd-utils.h"
+#include "gtd-utils-private.h"
+#include "gtd-widget.h"
+
+#include <gtk/gtk.h>
+
+#include <string.h>
+
+
+/* Combining diacritical mark?
+ * Basic range: [0x0300,0x036F]
+ * Supplement: [0x1DC0,0x1DFF]
+ * For Symbols: [0x20D0,0x20FF]
+ * Half marks: [0xFE20,0xFE2F]
+ */
+#define IS_CDM_UCS4(c) (((c) >= 0x0300 && (c) <= 0x036F) || \
+ ((c) >= 0x1DC0 && (c) <= 0x1DFF) || \
+ ((c) >= 0x20D0 && (c) <= 0x20FF) || \
+ ((c) >= 0xFE20 && (c) <= 0xFE2F))
+
+#define IS_SOFT_HYPHEN(c) ((c) == 0x00AD)
+
+
+/* Copied from tracker/src/libtracker-fts/tracker-parser-glib.c under the GPL
+ * And then from gnome-shell/src/shell-util.c
+ *
+ * Originally written by Aleksander Morgado <aleksander@gnu.org>
+ */
+gchar*
+gtd_normalize_casefold_and_unaccent (const gchar *str)
+{
+ g_autofree gchar *normalized = NULL;
+ gchar *tmp;
+ gint i = 0;
+ gint j = 0;
+ gint ilen;
+
+ if (str == NULL)
+ return NULL;
+
+ normalized = g_utf8_normalize (str, -1, G_NORMALIZE_NFKD);
+ tmp = g_utf8_casefold (normalized, -1);
+
+ ilen = strlen (tmp);
+
+ while (i < ilen)
+ {
+ gunichar unichar;
+ gchar *next_utf8;
+ gint utf8_len;
+
+ /* Get next character of the word as UCS4 */
+ unichar = g_utf8_get_char_validated (&tmp[i], -1);
+
+ /* Invalid UTF-8 character or end of original string. */
+ if (unichar == (gunichar) -1 ||
+ unichar == (gunichar) -2)
+ {
+ break;
+ }
+
+ /* Find next UTF-8 character */
+ next_utf8 = g_utf8_next_char (&tmp[i]);
+ utf8_len = next_utf8 - &tmp[i];
+
+ if (IS_CDM_UCS4 (unichar) || IS_SOFT_HYPHEN (unichar))
+ {
+ /* If the given unichar is a combining diacritical mark,
+ * just update the original index, not the output one */
+ i += utf8_len;
+ continue;
+ }
+
+ /* If already found a previous combining
+ * diacritical mark, indexes are different so
+ * need to copy characters. As output and input
+ * buffers may overlap, need to use memmove
+ * instead of memcpy */
+ if (i != j)
+ {
+ memmove (&tmp[j], &tmp[i], utf8_len);
+ }
+
+ /* Update both indexes */
+ i += utf8_len;
+ j += utf8_len;
+ }
+
+ /* Force proper string end */
+ tmp[j] = '\0';
+
+ return tmp;
+}
+
+GdkPaintable*
+gtd_create_circular_paintable (GdkRGBA *color,
+ gint size)
+{
+ g_autoptr (GtkSnapshot) snapshot = NULL;
+ GskRoundedRect rect;
+
+ snapshot = gtk_snapshot_new ();
+
+ gtk_snapshot_push_rounded_clip (snapshot,
+ gsk_rounded_rect_init_from_rect (&rect,
+ &GRAPHENE_RECT_INIT (0, 0, size, size),
+ size / 2.0));
+
+ gtk_snapshot_append_color (snapshot, color, &GRAPHENE_RECT_INIT (0, 0, size, size));
+
+ gtk_snapshot_pop (snapshot);
+
+ return gtk_snapshot_to_paintable (snapshot, &GRAPHENE_SIZE_INIT (size, size));
+}
+
+gint
+gtd_collate_compare_strings (const gchar *string_a,
+ const gchar *string_b)
+{
+ g_autofree gchar *collated_a = NULL;
+ g_autofree gchar *collated_b = NULL;
+
+ collated_a = g_utf8_collate_key (string_a, -1);
+ collated_b = g_utf8_collate_key (string_b, -1);
+
+ return g_strcmp0 (collated_a, collated_b);
+}
+
+void
+gtd_ensure_types (void)
+{
+ g_type_ensure (GTD_TYPE_BIN_LAYOUT);
+ g_type_ensure (GTD_TYPE_MAX_SIZE_LAYOUT);
+ g_type_ensure (GTD_TYPE_WIDGET);
+}
diff --git a/src/gtd-utils.h b/src/gtd-utils.h
new file mode 100644
index 0000000..bc03170
--- /dev/null
+++ b/src/gtd-utils.h
@@ -0,0 +1,35 @@
+/* gtd-utils.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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+gchar* gtd_normalize_casefold_and_unaccent (const gchar *str);
+
+gint gtd_collate_compare_strings (const gchar *string_a,
+ const gchar *string_b);
+
+GdkPaintable* gtd_create_circular_paintable (GdkRGBA *color,
+ gint size);
+
+G_END_DECLS
diff --git a/src/gtd-vcs.h.in b/src/gtd-vcs.h.in
new file mode 100644
index 0000000..9c499df
--- /dev/null
+++ b/src/gtd-vcs.h.in
@@ -0,0 +1,29 @@
+/* gtd-vcs.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>
+
+G_BEGIN_DECLS
+
+#define GTD_VCS_TAG "@VCS_TAG@"
+
+G_END_DECLS
diff --git a/src/gui/assets/all-done.svg b/src/gui/assets/all-done.svg
new file mode 100644
index 0000000..60f4194
--- /dev/null
+++ b/src/gui/assets/all-done.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><defs><linearGradient id="a"><stop offset="0" stop-color="#99c1f1"/><stop offset="1" stop-color="#99c1f1" stop-opacity="0"/></linearGradient><linearGradient xlink:href="#a" id="e" x1="787.16" y1="530.007" x2="843.9" y2="485.818" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.26477 0 0 1.26477 -726.017 -354.387)"/><linearGradient id="b"><stop offset="0" stop-color="#051224"/><stop offset="1" stop-color="#3584e4"/></linearGradient><linearGradient xlink:href="#c" id="d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(.28128 0 0 .28128 147.38 84.798)" x1="1275.703" y1="760" x2="281.122" y2="760"/><linearGradient id="c"><stop offset="0" stop-color="#1c71d8"/><stop offset="1" stop-color="#33d17a"/></linearGradient></defs><path d="M265.385 152.642v.525c0 18.165 14.725 32.89 32.89 32.89h16.829a7.5 7.5 0 017.5 7.502v3.075a7.5 7.5 0 01-7.5 7.5H185.896c-18.165 0-32.89 14.726-32.89 32.891v.526c0 18.165 14.725 32.89 32.89 32.89h95.935a7.5 7.5 0 017.5 7.501v1.875a7.5 7.5 0 01-7.5 7.501h-4.05c-18.166 0-32.892 14.726-32.892 32.89v.526c0 18.165 14.726 32.891 32.891 32.891h140.801c18.165 0 32.89-14.726 32.89-32.89v-.526c0-18.165-14.725-32.89-32.89-32.89h-16.738a7.5 7.5 0 01-7.501-7.502v-1.875a7.5 7.5 0 017.5-7.5H476.4c18.165 0 32.891-14.726 32.891-32.891v-.526c0-18.165-14.726-32.89-32.89-32.89h-19.974a9.017 9.017 0 01-9.017-9.018v-.042a9.017 9.017 0 019.017-9.017h6.847c18.165 0 32.89-14.726 32.89-32.891v-.525c0-18.165-14.725-32.891-32.89-32.891H298.275c-18.165 0-32.89 14.726-32.89 32.89z" fill="url(#d)"/><path d="M349.87 243.704c7.957 30.548 5.243 63.766-7.567 92.617-11.57 26.06-31.177 48.37-55.127 63.844-23.95 15.474-52.128 24.148-80.606 25.596-19.135.973-38.62-1.311-56.407-8.432-17.788-7.12-33.812-19.278-43.987-35.513-10.175-16.235-14.11-36.644-9.018-55.116 2.546-9.235 7.287-17.886 13.872-24.844 6.585-6.959 15.017-12.194 24.239-14.79 9.038-2.545 18.779-2.53 27.816.02 9.036 2.55 17.343 7.625 23.754 14.486 6.41 6.86 10.91 15.485 12.89 24.663 1.98 9.179 1.44 18.885-1.523 27.794-4.862 14.616-16.134 26.663-29.696 33.967-13.562 7.304-29.257 10.101-44.654 9.634-36.023-1.095-71.005-20.599-90.875-50.666-19.87-30.068-24.096-69.896-10.979-103.464" fill="none" stroke="url(#e)" stroke-width="7.589" stroke-linecap="round" stroke-dasharray="7.58864,30.3545"/><path d="M894.03 105.159l-201.825 159.61 81.097 30.332 17.013 54.224 19.53-40.557 79.396 29.693z" fill-opacity=".177"/><g stroke-width="1.357"><path d="M894.072 97.306l-201.824 159.61 197.036 73.693z" fill="#fff"/><path d="M894.072 97.306l-120.77 189.8 17.055 54.366-.736-48.22z" fill="#d3d3d3"/><path d="M790.357 341.472l19.565-40.622-20.3-7.597z" fill="#f6f5f4"/></g><path d="M350.202 91.408l-105.88 162.068h91.763l14.117 32.415 14.118-32.415h91.763z" fill-opacity=".143"/><g stroke-width=".748"><path d="M350.203 86.348l105.88 162.07h-211.76z" fill="#f6f5f4"/><path d="M350.203 88.787V280.83l-14.118-32.414c3.548-53.31 9-106.455 14.118-159.63z" fill="#fff"/><path d="M350.203 88.787V280.83l14.117-32.414s-8.77-106.472-14.117-159.63z" fill="#deddda"/></g></svg> \ No newline at end of file
diff --git a/src/gui/gtd-application.c b/src/gui/gtd-application.c
new file mode 100644
index 0000000..f89e25f
--- /dev/null
+++ b/src/gui/gtd-application.c
@@ -0,0 +1,331 @@
+/* gtd-application.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 "GtdApplication"
+
+#include "config.h"
+
+#include "gtd-application.h"
+#include "gtd-debug.h"
+#include "gtd-initial-setup-window.h"
+#include "gtd-log.h"
+#include "gtd-manager.h"
+#include "gtd-manager-protected.h"
+#include "gtd-vcs.h"
+#include "gtd-window.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <girepository.h>
+#include <glib/gi18n.h>
+
+
+struct _GtdApplication
+{
+ AdwApplication application;
+
+ GtkWindow *window;
+ GtkWidget *initial_setup;
+};
+
+static void gtd_application_activate_action (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data);
+
+static void gtd_application_show_about (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data);
+
+static void gtd_application_quit (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data);
+
+static void gtd_application_show_help (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data);
+
+G_DEFINE_TYPE (GtdApplication, gtd_application, ADW_TYPE_APPLICATION)
+
+static GOptionEntry cmd_options[] = {
+ { "quit", 'q', 0, G_OPTION_ARG_NONE, NULL, N_("Quit Endeavour"), NULL },
+ { "debug", 'd', 0, G_OPTION_ARG_NONE, NULL, N_("Enable debug messages"), NULL },
+ { "version", 'v', 0, G_OPTION_ARG_NONE, NULL, N_("Print version information and exit"), NULL },
+ { NULL }
+};
+
+static const GActionEntry gtd_application_entries[] = {
+ { "activate", gtd_application_activate_action },
+ { "about", gtd_application_show_about },
+ { "quit", gtd_application_quit },
+ { "help", gtd_application_show_help }
+};
+
+static void
+gtd_application_activate_action (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ GtdApplication *self = GTD_APPLICATION (user_data);
+
+ gtk_window_present (GTK_WINDOW (self->window));
+}
+
+static void
+gtd_application_show_about (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ GtdApplication *self;
+
+ static const gchar *developers[] = {
+ "Emmanuele Bassi <ebassi@gnome.org>",
+ "Georges Basile Stavracas Neto <georges.stavracas@gmail.com>",
+ "Isaque Galdino <igaldino@gmail.com>",
+ "Patrick Griffis <tingping@tingping.se>",
+ "Jamie Murphy <hello@itsjamie.dev>",
+ "Saiful B. Khan <saifulbkhan@gmail.com>",
+ NULL
+ };
+
+ static const gchar *designers[] = {
+ "Allan Day <allanpday@gmail.com>",
+ "Jakub Steiner <jimmac@gmail.com>",
+ "Tobias Bernard <tbernard@gnome.org>",
+ NULL
+ };
+
+ self = GTD_APPLICATION (user_data);
+
+ adw_show_about_window (GTK_WINDOW (self->window),
+ "application-name", _("Endeavour"),
+ "application-icon", APPLICATION_ID,
+ "version", GTD_VCS_TAG,
+ "copyright", _("Copyright \xC2\xA9 2015–2022 The Endeavour authors"),
+ "issue-url", "https://gitlab.gnome.org/World/Endeavour/-/issues",
+ "website", "https://gitlab.gnome.org/World/Endeavour",
+ "license-type", GTK_LICENSE_GPL_3_0,
+ "developers", developers,
+ "designers", designers,
+ "translator-credits", _("translator-credits"),
+ NULL);
+}
+
+static void
+gtd_application_quit (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ GtdApplication *self = GTD_APPLICATION (user_data);
+
+ gtk_window_destroy (self->window);
+}
+
+static void
+gtd_application_show_help (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ GtdApplication *self = GTD_APPLICATION (user_data);
+ gtk_show_uri (GTK_WINDOW (self->window) , "help:endeavour", GDK_CURRENT_TIME);
+}
+
+GtdApplication *
+gtd_application_new (void)
+{
+ return g_object_new (GTD_TYPE_APPLICATION,
+ "application-id", APPLICATION_ID,
+ "flags", G_APPLICATION_HANDLES_COMMAND_LINE,
+ "resource-base-path", "/org/gnome/todo",
+ NULL);
+}
+
+static void
+set_accel_for_action (GtdApplication *self,
+ const gchar *detailed_action_name,
+ const gchar *accel)
+{
+ const char *accels[] = { accel, NULL };
+
+ gtk_application_set_accels_for_action (GTK_APPLICATION (self), detailed_action_name, accels);
+}
+
+
+static void
+run_window (GtdApplication *self)
+{
+ gtk_window_present (GTK_WINDOW (self->window));
+}
+
+/*
+static void
+finish_initial_setup (GtdApplication *application)
+{
+ g_return_if_fail (GTD_IS_APPLICATION (application));
+
+ run_window (application);
+
+ gtd_manager_set_is_first_run (application->priv->manager, FALSE);
+
+ g_clear_pointer (&application->priv->initial_setup, gtk_widget_destroy);
+}
+
+static void
+run_initial_setup (GtdApplication *application)
+{
+ GtdApplicationPrivate *priv;
+
+ g_return_if_fail (GTD_IS_APPLICATION (application));
+
+ priv = application->priv;
+
+ if (!priv->initial_setup)
+ {
+ priv->initial_setup = gtd_initial_setup_window_new (application);
+
+ g_signal_connect (priv->initial_setup,
+ "cancel",
+ G_CALLBACK (gtk_widget_destroy),
+ application);
+
+ g_signal_connect_swapped (priv->initial_setup,
+ "done",
+ G_CALLBACK (finish_initial_setup),
+ application);
+ }
+
+ gtk_widget_show (priv->initial_setup);
+}
+*/
+
+static void
+gtd_application_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (gtd_application_parent_class)->finalize (object);
+}
+
+static void
+gtd_application_activate (GApplication *application)
+{
+ GTD_ENTRY;
+
+ /* FIXME: the initial setup is disabled for the 3.18 release because
+ * we can't create tasklists on GOA accounts.
+ */
+ run_window (GTD_APPLICATION (application));
+
+ GTD_EXIT;
+}
+
+static void
+gtd_application_startup (GApplication *application)
+{
+ GtdApplication *self;
+
+ GTD_ENTRY;
+
+ self = GTD_APPLICATION (application);
+
+ /* add actions */
+ g_action_map_add_action_entries (G_ACTION_MAP (self),
+ gtd_application_entries,
+ G_N_ELEMENTS (gtd_application_entries),
+ self);
+
+ set_accel_for_action (self, "app.quit", "<Control>q");
+ set_accel_for_action (self, "app.help", "F1");
+
+ G_APPLICATION_CLASS (gtd_application_parent_class)->startup (application);
+
+ /* window */
+ gtk_window_set_default_icon_name (APPLICATION_ID);
+ self->window = GTK_WINDOW (gtd_window_new (self));
+
+ /* Load the plugins */
+ gtd_manager_load_plugins (gtd_manager_get_default ());
+
+ GTD_EXIT;
+}
+
+static gint
+gtd_application_command_line (GApplication *app,
+ GApplicationCommandLine *command_line)
+{
+ GVariantDict *options;
+
+ options = g_application_command_line_get_options_dict (command_line);
+
+ if (g_variant_dict_contains (options, "quit"))
+ {
+ g_application_quit (app);
+ return 0;
+ }
+
+ g_application_activate (app);
+
+ return 0;
+}
+
+static gboolean
+gtd_application_local_command_line (GApplication *application,
+ gchar ***arguments,
+ gint *exit_status)
+{
+ g_application_add_option_group (application, g_irepository_get_option_group());
+
+ return G_APPLICATION_CLASS (gtd_application_parent_class)->local_command_line (application,
+ arguments,
+ exit_status);
+}
+
+static gint
+gtd_application_handle_local_options (GApplication *application,
+ GVariantDict *options)
+{
+ if (g_variant_dict_contains (options, "debug"))
+ gtd_log_init ();
+
+ if (g_variant_dict_contains (options, "version"))
+ {
+ g_print ("%s - Version %s\n", g_get_application_name (), PACKAGE_VERSION);
+ return 0;
+ }
+
+ return -1;
+}
+
+static void
+gtd_application_class_init (GtdApplicationClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GApplicationClass *application_class = G_APPLICATION_CLASS (klass);
+
+ object_class->finalize = gtd_application_finalize;
+
+ application_class->activate = gtd_application_activate;
+ application_class->startup = gtd_application_startup;
+ application_class->command_line = gtd_application_command_line;
+ application_class->local_command_line = gtd_application_local_command_line;
+ application_class->handle_local_options = gtd_application_handle_local_options;
+}
+
+static void
+gtd_application_init (GtdApplication *self)
+{
+ g_application_add_main_option_entries (G_APPLICATION (self), cmd_options);
+}
diff --git a/src/gui/gtd-application.h b/src/gui/gtd-application.h
new file mode 100644
index 0000000..864d72d
--- /dev/null
+++ b/src/gui/gtd-application.h
@@ -0,0 +1,36 @@
+/* gtd-application.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_APPLICATION_H
+#define GTD_APPLICATION_H
+
+#include "gtd-types.h"
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_APPLICATION (gtd_application_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdApplication, gtd_application, GTD, APPLICATION, AdwApplication)
+
+GtdApplication* gtd_application_new (void);
+
+G_END_DECLS
+
+#endif /* GTD_APPLICATION_H */
diff --git a/src/gui/gtd-bin-layout.c b/src/gui/gtd-bin-layout.c
new file mode 100644
index 0000000..7f081eb
--- /dev/null
+++ b/src/gui/gtd-bin-layout.c
@@ -0,0 +1,112 @@
+/* gtd-bin-layout.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-bin-layout.h"
+#include "gtd-widget.h"
+
+struct _GtdBinLayout
+{
+ GtkLayoutManager parent_instance;
+};
+
+G_DEFINE_TYPE (GtdBinLayout, gtd_bin_layout, GTK_TYPE_LAYOUT_MANAGER)
+
+static void
+gtd_bin_layout_measure (GtkLayoutManager *layout_manager,
+ GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (widget);
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (gtk_widget_should_layout (child))
+ {
+ int child_min = 0;
+ int child_nat = 0;
+ int child_min_baseline = -1;
+ int child_nat_baseline = -1;
+
+ gtk_widget_measure (child, orientation, for_size,
+ &child_min, &child_nat,
+ &child_min_baseline, &child_nat_baseline);
+
+ *minimum = MAX (*minimum, child_min);
+ *natural = MAX (*natural, child_nat);
+
+ if (child_min_baseline > -1)
+ *minimum_baseline = MAX (*minimum_baseline, child_min_baseline);
+ if (child_nat_baseline > -1)
+ *natural_baseline = MAX (*natural_baseline, child_nat_baseline);
+ }
+ }
+}
+
+static void
+gtd_bin_layout_allocate (GtkLayoutManager *layout_manager,
+ GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (widget);
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (child && gtk_widget_should_layout (child))
+ {
+ GskTransform *transform = NULL;
+
+ if (GTD_IS_WIDGET (child))
+ transform = gtd_widget_apply_transform (GTD_WIDGET (child), NULL);
+
+ gtk_widget_allocate (child, width, height, baseline, transform);
+ }
+ }
+}
+
+static void
+gtd_bin_layout_class_init (GtdBinLayoutClass *klass)
+{
+ GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
+
+ layout_manager_class->measure = gtd_bin_layout_measure;
+ layout_manager_class->allocate = gtd_bin_layout_allocate;
+}
+
+static void
+gtd_bin_layout_init (GtdBinLayout *self)
+{
+}
+
+GtkLayoutManager*
+gtd_bin_layout_new (void)
+{
+ return g_object_new (GTD_TYPE_BIN_LAYOUT, NULL);
+}
diff --git a/src/gui/gtd-bin-layout.h b/src/gui/gtd-bin-layout.h
new file mode 100644
index 0000000..8d66ead
--- /dev/null
+++ b/src/gui/gtd-bin-layout.h
@@ -0,0 +1,32 @@
+/* gtd-bin-layout.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_BIN_LAYOUT (gtd_bin_layout_get_type())
+G_DECLARE_FINAL_TYPE (GtdBinLayout, gtd_bin_layout, GTD, BIN_LAYOUT, GtkLayoutManager)
+
+GtkLayoutManager* gtd_bin_layout_new (void);
+
+G_END_DECLS
diff --git a/src/gui/gtd-color-button.c b/src/gui/gtd-color-button.c
new file mode 100644
index 0000000..4ff3e23
--- /dev/null
+++ b/src/gui/gtd-color-button.c
@@ -0,0 +1,273 @@
+/* gtd-color-button.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 "GtdColorButton"
+
+#include "gtd-color-button.h"
+#include "gtd-utils.h"
+
+#define INTENSITY(r, g, b) ((r) * 0.30 + (g) * 0.59 + (b) * 0.11)
+
+struct _GtdColorButton
+{
+ GtkWidget parent;
+
+ GdkRGBA color;
+
+ GtkWidget *selected_icon;
+};
+
+G_DEFINE_TYPE (GtdColorButton, gtd_color_button, GTK_TYPE_WIDGET)
+
+enum
+{
+ PROP_0,
+ PROP_COLOR,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/*
+ * GtkWidget overrides
+ */
+
+static void
+gtd_color_button_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ GtdColorButton *self;
+ gint height_request;
+ gint width_request;
+
+ self = GTD_COLOR_BUTTON (widget);
+
+ gtk_widget_get_size_request (widget, &width_request, &height_request);
+
+ gtk_widget_measure (self->selected_icon,
+ orientation,
+ for_size,
+ minimum,
+ natural,
+ NULL,
+ NULL);
+
+ if (orientation == GTK_ORIENTATION_VERTICAL)
+ {
+ *minimum = MAX (*minimum, height_request);
+ *natural = MAX (*natural, height_request);
+ }
+ else
+ {
+ *minimum = MAX (*minimum, width_request);
+ *natural = MAX (*natural, width_request);
+ }
+}
+
+static void
+gtd_color_button_size_allocate (GtkWidget *widget,
+ gint width,
+ gint height,
+ gint baseline)
+{
+ GtdColorButton *self = GTD_COLOR_BUTTON (widget);
+
+ gtk_widget_size_allocate (self->selected_icon,
+ &(GtkAllocation)
+ {
+ 0, 0,
+ width, height,
+ },
+ baseline);
+}
+
+static void
+gtd_color_button_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ GtdColorButton *self;
+ gint height;
+ gint width;
+
+ self = GTD_COLOR_BUTTON (widget);
+ width = gtk_widget_get_width (widget);
+ height = gtk_widget_get_height (widget);
+
+ gtk_snapshot_append_color (snapshot, &self->color, &GRAPHENE_RECT_INIT (0, 0, width, height));
+ gtk_widget_snapshot_child (widget, self->selected_icon, snapshot);
+}
+
+static void
+gtd_color_button_state_flags_changed (GtkWidget *widget,
+ GtkStateFlags previous_state)
+{
+ GtdColorButton *self;
+ gboolean selected;
+
+ self = GTD_COLOR_BUTTON (widget);
+ selected = gtk_widget_get_state_flags (widget) & GTK_STATE_FLAG_SELECTED;
+
+ if (selected)
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->selected_icon), "object-select-symbolic");
+ else
+ gtk_image_clear (GTK_IMAGE (self->selected_icon));
+
+ GTK_WIDGET_CLASS (gtd_color_button_parent_class)->state_flags_changed (widget, previous_state);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_color_button_finalize (GObject *object)
+{
+ GtdColorButton *self = (GtdColorButton *)object;
+
+ g_clear_pointer (&self->selected_icon, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (gtd_color_button_parent_class)->finalize (object);
+}
+
+static void
+gtd_color_button_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdColorButton *self = GTD_COLOR_BUTTON (object);
+
+ switch (prop_id)
+ {
+ case PROP_COLOR:
+ g_value_set_boxed (value, &self->color);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_color_button_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdColorButton *self = GTD_COLOR_BUTTON (object);
+
+ switch (prop_id)
+ {
+ case PROP_COLOR:
+ gtd_color_button_set_color (self, g_value_get_boxed (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_color_button_class_init (GtdColorButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_color_button_finalize;
+ object_class->get_property = gtd_color_button_get_property;
+ object_class->set_property = gtd_color_button_set_property;
+
+ widget_class->measure = gtd_color_button_measure;
+ widget_class->size_allocate = gtd_color_button_size_allocate;
+ widget_class->snapshot = gtd_color_button_snapshot;
+ widget_class->state_flags_changed = gtd_color_button_state_flags_changed;
+
+ properties[PROP_COLOR] = g_param_spec_boxed ("color",
+ "Color",
+ "Color of the button",
+ GDK_TYPE_RGBA,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_css_name (widget_class, "colorbutton");
+}
+
+static void
+gtd_color_button_init (GtdColorButton *self)
+{
+ self->selected_icon = gtk_image_new ();
+ gtk_widget_set_parent (self->selected_icon, GTK_WIDGET (self));
+
+ gtk_widget_set_can_focus (GTK_WIDGET (self), TRUE);
+}
+
+GtkWidget*
+gtd_color_button_new (const GdkRGBA *color)
+{
+ return g_object_new (GTD_TYPE_COLOR_BUTTON,
+ "color", color,
+ "overflow", GTK_OVERFLOW_HIDDEN,
+ NULL);
+}
+
+const GdkRGBA*
+gtd_color_button_get_color (GtdColorButton *self)
+{
+ g_return_val_if_fail (GTD_IS_COLOR_BUTTON (self), NULL);
+
+ return &self->color;
+}
+
+void
+gtd_color_button_set_color (GtdColorButton *self,
+ const GdkRGBA *color)
+{
+ g_return_if_fail (GTD_IS_COLOR_BUTTON (self));
+
+ if (gdk_rgba_equal (&self->color, color))
+ return;
+
+ self->color = *color;
+ self->color.alpha = 1.0;
+
+ /* ... and adjust the icon color */
+ if (INTENSITY (self->color.red, self->color.green, self->color.blue) > 0.5)
+ {
+ gtk_widget_add_css_class (GTK_WIDGET (self), "light");
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "dark");
+ }
+ else
+ {
+ gtk_widget_add_css_class (GTK_WIDGET (self), "dark");
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "light");
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COLOR]);
+}
+
diff --git a/src/gui/gtd-color-button.h b/src/gui/gtd-color-button.h
new file mode 100644
index 0000000..db18854
--- /dev/null
+++ b/src/gui/gtd-color-button.h
@@ -0,0 +1,38 @@
+/* gtd-color-button.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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_COLOR_BUTTON (gtd_color_button_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdColorButton, gtd_color_button, GTD, COLOR_BUTTON, GtkWidget)
+
+GtkWidget* gtd_color_button_new (const GdkRGBA *color);
+
+const GdkRGBA* gtd_color_button_get_color (GtdColorButton *self);
+
+void gtd_color_button_set_color (GtdColorButton *self,
+ const GdkRGBA *color);
+
+G_END_DECLS
diff --git a/src/gui/gtd-edit-pane.c b/src/gui/gtd-edit-pane.c
new file mode 100644
index 0000000..1de17ac
--- /dev/null
+++ b/src/gui/gtd-edit-pane.c
@@ -0,0 +1,602 @@
+/* gtd-edit-pane.c
+ *
+ * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ * Copyright (C) 2022 Jamie Murphy <hello@itsjamie.dev>
+ *
+ * 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 "GtdEditPane"
+
+#include "gtd-debug.h"
+#include "gtd-edit-pane.h"
+#include "gtd-manager.h"
+#include "gtd-markdown-renderer.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+
+#include <glib/gi18n.h>
+
+struct _GtdEditPane
+{
+ GtkBox parent;
+
+ GtkCalendar *calendar;
+ GtkMenuButton *date_button;
+ GtkTextView *notes_textview;
+
+ GCancellable *cancellable;
+
+ /* task bindings */
+ GBinding *notes_binding;
+
+ GtdTask *task;
+ GDateTime *new_date;
+};
+
+G_DEFINE_TYPE (GtdEditPane, gtd_edit_pane, GTK_TYPE_BOX)
+
+enum
+{
+ PROP_0,
+ PROP_TASK,
+ LAST_PROP
+};
+
+enum
+{
+ CHANGED,
+ REMOVE_TASK,
+ NUM_SIGNALS
+};
+
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+
+static void on_date_selected_cb (GtkCalendar *calendar,
+ GtdEditPane *self);
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+update_date_widgets (GtdEditPane *self, GDateTime *dt)
+{
+ gchar *text;
+
+ g_return_if_fail (GTD_IS_EDIT_PANE (self));
+
+ text = dt ? g_date_time_format (dt, "%x") : NULL;
+
+ g_signal_handlers_block_by_func (self->calendar, on_date_selected_cb, self);
+
+ if (!dt)
+ dt = g_date_time_new_now_local ();
+
+ gtk_calendar_select_day (self->calendar, dt);
+
+ g_signal_handlers_unblock_by_func (self->calendar, on_date_selected_cb, self);
+
+ gtk_menu_button_set_label (self->date_button, text ? text : _("No date set"));
+
+ g_free (text);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_date_popover_closed_cb (GtdEditPane *self)
+{
+ g_autofree gchar *text = NULL;
+
+ if (!self->new_date || !self->task)
+ return;
+
+ text = g_date_time_format (self->new_date, "%x");
+ gtk_menu_button_set_label (self->date_button, text);
+
+ g_signal_emit (self, signals[CHANGED], 0);
+}
+
+static void
+on_date_selected_cb (GtkCalendar *calendar,
+ GtdEditPane *self)
+{
+ g_autofree gchar *text = NULL;
+
+ GTD_ENTRY;
+
+ g_clear_pointer (&self->new_date, g_date_time_unref);
+ self->new_date = gtk_calendar_get_date (calendar);
+ text = g_date_time_format (self->new_date, "%x");
+
+ gtk_menu_button_set_label (self->date_button, text);
+
+ g_signal_emit (self, signals[CHANGED], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_delete_button_clicked_cb (GtkButton *button,
+ GtdEditPane *self)
+{
+ g_signal_emit (self, signals[REMOVE_TASK], 0, self->task);
+}
+
+static void
+on_no_date_button_clicked_cb (GtkButton *button,
+ GtdEditPane *self)
+{
+ GTD_ENTRY;
+
+ g_clear_pointer (&self->new_date, g_date_time_unref);
+ self->new_date = NULL;
+ gtk_calendar_clear_marks (GTK_CALENDAR (self->calendar));
+ update_date_widgets (self, self->new_date);
+
+ g_signal_emit (self, signals[CHANGED], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_today_button_clicked_cb (GtkButton *button,
+ GtdEditPane *self)
+{
+ g_autoptr (GDateTime) new_dt = g_date_time_new_now_local ();
+
+ GTD_ENTRY;
+
+ self->new_date = g_date_time_ref (new_dt);
+ update_date_widgets (self, self->new_date);
+
+ g_signal_emit (self, signals[CHANGED], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_tomorrow_button_clicked_cb (GtkButton *button,
+ GtdEditPane *self)
+{
+ g_autoptr (GDateTime) current_date = NULL;
+ g_autoptr (GDateTime) new_dt = NULL;
+
+ GTD_ENTRY;
+
+ current_date = g_date_time_new_now_local ();
+ new_dt = g_date_time_add_days (current_date, 1);
+
+ self->new_date = g_date_time_ref (new_dt);
+ update_date_widgets (self, self->new_date);
+
+ g_signal_emit (self, signals[CHANGED], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_priority_changed_cb (GtkComboBox *combobox,
+ GtdEditPane *self)
+{
+ GTD_ENTRY;
+
+ g_signal_emit (self, signals[CHANGED], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_text_buffer_changed_cb (GtkTextBuffer *buffer,
+ GParamSpec *pspec,
+ GtdEditPane *self)
+{
+ GTD_ENTRY;
+
+ g_signal_emit (self, signals[CHANGED], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_hyperlink_hover_cb (GtkEventControllerMotion *controller,
+ gdouble x,
+ gdouble y,
+ GtdEditPane *self)
+{
+ GtkTextView *text_view;
+ GtkTextIter iter;
+ gboolean hovering;
+ gint ex, ey;
+
+ text_view = GTK_TEXT_VIEW (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (controller)));
+
+ gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_WIDGET, x, y, &ex, &ey);
+
+ hovering = FALSE;
+
+ if (gtk_text_view_get_iter_at_location (text_view, &iter, ex, ey))
+ {
+ GSList *tags = NULL;
+ GSList *l = NULL;
+
+ tags = gtk_text_iter_get_tags (&iter);
+
+ for (l = tags; l; l = l->next)
+ {
+ g_autofree gchar *tag_name = NULL;
+ GtkTextTag *tag;
+
+ tag = l->data;
+
+ g_object_get (tag, "name", &tag_name, NULL);
+
+ if (g_strcmp0 (tag_name, "url") == 0)
+ {
+ hovering = TRUE;
+ break;
+ }
+ }
+ }
+
+ gtk_widget_set_cursor_from_name (GTK_WIDGET (text_view), hovering ? "pointer" : "text");
+}
+
+static void
+on_uri_shown_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+
+ gtk_show_uri_full_finish (GTK_WINDOW (source), result, &error);
+
+ if (error)
+ g_warning ("%s", error->message);
+}
+
+static void
+on_hyperlink_clicked_cb (GtkGestureClick *gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ GtdEditPane *self)
+{
+ GdkEventSequence *sequence;
+ GtkTextBuffer *buffer;
+ GtkTextView *text_view;
+ GtkTextIter end_iter;
+ GtkTextIter iter;
+ GSList *tags = NULL;
+ GSList *l = NULL;
+ gint ex = 0;
+ gint ey = 0;
+
+ GTD_ENTRY;
+
+ /* Claim the sequence to prevent propagating to GtkListBox and closing the row */
+ sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ text_view = GTK_TEXT_VIEW (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)));
+ buffer = gtk_text_view_get_buffer (text_view);
+
+ /* Ensure focus */
+ gtk_widget_grab_focus (GTK_WIDGET (text_view));
+
+ /* We shouldn't follow a link if the user has selected something */
+ if (gtk_text_buffer_get_has_selection (buffer))
+ return;
+
+ gtk_text_view_window_to_buffer_coords (text_view, GTK_TEXT_WINDOW_WIDGET, x, y, &ex, &ey);
+
+ if (!gtk_text_view_get_iter_at_location (text_view, &iter, ex, ey))
+ return;
+
+ tags = gtk_text_iter_get_tags (&iter);
+
+ for (l = tags; l; l = l->next)
+ {
+ g_autofree gchar *tag_name = NULL;
+ g_autofree gchar *url = NULL;
+ GtkTextIter url_start;
+ GtkTextIter url_end;
+ GtkTextTag *tag;
+ GtkRoot *root;
+
+ tag = l->data;
+
+ g_object_get (tag, "name", &tag_name, NULL);
+
+ if (g_strcmp0 (tag_name, "url") != 0)
+ continue;
+
+ gtk_text_buffer_get_iter_at_line (buffer, &iter, gtk_text_iter_get_line (&iter));
+ end_iter = iter;
+ gtk_text_iter_forward_to_line_end (&end_iter);
+
+ /* Find the beginning... */
+ if (!gtk_text_iter_forward_search (&iter, "(", GTK_TEXT_SEARCH_TEXT_ONLY, NULL, &url_start, NULL))
+ continue;
+
+ /* ... and the end of the URL */
+ if (!gtk_text_iter_forward_search (&iter, ")", GTK_TEXT_SEARCH_TEXT_ONLY, &url_end, NULL, &end_iter))
+ continue;
+
+ url = gtk_text_iter_get_text (&url_start, &url_end);
+ root = gtk_widget_get_root (GTK_WIDGET (text_view));
+
+ if (root && GTK_IS_WINDOW (root))
+ gtk_show_uri_full (GTK_WINDOW (root), url, GDK_CURRENT_TIME, self->cancellable, on_uri_shown_cb, self);
+ }
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_edit_pane_finalize (GObject *object)
+{
+ GtdEditPane *self = (GtdEditPane *) object;
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->task);
+
+ G_OBJECT_CLASS (gtd_edit_pane_parent_class)->finalize (object);
+}
+
+static void
+gtd_edit_pane_dispose (GObject *object)
+{
+ GtdEditPane *self = (GtdEditPane *) object;
+
+ g_clear_pointer (&self->new_date, g_date_time_unref);
+ g_clear_object (&self->task);
+
+ G_OBJECT_CLASS (gtd_edit_pane_parent_class)->dispose (object);
+}
+
+static void
+gtd_edit_pane_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdEditPane *self = GTD_EDIT_PANE (object);
+
+ switch (prop_id)
+ {
+ case PROP_TASK:
+ g_value_set_object (value, self->task);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_edit_pane_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdEditPane *self = GTD_EDIT_PANE (object);
+
+ switch (prop_id)
+ {
+ case PROP_TASK:
+ self->task = g_value_get_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_edit_pane_class_init (GtdEditPaneClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_edit_pane_finalize;
+ object_class->dispose = gtd_edit_pane_dispose;
+ object_class->get_property = gtd_edit_pane_get_property;
+ object_class->set_property = gtd_edit_pane_set_property;
+
+ /**
+ * GtdEditPane::task:
+ *
+ * The task that is actually being edited.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_TASK,
+ g_param_spec_object ("task",
+ "Task being edited",
+ "The task that is actually being edited",
+ GTD_TYPE_TASK,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdEditPane::changed:
+ *
+ * Emitted when the task was changed.
+ */
+ signals[CHANGED] = g_signal_new ("changed",
+ GTD_TYPE_EDIT_PANE,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdEditPane::task-removed:
+ *
+ * Emitted when the user wants to remove the task.
+ */
+ signals[REMOVE_TASK] = g_signal_new ("remove-task",
+ GTD_TYPE_EDIT_PANE,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_TASK);
+
+ /* template class */
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-edit-pane.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdEditPane, calendar);
+ gtk_widget_class_bind_template_child (widget_class, GtdEditPane, date_button);
+ gtk_widget_class_bind_template_child (widget_class, GtdEditPane, notes_textview);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_date_popover_closed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_date_selected_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_delete_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_hyperlink_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_hyperlink_hover_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_no_date_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_priority_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_text_buffer_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_today_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_tomorrow_button_clicked_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "editpane");
+}
+
+static void
+gtd_edit_pane_init (GtdEditPane *self)
+{
+ self->cancellable = g_cancellable_new ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget*
+gtd_edit_pane_new (void)
+{
+ return g_object_new (GTD_TYPE_EDIT_PANE, NULL);
+}
+
+/**
+ * gtd_edit_pane_get_task:
+ * @self: a #GtdEditPane
+ *
+ * Retrieves the currently edited #GtdTask of %pane, or %NULL if none is set.
+ *
+ * Returns: (transfer none): the current #GtdTask being edited from %pane.
+ */
+GtdTask*
+gtd_edit_pane_get_task (GtdEditPane *self)
+{
+ g_return_val_if_fail (GTD_IS_EDIT_PANE (self), NULL);
+
+ return self->task;
+}
+
+/**
+ * gtd_edit_pane_set_task:
+ * @pane: a #GtdEditPane
+ * @task: a #GtdTask or %NULL
+ *
+ * Sets %task as the currently editing task of %pane.
+ */
+void
+gtd_edit_pane_set_task (GtdEditPane *self,
+ GtdTask *task)
+{
+ g_return_if_fail (GTD_IS_EDIT_PANE (self));
+
+ if (self->task && self->new_date)
+ {
+ g_clear_pointer (&self->new_date, g_date_time_unref);
+ }
+
+ if (!g_set_object (&self->task, task))
+ return;
+
+ if (task)
+ {
+ GtkTextBuffer *buffer;
+
+ buffer = gtk_text_view_get_buffer (self->notes_textview);
+
+ g_signal_handlers_block_by_func (buffer, on_text_buffer_changed_cb, self);
+
+ /* due date */
+ self->new_date = gtd_task_get_due_date (self->task);
+ update_date_widgets (self, gtd_task_get_due_date (self->task));
+
+ /* description */
+ gtk_text_buffer_set_text (buffer, gtd_task_get_description (task), -1);
+
+ self->notes_binding = g_object_bind_property (buffer,
+ "text",
+ task,
+ "description",
+ G_BINDING_BIDIRECTIONAL);
+
+ g_signal_handlers_unblock_by_func (buffer, on_text_buffer_changed_cb, self);
+
+ }
+
+ g_object_notify (G_OBJECT (self), "task");
+}
+
+/**
+ * gtd_edit_pane_get_new_date_time:
+ * @self: a #GtdEditPane
+ *
+ * Retrieves the newly set #GDateTime of %self, or %NULL if none is set.
+ *
+ * Returns: (transfer none): the current #GDateTime set in %self.
+ */
+GDateTime*
+gtd_edit_pane_get_new_date_time (GtdEditPane *self)
+{
+ g_return_val_if_fail (GTD_IS_EDIT_PANE (self), NULL);
+
+ return self->new_date;
+}
+
+void
+gtd_edit_pane_set_markdown_renderer (GtdEditPane *self,
+ GtdMarkdownRenderer *renderer)
+{
+ GtkTextBuffer *buffer;
+
+ g_assert (GTD_IS_EDIT_PANE (self));
+ g_assert (GTD_IS_MARKDOWN_RENDERER (renderer));
+
+ buffer = gtk_text_view_get_buffer (self->notes_textview);
+
+ gtd_markdown_renderer_add_buffer (renderer, buffer);
+}
+
diff --git a/src/gui/gtd-edit-pane.h b/src/gui/gtd-edit-pane.h
new file mode 100644
index 0000000..fefaf9c
--- /dev/null
+++ b/src/gui/gtd-edit-pane.h
@@ -0,0 +1,47 @@
+/* gtd-edit-pane.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_EDIT_PANE_H
+#define GTD_EDIT_PANE_H
+
+#include "gtd-types.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_EDIT_PANE (gtd_edit_pane_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdEditPane, gtd_edit_pane, GTD, EDIT_PANE, GtkBox)
+
+GtkWidget* gtd_edit_pane_new (void);
+
+GtdTask* gtd_edit_pane_get_task (GtdEditPane *self);
+
+void gtd_edit_pane_set_task (GtdEditPane *self,
+ GtdTask *task);
+
+GDateTime* gtd_edit_pane_get_new_date_time (GtdEditPane *self);
+
+void gtd_edit_pane_set_markdown_renderer (GtdEditPane *self,
+ GtdMarkdownRenderer *renderer);
+
+G_END_DECLS
+
+#endif /* GTD_EDIT_PANE_H */
diff --git a/src/gui/gtd-edit-pane.ui b/src/gui/gtd-edit-pane.ui
new file mode 100644
index 0000000..9a96fd5
--- /dev/null
+++ b/src/gui/gtd-edit-pane.ui
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdEditPane" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="hexpand">1</property>
+ <property name="spacing">6</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-bottom">12</property>
+ <child>
+ <object class="GtkSeparator" id="separator"/>
+ </child>
+ <child>
+ <object class="GtkLabel" id="due_date_dim_label">
+ <property name="label" translatable="yes">D_ue Date</property>
+ <property name="use_underline">1</property>
+ <property name="mnemonic_widget">date_button</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">12</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <child>
+ <object class="GtkMenuButton" id="date_button">
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <property name="popover">date_popover</property>
+ <property name="icon-name">pan-down-symbolic</property>
+ </object>
+ </child>
+
+ <!-- <child> -->
+ <!-- <object class="GtkButton"> -->
+ <!-- <property name="label" translatable="yes">_Today</property> -->
+ <!-- <property name="use_underline">1</property> -->
+ <!-- <property name="can_focus">1</property> -->
+ <!-- <property name="receives_default">1</property> -->
+ <!-- <signal name="clicked" handler="on_today_button_clicked_cb" object="GtdEditPane" swapped="no"/> -->
+ <!-- </object> -->
+ <!-- </child> -->
+ <!-- <child> -->
+ <!-- <object class="GtkButton"> -->
+ <!-- <property name="label" translatable="yes">To_morrow</property> -->
+ <!-- <property name="use_underline">1</property> -->
+ <!-- <property name="can_focus">1</property> -->
+ <!-- <property name="receives_default">1</property> -->
+ <!-- <signal name="clicked" handler="on_tomorrow_button_clicked_cb" object="GtdEditPane" swapped="no"/> -->
+ <!-- </object> -->
+ <!-- </child> -->
+ <!-- <child> -->
+ <!-- <object class="GtkMenuButton" id="date_button"> -->
+ <!-- <property name="can_focus">1</property> -->
+ <!-- <property name="receives_default">1</property> -->
+ <!-- <property name="popover">date_popover</property> -->
+ <!-- <child> -->
+ <!-- <object class="GtkBox" id="date_button_box"> -->
+ <!-- <property name="spacing">7</property> -->
+ <!-- <child> -->
+ <!-- <object class="GtkLabel" id="date_label"> -->
+ <!-- <property name="width-chars">10</property> -->
+ <!-- </object> -->
+ <!-- </child> -->
+ <!-- <child> -->
+ <!-- <object class="GtkImage" id="date_button_image"> -->
+ <!-- <property name="icon_name">pan-down-symbolic</property> -->
+ <!-- </object> -->
+ <!-- </child> -->
+ <!-- </object> -->
+ <!-- </child> -->
+ <!-- </object> -->
+ <!-- </child> -->
+ <style>
+ <class name="linked"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">_Notes</property>
+ <property name="use_underline">1</property>
+ <property name="mnemonic_widget">notes_textview</property>
+ <property name="xalign">0</property>
+ <property name="margin-top">12</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hexpand">true</property>
+ <property name="vexpand">true</property>
+ <property name="can-focus">1</property>
+ <property name="min-content-height">200</property>
+ <style>
+ <class name="frame" />
+ </style>
+ <child>
+ <object class="GtkTextView" id="notes_textview">
+ <property name="can_focus">1</property>
+ <property name="accepts_tab">0</property>
+ <property name="left-margin">6</property>
+ <property name="right-margin">6</property>
+ <property name="pixels-above-lines">6</property>
+ <property name="wrap-mode">word-char</property>
+ <property name="buffer">text_buffer</property>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">bubble</property>
+ <signal name="pressed" handler="on_hyperlink_clicked_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEventControllerMotion">
+ <property name="propagation-phase">bubble</property>
+ <signal name="motion" handler="on_hyperlink_hover_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="label" translatable="yes">_Delete</property>
+ <property name="use_underline">1</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <property name="vexpand">1</property>
+ <property name="valign">end</property>
+ <property name="halign">end</property>
+ <property name="margin-top">12</property>
+ <signal name="clicked" handler="on_delete_button_clicked_cb" object="GtdEditPane" swapped="no"/>
+ <style>
+ <class name="destructive-action"/>
+ </style>
+ </object>
+ </child>
+ </template>
+ <object class="GtkTextBuffer" id="text_buffer">
+ <signal name="notify::text" handler="on_text_buffer_changed_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ <object class="GtkPopover" id="date_popover">
+ <property name="position">bottom</property>
+ <signal name="closed" handler="on_date_popover_closed_cb" swapped="yes"/>
+ <child>
+ <object class="GtkBox" id="date_popover_box">
+ <property name="orientation">vertical</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>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkCalendar" id="calendar">
+ <property name="can_focus">1</property>
+ <property name="show_week_numbers">1</property>
+ <signal name="day-selected" handler="on_date_selected_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="spacing">12</property>
+ <property name="homogeneous">true</property>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">_Today</property>
+ <property name="use_underline">1</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="on_today_button_clicked_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">To_morrow</property>
+ <property name="use_underline">1</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="on_tomorrow_button_clicked_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="no_date_button">
+ <property name="label" translatable="yes" context="taskdate">None</property>
+ <property name="can_focus">1</property>
+ <signal name="clicked" handler="on_no_date_button_clicked_cb" object="GtdEditPane" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/gui/gtd-initial-setup-window.c b/src/gui/gtd-initial-setup-window.c
new file mode 100644
index 0000000..ccea90b
--- /dev/null
+++ b/src/gui/gtd-initial-setup-window.c
@@ -0,0 +1,247 @@
+/* gtd-initial-setup-window.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 "GtdInitialSetupWindow"
+
+#include "gtd-provider.h"
+#include "gtd-application.h"
+#include "gtd-provider-selector.h"
+#include "gtd-initial-setup-window.h"
+#include "gtd-manager.h"
+
+#include <glib/gi18n.h>
+
+typedef struct
+{
+ GtkWidget *cancel_button;
+ GtkWidget *done_button;
+ GtkWidget *storage_selector;
+
+ GtdManager *manager;
+} GtdInitialSetupWindowPrivate;
+
+struct _GtdInitialSetupWindow
+{
+ GtkApplicationWindow parent;
+
+ /*<private>*/
+ GtdInitialSetupWindowPrivate *priv;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdInitialSetupWindow, gtd_initial_setup_window, GTK_TYPE_APPLICATION_WINDOW)
+
+enum {
+ PROP_0,
+ PROP_MANAGER,
+ LAST_PROP
+};
+
+enum {
+ CANCEL,
+ DONE,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0, };
+
+static void
+gtd_initial_setup_window__location_selected (GtdInitialSetupWindow *window,
+ GtdProvider *provider)
+{
+ GtdInitialSetupWindowPrivate *priv;
+
+ g_return_if_fail (GTD_IS_INITIAL_SETUP_WINDOW (window));
+
+ priv = window->priv;
+
+ gtk_widget_set_sensitive (priv->done_button, provider != NULL);
+
+ if (provider)
+ gtd_manager_set_default_provider (priv->manager, provider);
+}
+
+static void
+gtd_initial_setup_window__button_clicked (GtdInitialSetupWindow *window,
+ GtkWidget *button)
+{
+ GtdInitialSetupWindowPrivate *priv;
+
+ g_return_if_fail (GTD_IS_INITIAL_SETUP_WINDOW (window));
+
+ priv = window->priv;
+
+ if (button == priv->cancel_button)
+ {
+ g_signal_emit (window,
+ signals[CANCEL],
+ 0);
+ }
+ else if (button == priv->done_button)
+ {
+ g_signal_emit (window,
+ signals[DONE],
+ 0);
+ }
+}
+
+static void
+gtd_initial_setup_window_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (gtd_initial_setup_window_parent_class)->finalize (object);
+}
+
+static void
+gtd_initial_setup_window_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdInitialSetupWindow *self = GTD_INITIAL_SETUP_WINDOW (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_value_set_object (value, self->priv->manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_initial_setup_window_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdInitialSetupWindow *self = GTD_INITIAL_SETUP_WINDOW (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ self->priv->manager = g_value_get_object (value);
+ g_object_notify (object, "manager");
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_initial_setup_window_constructed (GObject *object)
+{
+ GtdInitialSetupWindowPrivate *priv;
+
+ G_OBJECT_CLASS (gtd_initial_setup_window_parent_class)->constructed (object);
+
+ priv = GTD_INITIAL_SETUP_WINDOW (object)->priv;
+
+ g_object_bind_property (object,
+ "manager",
+ priv->storage_selector,
+ "manager",
+ G_BINDING_DEFAULT);
+}
+
+static void
+gtd_initial_setup_window_class_init (GtdInitialSetupWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_initial_setup_window_finalize;
+ object_class->constructed = gtd_initial_setup_window_constructed;
+ object_class->get_property = gtd_initial_setup_window_get_property;
+ object_class->set_property = gtd_initial_setup_window_set_property;
+
+ /**
+ * GtdInitialSetupWindow::cancel:
+ *
+ * Emitted when the Cancel button is clicked. Should quit the
+ * application.
+ */
+ signals[CANCEL] = g_signal_new ("cancel",
+ GTD_TYPE_INITIAL_SETUP_WINDOW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdInitialSetupWindow::done:
+ *
+ * Emitted when the Done button is clicked. Should start the
+ * application.
+ */
+ signals[DONE] = g_signal_new ("done",
+ GTD_TYPE_INITIAL_SETUP_WINDOW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdInitialSetupWindow::manager:
+ *
+ * Manager of the application.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_MANAGER,
+ g_param_spec_object ("manager",
+ "Manager of the task",
+ "The singleton manager instance of the task",
+ GTD_TYPE_MANAGER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-initial-setup.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, GtdInitialSetupWindow, cancel_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GtdInitialSetupWindow, done_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GtdInitialSetupWindow, storage_selector);
+
+ gtk_widget_class_bind_template_callback (widget_class, gtd_initial_setup_window__button_clicked);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_initial_setup_window__location_selected);
+}
+
+static void
+gtd_initial_setup_window_init (GtdInitialSetupWindow *self)
+{
+ self->priv = gtd_initial_setup_window_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget*
+gtd_initial_setup_window_new (GtdApplication *application)
+{
+ g_return_val_if_fail (GTD_IS_APPLICATION (application), NULL);
+
+ return g_object_new (GTD_TYPE_INITIAL_SETUP_WINDOW,
+ "application", application,
+ "manager", gtd_manager_get_default (),
+ NULL);
+}
diff --git a/src/gui/gtd-initial-setup-window.h b/src/gui/gtd-initial-setup-window.h
new file mode 100644
index 0000000..8927581
--- /dev/null
+++ b/src/gui/gtd-initial-setup-window.h
@@ -0,0 +1,37 @@
+/* gtd-initial-setup-window.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_INITIAL_SETUP_WINDOW_H
+#define GTD_INITIAL_SETUP_WINDOW_H
+
+#include "gtd-types.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_INITIAL_SETUP_WINDOW (gtd_initial_setup_window_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdInitialSetupWindow, gtd_initial_setup_window, GTD, INITIAL_SETUP_WINDOW, GtkApplicationWindow)
+
+GtkWidget* gtd_initial_setup_window_new (GtdApplication *application);
+
+G_END_DECLS
+
+#endif /* GTD_INITIAL_SETUP_WINDOW_H */
diff --git a/src/gui/gtd-initial-setup-window.ui b/src/gui/gtd-initial-setup-window.ui
new file mode 100644
index 0000000..01c4b87
--- /dev/null
+++ b/src/gui/gtd-initial-setup-window.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdInitialSetupWindow" parent="GtkApplicationWindow">
+ <property name="width_request">800</property>
+ <property name="height_request">600</property>
+ <property name="resizable">0</property>
+ <property name="window_position">center</property>
+ <property name="default_width">800</property>
+ <property name="default_height">600</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="valign">center</property>
+ <property name="margin_bottom">48</property>
+ <property name="vexpand">1</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <child>
+ <object class="GtkLabel" id="welcome_label">
+ <property name="label" translatable="yes">Welcome</property>
+ <style>
+ <class name="main-title"/>
+ </style>
+ </object>
+ <packing>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle_label">
+ <property name="label" translatable="yes">Log in to online accounts to access your tasks</property>
+ <style>
+ <class name="main-subtitle"/>
+ </style>
+ </object>
+ <packing>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtdStorageSelector" id="storage_selector">
+ <property name="receives_default">True</property>
+ <property name="width_request">400</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <signal name="storage-selected" handler="gtd_initial_setup_window__location_selected" object="GtdInitialSetupWindow" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="headerbar">
+ <property name="title" translatable="yes">Endeavour Setup</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use_underline">1</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="gtd_initial_setup_window__button_clicked" object="GtdInitialSetupWindow" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="done_button">
+ <property name="label" translatable="yes">_Done</property>
+ <property name="use_underline">1</property>
+ <property name="sensitive">0</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="gtd_initial_setup_window__button_clicked" object="GtdInitialSetupWindow" swapped="yes"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gui/gtd-markdown-renderer.c b/src/gui/gtd-markdown-renderer.c
new file mode 100644
index 0000000..b257149
--- /dev/null
+++ b/src/gui/gtd-markdown-renderer.c
@@ -0,0 +1,357 @@
+/* gtd-markdown-buffer.c
+ *
+ * Copyright © 2018 Vyas Giridharan <vyasgiridhar27@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 "GtdMarkdownRenderer"
+
+#include "gtd-debug.h"
+#include "gtd-markdown-renderer.h"
+
+#define ITALICS_1 "*"
+#define ITALICS_2 "_"
+#define BOLD_1 "__"
+#define BOLD_2 "**"
+#define STRIKE "~~"
+#define HEAD_1 "#"
+#define HEAD_2 "##"
+#define HEAD_3 "###"
+#define LIST "+"
+
+struct _GtdMarkdownRenderer
+{
+ GObject parent_instance;
+
+ GHashTable *populated_buffers;
+};
+
+
+static void on_text_buffer_weak_notified_cb (gpointer data,
+ GObject *where_the_object_was);
+
+static void on_text_changed_cb (GtkTextBuffer *buffer,
+ GParamSpec *pspec,
+ GtdMarkdownRenderer *self);
+
+
+G_DEFINE_TYPE (GtdMarkdownRenderer, gtd_markdown_renderer, G_TYPE_OBJECT)
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+apply_link_tags (GtdMarkdownRenderer *self,
+ GtkTextBuffer *buffer,
+ GtkTextTag *link_tag,
+ GtkTextTag *url_tag,
+ GtkTextIter *text_start,
+ GtkTextIter *text_end)
+{
+ GtkTextIter symbol_start;
+ GtkTextIter symbol_end;
+ GtkTextIter target_start;
+ GtkTextIter target_end;
+ GtkTextIter iter;
+
+ GTD_ENTRY;
+
+ iter = *text_start;
+
+ /*
+ * We advance in pairs of [...], and inside the loop we check if the very next character
+ * after ']' is '('. No spaces are allowed. Only if this condition is satisfied that we
+ * claim this to be a link, and render it as such.
+ */
+ while (gtk_text_iter_forward_search (&iter, "[", GTK_TEXT_SEARCH_TEXT_ONLY, &symbol_start, &target_start, text_end) &&
+ gtk_text_iter_forward_search (&target_start, "]", GTK_TEXT_SEARCH_TEXT_ONLY, &target_end, &symbol_end, text_end))
+ {
+ GtkTextIter url_start;
+ GtkTextIter url_end;
+
+ iter = symbol_end;
+
+ /* Advance a single position */
+ url_start = symbol_end;
+
+ /* Only consider valid if the character after ']' is '(' */
+ if (gtk_text_iter_get_char (&url_start) != '(')
+ continue;
+
+ /*
+ * Try and find the matching (...), if it fails, iter is set to the previous ']' so
+ * we don't enter in an infinite loop
+ */
+ if (!gtk_text_iter_forward_search (&iter, "(", GTK_TEXT_SEARCH_TEXT_ONLY, NULL, &url_start, text_end) ||
+ !gtk_text_iter_forward_search (&iter, ")", GTK_TEXT_SEARCH_TEXT_ONLY, &url_end, NULL, text_end))
+ {
+ continue;
+ }
+
+ /* Apply both the link and url tags */
+ gtk_text_buffer_apply_tag (buffer, link_tag, &target_start, &target_end);
+ gtk_text_buffer_apply_tag (buffer, url_tag, &url_start, &url_end);
+
+ iter = url_end;
+ }
+}
+
+static void
+apply_markdown_tag (GtdMarkdownRenderer *self,
+ GtkTextBuffer *buffer,
+ GtkTextTag *tag,
+ const gchar *symbol,
+ GtkTextIter *text_start,
+ GtkTextIter *text_end,
+ gboolean paired)
+{
+ GtkTextIter symbol_start;
+ GtkTextIter symbol_end;
+ GtkTextIter iter;
+
+ iter = *text_start;
+
+ while (gtk_text_iter_forward_search (&iter, symbol, GTK_TEXT_SEARCH_TEXT_ONLY, &symbol_start, &symbol_end, text_end))
+ {
+ GtkTextIter tag_start;
+ GtkTextIter tag_end;
+
+ tag_start = symbol_start;
+ tag_end = symbol_end;
+
+ /* Iter is initially at the end of the found symbol, to avoid infinite loops */
+ iter = symbol_end;
+
+ if (paired)
+ {
+ /*
+ * If the markdown tag is in the form of pairs (e.g. **bold**, __italics__, etc), then we should
+ * search the symbol twice. The first marks the start and the second marks the end of the section
+ * of the text that needs the tag.
+ *
+ * We also ignore the tag if it's not contained in the same line of the start.
+ */
+ if (!gtk_text_iter_forward_search (&tag_end, symbol, GTK_TEXT_SEARCH_TEXT_ONLY, NULL, &tag_end, text_end) ||
+ gtk_text_iter_get_line (&tag_end) != gtk_text_iter_get_line (&symbol_start))
+ {
+ continue;
+ }
+ }
+ else
+ {
+ /*
+ * If the markdown tag is not paired (e.g. ## header), then it is just applied at the start of
+ * the line. As such, we must search for the symbol - and this is where the tag starts - but move
+ * straight to the end of the line.
+ */
+ gtk_text_iter_forward_to_line_end (&tag_end);
+
+ /* Only apply this tag if this is the start of the line */
+ if (gtk_text_iter_get_line_offset (&tag_start) != 0)
+ continue;
+ }
+
+ /* Apply the tag */
+ gtk_text_buffer_apply_tag (buffer, tag, &tag_start, &tag_end);
+
+ /*
+ * If we applied the tag, jump the iter to the end of the tag. We are already guaranteed
+ * to not run into infinite loops, but this skips a bigger section of the buffer too and
+ * can save a tiny few cycles
+ */
+ iter = tag_end;
+ }
+}
+
+static void
+populate_tag_table (GtdMarkdownRenderer *self,
+ GtkTextBuffer *buffer)
+{
+ gtk_text_buffer_create_tag (buffer,
+ "italic",
+ "style",
+ PANGO_STYLE_ITALIC,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "bold",
+ "weight",
+ PANGO_WEIGHT_BOLD,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "head_1",
+ "weight",
+ PANGO_WEIGHT_BOLD,
+ "scale",
+ PANGO_SCALE_XX_LARGE,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "head_2",
+ "weight",
+ PANGO_WEIGHT_BOLD,
+ "scale",
+ PANGO_SCALE_SMALL,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "head_3",
+ "weight",
+ PANGO_WEIGHT_BOLD,
+ "scale",
+ PANGO_SCALE_SMALL,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "strikethrough",
+ "strikethrough",
+ TRUE,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "list-indent",
+ "indent",
+ 20,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "url",
+ "foreground",
+ "blue",
+ "underline",
+ PANGO_UNDERLINE_SINGLE,
+ NULL);
+
+ gtk_text_buffer_create_tag (buffer,
+ "link-text",
+ "weight",
+ PANGO_WEIGHT_BOLD,
+ "foreground",
+ "#555F61",
+ NULL);
+
+ /*
+ * Add a weak ref so we can remove from the map of populated buffers when it's
+ * finalized.
+ */
+ g_object_weak_ref (G_OBJECT (buffer), on_text_buffer_weak_notified_cb, self);
+
+ /* Add to the map of populated buffers */
+ g_hash_table_add (self->populated_buffers, buffer);
+ g_signal_connect (buffer, "notify::text", G_CALLBACK (on_text_changed_cb), self);
+
+ g_debug ("Added buffer %p to markdown renderer", buffer);
+}
+
+static void
+render_markdown (GtdMarkdownRenderer *self,
+ GtkTextBuffer *buffer)
+{
+ GtkTextTagTable *tag_table;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ GTD_ENTRY;
+
+ /* TODO: render in idle */
+
+ /* Wipe out the previous tags */
+ gtk_text_buffer_get_start_iter (buffer, &start);
+ gtk_text_buffer_get_end_iter (buffer, &end);
+
+ gtk_text_buffer_remove_all_tags (buffer, &start, &end);
+
+ /* Apply the tags */
+ tag_table = gtk_text_buffer_get_tag_table (buffer);
+
+#define TAG(x) gtk_text_tag_table_lookup(tag_table, x)
+
+ apply_markdown_tag (self, buffer, TAG ("bold"), BOLD_2, &start, &end, TRUE);
+ apply_markdown_tag (self, buffer, TAG ("bold"), BOLD_1, &start, &end, TRUE);
+ apply_markdown_tag (self, buffer, TAG ("italic"), ITALICS_2, &start, &end, TRUE);
+ apply_markdown_tag (self, buffer, TAG ("italic"), ITALICS_1, &start, &end, TRUE);
+ apply_markdown_tag (self, buffer, TAG ("head_3"), HEAD_3, &start, &end, FALSE);
+ apply_markdown_tag (self, buffer, TAG ("head_2"), HEAD_2, &start, &end, FALSE);
+ apply_markdown_tag (self, buffer, TAG ("head_1"), HEAD_1, &start, &end, FALSE);
+ apply_markdown_tag (self, buffer, TAG ("strikethrough"), STRIKE, &start, &end, TRUE);
+ apply_markdown_tag (self, buffer, TAG ("list_indent"), LIST, &start, &end, FALSE);
+
+ apply_link_tags (self, buffer, TAG ("link-text"), TAG ("url"), &start, &end);
+
+#undef TAG
+
+ GTD_EXIT;
+}
+
+/*
+ * Callbacks
+ */
+
+static void
+on_text_buffer_weak_notified_cb (gpointer data,
+ GObject *where_the_object_was)
+{
+ GtdMarkdownRenderer *self = GTD_MARKDOWN_RENDERER (data);
+
+ g_hash_table_remove (self->populated_buffers, where_the_object_was);
+
+ g_debug ("Buffer %p died and was removed from markdown renderer", where_the_object_was);
+}
+
+
+static void
+on_text_changed_cb (GtkTextBuffer *buffer,
+ GParamSpec *pspec,
+ GtdMarkdownRenderer *self)
+{
+ render_markdown (self, buffer);
+}
+
+static void
+gtd_markdown_renderer_class_init (GtdMarkdownRendererClass *klass)
+{
+}
+
+void
+gtd_markdown_renderer_init (GtdMarkdownRenderer *self)
+{
+ self->populated_buffers = g_hash_table_new (g_direct_hash, g_direct_equal);
+}
+
+GtdMarkdownRenderer*
+gtd_markdown_renderer_new (void)
+{
+ return g_object_new (GTD_TYPE_MARKDOWN_RENDERER, NULL);
+}
+
+void
+gtd_markdown_renderer_add_buffer (GtdMarkdownRenderer *self,
+ GtkTextBuffer *buffer)
+{
+ g_return_if_fail (GTD_IS_MARKDOWN_RENDERER (self));
+
+ GTD_ENTRY;
+
+ /* If the text buffer is not poopulated yet, do it now */
+ if (!g_hash_table_contains (self->populated_buffers, buffer))
+ populate_tag_table (self, buffer);
+
+ render_markdown (self, buffer);
+
+ GTD_EXIT;
+}
diff --git a/src/gui/gtd-markdown-renderer.h b/src/gui/gtd-markdown-renderer.h
new file mode 100644
index 0000000..1c2593b
--- /dev/null
+++ b/src/gui/gtd-markdown-renderer.h
@@ -0,0 +1,35 @@
+/* gtd-markdown-buffer.h
+ *
+ * Copyright (C) 2018 Vyas Giridharan <vyasgiridhar27@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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_MARKDOWN_RENDERER (gtd_markdown_renderer_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdMarkdownRenderer, gtd_markdown_renderer, GTD, MARKDOWN_RENDERER, GObject)
+
+GtdMarkdownRenderer* gtd_markdown_renderer_new (void);
+
+void gtd_markdown_renderer_add_buffer (GtdMarkdownRenderer*self,
+ GtkTextBuffer *buffer);
+
+G_END_DECLS
+
diff --git a/src/gui/gtd-max-size-layout.c b/src/gui/gtd-max-size-layout.c
new file mode 100644
index 0000000..348b2ed
--- /dev/null
+++ b/src/gui/gtd-max-size-layout.c
@@ -0,0 +1,474 @@
+/* gtd-max-size-layout.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-max-size-layout.h"
+#include "gtd-widget.h"
+
+struct _GtdMaxSizeLayout
+{
+ GtkLayoutManager parent;
+
+ gint max_height;
+ gint max_width;
+ gint max_width_chars;
+ gint width_chars;
+};
+
+G_DEFINE_TYPE (GtdMaxSizeLayout, gtd_max_size_layout, GTK_TYPE_LAYOUT_MANAGER)
+
+enum
+{
+ PROP_0,
+ PROP_MAX_HEIGHT,
+ PROP_MAX_WIDTH_CHARS,
+ PROP_MAX_WIDTH,
+ PROP_WIDTH_CHARS,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/*
+ * GtkLayoutManager overrides
+ */
+
+static void
+gtd_max_size_layout_measure (GtkLayoutManager *layout_manager,
+ GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ GtdMaxSizeLayout *self = (GtdMaxSizeLayout *)layout_manager;
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (widget);
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ gint child_min_baseline = -1;
+ gint child_nat_baseline = -1;
+ gint child_min = 0;
+ gint child_nat = 0;
+
+ if (!gtk_widget_should_layout (child))
+ continue;
+
+ gtk_widget_measure (child, orientation, for_size,
+ &child_min, &child_nat,
+ &child_min_baseline, &child_nat_baseline);
+
+ *minimum = MAX (*minimum, child_min);
+ *natural = MAX (*natural, child_nat);
+
+ if (child_min_baseline > -1)
+ *minimum_baseline = MAX (*minimum_baseline, child_min_baseline);
+ if (child_nat_baseline > -1)
+ *natural_baseline = MAX (*natural_baseline, child_nat_baseline);
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ PangoFontMetrics *metrics;
+ PangoContext *context;
+ gint char_width;
+
+ context = gtk_widget_get_pango_context (widget);
+ metrics = pango_context_get_metrics (context,
+ pango_context_get_font_description (context),
+ pango_context_get_language (context));
+
+ char_width = MAX (pango_font_metrics_get_approximate_char_width (metrics),
+ pango_font_metrics_get_approximate_digit_width (metrics));
+
+ if (self->width_chars > 0)
+ {
+ gint width_chars;
+
+ width_chars = char_width * self->width_chars / PANGO_SCALE;
+ *minimum = MAX (*minimum, width_chars);
+ *natural = MAX (*natural, width_chars);
+ }
+
+ if (self->max_width_chars > 0)
+ {
+ gint max_width_chars;
+
+ max_width_chars = char_width * self->max_width_chars / PANGO_SCALE;
+ *minimum = MIN (*minimum, max_width_chars);
+ *natural = MAX (*natural, max_width_chars);
+ }
+
+ if (self->max_width > 0)
+ {
+ *minimum = MIN (*minimum, self->max_width);
+ *natural = MAX (*natural, self->max_width);
+ }
+
+ pango_font_metrics_unref (metrics);
+ }
+ else if (self->max_height > 0)
+ {
+ *minimum = MIN (*minimum, self->max_height);
+ *natural = MAX (*natural, self->max_height);
+ }
+}
+
+static void
+gtd_max_size_layout_allocate (GtkLayoutManager *layout_manager,
+ GtkWidget *widget,
+ gint width,
+ gint height,
+ gint baseline)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (widget);
+ child != NULL;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (child && gtk_widget_should_layout (child))
+ {
+ GskTransform *transform = NULL;
+
+ if (GTD_IS_WIDGET (child))
+ transform = gtd_widget_apply_transform (GTD_WIDGET (child), NULL);
+
+ gtk_widget_allocate (child, width, height, baseline, transform);
+ }
+ }
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_max_size_layout_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdMaxSizeLayout *self = GTD_MAX_SIZE_LAYOUT (object);
+
+ switch (prop_id)
+ {
+ case PROP_MAX_HEIGHT:
+ g_value_set_int (value, self->max_height);
+ break;
+
+ case PROP_MAX_WIDTH:
+ g_value_set_int (value, self->max_width);
+ break;
+
+ case PROP_MAX_WIDTH_CHARS:
+ g_value_set_int (value, self->max_width_chars);
+ break;
+
+ case PROP_WIDTH_CHARS:
+ g_value_set_int (value, self->width_chars);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_max_size_layout_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdMaxSizeLayout *self = GTD_MAX_SIZE_LAYOUT (object);
+
+ switch (prop_id)
+ {
+ case PROP_MAX_HEIGHT:
+ gtd_max_size_layout_set_max_height (self, g_value_get_int (value));
+ break;
+
+ case PROP_MAX_WIDTH:
+ gtd_max_size_layout_set_max_width (self, g_value_get_int (value));
+ break;
+
+ case PROP_MAX_WIDTH_CHARS:
+ gtd_max_size_layout_set_max_width_chars (self, g_value_get_int (value));
+ break;
+
+ case PROP_WIDTH_CHARS:
+ gtd_max_size_layout_set_width_chars (self, g_value_get_int (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_max_size_layout_class_init (GtdMaxSizeLayoutClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkLayoutManagerClass *layout_manager_class = GTK_LAYOUT_MANAGER_CLASS (klass);
+
+ layout_manager_class->measure = gtd_max_size_layout_measure;
+ layout_manager_class->allocate = gtd_max_size_layout_allocate;
+
+ object_class->get_property = gtd_max_size_layout_get_property;
+ object_class->set_property = gtd_max_size_layout_set_property;
+
+ /**
+ * GtdMaxSizeLayout:max-height:
+ *
+ * Sets the maximum height of the #GtkWidget.
+ */
+ properties[PROP_MAX_HEIGHT] = g_param_spec_int ("max-height",
+ "Max Height",
+ "Max Height",
+ -1,
+ G_MAXINT,
+ -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdMaxSizeLayout:max-width:
+ *
+ * Sets the maximum width of the #GtkWidget.
+ */
+ properties[PROP_MAX_WIDTH] = g_param_spec_int ("max-width",
+ "Max Width",
+ "Max Width",
+ -1,
+ G_MAXINT,
+ -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdMaxSizeLayout:max-width-chars:
+ *
+ * Sets the maximum size of the #GtkWidget in characters.
+ */
+ properties[PROP_MAX_WIDTH_CHARS] = g_param_spec_int ("max-width-chars",
+ "Max Width Chars",
+ "Max Width Chars",
+ -1,
+ G_MAXINT,
+ -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdMaxSizeLayout:width-chars:
+ *
+ * Sets the size of the #GtkWidget in characters.
+ */
+ properties[PROP_WIDTH_CHARS] = g_param_spec_int ("width-chars",
+ "Width Chars",
+ "Width Chars",
+ -1,
+ G_MAXINT,
+ -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtd_max_size_layout_init (GtdMaxSizeLayout *self)
+{
+ self->max_height = -1;
+ self->max_width = -1;
+ self->max_width_chars = -1;
+ self->width_chars = -1;
+}
+
+/**
+ * gtd_max_size_layout_new:
+ *
+ * Creates a new #GtdMaxSizeLayout.
+ *
+ * Returns: (transfer full): a #GtdMaxSizeLayout
+ */
+GtkLayoutManager*
+gtd_max_size_layout_new (void)
+{
+ return g_object_new (GTD_TYPE_MAX_SIZE_LAYOUT, NULL);
+}
+
+/**
+ * gtd_max_size_layout_get_max_height:
+ *
+ * Retrieves the maximum height of @self.
+ *
+ * Returns: maximum height
+ */
+gint
+gtd_max_size_layout_get_max_height (GtdMaxSizeLayout *self)
+{
+ g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1);
+
+ return self->max_height;
+}
+
+/**
+ * gtd_max_size_layout_set_max_height:
+ * @self: a #GtdMaxSizeLayout
+ * @max_height: maximum height of the widget @self is attached to
+ *
+ * Sets the maximum height @self has.
+ */
+void
+gtd_max_size_layout_set_max_height (GtdMaxSizeLayout *self,
+ gint max_height)
+{
+ g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self));
+ g_return_if_fail (max_height >= -1);
+
+ if (self->max_height == max_height)
+ return;
+
+ self->max_height = max_height;
+ gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_HEIGHT]);
+}
+
+/**
+ * gtd_max_size_layout_get_max_width:
+ *
+ * Retrieves the maximum width of @self.
+ *
+ * Returns: maximum width
+ */
+gint
+gtd_max_size_layout_get_max_width (GtdMaxSizeLayout *self)
+{
+
+ g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1);
+
+ return self->max_width;
+}
+
+/**
+ * gtd_max_size_layout_set_max_width:
+ * @self: a #GtdMaxSizeLayout
+ * @max_width: maximum width of the widget @self is attached to
+ *
+ * Sets the maximum width @self has.
+ */
+void
+gtd_max_size_layout_set_max_width (GtdMaxSizeLayout *self,
+ gint max_width)
+{
+ g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self));
+ g_return_if_fail (max_width >= -1);
+
+ if (self->max_width == max_width)
+ return;
+
+ self->max_width = max_width;
+ gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_WIDTH]);
+}
+
+/**
+ * gtd_max_size_layout_get_max_width_chars:
+ *
+ * Retrieves the maximum width in characters of @self.
+ *
+ * Returns: maximum width in characters
+ */
+gint
+gtd_max_size_layout_get_max_width_chars (GtdMaxSizeLayout *self)
+{
+ g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1);
+
+ return self->max_width_chars;
+}
+
+/**
+ * gtd_max_size_layout_set_max_width_chars:
+ * @self: a #GtdMaxSizeLayout
+ * @max_width_chars: maximum width of the widget @self is attached to, in character length
+ *
+ * Sets the maximum width @self has, in characters length. It is a programming
+ * error to set a value smaller than #GtdMaxSizeLayout:width-layout.
+ */
+void
+gtd_max_size_layout_set_max_width_chars (GtdMaxSizeLayout *self,
+ gint max_width_chars)
+{
+ g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self));
+ g_return_if_fail (max_width_chars >= -1);
+ g_return_if_fail (self->width_chars == -1 || max_width_chars >= self->width_chars);
+
+ if (self->max_width_chars == max_width_chars)
+ return;
+
+ self->max_width_chars = max_width_chars;
+ gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_WIDTH_CHARS]);
+}
+
+/**
+ * gtd_max_size_layout_get_width_chars:
+ *
+ * Retrieves the minimum width in characters of @self.
+ *
+ * Returns: minimum width in characters
+ */
+gint
+gtd_max_size_layout_get_width_chars (GtdMaxSizeLayout *self)
+{
+ g_return_val_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self), -1);
+
+ return self->width_chars;
+}
+
+/**
+ * gtd_max_size_layout_set_width_chars:
+ * @self: a #GtdMaxSizeLayout
+ * @width_chars: minimum width of the widget @self is attached to, in character length
+ *
+ * Sets the minimum width @self has, in characters length. It is a programming
+ * error to set a value bigger than #GtdMaxSizeLayout:max-width-layout.
+ */
+void
+gtd_max_size_layout_set_width_chars (GtdMaxSizeLayout *self,
+ gint width_chars)
+{
+ g_return_if_fail (GTD_IS_MAX_SIZE_LAYOUT (self));
+ g_return_if_fail (width_chars >= -1);
+ g_return_if_fail (self->max_width_chars == -1 || width_chars <= self->max_width_chars);
+
+ if (self->width_chars == width_chars)
+ return;
+
+ self->width_chars = width_chars;
+ gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WIDTH_CHARS]);
+}
diff --git a/src/gui/gtd-max-size-layout.h b/src/gui/gtd-max-size-layout.h
new file mode 100644
index 0000000..2859d31
--- /dev/null
+++ b/src/gui/gtd-max-size-layout.h
@@ -0,0 +1,52 @@
+/* gtd-max-size-layout.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_MAX_SIZE_LAYOUT (gtd_max_size_layout_get_type())
+G_DECLARE_FINAL_TYPE (GtdMaxSizeLayout, gtd_max_size_layout, GTD, MAX_SIZE_LAYOUT, GtkLayoutManager)
+
+GtkLayoutManager* gtd_max_size_layout_new (void);
+
+gint gtd_max_size_layout_get_max_height (GtdMaxSizeLayout *self);
+
+void gtd_max_size_layout_set_max_height (GtdMaxSizeLayout *self,
+ gint max_height);
+
+gint gtd_max_size_layout_get_max_width (GtdMaxSizeLayout *self);
+
+void gtd_max_size_layout_set_max_width (GtdMaxSizeLayout *self,
+ gint max_width);
+
+gint gtd_max_size_layout_get_max_width_chars (GtdMaxSizeLayout *self);
+
+void gtd_max_size_layout_set_max_width_chars (GtdMaxSizeLayout *self,
+ gint max_width_chars);
+
+gint gtd_max_size_layout_get_width_chars (GtdMaxSizeLayout *self);
+
+void gtd_max_size_layout_set_width_chars (GtdMaxSizeLayout *self,
+ gint width_chars);
+
+G_END_DECLS
diff --git a/src/gui/gtd-menu-button.c b/src/gui/gtd-menu-button.c
new file mode 100644
index 0000000..e9f12be
--- /dev/null
+++ b/src/gui/gtd-menu-button.c
@@ -0,0 +1,1056 @@
+/* gtd-menu-button.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-menu-button.h"
+
+typedef struct
+{
+ GIcon *icon;
+
+ GtkWidget *button;
+ GtkWidget *popover; /* Only one at a time can be set */
+ GMenuModel *model;
+
+ GtdMenuButtonCreatePopupFunc create_popup_func;
+ gpointer create_popup_user_data;
+ GDestroyNotify create_popup_destroy_notify;
+
+ GtkWidget *label_widget;
+ GtkWidget *align_widget;
+ GtkWidget *arrow_widget;
+ GtkArrowType arrow_type;
+} GtdMenuButtonPrivate;
+
+enum
+{
+ PROP_0,
+ PROP_MENU_MODEL,
+ PROP_ALIGN_WIDGET,
+ PROP_DIRECTION,
+ PROP_POPOVER,
+ PROP_GICON,
+ PROP_LABEL,
+ PROP_USE_UNDERLINE,
+ PROP_HAS_FRAME,
+ LAST_PROP
+};
+
+static GParamSpec *menu_button_props[LAST_PROP];
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdMenuButton, gtd_menu_button, GTK_TYPE_WIDGET)
+
+static void
+update_sensitivity (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ gtk_widget_set_sensitive (priv->button,
+ priv->popover != NULL ||
+ priv->create_popup_func != NULL);
+}
+
+static void
+update_popover_direction (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ if (!priv->popover)
+ return;
+
+ switch (priv->arrow_type)
+ {
+ case GTK_ARROW_UP:
+ gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_TOP);
+ break;
+ case GTK_ARROW_DOWN:
+ case GTK_ARROW_NONE:
+ gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_BOTTOM);
+ break;
+ case GTK_ARROW_LEFT:
+ gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_LEFT);
+ break;
+ case GTK_ARROW_RIGHT:
+ gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_RIGHT);
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+set_arrow_type (GtkImage *image,
+ GtkArrowType arrow_type)
+{
+ switch (arrow_type)
+ {
+ case GTK_ARROW_NONE:
+ gtk_image_set_from_icon_name (image, "open-menu-symbolic");
+ break;
+ case GTK_ARROW_DOWN:
+ gtk_image_set_from_icon_name (image, "pan-down-symbolic");
+ break;
+ case GTK_ARROW_UP:
+ gtk_image_set_from_icon_name (image, "pan-up-symbolic");
+ break;
+ case GTK_ARROW_LEFT:
+ gtk_image_set_from_icon_name (image, "pan-start-symbolic");
+ break;
+ case GTK_ARROW_RIGHT:
+ gtk_image_set_from_icon_name (image, "pan-end-symbolic");
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+add_arrow (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv;
+ GtkWidget *arrow;
+
+ priv = gtd_menu_button_get_instance_private (self);
+
+ arrow = gtk_image_new ();
+ set_arrow_type (GTK_IMAGE (arrow), priv->arrow_type);
+ gtk_button_set_child (GTK_BUTTON (priv->button), arrow);
+ priv->arrow_widget = arrow;
+}
+
+static void
+set_align_widget_pointer (GtdMenuButton *self,
+ GtkWidget *align_widget)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ if (priv->align_widget)
+ g_object_remove_weak_pointer (G_OBJECT (priv->align_widget), (gpointer *) &priv->align_widget);
+
+ priv->align_widget = align_widget;
+
+ if (priv->align_widget)
+ g_object_add_weak_pointer (G_OBJECT (priv->align_widget), (gpointer *) &priv->align_widget);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static gboolean
+menu_deactivate_cb (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), FALSE);
+
+ return TRUE;
+}
+
+static void
+popover_destroy_cb (GtdMenuButton *self)
+{
+ gtd_menu_button_set_popover (self, NULL);
+}
+
+
+/*
+ * GtkWidget overrides
+ */
+
+static void
+gtd_menu_button_state_flags_changed (GtkWidget *widget,
+ GtkStateFlags previous_state_flags)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (widget);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ if (!gtk_widget_is_sensitive (widget))
+ {
+ if (priv->popover)
+ gtk_widget_hide (priv->popover);
+ }
+}
+
+static void
+gtd_menu_button_toggled (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ const gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->button));
+
+ /* Might set a new menu/popover */
+ if (active && priv->create_popup_func)
+ {
+ priv->create_popup_func (self, priv->create_popup_user_data);
+ }
+
+ if (priv->popover)
+ {
+ if (active)
+ gtk_popover_popup (GTK_POPOVER (priv->popover));
+ else
+ gtk_popover_popdown (GTK_POPOVER (priv->popover));
+ }
+}
+
+static void
+gtd_menu_button_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (widget);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ gtk_widget_measure (priv->button,
+ orientation,
+ for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+
+}
+
+static void
+gtd_menu_button_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ GtdMenuButton *self= GTD_MENU_BUTTON (widget);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ gtk_widget_size_allocate (priv->button,
+ &(GtkAllocation) { 0, 0, width, height },
+ baseline);
+ if (priv->popover)
+ gtk_popover_present (GTK_POPOVER (priv->popover));
+}
+
+static gboolean
+gtd_menu_button_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (widget);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ if (priv->popover && gtk_widget_get_visible (priv->popover))
+ return gtk_widget_child_focus (priv->popover, direction);
+ else
+ return gtk_widget_child_focus (priv->button, direction);
+}
+
+static gboolean
+gtd_menu_button_grab_focus (GtkWidget *widget)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (widget);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ return gtk_widget_grab_focus (priv->button);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_menu_button_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (object);
+
+ switch (property_id)
+ {
+ case PROP_MENU_MODEL:
+ gtd_menu_button_set_menu_model (self, g_value_get_object (value));
+ break;
+
+ case PROP_ALIGN_WIDGET:
+ gtd_menu_button_set_align_widget (self, g_value_get_object (value));
+ break;
+
+ case PROP_DIRECTION:
+ gtd_menu_button_set_direction (self, g_value_get_enum (value));
+ break;
+
+ case PROP_POPOVER:
+ gtd_menu_button_set_popover (self, g_value_get_object (value));
+ break;
+
+ case PROP_GICON:
+ gtd_menu_button_set_gicon (self, g_value_get_object (value));
+ break;
+
+ case PROP_LABEL:
+ gtd_menu_button_set_label (self, g_value_get_string (value));
+ break;
+
+ case PROP_USE_UNDERLINE:
+ gtd_menu_button_set_use_underline (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_HAS_FRAME:
+ gtd_menu_button_set_has_frame (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtd_menu_button_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (object);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ switch (property_id)
+ {
+ case PROP_MENU_MODEL:
+ g_value_set_object (value, priv->model);
+ break;
+
+ case PROP_ALIGN_WIDGET:
+ g_value_set_object (value, priv->align_widget);
+ break;
+
+ case PROP_DIRECTION:
+ g_value_set_enum (value, priv->arrow_type);
+ break;
+
+ case PROP_POPOVER:
+ g_value_set_object (value, priv->popover);
+ break;
+
+ case PROP_GICON:
+ g_value_set_object (value, priv->icon);
+ break;
+
+ case PROP_LABEL:
+ g_value_set_string (value, gtd_menu_button_get_label (GTD_MENU_BUTTON (object)));
+ break;
+
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, gtd_menu_button_get_use_underline (GTD_MENU_BUTTON (object)));
+ break;
+
+ case PROP_HAS_FRAME:
+ g_value_set_boolean (value, gtd_menu_button_get_has_frame (GTD_MENU_BUTTON (object)));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtd_menu_button_dispose (GObject *object)
+{
+ GtdMenuButton *self = GTD_MENU_BUTTON (object);
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ if (priv->popover)
+ {
+ g_signal_handlers_disconnect_by_func (priv->popover, menu_deactivate_cb, object);
+ g_signal_handlers_disconnect_by_func (priv->popover, popover_destroy_cb, object);
+ gtk_widget_unparent (priv->popover);
+ priv->popover = NULL;
+ }
+
+ set_align_widget_pointer (GTD_MENU_BUTTON (object), NULL);
+
+ g_clear_object (&priv->model);
+ g_clear_pointer (&priv->button, gtk_widget_unparent);
+
+ if (priv->create_popup_destroy_notify)
+ priv->create_popup_destroy_notify (priv->create_popup_user_data);
+
+ G_OBJECT_CLASS (gtd_menu_button_parent_class)->dispose (object);
+}
+
+static void
+gtd_menu_button_class_init (GtdMenuButtonClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gobject_class->set_property = gtd_menu_button_set_property;
+ gobject_class->get_property = gtd_menu_button_get_property;
+ gobject_class->dispose = gtd_menu_button_dispose;
+
+ widget_class->measure = gtd_menu_button_measure;
+ widget_class->size_allocate = gtd_menu_button_size_allocate;
+ widget_class->state_flags_changed = gtd_menu_button_state_flags_changed;
+ widget_class->focus = gtd_menu_button_focus;
+ widget_class->grab_focus = gtd_menu_button_grab_focus;
+
+ /**
+ * GtdMenuButton:menu-model:
+ *
+ * The #GMenuModel from which the popup will be created.
+ *
+ * See gtd_menu_button_set_menu_model() for the interaction with the
+ * #GtdMenuButton:popup property.
+ */
+ menu_button_props[PROP_MENU_MODEL] =
+ g_param_spec_object ("menu-model",
+ "Menu model",
+ "The model from which the popup is made.",
+ G_TYPE_MENU_MODEL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdMenuButton:align-widget:
+ *
+ * The #GtkWidget to use to align the menu with.
+ */
+ menu_button_props[PROP_ALIGN_WIDGET] =
+ g_param_spec_object ("align-widget",
+ "Align with",
+ "The parent widget which the menu should align with.",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdMenuButton:direction:
+ *
+ * The #GtkArrowType representing the direction in which the
+ * menu or popover will be popped out.
+ */
+ menu_button_props[PROP_DIRECTION] =
+ g_param_spec_enum ("direction",
+ "Direction",
+ "The direction the arrow should point.",
+ GTK_TYPE_ARROW_TYPE,
+ GTK_ARROW_DOWN,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GtdMenuButton:popover:
+ *
+ * The #GtkPopover that will be popped up when the button is clicked.
+ */
+ menu_button_props[PROP_POPOVER] =
+ g_param_spec_object ("popover",
+ "Popover",
+ "The popover",
+ GTK_TYPE_POPOVER,
+ G_PARAM_READWRITE);
+
+ menu_button_props[PROP_GICON] =
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "The name of the icon used to automatically populate the button",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GtdMenuButton:gicon:
+ *
+ */
+ menu_button_props[PROP_GICON] =
+ g_param_spec_object ("gicon",
+ "GIcon",
+ "A GIcon",
+ G_TYPE_ICON,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ menu_button_props[PROP_LABEL] =
+ g_param_spec_string ("label",
+ "Label",
+ "The label for the button",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ menu_button_props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ "Use underline",
+ "If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ menu_button_props[PROP_HAS_FRAME] =
+ g_param_spec_boolean ("has-frame",
+ "Has frame",
+ "Whether the button has a frame",
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (gobject_class, LAST_PROP, menu_button_props);
+
+ gtk_widget_class_set_css_name (widget_class, "menubutton");
+}
+
+static void
+gtd_menu_button_init (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ priv->arrow_type = GTK_ARROW_DOWN;
+
+ priv->button = gtk_toggle_button_new ();
+ gtk_widget_set_parent (priv->button, GTK_WIDGET (self));
+ g_signal_connect_swapped (priv->button, "toggled", G_CALLBACK (gtd_menu_button_toggled), self);
+ add_arrow (self);
+
+ gtk_widget_set_sensitive (priv->button, FALSE);
+
+ gtk_widget_add_css_class (GTK_WIDGET (self), "popup");
+}
+
+/**
+ * gtd_menu_button_new:
+ *
+ * Creates a new #GtdMenuButton widget with downwards-pointing
+ * arrow as the only child. You can replace the child widget
+ * with another #GtkWidget should you wish to.
+ *
+ * Returns: The newly created #GtdMenuButton widget
+ */
+GtkWidget *
+gtd_menu_button_new (void)
+{
+ return g_object_new (GTD_TYPE_MENU_BUTTON, NULL);
+}
+
+/**
+ * gtd_menu_button_set_menu_model:
+ * @self: a #GtdMenuButton
+ * @menu_model: (nullable): a #GMenuModel, or %NULL to unset and disable the
+ * button
+ *
+ * Sets the #GMenuModel from which the popup will be constructed,
+ * or %NULL to dissociate any existing menu model and disable the button.
+ *
+ * A #GtkPopover will be created from the menu model with gtk_popover_menu_new_from_model().
+ * Actions will be connected as documented for this function.
+ *
+ * If #GtdMenuButton:popover is already set, it will be dissociated from the @menu_button,
+ * and the property is set to %NULL.
+ */
+void
+gtd_menu_button_set_menu_model (GtdMenuButton *self,
+ GMenuModel *menu_model)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+ g_return_if_fail (G_IS_MENU_MODEL (menu_model) || menu_model == NULL);
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ if (menu_model)
+ g_object_ref (menu_model);
+
+ if (menu_model)
+ {
+ GtkWidget *popover;
+
+ popover = gtk_popover_menu_new_from_model (menu_model);
+ gtd_menu_button_set_popover (self, popover);
+ }
+ else
+ {
+ gtd_menu_button_set_popover (self, NULL);
+ }
+
+ priv->model = menu_model;
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_MENU_MODEL]);
+
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * gtd_menu_button_get_menu_model:
+ * @self: a #GtdMenuButton
+ *
+ * Returns the #GMenuModel used to generate the popup.
+ *
+ * Returns: (nullable) (transfer none): a #GMenuModel or %NULL
+ */
+GMenuModel *
+gtd_menu_button_get_menu_model (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL);
+
+ return priv->model;
+}
+
+/**
+ * gtd_menu_button_set_align_widget:
+ * @self: a #GtdMenuButton
+ * @align_widget: (allow-none): a #GtkWidget
+ *
+ * Sets the #GtkWidget to use to line the menu with when popped up.
+ * Note that the @align_widget must contain the #GtdMenuButton itself.
+ *
+ * Setting it to %NULL means that the menu will be aligned with the
+ * button itself.
+ *
+ * Note that this property is only used with menus currently,
+ * and not for popovers.
+ */
+void
+gtd_menu_button_set_align_widget (GtdMenuButton *self,
+ GtkWidget *align_widget)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+ g_return_if_fail (align_widget == NULL || gtk_widget_is_ancestor (GTK_WIDGET (self), align_widget));
+
+ if (priv->align_widget == align_widget)
+ return;
+
+ set_align_widget_pointer (self, align_widget);
+
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_ALIGN_WIDGET]);
+}
+
+/**
+ * gtd_menu_button_get_align_widget:
+ * @self: a #GtdMenuButton
+ *
+ * Returns the parent #GtkWidget to use to line up with menu.
+ *
+ * Returns: (nullable) (transfer none): a #GtkWidget value or %NULL
+ */
+GtkWidget *
+gtd_menu_button_get_align_widget (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL);
+
+ return priv->align_widget;
+}
+
+/**
+ * gtd_menu_button_set_direction:
+ * @self: a #GtdMenuButton
+ * @direction: a #GtkArrowType
+ *
+ * Sets the direction in which the popup will be popped up, as
+ * well as changing the arrow’s direction. The child will not
+ * be changed to an arrow if it was customized.
+ *
+ * If the does not fit in the available space in the given direction,
+ * GTK+ will its best to keep it inside the screen and fully visible.
+ *
+ * If you pass %GTK_ARROW_NONE for a @direction, the popup will behave
+ * as if you passed %GTK_ARROW_DOWN (although you won’t see any arrows).
+ */
+void
+gtd_menu_button_set_direction (GtdMenuButton *self,
+ GtkArrowType direction)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+ GtkWidget *child;
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ if (priv->arrow_type == direction)
+ return;
+
+ priv->arrow_type = direction;
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_DIRECTION]);
+
+ /* Is it custom content? We don't change that */
+ child = gtk_button_get_child (GTK_BUTTON (priv->button));
+ if (priv->arrow_widget != child)
+ return;
+
+ set_arrow_type (GTK_IMAGE (child), priv->arrow_type);
+ update_popover_direction (self);
+}
+
+/**
+ * gtd_menu_button_get_direction:
+ * @self: a #GtdMenuButton
+ *
+ * Returns the direction the popup will be pointing at when popped up.
+ *
+ * Returns: a #GtkArrowType value
+ */
+GtkArrowType
+gtd_menu_button_get_direction (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), GTK_ARROW_DOWN);
+
+ return priv->arrow_type;
+}
+
+/**
+ * gtd_menu_button_set_popover:
+ * @self: a #GtdMenuButton
+ * @popover: (nullable): a #GtkPopover, or %NULL to unset and disable the button
+ *
+ * Sets the #GtkPopover that will be popped up when the @menu_button is clicked,
+ * or %NULL to dissociate any existing popover and disable the button.
+ *
+ * If #GtdMenuButton:menu-model is set, the menu model is dissociated from the
+ * @menu_button, and the property is set to %NULL.
+ */
+void
+gtd_menu_button_set_popover (GtdMenuButton *self,
+ GtkWidget *popover)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+ g_return_if_fail (GTK_IS_POPOVER (popover) || popover == NULL);
+
+ g_object_freeze_notify (G_OBJECT (self));
+
+ g_clear_object (&priv->model);
+
+ if (priv->popover)
+ {
+ if (gtk_widget_get_visible (priv->popover))
+ gtk_widget_hide (priv->popover);
+
+ g_signal_handlers_disconnect_by_func (priv->popover, menu_deactivate_cb, self);
+ g_signal_handlers_disconnect_by_func (priv->popover, popover_destroy_cb, self);
+
+ gtk_widget_unparent (priv->popover);
+ }
+
+ priv->popover = popover;
+
+ if (popover)
+ {
+ gtk_widget_set_parent (priv->popover, GTK_WIDGET (self));
+ g_signal_connect_swapped (priv->popover, "closed", G_CALLBACK (menu_deactivate_cb), self);
+ g_signal_connect_swapped (priv->popover, "destroy", G_CALLBACK (popover_destroy_cb), self);
+ update_popover_direction (self);
+ }
+
+ update_sensitivity (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_POPOVER]);
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_MENU_MODEL]);
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+/**
+ * gtd_menu_button_get_popover:
+ * @self: a #GtdMenuButton
+ *
+ * Returns the #GtkPopover that pops out of the button.
+ * If the button is not using a #GtkPopover, this function
+ * returns %NULL.
+ *
+ * Returns: (nullable) (transfer none): a #GtkPopover or %NULL
+ */
+GtkPopover *
+gtd_menu_button_get_popover (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL);
+
+ return GTK_POPOVER (priv->popover);
+}
+
+/**
+ * gtd_menu_button_set_icon_name:
+ * @self: a #GtdMenuButton
+ * @icon_name: the icon name
+ *
+ * Sets the name of an icon to show inside the menu button.
+ */
+void
+gtd_menu_button_set_gicon (GtdMenuButton *self,
+ GIcon *icon)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+ GtkWidget *box;
+ GtkWidget *icon_image;
+ GtkWidget *image;
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_margin_start (box, 6);
+ gtk_widget_set_margin_end (box, 6);
+
+ icon_image = gtk_image_new_from_gicon (icon);
+ gtk_widget_set_hexpand (icon_image, TRUE);
+
+ image = gtk_image_new_from_icon_name ("pan-down-symbolic");
+
+ gtk_box_append (GTK_BOX (box), icon_image);
+ gtk_box_append (GTK_BOX (box), image);
+ gtk_button_set_child (GTK_BUTTON (priv->button), box);
+
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_GICON]);
+}
+
+/**
+ * gtd_menu_button_get_gicon:
+ * @self: a #GtdMenuButton
+ *
+ * Gets the name of the icon shown in the button.
+ *
+ * Returns: (transfer full): the name of the icon shown in the button
+ */
+GIcon*
+gtd_menu_button_get_gicon (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL);
+
+ return priv->icon;
+}
+
+/**
+ * gtd_menu_button_set_label:
+ * @self: a #GtdMenuButton
+ * @label: the label
+ *
+ * Sets the label to show inside the menu button.
+ */
+void
+gtd_menu_button_set_label (GtdMenuButton *self,
+ const gchar *label)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+ GtkWidget *box;
+ GtkWidget *label_widget;
+ GtkWidget *image;
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_margin_start (box, 6);
+ gtk_widget_set_margin_end (box, 6);
+
+ label_widget = gtk_label_new (label);
+ gtk_label_set_xalign (GTK_LABEL (label_widget), 0);
+ gtk_label_set_use_underline (GTK_LABEL (label_widget),
+ gtk_button_get_use_underline (GTK_BUTTON (priv->button)));
+ gtk_widget_set_hexpand (label_widget, TRUE);
+ image = gtk_image_new_from_icon_name ("pan-down-symbolic");
+ gtk_box_append (GTK_BOX (box), label_widget);
+ gtk_box_append (GTK_BOX (box), image);
+ gtk_button_set_child (GTK_BUTTON (priv->button), box);
+ priv->label_widget = label_widget;
+
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_LABEL]);
+}
+
+/**
+ * gtd_menu_button_get_label:
+ * @self: a #GtdMenuButton
+ *
+ * Gets the label shown in the button
+ *
+ * Returns: the label shown in the button
+ */
+const gchar*
+gtd_menu_button_get_label (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+ GtkWidget *child;
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), NULL);
+
+ child = gtk_button_get_child (GTK_BUTTON (priv->button));
+ if (GTK_IS_BOX (child))
+ {
+ child = gtk_widget_get_first_child (child);
+ return gtk_label_get_label (GTK_LABEL (child));
+ }
+
+ return NULL;
+}
+
+/**
+ * gtd_menu_button_set_has_frame:
+ * @self: a #GtdMenuButton
+ * @has_frame: whether the button should have a visible frame
+ *
+ * Sets the style of the button.
+ */
+void
+gtd_menu_button_set_has_frame (GtdMenuButton *self,
+ gboolean has_frame)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ if (gtk_button_get_has_frame (GTK_BUTTON (priv->button)) == has_frame)
+ return;
+
+ gtk_button_set_has_frame (GTK_BUTTON (priv->button), has_frame);
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_HAS_FRAME]);
+}
+
+/**
+ * gtd_menu_button_get_has_frame:
+ * @self: a #GtdMenuButton
+ *
+ * Returns whether the button has a frame.
+ *
+ * Returns: %TRUE if the button has a frame
+ */
+gboolean
+gtd_menu_button_get_has_frame (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), TRUE);
+
+ return gtk_button_get_has_frame (GTK_BUTTON (priv->button));
+}
+
+/**
+ * gtd_menu_button_popup:
+ * @self: a #GtdMenuButton
+ *
+ * Pop up the menu.
+ */
+void
+gtd_menu_button_popup (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), TRUE);
+}
+
+/**
+ * gtd_menu_button_popdown:
+ * @self: a #GtdMenuButton
+ *
+ * Dismiss the menu.
+ */
+void
+gtd_menu_button_popdown (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->button), FALSE);
+}
+
+/**
+ * gtd_menu_button_set_create_popup_func:
+ * @self: a #GtdMenuButton
+ * @func: (nullable): function to call when a popuop is about to
+ * be shown, but none has been provided via other means, or %NULL
+ * to reset to default behavior.
+ * @user_data: (closure): user data to pass to @func.
+ * @destroy_notify: (nullable): destroy notify for @user_data
+ *
+ * Sets @func to be called when a popup is about to be shown.
+ * @func should use one of
+ *
+ * - gtd_menu_button_set_popover()
+ * - gtd_menu_button_set_menu_model()
+ *
+ * to set a popup for @menu_button.
+ * If @func is non-%NULL, @menu_button will always be sensitive.
+ *
+ * Using this function will not reset the menu widget attached to @menu_button.
+ * Instead, this can be done manually in @func.
+ */
+void
+gtd_menu_button_set_create_popup_func (GtdMenuButton *self,
+ GtdMenuButtonCreatePopupFunc func,
+ gpointer user_data,
+ GDestroyNotify destroy_notify)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ if (priv->create_popup_destroy_notify)
+ priv->create_popup_destroy_notify (priv->create_popup_user_data);
+
+ priv->create_popup_func = func;
+ priv->create_popup_user_data = user_data;
+ priv->create_popup_destroy_notify = destroy_notify;
+
+ update_sensitivity (self);
+}
+
+/**
+ * gtd_menu_button_set_use_underline:
+ * @self: a #GtdMenuButton
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text indicates the next character should be
+ * used for the mnemonic accelerator key.
+ */
+void
+gtd_menu_button_set_use_underline (GtdMenuButton *self,
+ gboolean use_underline)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_MENU_BUTTON (self));
+
+ if (gtk_button_get_use_underline (GTK_BUTTON (priv->button)) == use_underline)
+ return;
+
+ gtk_button_set_use_underline (GTK_BUTTON (priv->button), use_underline);
+ if (priv->label_widget)
+ gtk_label_set_use_underline (GTK_LABEL (priv->label_widget), use_underline);
+
+ g_object_notify_by_pspec (G_OBJECT (self), menu_button_props[PROP_USE_UNDERLINE]);
+}
+
+/**
+ * gtd_menu_button_get_use_underline:
+ * @self: a #GtdMenuButton
+ *
+ * Returns whether an embedded underline in the text indicates a
+ * mnemonic. See gtd_menu_button_set_use_underline().
+ *
+ * Returns: %TRUE whether an embedded underline in the text indicates
+ * the mnemonic accelerator keys.
+ */
+gboolean
+gtd_menu_button_get_use_underline (GtdMenuButton *self)
+{
+ GtdMenuButtonPrivate *priv = gtd_menu_button_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_MENU_BUTTON (self), FALSE);
+
+ return gtk_button_get_use_underline (GTK_BUTTON (priv->button));
+}
diff --git a/src/gui/gtd-menu-button.h b/src/gui/gtd-menu-button.h
new file mode 100644
index 0000000..191806a
--- /dev/null
+++ b/src/gui/gtd-menu-button.h
@@ -0,0 +1,104 @@
+/* gtd-menu-button.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_MENU_BUTTON (gtd_menu_button_get_type())
+G_DECLARE_DERIVABLE_TYPE (GtdMenuButton, gtd_menu_button, GTD, MENU_BUTTON, GtkWidget)
+
+struct _GtdMenuButtonClass
+{
+ GtkWidgetClass parent_class;
+};
+
+/**
+ * GtdMenuButtonCreatePopupFunc:
+ * @self: the #GtdMenuButton
+ *
+ * User-provided callback function to create a popup for @menu_button on demand.
+ * This function is called when the popoup of @menu_button is shown, but none has
+ * been provided via gtd_menu_buton_set_popup(), gtd_menu_button_set_popover()
+ * or gtd_menu_button_set_menu_model().
+ */
+typedef void (*GtdMenuButtonCreatePopupFunc) (GtdMenuButton *self,
+ gpointer user_data);
+
+GtkWidget* gtd_menu_button_new (void);
+
+void gtd_menu_button_set_popover (GtdMenuButton *self,
+ GtkWidget *popover);
+
+GtkPopover* gtd_menu_button_get_popover (GtdMenuButton *self);
+
+void gtd_menu_button_set_direction (GtdMenuButton *self,
+ GtkArrowType direction);
+
+GtkArrowType gtd_menu_button_get_direction (GtdMenuButton *self);
+
+void gtd_menu_button_set_menu_model (GtdMenuButton *self,
+ GMenuModel *menu_model);
+
+GMenuModel* gtd_menu_button_get_menu_model (GtdMenuButton *self);
+
+
+void gtd_menu_button_set_align_widget (GtdMenuButton *self,
+ GtkWidget *align_widget);
+
+GtkWidget* gtd_menu_button_get_align_widget (GtdMenuButton *self);
+
+
+void gtd_menu_button_set_gicon (GtdMenuButton *self,
+ GIcon *icon);
+
+GIcon* gtd_menu_button_get_gicon (GtdMenuButton *self);
+
+
+void gtd_menu_button_set_label (GtdMenuButton *self,
+ const gchar *label);
+
+const gchar* gtd_menu_button_get_label (GtdMenuButton *self);
+
+
+void gtd_menu_button_set_use_underline (GtdMenuButton *self,
+ gboolean use_underline);
+
+gboolean gtd_menu_button_get_use_underline (GtdMenuButton *self);
+
+void gtd_menu_button_set_has_frame (GtdMenuButton *self,
+ gboolean has_frame);
+
+gboolean gtd_menu_button_get_has_frame (GtdMenuButton *self);
+
+void gtd_menu_button_popup (GtdMenuButton *self);
+
+void gtd_menu_button_popdown (GtdMenuButton *self);
+
+
+void gtd_menu_button_set_create_popup_func (GtdMenuButton *self,
+ GtdMenuButtonCreatePopupFunc func,
+ gpointer user_data,
+ GDestroyNotify destroy_notify);
+
+
+G_END_DECLS
diff --git a/src/gui/gtd-new-task-row.c b/src/gui/gtd-new-task-row.c
new file mode 100644
index 0000000..956bbef
--- /dev/null
+++ b/src/gui/gtd-new-task-row.c
@@ -0,0 +1,374 @@
+/* gtd-new-task-row.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 "GtdNewTaskRow"
+
+#include "gtd-debug.h"
+#include "gtd-manager.h"
+#include "gtd-new-task-row.h"
+#include "gtd-provider.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-task-list-popover.h"
+#include "gtd-task-list-view.h"
+#include "gtd-utils.h"
+
+#include <glib/gi18n.h>
+#include <math.h>
+
+struct _GtdNewTaskRow
+{
+ GtkListBoxRow parent;
+
+ GtkEntry *entry;
+ GtdTaskListPopover *tasklist_popover;
+
+ gboolean show_list_selector;
+};
+
+G_DEFINE_TYPE (GtdNewTaskRow, gtd_new_task_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum
+{
+ ENTER,
+ EXIT,
+ NUM_SIGNALS
+};
+
+enum
+{
+ PROP_0,
+ N_PROPS
+};
+
+static guint signals [NUM_SIGNALS] = { 0, };
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+update_secondary_icon (GtdNewTaskRow *self)
+{
+ g_autoptr (GdkPaintable) paintable = NULL;
+ g_autoptr (GdkRGBA) color = NULL;
+ g_autofree gchar *tooltip = NULL;
+ GtdTaskList *selected_list;
+
+ if (!self->show_list_selector)
+ {
+ gtk_entry_set_icon_from_paintable (self->entry, GTK_ENTRY_ICON_SECONDARY, NULL);
+ return;
+ }
+
+ selected_list = gtd_task_list_popover_get_task_list (GTD_TASK_LIST_POPOVER (self->tasklist_popover));
+
+ if (!selected_list)
+ return;
+
+ color = gtd_task_list_get_color (selected_list);
+ paintable = gtd_create_circular_paintable (color, 12);
+
+ gtk_entry_set_icon_from_paintable (self->entry, GTK_ENTRY_ICON_SECONDARY, paintable);
+
+ /* Translators: %1$s is the task list name, %2$s is the provider name */
+ tooltip = g_strdup_printf (_("%1$s \t <small>%2$s</small>"),
+ gtd_task_list_get_name (selected_list),
+ gtd_provider_get_description (gtd_task_list_get_provider (selected_list)));
+ gtk_entry_set_icon_tooltip_markup (self->entry, GTK_ENTRY_ICON_SECONDARY, tooltip);
+}
+
+static void
+show_task_list_selector_popover (GtdNewTaskRow *self)
+{
+ GdkRectangle rect;
+
+ gtk_entry_get_icon_area (self->entry, GTK_ENTRY_ICON_SECONDARY, &rect);
+ gtk_popover_set_pointing_to (GTK_POPOVER (self->tasklist_popover), &rect);
+ gtk_popover_popup (GTK_POPOVER (self->tasklist_popover));
+}
+
+/*
+ * Callbacks
+ */
+
+static void
+on_task_created_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+ GtdNewTaskRow *self;
+ GtdTask *new_task;
+
+ self = GTD_NEW_TASK_ROW (user_data);
+ new_task = gtd_provider_create_task_finish (GTD_PROVIDER (object), result, &error);
+
+ if (!new_task)
+ {
+ g_warning ("Error creating task: %s", error->message);
+
+ gtd_manager_emit_error_message (gtd_manager_get_default (),
+ _("An error occurred while creating a task"),
+ error->message,
+ NULL,
+ NULL);
+ }
+
+ gtk_editable_set_text (GTK_EDITABLE (self->entry), "");
+ gtk_widget_set_sensitive (GTK_WIDGET (self->entry), TRUE);
+ gtk_widget_grab_focus (GTK_WIDGET (self->entry));
+}
+
+static void
+entry_activated_cb (GtdNewTaskRow *self)
+{
+ GtdTaskListView *view;
+ GtdTaskList *list;
+ GListModel *model;
+
+ /* Cannot create empty tasks */
+ if (gtk_entry_get_text_length (self->entry) == 0)
+ return;
+
+ view = GTD_TASK_LIST_VIEW (gtk_widget_get_ancestor (GTK_WIDGET (self), GTD_TYPE_TASK_LIST_VIEW));
+
+ /* If there's a task list set, always go for it */
+ model = gtd_task_list_view_get_model (view);
+ list = GTD_IS_TASK_LIST (model) ? GTD_TASK_LIST (model) : NULL;
+
+ /*
+ * If there is no current list set, use the default list from the
+ * default provider.
+ */
+ if (!list)
+ list = gtd_task_list_popover_get_task_list (GTD_TASK_LIST_POPOVER (self->tasklist_popover));
+
+ if (!list)
+ list = gtd_manager_get_inbox (gtd_manager_get_default ());
+
+ g_return_if_fail (GTD_IS_TASK_LIST (list));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->entry), FALSE);
+
+ gtd_provider_create_task (gtd_task_list_get_provider (list),
+ list,
+ gtk_editable_get_text (GTK_EDITABLE (self->entry)),
+ gtd_task_list_view_get_default_date (view),
+ NULL,
+ on_task_created_cb,
+ self);
+}
+
+static void
+on_entry_icon_released_cb (GtkEntry *entry,
+ GtkEntryIconPosition position,
+ GtdNewTaskRow *self)
+{
+ switch (position)
+ {
+ case GTK_ENTRY_ICON_PRIMARY:
+ entry_activated_cb (self);
+ break;
+
+ case GTK_ENTRY_ICON_SECONDARY:
+ show_task_list_selector_popover (self);
+ break;
+ }
+}
+
+static void
+on_focus_enter_cb (GtkEventControllerKey *event_controller,
+ GtdNewTaskRow *self)
+{
+ GTD_ENTRY;
+
+ g_signal_emit (self, signals[ENTER], 0);
+
+ GTD_EXIT;
+}
+
+static void
+on_tasklist_popover_changed_cb (GtdTaskListPopover *popover,
+ GParamSpec *pspec,
+ GtdNewTaskRow *self)
+{
+ GTD_ENTRY;
+
+ update_secondary_icon (self);
+
+ GTD_EXIT;
+}
+
+static void
+on_tasklist_popover_closed_cb (GtdTaskListPopover *popover,
+ GtdNewTaskRow *self)
+{
+ GTD_ENTRY;
+
+ //gtk_entry_grab_focus_without_selecting (self->entry);
+ gtk_widget_grab_focus (GTK_WIDGET (self->entry));
+
+ GTD_EXIT;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_new_task_row_dispose (GObject *object)
+{
+ GtdNewTaskRow *self = (GtdNewTaskRow *) object;
+
+ if (self->tasklist_popover)
+ {
+ gtk_widget_unparent (GTK_WIDGET (self->tasklist_popover));
+ self->tasklist_popover = NULL;
+ }
+
+ if (self->entry)
+ {
+ gtk_widget_unparent (GTK_WIDGET (self->entry));
+ self->entry = NULL;
+ }
+
+ G_OBJECT_CLASS (gtd_new_task_row_parent_class)->dispose (object);
+}
+
+static void
+gtd_new_task_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+}
+
+static void
+gtd_new_task_row_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_new_task_row_class_init (GtdNewTaskRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gtd_new_task_row_dispose;
+ object_class->get_property = gtd_new_task_row_get_property;
+ object_class->set_property = gtd_new_task_row_set_property;
+
+ /**
+ * GtdNewTaskRow::enter:
+ *
+ * Emitted when the row is focused and in the editing state.
+ */
+ signals[ENTER] = g_signal_new ("enter",
+ GTD_TYPE_NEW_TASK_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdNewTaskRow::exit:
+ *
+ * Emitted when the row is unfocused and leaves the editing state.
+ */
+ signals[EXIT] = g_signal_new ("exit",
+ GTD_TYPE_NEW_TASK_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-new-task-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdNewTaskRow, entry);
+ gtk_widget_class_bind_template_child (widget_class, GtdNewTaskRow, tasklist_popover);
+
+ gtk_widget_class_bind_template_callback (widget_class, entry_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_entry_icon_released_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_focus_enter_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_tasklist_popover_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_tasklist_popover_closed_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "newtaskrow");
+
+ g_type_ensure (GTD_TYPE_TASK_LIST_POPOVER);
+}
+
+static void
+gtd_new_task_row_init (GtdNewTaskRow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_widget_set_parent (GTK_WIDGET (self->tasklist_popover), GTK_WIDGET (self->entry));
+ update_secondary_icon (self);
+}
+
+GtkWidget*
+gtd_new_task_row_new (void)
+{
+ return g_object_new (GTD_TYPE_NEW_TASK_ROW, NULL);
+}
+
+gboolean
+gtd_new_task_row_get_active (GtdNewTaskRow *self)
+{
+ g_return_val_if_fail (GTD_IS_NEW_TASK_ROW (self), FALSE);
+
+ return gtk_widget_has_focus (GTK_WIDGET (self->entry));
+}
+
+void
+gtd_new_task_row_set_active (GtdNewTaskRow *self,
+ gboolean active)
+{
+ g_return_if_fail (GTD_IS_NEW_TASK_ROW (self));
+
+ if (active)
+ gtk_widget_grab_focus (GTK_WIDGET (self->entry));
+}
+
+void
+gtd_new_task_row_set_show_list_selector (GtdNewTaskRow *self,
+ gboolean show_list_selector)
+{
+ g_return_if_fail (GTD_IS_NEW_TASK_ROW (self));
+
+ if (self->show_list_selector == show_list_selector)
+ return;
+
+ self->show_list_selector = show_list_selector;
+ update_secondary_icon (self);
+}
diff --git a/src/gui/gtd-new-task-row.h b/src/gui/gtd-new-task-row.h
new file mode 100644
index 0000000..cb97fe8
--- /dev/null
+++ b/src/gui/gtd-new-task-row.h
@@ -0,0 +1,43 @@
+/* gtd-new-task-row.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_NEW_TASK_ROW_H
+#define GTD_NEW_TASK_ROW_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_NEW_TASK_ROW (gtd_new_task_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdNewTaskRow, gtd_new_task_row, GTD, NEW_TASK_ROW, GtkListBoxRow)
+
+GtkWidget* gtd_new_task_row_new (void);
+
+gboolean gtd_new_task_row_get_active (GtdNewTaskRow *self);
+
+void gtd_new_task_row_set_active (GtdNewTaskRow *self,
+ gboolean active);
+
+void gtd_new_task_row_set_show_list_selector (GtdNewTaskRow *self,
+ gboolean show_list_selector);
+
+G_END_DECLS
+
+#endif /* GTD_NEW_TASK_ROW_H */
+
diff --git a/src/gui/gtd-new-task-row.ui b/src/gui/gtd-new-task-row.ui
new file mode 100644
index 0000000..a0da71d
--- /dev/null
+++ b/src/gui/gtd-new-task-row.ui
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdNewTaskRow" parent="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <property name="can_focus">1</property>
+ <property name="margin-top">12</property>
+ <property name="height-request">42</property>
+ <property name="css-name">newtaskrow</property>
+ <child>
+ <object class="GtkEntry" id="entry">
+ <property name="can_focus">1</property>
+ <property name="hexpand">1</property>
+ <property name="placeholder-text" translatable="yes">New task…</property>
+ <property name="primary-icon-name">list-add-symbolic</property>
+ <signal name="activate" handler="entry_activated_cb" object="GtdNewTaskRow" swapped="yes"/>
+ <signal name="icon-release" handler="on_entry_icon_released_cb" object="GtdNewTaskRow" swapped="no"/>
+ <child>
+ <object class="GtkEventControllerFocus">
+ <signal name="enter" handler="on_focus_enter_cb" object="GtdNewTaskRow" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtdTaskListPopover" id="tasklist_popover">
+ <property name="can_focus">False</property>
+ <signal name="notify::task-list" handler="on_tasklist_popover_changed_cb" object="GtdNewTaskRow" swapped="no"/>
+ <signal name="closed" handler="on_tasklist_popover_closed_cb" object="GtdNewTaskRow" swapped="no" after="yes"/>
+ </object>
+</interface>
diff --git a/src/gui/gtd-omni-area-addin.c b/src/gui/gtd-omni-area-addin.c
new file mode 100644
index 0000000..3ceec50
--- /dev/null
+++ b/src/gui/gtd-omni-area-addin.c
@@ -0,0 +1,69 @@
+/* gtd-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-omni-area-addin.h"
+
+#include "gtd-omni-area.h"
+
+G_DEFINE_INTERFACE (GtdOmniAreaAddin, gtd_omni_area_addin, G_TYPE_OBJECT)
+
+static void
+gtd_omni_area_addin_default_init (GtdOmniAreaAddinInterface *iface)
+{
+}
+
+/**
+ * gtd_omni_area_addin_load:
+ * @self: an #GtdOmniAreaAddin
+ * @omni_bar: an #GtdOmniArea
+ *
+ * Requests that the #GtdOmniAreaAddin initialize, possibly modifying
+ * @omni_bar as necessary.
+ */
+void
+gtd_omni_area_addin_load (GtdOmniAreaAddin *self,
+ GtdOmniArea *omni_bar)
+{
+ g_return_if_fail (GTD_IS_OMNI_AREA_ADDIN (self));
+ g_return_if_fail (GTD_IS_OMNI_AREA (omni_bar));
+
+ if (GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->load)
+ GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->load (self, omni_bar);
+}
+
+/**
+ * gtd_omni_area_addin_unload:
+ * @self: an #GtdOmniAreaAddin
+ * @omni_bar: an #GtdOmniArea
+ *
+ * Requests that the #GtdOmniAreaAddin shutdown, possibly modifying
+ * @omni_bar as necessary to return it to the original state before
+ * the addin was loaded.
+ */
+void
+gtd_omni_area_addin_unload (GtdOmniAreaAddin *self,
+ GtdOmniArea *omni_bar)
+{
+ g_return_if_fail (GTD_IS_OMNI_AREA_ADDIN (self));
+ g_return_if_fail (GTD_IS_OMNI_AREA (omni_bar));
+
+ if (GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->unload)
+ GTD_OMNI_AREA_ADDIN_GET_IFACE (self)->unload (self, omni_bar);
+}
diff --git a/src/gui/gtd-omni-area-addin.h b/src/gui/gtd-omni-area-addin.h
new file mode 100644
index 0000000..28e3bd9
--- /dev/null
+++ b/src/gui/gtd-omni-area-addin.h
@@ -0,0 +1,49 @@
+/* gtd-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>
+
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_OMNI_AREA_ADDIN (gtd_omni_area_addin_get_type ())
+G_DECLARE_INTERFACE (GtdOmniAreaAddin, gtd_omni_area_addin, GTD, OMNI_AREA_ADDIN, GObject)
+
+struct _GtdOmniAreaAddinInterface
+{
+ GTypeInterface parent;
+
+ void (*load) (GtdOmniAreaAddin *self,
+ GtdOmniArea *omni_bar);
+
+ void (*unload) (GtdOmniAreaAddin *self,
+ GtdOmniArea *omni_bar);
+};
+
+void gtd_omni_area_addin_load (GtdOmniAreaAddin *self,
+ GtdOmniArea *omni_bar);
+
+void gtd_omni_area_addin_unload (GtdOmniAreaAddin *self,
+ GtdOmniArea *omni_bar);
+
+G_END_DECLS
diff --git a/src/gui/gtd-omni-area.c b/src/gui/gtd-omni-area.c
new file mode 100644
index 0000000..828f36c
--- /dev/null
+++ b/src/gui/gtd-omni-area.c
@@ -0,0 +1,256 @@
+/* gtd-omni-area.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 "GtdOmniArea"
+
+#include "gtd-omni-area.h"
+
+#include "gtd-debug.h"
+#include "gtd-max-size-layout.h"
+#include "gtd-omni-area-addin.h"
+
+#include <libpeas/peas.h>
+
+struct _GtdOmniArea
+{
+ GtdWidget parent;
+
+ GtkStack *main_stack;
+ GtkStack *status_stack;
+
+ PeasExtensionSet *addins;
+
+ GQueue *messages;
+ guint current;
+
+ guint switch_messages_timeout_id;
+};
+
+G_DEFINE_TYPE (GtdOmniArea, gtd_omni_area, GTD_TYPE_WIDGET)
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+show_message (GtdOmniArea *self,
+ guint message_index)
+{
+ const gchar *message_id;
+
+ message_id = g_queue_peek_nth (self->messages, message_index);
+
+ gtk_stack_set_visible_child_name (self->status_stack, message_id);
+ self->current = message_index;
+}
+
+
+
+/*
+ * Callbacks
+ */
+
+static gboolean
+switch_message_cb (gpointer user_data)
+{
+ GtdOmniArea *self = GTD_OMNI_AREA (user_data);
+ gint next_message_index;
+ guint n_messages;
+
+ n_messages = g_queue_get_length (self->messages);
+ gtk_stack_set_visible_child_name (self->main_stack, n_messages > 0 ? "messages" : "placeholder");
+
+ next_message_index = (self->current + 1) % n_messages;
+ show_message (self, next_message_index);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+on_omni_area_addin_added_cb (PeasExtensionSet *extension_set,
+ PeasPluginInfo *plugin_info,
+ GtdOmniAreaAddin *addin,
+ GtdOmniArea *self)
+{
+ gtd_omni_area_addin_load (addin, self);
+}
+
+static void
+on_omni_area_addin_removed_cb (PeasExtensionSet *extension_set,
+ PeasPluginInfo *plugin_info,
+ GtdOmniAreaAddin *addin,
+ GtdOmniArea *self)
+{
+ gtd_omni_area_addin_unload (addin, self);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_omni_area_dispose (GObject *object)
+{
+ GtdOmniArea *self = GTD_OMNI_AREA (object);
+
+ g_clear_object (&self->addins);
+
+ G_OBJECT_CLASS (gtd_omni_area_parent_class)->dispose (object);
+}
+
+static void
+gtd_omni_area_finalize (GObject *object)
+{
+ GtdOmniArea *self = (GtdOmniArea *)object;
+
+ g_clear_handle_id (&self->switch_messages_timeout_id, g_source_remove);
+ g_queue_free_full (self->messages, g_free);
+
+ G_OBJECT_CLASS (gtd_omni_area_parent_class)->finalize (object);
+}
+
+static void
+gtd_omni_area_class_init (GtdOmniAreaClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gtd_omni_area_dispose;
+ object_class->finalize = gtd_omni_area_finalize;
+
+ g_type_ensure (GTD_TYPE_MAX_SIZE_LAYOUT);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-omni-area.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdOmniArea, main_stack);
+ gtk_widget_class_bind_template_child (widget_class, GtdOmniArea, status_stack);
+
+ gtk_widget_class_set_css_name (widget_class, "omniarea");
+}
+
+static void
+gtd_omni_area_init (GtdOmniArea *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->messages = g_queue_new ();
+ self->addins = peas_extension_set_new (peas_engine_get_default (),
+ GTD_TYPE_OMNI_AREA_ADDIN,
+ NULL);
+
+ peas_extension_set_foreach (self->addins,
+ (PeasExtensionSetForeachFunc) on_omni_area_addin_added_cb,
+ self);
+
+ g_signal_connect (self->addins, "extension-added", G_CALLBACK (on_omni_area_addin_added_cb), self);
+ g_signal_connect (self->addins, "extension-removed", G_CALLBACK (on_omni_area_addin_removed_cb), self);
+}
+
+/**
+ * gtd_omni_area_push_message:
+ * @self: a #GtdOmniArea
+ * @id: an identifier for this notification
+ * @text: user visible text of the notification
+ * @icon: (nullable): a #GIcon
+ *
+ * Pushes a new message to @self.
+ *
+ */
+void
+gtd_omni_area_push_message (GtdOmniArea *self,
+ const gchar *id,
+ const gchar *text,
+ GIcon *icon)
+{
+ GtkWidget *label;
+ GtkWidget *box;
+
+ g_return_if_fail (GTD_IS_OMNI_AREA (self));
+ g_return_if_fail (id != NULL);
+ g_return_if_fail (text != NULL);
+
+ GTD_ENTRY;
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 18);
+
+ label = gtk_label_new (text);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+ gtk_box_append (GTK_BOX (box), label);
+
+ if (icon)
+ {
+ GtkWidget *image = gtk_image_new_from_gicon (icon);
+
+ gtk_widget_add_css_class (image, "dim-label");
+ gtk_box_append (GTK_BOX (box), image);
+ }
+
+ gtk_stack_add_named (self->status_stack, box, id);
+
+ g_debug ("Adding message '%s' to Omni Area", id);
+
+ g_queue_push_tail (self->messages, g_strdup (id));
+ show_message (self, g_queue_get_length (self->messages) - 1);
+
+ g_clear_handle_id (&self->switch_messages_timeout_id, g_source_remove);
+ self->switch_messages_timeout_id = g_timeout_add (7500, switch_message_cb, self);
+
+ GTD_EXIT;
+}
+
+/**
+ * gtd_omni_area_withdraw_message:
+ * @self: a #GtdOmniArea
+ * @id: an identifier for this notification
+ *
+ * Withdraws a message from @self. If a message with @id doesn't
+ * exist, nothing happens.
+ */
+void
+gtd_omni_area_withdraw_message (GtdOmniArea *self,
+ const gchar *id)
+{
+ GtkWidget *widget;
+ GList *l;
+
+ g_return_if_fail (GTD_IS_OMNI_AREA (self));
+ g_return_if_fail (id != NULL);
+
+ GTD_ENTRY;
+
+ widget = gtk_stack_get_child_by_name (self->status_stack, id);
+ if (!widget)
+ return;
+
+ g_debug ("Removing message '%s' from Omni Area", id);
+
+ gtk_stack_remove (self->status_stack, widget);
+
+ l = g_queue_find_custom (self->messages, id, (GCompareFunc) g_strcmp0);
+ g_queue_delete_link (self->messages, l);
+
+ if (g_queue_get_length (self->messages) == 0)
+ gtk_stack_set_visible_child_name (self->main_stack, "placeholder");
+
+ GTD_EXIT;
+}
diff --git a/src/gui/gtd-omni-area.h b/src/gui/gtd-omni-area.h
new file mode 100644
index 0000000..9d49c38
--- /dev/null
+++ b/src/gui/gtd-omni-area.h
@@ -0,0 +1,41 @@
+/* gtd-omni-area.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 <glib-object.h>
+
+#include "gtd-widget.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_OMNI_AREA (gtd_omni_area_get_type())
+G_DECLARE_FINAL_TYPE (GtdOmniArea, gtd_omni_area, GTD, OMNI_AREA, GtdWidget)
+
+void gtd_omni_area_push_message (GtdOmniArea *self,
+ const gchar *id,
+ const gchar *text,
+ GIcon *icon);
+
+void gtd_omni_area_withdraw_message (GtdOmniArea *self,
+ const gchar *id);
+
+G_END_DECLS
diff --git a/src/gui/gtd-omni-area.ui b/src/gui/gtd-omni-area.ui
new file mode 100644
index 0000000..b04b918
--- /dev/null
+++ b/src/gui/gtd-omni-area.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtdOmniArea" parent="GtdWidget">
+ <property name="layout-manager">
+ <object class="GtkBinLayout" />
+ </property>
+ <child>
+ <object class="GtkCenterBox">
+
+ <child type="start">
+ <object class="GtkBox" id="start_box">
+ <property name="margin-end">12</property>
+ </object>
+ </child>
+
+ <child type="center">
+ <object class="GtkFrame">
+ <property name="css-name"></property>
+ <property name="layout-manager">
+ <object class="GtdMaxSizeLayout">
+ <property name="width-chars">25</property>
+ <property name="max-width-chars">40</property>
+ </object>
+ </property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="hexpand">true</property>
+ <property name="halign">center</property>
+
+ <child>
+ <object class="GtkStack" id="main_stack">
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="hhomogeneous">true</property>
+ <property name="transition-type">slide-up-down</property>
+ <property name="transition-duration">500</property>
+
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">placeholder</property>
+ <property name="child">
+ <object class="AdwWindowTitle">
+ <property name="title" translatable="yes">Endeavour</property>
+ </object>
+ </property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">messages</property>
+ <property name="child">
+ <object class="GtkStack" id="status_stack">
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="hhomogeneous">true</property>
+ <property name="transition-type">slide-up-down</property>
+ <property name="transition-duration">500</property>
+ </object>
+ </property>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ <child type="end">
+ <object class="GtkBox" id="end_box">
+ <property name="margin-start">12</property>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/src/gui/gtd-panel.c b/src/gui/gtd-panel.c
new file mode 100644
index 0000000..5c1b5df
--- /dev/null
+++ b/src/gui/gtd-panel.c
@@ -0,0 +1,313 @@
+/* gtd-panel.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 "GtdPanel"
+
+#include "gtd-panel.h"
+
+/**
+ * SECTION:gtd-panel
+ * @short_description: interface for panels
+ * @title:GtdPanel
+ * @stability:Unstable
+ *
+ * The #GtdPanel interface must be implemented by plugins that want
+ * a given widget to be shown as a panel in the main window. Examples
+ * of panels are the "Today" and "Scheduled" panels.
+ *
+ * A panel must have a unique name (see #GtdPanel:name) and a title.
+ * The title can change dynamically. Avoid long titles.
+ *
+ * The panel may also provide header widgets, which will be placed
+ * in the headerbar according to the #GtkWidget:halign property. See
+ * gtd_panel_get_header_widgets() for a detailed explanation.
+ *
+ * The #GtdPanel:icon and #GtdPanel:priority properties are used by
+ * the sidebar. The former is used to display the icon, and the latter
+ * is used to determine the position of the panel relative to the
+ * others panels.
+ *
+ * At last, a #GtdPanel implementation may provide a #GMenu that will
+ * be appended to the window menu. Alternatively, a #GtkPopover can
+ * also be set. Popovers are used when both a menu and a popover are
+ * provided.
+ */
+
+G_DEFINE_INTERFACE (GtdPanel, gtd_panel, GTK_TYPE_WIDGET)
+
+static void
+gtd_panel_default_init (GtdPanelInterface *iface)
+{
+
+ /**
+ * GtdPanel::icon:
+ *
+ * The icon of the panel.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_object ("icon",
+ "Icon of the panel",
+ "The icon of the panel",
+ G_TYPE_ICON,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdPanel::name:
+ *
+ * The identifier name of the panel. It is used as the #GtkStack
+ * name, so be sure to use a specific name that won't collide with
+ * other plugins.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("name",
+ "The name of the panel",
+ "The identifier name of the panel",
+ NULL,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdPanel::priority:
+ *
+ * The priority of the panel.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_uint ("priority",
+ "Priority of the panel",
+ "The priority of the panel",
+ 0,
+ G_MAXUINT32,
+ 0,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdPanel::subtitle:
+ *
+ * The subtitle of the panel. It usually is the counter of not
+ * completed tasks.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("subtitle",
+ "The subtitle of the panel",
+ "The user-visible subtitle of the panel",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdPanel::title:
+ *
+ * The user-visible title of the panel. It is used as the #GtkStack
+ * title.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("title",
+ "The title of the panel",
+ "The user-visible title of the panel",
+ NULL,
+ G_PARAM_READABLE));
+
+ /**
+ * GtdPanel::menu:
+ *
+ * A #GMenu of entries of the window menu.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_object ("menu",
+ "The title of the panel",
+ "The user-visible title of the panel",
+ G_TYPE_MENU,
+ G_PARAM_READABLE));
+
+}
+
+/**
+ * gtd_panel_get_panel_name:
+ * @panel: a #GtdPanel
+ *
+ * Retrieves the name of @panel
+ *
+ * Returns: (transfer none): the name of @panel
+ */
+const gchar*
+gtd_panel_get_panel_name (GtdPanel *panel)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (panel), NULL);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_panel_name, NULL);
+
+ return GTD_PANEL_GET_IFACE (panel)->get_panel_name (panel);
+}
+
+/**
+ * gtd_panel_get_panel_title:
+ * @panel: a #GtdPanel
+ *
+ * Retrieves the title of @panel
+ *
+ * Returns: (transfer none): the title of @panel
+ */
+const gchar*
+gtd_panel_get_panel_title (GtdPanel *panel)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (panel), NULL);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_panel_title, NULL);
+
+ return GTD_PANEL_GET_IFACE (panel)->get_panel_title (panel);
+}
+
+/**
+ * gtd_panel_get_header_widgets:
+ * @panel: a #GtdPanel
+ *
+ * Retrieves the list of widgets to be placed at headerbar. The
+ * position of the widget is determined by the #GtkWidget::halign
+ * property.
+ *
+ * Widgets with @GTK_ALIGN_START halign will be packed into the
+ * start of the headerbar, and @GTK_ALIGN_END at the end. Other
+ * values are silently ignored.
+ *
+ * Returns: (transfer container) (element-type Gtk.Widget): the list of #GtkWidget
+ */
+GList*
+gtd_panel_get_header_widgets (GtdPanel *panel)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (panel), NULL);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_header_widgets, NULL);
+
+ return GTD_PANEL_GET_IFACE (panel)->get_header_widgets (panel);
+}
+
+/**
+ * gtd_panel_get_menu:
+ * @panel: a #GtdPanel
+ *
+ * Retrieves the gear menu of @panel.
+ *
+ * Returns: (transfer none): a #GMenu
+ */
+const GMenu*
+gtd_panel_get_menu (GtdPanel *panel)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (panel), NULL);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (panel)->get_menu, NULL);
+
+ return GTD_PANEL_GET_IFACE (panel)->get_menu (panel);
+}
+
+/**
+ * gtd_panel_get_icon:
+ * @self: a #GtdPanel
+ *
+ * Retrieves the icon of @self.
+ *
+ * Returns: (transfer full)(nullable): a #GIcon
+ */
+GIcon*
+gtd_panel_get_icon (GtdPanel *self)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (self), NULL);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (self)->get_icon, NULL);
+
+ return GTD_PANEL_GET_IFACE (self)->get_icon (self);
+}
+
+/**
+ * gtd_panel_get_popover:
+ * @self: a #GtdPanel
+ *
+ * Retrieves the popover of @self. It is used as the
+ * window menu when available.
+ *
+ * Returns: (nullable)(transfer none): a #GtkPopover
+ */
+GtkPopover*
+gtd_panel_get_popover (GtdPanel *self)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (self), 0);
+
+ if (GTD_PANEL_GET_IFACE (self)->get_popover)
+ return GTD_PANEL_GET_IFACE (self)->get_popover (self);
+
+ return NULL;
+}
+
+/**
+ * gtd_panel_get_priority:
+ * @self: a #GtdPanel
+ *
+ * Retrieves the priority of @self. This value is used to
+ * determine the position of the panel in the sidebar.
+ *
+ * Returns: the priority of @self
+ */
+guint32
+gtd_panel_get_priority (GtdPanel *self)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (self), 0);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (self)->get_priority, 0);
+
+ return GTD_PANEL_GET_IFACE (self)->get_priority (self);
+}
+
+/**
+ * gtd_panel_get_subtitle:
+ * @self: a #GtdPanel
+ *
+ * Retrieves the subtitle of @panel
+ *
+ * Returns: (transfer full): the subtitle of @panel
+ */
+gchar*
+gtd_panel_get_subtitle (GtdPanel *self)
+{
+ g_return_val_if_fail (GTD_IS_PANEL (self), NULL);
+ g_return_val_if_fail (GTD_PANEL_GET_IFACE (self)->get_icon, NULL);
+
+ return GTD_PANEL_GET_IFACE (self)->get_subtitle (self);
+}
+
+/**
+ * gtd_panel_activate:
+ * @self: a #GtdPanel
+ * @parameters: (nullable): parameters of the panel
+ *
+ * Activates the panel with @parameters. The passed parameters
+ * are in free form, to allow panels have any input they want.
+ *
+ * This is an optional vfunc.
+ */
+void
+gtd_panel_activate (GtdPanel *self,
+ GVariant *parameters)
+{
+
+ g_return_if_fail (GTD_IS_PANEL (self));
+
+ if (GTD_PANEL_GET_IFACE (self)->activate)
+ {
+ g_autofree gchar *formatted_params = NULL;
+
+ if (parameters)
+ formatted_params = g_variant_print (parameters, TRUE);
+
+ g_debug ("Activating %s with parameters %s",
+ G_OBJECT_TYPE_NAME (self),
+ formatted_params);
+
+ GTD_PANEL_GET_IFACE (self)->activate (self, parameters);
+ }
+}
diff --git a/src/gui/gtd-panel.h b/src/gui/gtd-panel.h
new file mode 100644
index 0000000..787a0e3
--- /dev/null
+++ b/src/gui/gtd-panel.h
@@ -0,0 +1,77 @@
+/* gtd-panel.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_H
+#define GTD_PANEL_H
+
+#include <glib-object.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PANEL (gtd_panel_get_type ())
+
+G_DECLARE_INTERFACE (GtdPanel, gtd_panel, GTD, PANEL, GtkWidget)
+
+struct _GtdPanelInterface
+{
+ GTypeInterface parent;
+
+ const gchar* (*get_panel_name) (GtdPanel *panel);
+
+ const gchar* (*get_panel_title) (GtdPanel *panel);
+
+ GList* (*get_header_widgets) (GtdPanel *panel);
+
+ const GMenu* (*get_menu) (GtdPanel *panel);
+
+ GIcon* (*get_icon) (GtdPanel *self);
+
+ GtkPopover* (*get_popover) (GtdPanel *self);
+
+ guint32 (*get_priority) (GtdPanel *self);
+
+ gchar* (*get_subtitle) (GtdPanel *self);
+
+ void (*activate) (GtdPanel *self,
+ GVariant *parameters);
+};
+
+const gchar* gtd_panel_get_panel_name (GtdPanel *panel);
+
+const gchar* gtd_panel_get_panel_title (GtdPanel *panel);
+
+GList* gtd_panel_get_header_widgets (GtdPanel *panel);
+
+const GMenu* gtd_panel_get_menu (GtdPanel *panel);
+
+GIcon* gtd_panel_get_icon (GtdPanel *self);
+
+GtkPopover* gtd_panel_get_popover (GtdPanel *self);
+
+guint32 gtd_panel_get_priority (GtdPanel *self);
+
+gchar* gtd_panel_get_subtitle (GtdPanel *self);
+
+void gtd_panel_activate (GtdPanel *self,
+ GVariant *parameters);
+
+G_END_DECLS
+
+#endif /* GTD_PANEL_H */
diff --git a/src/gui/gtd-provider-popover.c b/src/gui/gtd-provider-popover.c
new file mode 100644
index 0000000..0fa580d
--- /dev/null
+++ b/src/gui/gtd-provider-popover.c
@@ -0,0 +1,245 @@
+/* gtd-provider-popover.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 "GtdProviderPopover"
+
+#include "gtd-manager.h"
+#include "gtd-provider.h"
+#include "gtd-provider-popover.h"
+#include "gtd-provider-selector.h"
+#include "gtd-task-list.h"
+
+#include <glib/gi18n.h>
+
+struct _GtdProviderPopover
+{
+ GtkPopover parent;
+
+ GtkWidget *change_location_button;
+ GtkWidget *location_provider_image;
+ GtkWidget *new_list_create_button;
+ GtkEditable *new_list_name_entry;
+ GtkWidget *stack;
+ GtkWidget *provider_selector;
+};
+
+G_DEFINE_TYPE (GtdProviderPopover, gtd_provider_popover, GTK_TYPE_POPOVER)
+
+static void
+clear_and_hide (GtdProviderPopover *popover)
+{
+ GtdManager *manager;
+ GList *locations;
+ GList *l;
+
+ g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover));
+
+ manager = gtd_manager_get_default ();
+
+ /* Select the default source again */
+ locations = gtd_manager_get_providers (manager);
+
+ for (l = locations; l != NULL; l = l->next)
+ {
+ if (FALSE)//gtd_provider_get_is_default (l->data))
+ {
+ gtd_provider_selector_set_selected_provider (GTD_PROVIDER_SELECTOR (popover->provider_selector), l->data);
+ break;
+ }
+ }
+
+ g_list_free (locations);
+
+ /* By clearing the text, Create button will get insensitive */
+ gtk_editable_set_text (popover->new_list_name_entry, "");
+
+ /* Hide the popover at last */
+ gtk_widget_hide (GTK_WIDGET (popover));
+}
+
+static void
+gtd_provider_popover__closed (GtdProviderPopover *popover)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover));
+
+ gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "main");
+}
+
+static void
+on_task_list_created_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+
+ gtd_provider_create_task_list_finish (GTD_PROVIDER (source), result, &error);
+
+ if (error)
+ {
+ g_warning ("Error creating task list: %s", error->message);
+
+ gtd_manager_emit_error_message (gtd_manager_get_default (),
+ _("An error occurred while creating a task list"),
+ error->message,
+ NULL,
+ NULL);
+ }
+}
+
+static void
+create_task_list (GtdProviderPopover *popover)
+{
+ GtdProvider *provider;
+ const gchar *name;
+
+ provider = gtd_provider_selector_get_selected_provider (GTD_PROVIDER_SELECTOR (popover->provider_selector));
+ name = gtk_editable_get_text (popover->new_list_name_entry);
+
+ gtd_provider_create_task_list (provider,
+ name,
+ NULL,
+ on_task_list_created_cb,
+ popover);
+}
+
+static void
+gtd_provider_popover__entry_activate (GtdProviderPopover *popover,
+ GtkEntry *entry)
+{
+ if (gtk_entry_get_text_length (entry) > 0)
+ {
+ create_task_list (popover);
+ clear_and_hide (popover);
+ }
+}
+
+static void
+gtd_provider_popover__action_button_clicked (GtdProviderPopover *popover,
+ GtkWidget *button)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover));
+
+ if (button == popover->new_list_create_button)
+ create_task_list (popover);
+
+ clear_and_hide (popover);
+}
+
+static void
+gtd_provider_popover__text_changed_cb (GtdProviderPopover *popover,
+ GParamSpec *spec,
+ GtkEntry *entry)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover));
+ g_return_if_fail (GTK_IS_ENTRY (entry));
+
+ gtk_widget_set_sensitive (popover->new_list_create_button, gtk_entry_get_text_length (entry) > 0);
+}
+
+static void
+gtd_provider_popover__change_location_clicked (GtdProviderPopover *popover,
+ GtkWidget *button)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover));
+
+ if (button == popover->change_location_button)
+ gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "selector");
+ else
+ gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "main");
+}
+
+static void
+gtd_provider_popover__provider_selected (GtdProviderPopover *popover,
+ GtdProvider *provider)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_POPOVER (popover));
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+
+ if (provider)
+ {
+ gtk_image_set_from_gicon (GTK_IMAGE (popover->location_provider_image), gtd_provider_get_icon (provider));
+ gtk_widget_set_tooltip_text (popover->change_location_button, gtd_provider_get_name (provider));
+
+ /* Go back immediately after selecting a provider */
+ gtk_stack_set_visible_child_name (GTK_STACK (popover->stack), "main");
+
+ if (gtk_widget_is_visible (GTK_WIDGET (popover)))
+ gtk_widget_grab_focus (GTK_WIDGET (popover->new_list_name_entry));
+ }
+}
+
+static void
+gtd_provider_popover_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (gtd_provider_popover_parent_class)->finalize (object);
+}
+
+static void
+gtd_provider_popover_constructed (GObject *object)
+{
+ GtdProviderPopover *popover;
+ GtdProvider *provider;
+
+ G_OBJECT_CLASS (gtd_provider_popover_parent_class)->constructed (object);
+
+ popover = GTD_PROVIDER_POPOVER (object);
+ provider = gtd_provider_selector_get_selected_provider (GTD_PROVIDER_SELECTOR (popover->provider_selector));
+
+ if (provider)
+ gtk_image_set_from_gicon (GTK_IMAGE (popover->location_provider_image), gtd_provider_get_icon (provider));
+}
+
+static void
+gtd_provider_popover_class_init (GtdProviderPopoverClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_provider_popover_finalize;
+ object_class->constructed = gtd_provider_popover_constructed;
+
+ g_type_ensure (GTD_TYPE_PROVIDER_SELECTOR);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-provider-popover.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, change_location_button);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, location_provider_image);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, new_list_create_button);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, new_list_name_entry);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, stack);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderPopover, provider_selector);
+
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__action_button_clicked);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__change_location_clicked);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__closed);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__entry_activate);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__provider_selected);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_popover__text_changed_cb);
+}
+
+static void
+gtd_provider_popover_init (GtdProviderPopover *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget*
+gtd_provider_popover_new (void)
+{
+ return g_object_new (GTD_TYPE_PROVIDER_POPOVER, NULL);
+}
diff --git a/src/gui/gtd-provider-popover.h b/src/gui/gtd-provider-popover.h
new file mode 100644
index 0000000..9b97583
--- /dev/null
+++ b/src/gui/gtd-provider-popover.h
@@ -0,0 +1,34 @@
+/* gtd-provider-popover.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_POPOVER_H
+#define GTD_PROVIDER_POPOVER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PROVIDER_POPOVER (gtd_provider_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdProviderPopover, gtd_provider_popover, GTD, PROVIDER_POPOVER, GtkPopover)
+
+GtkWidget* gtd_provider_popover_new (void);
+
+G_END_DECLS
+
+#endif /* GTD_PROVIDER_POPOVER_H */
diff --git a/src/gui/gtd-provider-popover.ui b/src/gui/gtd-provider-popover.ui
new file mode 100644
index 0000000..75c033f
--- /dev/null
+++ b/src/gui/gtd-provider-popover.ui
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtdProviderPopover" parent="GtkPopover">
+ <signal name="closed" handler="gtd_provider_popover__closed" swapped="no"/>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="vhomogeneous">0</property>
+ <property name="interpolate_size">1</property>
+ <property name="transition_duration">300</property>
+ <property name="transition_type">slide-left-right</property>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">main</property>
+ <property name="child">
+ <object class="GtkGrid" id="new_list_popover_grid">
+ <property name="hexpand">1</property>
+ <property name="row_spacing">12</property>
+ <property name="column_spacing">12</property>
+ <child>
+ <object class="GtkButton" id="new_list_create_button">
+ <property name="label" translatable="yes">Create _List</property>
+ <property name="use_underline">1</property>
+ <property name="sensitive">0</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="gtd_provider_popover__action_button_clicked" object="GtdProviderPopover" swapped="yes"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="new_list_cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use_underline">1</property>
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="gtd_provider_popover__action_button_clicked" object="GtdProviderPopover" swapped="yes"/>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="new_list_popover_dim_label">
+ <property name="label" translatable="yes">List Name</property>
+ <property name="xalign">0</property>
+ <property name="halign">0</property>
+ <style>
+ <class name="heading"/>
+ </style>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="column-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="location_box">
+ <child>
+ <object class="GtkEntry" id="new_list_name_entry">
+ <property name="can_focus">1</property>
+ <property name="hexpand">1</property>
+ <property name="width_chars">35</property>
+ <signal name="notify::text" handler="gtd_provider_popover__text_changed_cb" object="GtdProviderPopover" swapped="yes"/>
+ <signal name="activate" handler="gtd_provider_popover__entry_activate" object="GtdProviderPopover" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="change_location_button">
+ <property name="can_focus">1</property>
+ <property name="receives_default">1</property>
+ <signal name="clicked" handler="gtd_provider_popover__change_location_clicked" object="GtdProviderPopover" swapped="yes"/>
+ <child>
+ <object class="GtkImage" id="location_provider_image">
+ <property name="pixel_size">16</property>
+ <property name="icon_name">goa-account</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <style>
+ <class name="linked"/>
+ </style>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ <property name="column-span">2</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">selector</property>
+ <property name="child">
+ <object class="GtkGrid" id="selector_grid">
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="title_label">
+ <property name="hexpand">1</property>
+ <property name="label" translatable="yes">Select a storage location</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="column-span">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="back_button">
+ <property name="halign">start</property>
+ <signal name="clicked" handler="gtd_provider_popover__change_location_clicked" object="GtdProviderPopover" swapped="yes"/>
+ <style>
+ <class name="flat" />
+ </style>
+ <child>
+ <object class="GtkImage" id="back_image">
+ <property name="icon-name">go-previous-symbolic</property>
+ <property name="pixel-size">16</property>
+ </object>
+ </child>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="column-span">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtdProviderSelector" id="provider_selector">
+ <property name="can_focus">True</property>
+ <property name="show_local">True</property>
+ <property name="show_stub_rows">False</property>
+ <signal name="provider-selected" handler="gtd_provider_popover__provider_selected" object="GtdProviderPopover" swapped="yes"/>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ <property name="column-span">2</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gui/gtd-provider-row.c b/src/gui/gtd-provider-row.c
new file mode 100644
index 0000000..29aeebd
--- /dev/null
+++ b/src/gui/gtd-provider-row.c
@@ -0,0 +1,240 @@
+/* gtd-provider-row.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 "GtdProviderRow"
+
+#include "gtd-provider.h"
+#include "gtd-provider-row.h"
+
+#include <glib/gi18n.h>
+
+typedef struct
+{
+ GtkImage *icon;
+ GtkLabel *name;
+ GtkLabel *description;
+ GtkLabel *enabled;
+ GtkImage *selected;
+
+ GtdProvider *provider;
+} GtdProviderRowPrivate;
+
+struct _GtdProviderRow
+{
+ GtkListBoxRow parent;
+
+ /*< private >*/
+ GtdProviderRowPrivate *priv;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GtdProviderRow, gtd_provider_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+ PROP_0,
+ PROP_PROVIDER,
+ LAST_PROP
+};
+
+static void
+gtd_provider_row_finalize (GObject *object)
+{
+ GtdProviderRow *self = (GtdProviderRow *)object;
+ GtdProviderRowPrivate *priv = gtd_provider_row_get_instance_private (self);
+
+ g_clear_object (&priv->provider);
+
+ G_OBJECT_CLASS (gtd_provider_row_parent_class)->finalize (object);
+}
+
+static void
+gtd_provider_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdProviderRow *self = GTD_PROVIDER_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_PROVIDER:
+ g_value_set_object (value, self->priv->provider);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_provider_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdProviderRow *self = GTD_PROVIDER_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_PROVIDER:
+ self->priv->provider = g_value_get_object (value);
+
+ if (!self->priv->provider)
+ break;
+
+ g_object_ref (self->priv->provider);
+
+ g_object_bind_property (self->priv->provider,
+ "name",
+ self->priv->name,
+ "label",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+
+ g_object_bind_property (self->priv->provider,
+ "description",
+ self->priv->description,
+ "label",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+
+ g_object_bind_property (self->priv->provider,
+ "enabled",
+ self->priv->enabled,
+ "visible",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+
+ g_object_bind_property (self->priv->provider,
+ "icon",
+ self->priv->icon,
+ "gicon",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_provider_row_class_init (GtdProviderRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_provider_row_finalize;
+ object_class->get_property = gtd_provider_row_get_property;
+ object_class->set_property = gtd_provider_row_set_property;
+
+ /**
+ * GtdProviderRow::provider:
+ *
+ * #GtdProvider related to this row.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_PROVIDER,
+ g_param_spec_object ("provider",
+ "Provider of the row",
+ "The provider that this row holds",
+ GTD_TYPE_PROVIDER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-provider-row.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, icon);
+ gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, name);
+ gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, description);
+ gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, enabled);
+ gtk_widget_class_bind_template_child_private (widget_class, GtdProviderRow, selected);
+}
+
+static void
+gtd_provider_row_init (GtdProviderRow *self)
+{
+ self->priv = gtd_provider_row_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+
+/**
+ * gtd_provider_row_new:
+ * @provider: a #GtdProvider
+ *
+ * Created a new #GtdProviderRow with @account information.
+ *
+ * Returns: (transfer full): a new #GtdProviderRow
+ */
+GtkWidget*
+gtd_provider_row_new (GtdProvider *provider)
+{
+ return g_object_new (GTD_TYPE_PROVIDER_ROW,
+ "provider", provider,
+ NULL);
+}
+
+/**
+ * gtd_provider_row_get_provider:
+ * @row: a #GtdProviderRow
+ *
+ * Retrieves the #GtdProvider associated with @row.
+ *
+ * Returns: (transfer none): the #GtdProvider associated with @row.
+ */
+GtdProvider*
+gtd_provider_row_get_provider (GtdProviderRow *row)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER_ROW (row), NULL);
+
+ return row->priv->provider;
+}
+
+/**
+ * gtd_provider_row_get_selected:
+ * @row: a #GtdProviderRow
+ *
+ * Whether @row is the currently selected row or not.
+ *
+ * Returns: %TRUE if the row is currently selected, %FALSE otherwise.
+ */
+gboolean
+gtd_provider_row_get_selected (GtdProviderRow *row)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER_ROW (row), FALSE);
+
+ return gtk_widget_get_visible (GTK_WIDGET (row->priv->selected));
+}
+
+/**
+ * gtd_provider_row_set_selected:
+ * @row: a #GtdProviderRow
+ * @selected: %TRUE if row is selected (i.e. the selection
+ * mark is visible)
+ *
+ * Sets @row as the currently selected row.
+ */
+void
+gtd_provider_row_set_selected (GtdProviderRow *row,
+ gboolean selected)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_ROW (row));
+
+ if (selected != gtk_widget_get_visible (GTK_WIDGET (row->priv->selected)))
+ {
+ gtk_widget_set_visible (GTK_WIDGET (row->priv->selected), selected);
+ }
+}
diff --git a/src/gui/gtd-provider-row.h b/src/gui/gtd-provider-row.h
new file mode 100644
index 0000000..8ccabeb
--- /dev/null
+++ b/src/gui/gtd-provider-row.h
@@ -0,0 +1,44 @@
+/* gtd-provider-row.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_GOA_ROW_H
+#define GTD_GOA_ROW_H
+
+#include "gtd-types.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PROVIDER_ROW (gtd_provider_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdProviderRow, gtd_provider_row, GTD, PROVIDER_ROW, GtkListBoxRow)
+
+GtkWidget* gtd_provider_row_new (GtdProvider *provider);
+
+gboolean gtd_provider_row_get_selected (GtdProviderRow *row);
+
+void gtd_provider_row_set_selected (GtdProviderRow *row,
+ gboolean selected);
+
+GtdProvider* gtd_provider_row_get_provider (GtdProviderRow *row);
+
+G_END_DECLS
+
+#endif /* GTD_GOA_ROW_H */
diff --git a/src/gui/gtd-provider-row.ui b/src/gui/gtd-provider-row.ui
new file mode 100644
index 0000000..9c3ce60
--- /dev/null
+++ b/src/gui/gtd-provider-row.ui
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdProviderRow" parent="GtkListBoxRow">
+ <property name="can_focus">1</property>
+ <property name="selectable">0</property>
+ <child>
+ <object class="GtkGrid" id="grid">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="column_spacing">12</property>
+ <child>
+ <object class="GtkImage" id="selected">
+ <property name="visible">0</property>
+ <property name="icon_name">emblem-ok-symbolic</property>
+ <layout>
+ <property name="column">3</property>
+ <property name="row">0</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="name">
+ <property name="hexpand">1</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="icon">
+ <property name="pixel_size">32</property>
+ <property name="icon_name">goa-account</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="description">
+ <property name="xalign">0</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="enabled">
+ <property name="label" translatable="yes">Off</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">0</property>
+ <property name="row-span">2</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gui/gtd-provider-selector.c b/src/gui/gtd-provider-selector.c
new file mode 100644
index 0000000..5f8f5cb
--- /dev/null
+++ b/src/gui/gtd-provider-selector.c
@@ -0,0 +1,695 @@
+/* gtd-provider-selector.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 "GtdProviderSelector"
+
+#include "gtd-application.h"
+#include "gtd-manager.h"
+#include "gtd-provider.h"
+#include "gtd-provider-row.h"
+#include "gtd-provider-selector.h"
+
+#include <glib/gi18n.h>
+
+struct _GtdProviderSelector
+{
+ GtkBox parent;
+
+ GtkListBox *listbox;
+ GtkWidget *local_check;
+
+ /* stub rows */
+ GtkWidget *exchange_stub_row;
+ GtkWidget *google_stub_row;
+ GtkWidget *owncloud_stub_row;
+ GtkWidget *local_row;
+
+ gint show_local_provider : 1;
+ gint show_stub_rows : 1;
+};
+
+G_DEFINE_TYPE (GtdProviderSelector, gtd_provider_selector, GTK_TYPE_BOX)
+
+enum {
+ PROP_0,
+ PROP_SHOW_LOCAL,
+ PROP_SHOW_STUB_ROWS,
+ LAST_PROP
+};
+
+enum {
+ PROVIDER_SELECTED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0, };
+
+static void
+spawn (const gchar *action,
+ const gchar *arg)
+{
+ const gchar *command[] = {"gnome-control-center", "online-accounts", action, arg, NULL};
+ g_spawn_async (NULL, (gchar **) command, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL, NULL, NULL, NULL, NULL);
+}
+
+/**
+ * display_header_func:
+ *
+ * Shows a separator before each row.
+ */
+static void
+display_header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data)
+{
+ if (before != NULL)
+ {
+ GtkWidget *header;
+
+ header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+
+ gtk_list_box_row_set_header (row, header);
+ gtk_widget_show (header);
+ }
+}
+
+static void
+unselect_rows (GtdProviderSelector *self)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (GTD_IS_PROVIDER_ROW (child))
+ gtd_provider_row_set_selected (GTD_PROVIDER_ROW (child), FALSE);
+ }
+}
+
+static void
+default_provider_changed_cb (GtdProviderSelector *selector)
+{
+ GtdProvider *current;
+ GtdManager *manager;
+ GtkWidget *child;
+
+ manager = gtd_manager_get_default ();
+ current = gtd_manager_get_default_provider (manager);
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ GtdProviderRow *provider_row;
+ GtdProvider *provider;
+
+ if (!GTD_IS_PROVIDER_ROW (child))
+ continue;
+
+ provider_row = GTD_PROVIDER_ROW (child);
+ provider = gtd_provider_row_get_provider (provider_row);
+
+ gtd_provider_row_set_selected (provider_row, provider == current);
+ }
+
+ g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, current);
+}
+
+static void
+gtd_provider_selector__listbox_row_activated (GtdProviderSelector *selector,
+ GtkWidget *row)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+
+ /* The row is either one of the stub rows, or a GtdGoaRow */
+ if (row == selector->google_stub_row)
+ {
+ spawn ("add", "google");
+ }
+ else if (row == selector->owncloud_stub_row)
+ {
+ spawn ("add", "owncloud");
+ }
+ else if (row == selector->exchange_stub_row)
+ {
+ spawn ("add", "exchange");
+ }
+ else
+ {
+ GtdProvider *provider;
+
+ provider = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (row));
+
+ unselect_rows (selector);
+
+ /*
+ * If the account has it's calendars disabled, we cannot let it
+ * be a default provider location. Instead, open the Control Center
+ * to give the user the ability to change it.
+ */
+ if (gtd_provider_get_enabled (provider))
+ {
+ gtd_provider_row_set_selected (GTD_PROVIDER_ROW (row), TRUE);
+ g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, provider);
+ }
+ else
+ {
+ spawn ((gchar*) gtd_provider_get_id (provider), NULL);
+ }
+ }
+}
+
+static void
+on_local_check_toggled_cb (GtdProviderSelector *selector,
+ GtkCheckButton *check)
+{
+
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+
+ /*
+ * Unset the currently selected provider location row when the check button is
+ * activated. No need to do this when deactivated, since we already did.
+ */
+
+ if (gtk_check_button_get_active (check))
+ {
+ GtdProvider *local_provider;
+
+ local_provider = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (selector->local_row));
+
+ unselect_rows (selector);
+
+ /*
+ * Sets the provider location to "local", and don't unset it if the
+ * check gets deactivated.
+ */
+ g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, local_provider);
+ }
+ else
+ {
+ g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, NULL);
+ }
+}
+
+static void
+remove_provider (GtdProviderSelector *selector,
+ GtdProvider *provider)
+{
+ GtkWidget *child;
+ gint exchange;
+ gint google;
+ gint owncloud;
+
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+
+ exchange = google = owncloud = 0;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ GtdProviderRow *provider_row;
+ GtdProvider *row_provider;
+ const gchar *provider_id;
+
+ if (!GTD_IS_PROVIDER_ROW (child))
+ continue;
+
+ provider_row = GTD_PROVIDER_ROW (child);
+
+ row_provider = gtd_provider_row_get_provider (provider_row);
+ provider_id = gtd_provider_get_id (row_provider);
+
+ if (row_provider == provider)
+ {
+ gtk_list_box_remove (selector->listbox, child);
+ }
+ else
+ {
+ if (g_strcmp0 (provider_id, "exchange") == 0)
+ exchange++;
+ else if (g_strcmp0 (provider_id, "google") == 0)
+ google++;
+ else if (g_strcmp0 (provider_id, "owncloud") == 0)
+ owncloud++;
+ }
+ }
+
+ gtk_widget_set_visible (selector->exchange_stub_row, exchange == 0);
+ gtk_widget_set_visible (selector->google_stub_row, google == 0);
+ gtk_widget_set_visible (selector->owncloud_stub_row, owncloud == 0);
+}
+
+static void
+add_provider (GtdProviderSelector *selector,
+ GtdProvider *provider)
+{
+ GtkWidget *row;
+ const gchar *provider_id;
+
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+ g_return_if_fail (GTD_IS_PROVIDER (provider));
+
+ row = gtd_provider_row_new (provider);
+ provider_id = gtd_provider_get_id (provider);
+
+ g_debug ("Adding provider %s", provider_id);
+
+ gtk_list_box_insert (selector->listbox, row, -1);
+
+ /* track the local provider row */
+ if (g_str_has_prefix (provider_id, "local") == 0)
+ {
+ gtk_widget_set_visible (row, selector->show_local_provider);
+ selector->local_row = row;
+ }
+
+ /* Auto selects the default provider row when needed */
+ if (!gtd_provider_selector_get_selected_provider (selector))
+ gtd_provider_selector_set_selected_provider (selector, provider);
+
+ /* hide the related stub row */
+ if (g_str_has_prefix (provider_id, "exchange") == 0)
+ gtk_widget_hide (selector->exchange_stub_row);
+ else if (g_str_has_prefix (provider_id, "google") == 0)
+ gtk_widget_hide (selector->google_stub_row);
+ else if (g_str_has_prefix (provider_id, "owncloud") == 0)
+ gtk_widget_hide (selector->owncloud_stub_row);
+}
+
+static void
+gtd_provider_selector__fill_accounts (GtdProviderSelector *selector)
+{
+ GtdManager *manager;
+ GList *providers;
+ GList *l;
+
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+
+ manager = gtd_manager_get_default ();
+
+ /* load accounts */
+ providers = gtd_manager_get_providers (manager);
+
+ for (l = providers; l != NULL; l = l->next)
+ add_provider (selector, l->data);
+
+ g_list_free (providers);
+}
+
+static gint
+sort_func (GtkListBoxRow *row1,
+ GtkListBoxRow *row2,
+ gpointer user_data)
+{
+ GtdProvider *provider1;
+ GtdProvider *provider2;
+
+ if (!GTD_IS_PROVIDER_ROW (row1))
+ return 1;
+ else if (!GTD_IS_PROVIDER_ROW (row2))
+ return -1;
+
+ provider1 = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (row1));
+ provider2 = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (row2));
+
+ return provider2 != provider1;//gtd_provider_compare (provider1, provider2);
+}
+
+static void
+gtd_provider_selector_constructed (GObject *object)
+{
+ GtdProviderSelector *self = GTD_PROVIDER_SELECTOR (object);
+
+ G_OBJECT_CLASS (gtd_provider_selector_parent_class)->constructed (object);
+
+ gtk_list_box_set_header_func (GTK_LIST_BOX (self->listbox),
+ display_header_func,
+ NULL,
+ NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->listbox),
+ (GtkListBoxSortFunc) sort_func,
+ NULL,
+ NULL);
+}
+
+static void
+gtd_provider_selector_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (gtd_provider_selector_parent_class)->finalize (object);
+}
+
+static void
+gtd_provider_selector_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdProviderSelector *self = GTD_PROVIDER_SELECTOR (object);
+
+ switch (prop_id)
+ {
+ case PROP_SHOW_LOCAL:
+ g_value_set_boolean (value, self->show_local_provider);
+ break;
+
+ case PROP_SHOW_STUB_ROWS:
+ g_value_set_boolean (value, self->show_stub_rows);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_provider_selector_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdProviderSelector *self = GTD_PROVIDER_SELECTOR (object);
+
+ switch (prop_id)
+ {
+ case PROP_SHOW_LOCAL:
+ gtd_provider_selector_show_local (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SHOW_STUB_ROWS:
+ gtd_provider_selector_set_show_stub_rows (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_provider_selector_class_init (GtdProviderSelectorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_provider_selector_finalize;
+ object_class->constructed = gtd_provider_selector_constructed;
+ object_class->get_property = gtd_provider_selector_get_property;
+ object_class->set_property = gtd_provider_selector_set_property;
+
+ /**
+ * GtdProviderSelector::location-selected:
+ *
+ * Emitted when a provider location is selected.
+ */
+ signals[PROVIDER_SELECTED] = g_signal_new ("provider-selected",
+ GTD_TYPE_PROVIDER_SELECTOR,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ GTD_TYPE_PROVIDER);
+
+
+ /**
+ * GtdProviderSelector::show-local-provider:
+ *
+ * Whether it should show a row for the local provider.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_SHOW_LOCAL,
+ g_param_spec_boolean ("show-local",
+ "Show local provider row",
+ "Whether should show a local provider row instead of a checkbox",
+ FALSE,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdProviderSelector::show-stub-rows:
+ *
+ * Whether it should show stub rows for non-added accounts.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_SHOW_STUB_ROWS,
+ g_param_spec_boolean ("show-stub-rows",
+ "Show stub rows",
+ "Whether should show stub rows for non-added accounts",
+ TRUE,
+ G_PARAM_READWRITE));
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-provider-selector.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, exchange_stub_row);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, google_stub_row);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, listbox);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, local_check);
+ gtk_widget_class_bind_template_child (widget_class, GtdProviderSelector, owncloud_stub_row);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_local_check_toggled_cb);
+ gtk_widget_class_bind_template_callback (widget_class, gtd_provider_selector__listbox_row_activated);
+}
+
+static void
+gtd_provider_selector_init (GtdProviderSelector *self)
+{
+ GtdManager *manager;
+
+ self->show_stub_rows = TRUE;
+
+ /* Setup the manager */
+ manager = gtd_manager_get_default ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtd_provider_selector__fill_accounts (self);
+
+ g_signal_connect_swapped (manager,
+ "notify::default-provider",
+ G_CALLBACK (default_provider_changed_cb),
+ self);
+
+ g_signal_connect_swapped (manager,
+ "provider-added",
+ G_CALLBACK (add_provider),
+ self);
+
+ g_signal_connect_swapped (manager,
+ "provider-removed",
+ G_CALLBACK (remove_provider),
+ self);
+
+}
+
+/**
+ * gtd_provider_selector_new:
+ *
+ * Creates a new #GtdProviderSelector.
+ *
+ * Returns: (transfer full): a new #GtdProviderSelector
+ */
+GtkWidget*
+gtd_provider_selector_new (void)
+{
+ return g_object_new (GTD_TYPE_PROVIDER_SELECTOR, NULL);
+}
+
+/**
+ * gtd_provider_selector_show_local:
+ *
+ * Shows a row for local provider item.
+ */
+void
+gtd_provider_selector_show_local (GtdProviderSelector *selector,
+ gboolean show)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+
+ if (selector->show_local_provider != show)
+ {
+ selector->show_local_provider = show;
+
+ gtk_widget_set_visible (selector->local_check, !show);
+
+ if (selector->local_row)
+ gtk_widget_set_visible (selector->local_row, show);
+
+ g_object_notify (G_OBJECT (selector), "show-local");
+ }
+}
+
+/**
+ * gtd_provider_selector_get_selected_provider:
+ * @selector: a #GtdProviderSelector
+ *
+ * Retrieves the currently selected #GtdProvider, or %NULL if
+ * none is selected.
+ *
+ * Returns: (transfer none): the selected #GtdProvider
+ */
+GtdProvider*
+gtd_provider_selector_get_selected_provider (GtdProviderSelector *selector)
+{
+ GtdProvider *provider;
+ GtkWidget *child;
+
+ g_return_val_if_fail (GTD_IS_PROVIDER_SELECTOR (selector), NULL);
+
+ provider = NULL;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ GtdProviderRow *provider_row;
+
+ if (!GTD_IS_PROVIDER_ROW (child))
+ continue;
+
+ provider_row = GTD_PROVIDER_ROW (child);
+ if (gtd_provider_row_get_selected (provider_row))
+ {
+ provider = gtd_provider_row_get_provider (provider_row);
+ break;
+ }
+ }
+
+ return provider;
+}
+
+/**
+ * gtd_provider_selector_set_selected_provider:
+ * @selector: a #GtdProviderSelector
+ * @provider: a #GtdProvider
+ *
+ * Selects @provider in the given #GtdProviderSelector.
+ */
+void
+gtd_provider_selector_set_selected_provider (GtdProviderSelector *selector,
+ GtdProvider *provider)
+{
+ GtkWidget *child;
+
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ GtdProviderRow *provider_row;
+
+ if (!GTD_IS_PROVIDER_ROW (child))
+ continue;
+
+ provider_row = GTD_PROVIDER_ROW (child);
+ gtd_provider_row_set_selected (provider_row,
+ gtd_provider_row_get_provider (provider_row) == provider);
+ g_signal_emit (selector, signals[PROVIDER_SELECTED], 0, provider);
+ }
+}
+
+/**
+ * gtd_provider_selector_get_show_stub_rows:
+ * @selector: a #GtdProviderSelector
+ *
+ * Retrieves the ::show-stub-rows property.
+ *
+ * Returns: %TRUE if it shows stub rows, %FALSE if it hides them.
+ */
+gboolean
+gtd_provider_selector_get_show_stub_rows (GtdProviderSelector *selector)
+{
+ g_return_val_if_fail (GTD_IS_PROVIDER_SELECTOR (selector), FALSE);
+
+ return selector->show_stub_rows;
+}
+
+/**
+ * gtd_provider_selector_set_show_stub_rows:
+ * @selector: a #GtdProviderSelector
+ * @show_stub_rows: %TRUE to show stub rows, %FALSE to hide them.
+ *
+ * Sets the #GtdProviderSelector::show-stub-rows property.
+ *
+ * Returns:
+ */
+void
+gtd_provider_selector_set_show_stub_rows (GtdProviderSelector *selector,
+ gboolean show_stub_rows)
+{
+ g_return_if_fail (GTD_IS_PROVIDER_SELECTOR (selector));
+
+ if (selector->show_stub_rows != show_stub_rows)
+ {
+ selector->show_stub_rows = show_stub_rows;
+
+ /*
+ * If we're showing the stub rows, it must check which ones should be shown.
+ * We don't want to show stub rows for
+ */
+ if (show_stub_rows)
+ {
+ GtkWidget *child;
+ gint google_counter;
+ gint exchange_counter;
+ gint owncloud_counter;
+
+ google_counter = 0;
+ exchange_counter = 0;
+ owncloud_counter = 0;
+
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (selector->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (GTD_IS_PROVIDER_ROW (child))
+ {
+ GtdProvider *provider = gtd_provider_row_get_provider (GTD_PROVIDER_ROW (child));
+ const gchar *type;
+
+ type = gtd_provider_get_id (provider);
+
+ if (g_strcmp0 (type, "google") == 0)
+ google_counter++;
+ else if (g_strcmp0 (type, "exchange") == 0)
+ exchange_counter++;
+ else if (g_strcmp0 (type, "owncloud") == 0)
+ owncloud_counter++;
+ }
+ }
+
+ gtk_widget_set_visible (selector->google_stub_row, google_counter == 0);
+ gtk_widget_set_visible (selector->exchange_stub_row, exchange_counter == 0);
+ gtk_widget_set_visible (selector->owncloud_stub_row, owncloud_counter == 0);
+ }
+ else
+ {
+ gtk_widget_hide (selector->exchange_stub_row);
+ gtk_widget_hide (selector->google_stub_row);
+ gtk_widget_hide (selector->owncloud_stub_row);
+ }
+
+ g_object_notify (G_OBJECT (selector), "show-stub-rows");
+ }
+}
diff --git a/src/gui/gtd-provider-selector.h b/src/gui/gtd-provider-selector.h
new file mode 100644
index 0000000..4a29f70
--- /dev/null
+++ b/src/gui/gtd-provider-selector.h
@@ -0,0 +1,50 @@
+/* gtd-provider-selector.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_SELECTOR_H
+#define GTD_PROVIDER_SELECTOR_H
+
+#include "gtd-types.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_PROVIDER_SELECTOR (gtd_provider_selector_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdProviderSelector, gtd_provider_selector, GTD, PROVIDER_SELECTOR, GtkBox)
+
+GtkWidget* gtd_provider_selector_new (void);
+
+void gtd_provider_selector_show_local (GtdProviderSelector *selector,
+ gboolean show);
+
+GtdProvider* gtd_provider_selector_get_selected_provider (GtdProviderSelector *selector);
+
+void gtd_provider_selector_set_selected_provider (GtdProviderSelector *selector,
+ GtdProvider *provider);
+
+gboolean gtd_provider_selector_get_show_stub_rows (GtdProviderSelector *selector);
+
+void gtd_provider_selector_set_show_stub_rows (GtdProviderSelector *selector,
+ gboolean show_stub_rows);
+
+G_END_DECLS
+
+#endif /* GTD_PROVIDER_SELECTOR_H */
diff --git a/src/gui/gtd-provider-selector.ui b/src/gui/gtd-provider-selector.ui
new file mode 100644
index 0000000..006b546
--- /dev/null
+++ b/src/gui/gtd-provider-selector.ui
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdProviderSelector" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkFrame" id="frame">
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="sensitive" bind-source="local_check" bind-property="active" bind-flags="default|sync-create|invert-boolean"/>
+ <property name="hexpand">1</property>
+ <property name="vexpand">1</property>
+ <property name="selection_mode">none</property>
+ <signal name="row-activated" handler="gtd_provider_selector__listbox_row_activated" object="GtdProviderSelector" swapped="yes"/>
+ <style>
+ <class name="boxed-list"/>
+ </style>
+ <child>
+ <object class="GtkListBoxRow" id="google_stub_row">
+ <property name="can_focus">1</property>
+ <property name="tooltip_text" translatable="yes">Click to add a new Google account</property>
+ <child>
+ <object class="GtkBox" id="google_stub_row_box">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkImage" id="google_stub_row_icon_image">
+ <property name="pixel_size">32</property>
+ <property name="icon_name">goa-account-google</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="google_stub_row_label">
+ <property name="label" translatable="yes">Google</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="owncloud_stub_row">
+ <property name="can_focus">1</property>
+ <property name="tooltip_text" translatable="yes">Click to add a new ownCloud account</property>
+ <child>
+ <object class="GtkBox" id="box3">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkImage" id="image2">
+ <property name="pixel_size">32</property>
+ <property name="icon_name">goa-account-owncloud</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label2">
+ <property name="label" translatable="yes">ownCloud</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="exchange_stub_row">
+ <property name="can_focus">1</property>
+ <property name="tooltip_text" translatable="yes">Click to add a new Microsoft Exchange account</property>
+ <child>
+ <object class="GtkBox" id="box4">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkImage" id="image3">
+ <property name="pixel_size">32</property>
+ <property name="icon_name">goa-account</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="label" translatable="yes">Microsoft Exchange</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="local_check">
+ <property name="label" translatable="yes">Or you can just store your tasks on this computer</property>
+ <property name="can_focus">1</property>
+ <signal name="toggled" handler="on_local_check_toggled_cb" object="GtdProviderSelector" swapped="yes"/>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gui/gtd-star-widget.c b/src/gui/gtd-star-widget.c
new file mode 100644
index 0000000..cb6991b
--- /dev/null
+++ b/src/gui/gtd-star-widget.c
@@ -0,0 +1,193 @@
+/* gtd-star-widget.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-star-widget.h"
+
+struct _GtdStarWidget
+{
+ GtdWidget parent;
+
+ GtkWidget *filled_star;
+ GtkWidget *empty_star;
+
+ gboolean active;
+};
+
+G_DEFINE_TYPE (GtdStarWidget, gtd_star_widget, GTD_TYPE_WIDGET)
+
+enum
+{
+ PROP_0,
+ PROP_ACTIVE,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_star_widget_clicked_cb (GtkGestureClick *gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ GtdStarWidget *self)
+{
+ gtd_star_widget_set_active (self, !self->active);
+ gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_star_widget_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdStarWidget *self = GTD_STAR_WIDGET (object);
+
+ switch (prop_id)
+ {
+ case PROP_ACTIVE:
+ g_value_set_boolean (value, self->active);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_star_widget_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdStarWidget *self = GTD_STAR_WIDGET (object);
+
+ switch (prop_id)
+ {
+ case PROP_ACTIVE:
+ gtd_star_widget_set_active (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_star_widget_class_init (GtdStarWidgetClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gtd_star_widget_get_property;
+ object_class->set_property = gtd_star_widget_set_property;
+
+ /**
+ * GtdStarWidget:active:
+ *
+ * Whether the star widget is active or not. When active, the
+ * star appears filled.
+ */
+ properties[PROP_ACTIVE] = g_param_spec_boolean ("active",
+ "Active",
+ "Active",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "star");
+}
+
+static void
+gtd_star_widget_init (GtdStarWidget *self)
+{
+ GtkGesture *click_gesture;
+
+ click_gesture = gtk_gesture_click_new ();
+ gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (click_gesture), FALSE);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (click_gesture), TRUE);
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (click_gesture), GDK_BUTTON_PRIMARY);
+ gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (click_gesture), GTK_PHASE_CAPTURE);
+ g_signal_connect (click_gesture, "pressed", G_CALLBACK (on_star_widget_clicked_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (click_gesture));
+
+ gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "default");
+
+ self->empty_star = gtk_image_new_from_icon_name ("non-starred-symbolic");
+ gtk_widget_set_parent (self->empty_star, GTK_WIDGET (self));
+
+ self->filled_star = gtk_image_new_from_icon_name ("starred-symbolic");
+ gtk_widget_set_parent (self->filled_star, GTK_WIDGET (self));
+ gtk_widget_hide (self->filled_star);
+
+ g_object_bind_property (self->filled_star,
+ "visible",
+ self->empty_star,
+ "visible",
+ G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
+}
+
+GtkWidget*
+gtd_star_widget_new (void)
+{
+ return g_object_new (GTD_TYPE_STAR_WIDGET, NULL);
+}
+
+gboolean
+gtd_star_widget_get_active (GtdStarWidget *self)
+{
+ g_return_val_if_fail (GTD_IS_STAR_WIDGET (self), FALSE);
+
+ return self->active;
+}
+
+void
+gtd_star_widget_set_active (GtdStarWidget *self,
+ gboolean active)
+{
+ g_return_if_fail (GTD_IS_STAR_WIDGET (self));
+
+ if (self->active == active)
+ return;
+
+ self->active = active;
+ gtk_widget_set_visible (self->filled_star, active);
+
+ if (active)
+ gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE);
+ else
+ gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED);
+
+ /* TODO: explosion effect */
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACTIVE]);
+}
diff --git a/src/gui/gtd-star-widget.h b/src/gui/gtd-star-widget.h
new file mode 100644
index 0000000..6f93b44
--- /dev/null
+++ b/src/gui/gtd-star-widget.h
@@ -0,0 +1,37 @@
+/* gtd-star-widget.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_STAR_WIDGET (gtd_star_widget_get_type())
+G_DECLARE_FINAL_TYPE (GtdStarWidget, gtd_star_widget, GTD, STAR_WIDGET, GtdWidget)
+
+GtkWidget* gtd_star_widget_new (void);
+
+gboolean gtd_star_widget_get_active (GtdStarWidget *self);
+
+void gtd_star_widget_set_active (GtdStarWidget *self,
+ gboolean active);
+
+G_END_DECLS
diff --git a/src/gui/gtd-task-list-popover.c b/src/gui/gtd-task-list-popover.c
new file mode 100644
index 0000000..7d556a0
--- /dev/null
+++ b/src/gui/gtd-task-list-popover.c
@@ -0,0 +1,269 @@
+/* gtd-task-list-popover.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 "GtdTaskListPopover"
+
+#include "models/gtd-list-model-filter.h"
+#include "gtd-debug.h"
+#include "gtd-manager.h"
+#include "gtd-provider.h"
+#include "gtd-task-list.h"
+#include "gtd-task-list-popover.h"
+#include "gtd-utils.h"
+
+struct _GtdTaskListPopover
+{
+ GtkPopover parent;
+
+ GtkFilterListModel *filter_model;
+
+ GtkSizeGroup *sizegroup;
+ GtkListBox *listbox;
+ GtkEditable *search_entry;
+
+ GtdTaskList *selected_list;
+ GtdManager *manager;
+};
+
+G_DEFINE_TYPE (GtdTaskListPopover, gtd_task_list_popover, GTK_TYPE_POPOVER)
+
+enum
+{
+ PROP_0,
+ PROP_TASK_LIST,
+ N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+set_selected_tasklist (GtdTaskListPopover *self,
+ GtdTaskList *list)
+{
+ GtdManager *manager;
+
+ manager = gtd_manager_get_default ();
+
+ /* NULL list means the inbox */
+ if (!list)
+ list = gtd_manager_get_inbox (manager);
+
+ if (g_set_object (&self->selected_list, list))
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TASK_LIST]);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static gboolean
+filter_listbox_cb (gpointer item,
+ gpointer user_data)
+{
+ GtdTaskListPopover *self;
+ g_autofree gchar *normalized_list_name = NULL;
+ g_autofree gchar *normalized_search_query = NULL;
+ GtdTaskList *list;
+
+ self = (GtdTaskListPopover*) user_data;
+ list = (GtdTaskList*) item;
+
+ normalized_search_query = gtd_normalize_casefold_and_unaccent (gtk_editable_get_text (self->search_entry));
+ normalized_list_name = gtd_normalize_casefold_and_unaccent (gtd_task_list_get_name (list));
+
+ return g_strrstr (normalized_list_name, normalized_search_query) != NULL;
+}
+
+static GtkWidget*
+create_list_row_cb (gpointer item,
+ gpointer data)
+{
+ g_autoptr (GdkPaintable) paintable = NULL;
+ g_autoptr (GdkRGBA) color = NULL;
+ GtdTaskListPopover *self;
+ GtkWidget *provider;
+ GtkWidget *icon;
+ GtkWidget *name;
+ GtkWidget *row;
+ GtkWidget *box;
+
+ self = GTD_TASK_LIST_POPOVER (data);
+ box = g_object_new (GTK_TYPE_BOX,
+ "orientation", GTK_ORIENTATION_HORIZONTAL,
+ "spacing", 12,
+ "margin-top", 6,
+ "margin-bottom", 6,
+ "margin-start", 6,
+ "margin-end", 6,
+ NULL);
+
+ /* Icon */
+ color = gtd_task_list_get_color (item);
+ paintable = gtd_create_circular_paintable (color, 12);
+ icon = gtk_image_new_from_paintable (paintable);
+ gtk_widget_set_size_request (icon, 12, 12);
+ gtk_widget_set_halign (icon, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign (icon, GTK_ALIGN_CENTER);
+
+ gtk_box_append (GTK_BOX (box), icon);
+
+ /* Tasklist name */
+ name = g_object_new (GTK_TYPE_LABEL,
+ "label", gtd_task_list_get_name (item),
+ "xalign", 0.0,
+ "hexpand", TRUE,
+ NULL);
+
+ gtk_box_append (GTK_BOX (box), name);
+
+ /* Provider name */
+ provider = g_object_new (GTK_TYPE_LABEL,
+ "label", gtd_provider_get_description (gtd_task_list_get_provider (item)),
+ "xalign", 0.0,
+ NULL);
+ gtk_widget_add_css_class (provider, "dim-label");
+ gtk_size_group_add_widget (self->sizegroup, provider);
+ gtk_box_append (GTK_BOX (box), provider);
+
+ /* The row itself */
+ row = gtk_list_box_row_new ();
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box);
+
+ g_object_set_data (G_OBJECT (row), "tasklist", item);
+
+ return row;
+}
+
+static void
+on_listbox_row_activated_cb (GtkListBox *listbox,
+ GtkListBoxRow *row,
+ GtdTaskListPopover *self)
+{
+ GtdTaskList *list = g_object_get_data (G_OBJECT (row), "tasklist");
+
+ set_selected_tasklist (self, list);
+ gtk_popover_popdown (GTK_POPOVER (self));
+ gtk_editable_set_text (self->search_entry, "");
+}
+
+static void
+on_popover_closed_cb (GtkPopover *popover,
+ GtdTaskListPopover *self)
+{
+ gtk_editable_set_text (self->search_entry, "");
+}
+
+static void
+on_search_entry_activated_cb (GtkEntry *entry,
+ GtdTaskListPopover *self)
+{
+ g_autoptr (GtdTaskList) list = g_list_model_get_item (G_LIST_MODEL (self->filter_model), 0);
+
+ set_selected_tasklist (self, list);
+ gtk_popover_popdown (GTK_POPOVER (self));
+ gtk_editable_set_text (self->search_entry, "");
+}
+
+static void
+on_search_entry_search_changed_cb (GtkEntry *search_entry,
+ GtdTaskListPopover *self)
+{
+ GtkFilter *filter;
+
+ filter = gtk_filter_list_model_get_filter (self->filter_model);
+ gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_list_popover_finalize (GObject *object)
+{
+ GtdTaskListPopover *self = (GtdTaskListPopover *)object;
+
+ g_clear_object (&self->selected_list);
+
+ G_OBJECT_CLASS (gtd_task_list_popover_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_list_popover_class_init (GtdTaskListPopoverClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_task_list_popover_finalize;
+
+ properties[PROP_TASK_LIST] = g_param_spec_object ("task-list",
+ "Task list",
+ "Task list",
+ GTD_TYPE_TASK_LIST,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-list-popover.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListPopover, listbox);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListPopover, search_entry);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListPopover, sizegroup);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_popover_closed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_search_entry_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_search_entry_search_changed_cb);
+}
+
+static void
+gtd_task_list_popover_init (GtdTaskListPopover *self)
+{
+ GtdManager *manager = gtd_manager_get_default ();
+ GtkCustomFilter *filter;
+
+ filter = gtk_custom_filter_new (filter_listbox_cb, self, NULL);
+ self->filter_model = gtk_filter_list_model_new (gtd_manager_get_task_lists_model (manager),
+ GTK_FILTER (filter));
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_bind_model (self->listbox,
+ G_LIST_MODEL (self->filter_model),
+ create_list_row_cb,
+ self,
+ NULL);
+
+ self->manager = manager;
+
+ set_selected_tasklist (self, NULL);
+}
+
+GtdTaskList*
+gtd_task_list_popover_get_task_list (GtdTaskListPopover *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK_LIST_POPOVER (self), NULL);
+
+ return self->selected_list;
+}
diff --git a/src/gui/gtd-task-list-popover.h b/src/gui/gtd-task-list-popover.h
new file mode 100644
index 0000000..2a57ea9
--- /dev/null
+++ b/src/gui/gtd-task-list-popover.h
@@ -0,0 +1,34 @@
+/* gtd-task-list-popover.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_POPOVER (gtd_task_list_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdTaskListPopover, gtd_task_list_popover, GTD, TASK_LIST_POPOVER, GtkPopover)
+
+GtdTaskList* gtd_task_list_popover_get_task_list (GtdTaskListPopover *self);
+
+G_END_DECLS
diff --git a/src/gui/gtd-task-list-popover.ui b/src/gui/gtd-task-list-popover.ui
new file mode 100644
index 0000000..9a8cc9e
--- /dev/null
+++ b/src/gui/gtd-task-list-popover.ui
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtdTaskListPopover" parent="GtkPopover">
+ <signal name="closed" handler="on_popover_closed_cb" object="GtdTaskListPopover" swapped="no"/>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="spacing">12</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <signal name="activate" handler="on_search_entry_activated_cb" object="GtdTaskListPopover" swapped="no"/>
+ <signal name="search-changed" handler="on_search_entry_search_changed_cb" object="GtdTaskListPopover" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hscrollbar-policy">never</property>
+ <property name="max-content-height">480</property>
+ <property name="propagate-natural-height">1</property>
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="selection_mode">none</property>
+ <signal name="row-activated" handler="on_listbox_row_activated_cb" object="GtdTaskListPopover" swapped="no"/>
+ <style>
+ <class name="background"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup" id="sizegroup"/>
+</interface>
diff --git a/src/gui/gtd-task-list-view.c b/src/gui/gtd-task-list-view.c
new file mode 100644
index 0000000..6810c17
--- /dev/null
+++ b/src/gui/gtd-task-list-view.c
@@ -0,0 +1,1220 @@
+/* gtd-task-list-view.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 "GtdTaskListView"
+
+#include "gtd-debug.h"
+#include "gtd-edit-pane.h"
+#include "gtd-task-list-view.h"
+#include "gtd-task-list-view-model.h"
+#include "gtd-manager.h"
+#include "gtd-markdown-renderer.h"
+#include "gtd-new-task-row.h"
+#include "gtd-notification.h"
+#include "gtd-provider.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-task-row.h"
+#include "gtd-utils-private.h"
+#include "gtd-widget.h"
+#include "gtd-window.h"
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+/**
+ * SECTION:gtd-task-list-view
+ * @Short_description: A widget to display tasklists
+ * @Title:GtdTaskListView
+ *
+ * The #GtdTaskListView widget shows the tasks of a #GtdTaskList with
+ * various options to fine-tune the appearance. Alternatively, one can
+ * pass a #GList of #GtdTask objects.
+ *
+ * It supports custom sorting and header functions, so the tasks can be
+ * sorted in various ways. See the "Today" and "Scheduled" panels for reference
+ * implementations.
+ *
+ * Example:
+ * |[
+ * GtdTaskListView *view = gtd_task_list_view_new ();
+ *
+ * gtd_task_list_view_set_model (view, model);
+ *
+ * // Date which tasks will be automatically assigned
+ * gtd_task_list_view_set_default_date (view, now);
+ * ]|
+ *
+ */
+
+struct _GtdTaskListView
+{
+ GtkBox parent;
+
+ AdwStatusPage *empty_list_widget;
+ GtkListBox *listbox;
+ GtkStack *main_stack;
+ GtkWidget *scrolled_window;
+
+ /* internal */
+ gboolean can_toggle;
+ gboolean show_due_date;
+ gboolean show_list_name;
+ GtdTaskListViewModel *view_model;
+ GListModel *model;
+ GDateTime *default_date;
+
+ GListModel *incomplete_tasks_model;
+ guint n_incomplete_tasks;
+
+ guint scroll_to_bottom_handler_id;
+
+ GHashTable *task_to_row;
+
+ /* Markup renderer*/
+ GtdMarkdownRenderer *renderer;
+
+ /* DnD */
+ guint scroll_timeout_id;
+ gboolean scroll_up;
+
+ /* action */
+ GActionGroup *action_group;
+
+ /* Custom header function data */
+ GtdTaskListViewHeaderFunc header_func;
+ gpointer header_user_data;
+
+ GtdTaskRow *active_row;
+ GtkSizeGroup *due_date_sizegroup;
+ GtkSizeGroup *tasklist_name_sizegroup;
+};
+
+#define DND_SCROLL_OFFSET 24 //px
+#define TASK_REMOVED_NOTIFICATION_ID "task-removed-id"
+
+
+static gboolean filter_complete_func (gpointer item,
+ gpointer user_data);
+
+static void on_clear_completed_tasks_activated_cb (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data);
+
+static void on_incomplete_tasks_items_changed_cb (GListModel *model,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GtdTaskListView *self);
+
+static void on_remove_task_row_cb (GtdTaskRow *row,
+ GtdTaskListView *self);
+
+static void on_task_row_entered_cb (GtdTaskListView *self,
+ GtdTaskRow *row);
+
+static void on_task_row_exited_cb (GtdTaskListView *self,
+ GtdTaskRow *row);
+
+static gboolean scroll_to_bottom_cb (gpointer data);
+
+
+G_DEFINE_TYPE (GtdTaskListView, gtd_task_list_view, GTK_TYPE_BOX)
+
+static const GActionEntry gtd_task_list_view_entries[] = {
+ { "clear-completed-tasks", on_clear_completed_tasks_activated_cb },
+};
+
+typedef struct
+{
+ GtdTaskListView *view;
+ GtdTask *task;
+} RemoveTaskData;
+
+enum {
+ PROP_0,
+ PROP_SHOW_LIST_NAME,
+ PROP_SHOW_DUE_DATE,
+ LAST_PROP
+};
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+set_active_row (GtdTaskListView *self,
+ GtdTaskRow *row)
+{
+ if (self->active_row == row)
+ return;
+
+ if (self->active_row)
+ gtd_task_row_set_active (self->active_row, FALSE);
+
+ self->active_row = row;
+
+ if (row)
+ {
+ gtd_task_row_set_active (row, TRUE);
+ gtk_widget_grab_focus (GTK_WIDGET (row));
+ }
+}
+
+static void
+schedule_scroll_to_bottom (GtdTaskListView *self)
+{
+ if (self->scroll_to_bottom_handler_id > 0)
+ return;
+
+ self->scroll_to_bottom_handler_id = g_timeout_add (250, scroll_to_bottom_cb, self);
+}
+
+static void
+update_empty_state (GtdTaskListView *self)
+{
+ gboolean show_empty_list_widget;
+ gboolean is_empty;
+
+ g_assert (GTD_IS_TASK_LIST_VIEW (self));
+
+ if (!self->model)
+ return;
+
+ is_empty = g_list_model_get_n_items (self->model) == 0;
+
+ show_empty_list_widget = !GTD_IS_TASK_LIST (self->model) &&
+ (is_empty || self->n_incomplete_tasks == 0);
+ gtk_stack_set_visible_child_name (self->main_stack,
+ show_empty_list_widget ? "empty-list" : "task-list");
+}
+
+static void
+update_incomplete_tasks_model (GtdTaskListView *self)
+{
+ if (!self->incomplete_tasks_model)
+ {
+ g_autoptr (GtkFilterListModel) filter_model = NULL;
+ GtkCustomFilter *filter;
+
+ filter = gtk_custom_filter_new (filter_complete_func, self, NULL);
+ filter_model = gtk_filter_list_model_new (NULL, GTK_FILTER (filter));
+ gtk_filter_list_model_set_incremental (filter_model, TRUE);
+
+ self->incomplete_tasks_model = G_LIST_MODEL (g_steal_pointer (&filter_model));
+ }
+
+ gtk_filter_list_model_set_model (GTK_FILTER_LIST_MODEL (self->incomplete_tasks_model),
+ self->model);
+ self->n_incomplete_tasks = g_list_model_get_n_items (self->incomplete_tasks_model);
+
+ g_signal_connect (self->incomplete_tasks_model,
+ "items-changed",
+ G_CALLBACK (on_incomplete_tasks_items_changed_cb),
+ self);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static gboolean
+filter_complete_func (gpointer item,
+ gpointer user_data)
+{
+ GtdTask *task = (GtdTask*) item;
+ return !gtd_task_get_complete (task);
+}
+
+static void
+on_incomplete_tasks_items_changed_cb (GListModel *model,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GtdTaskListView *self)
+{
+ self->n_incomplete_tasks -= n_removed;
+ self->n_incomplete_tasks += n_added;
+
+ update_empty_state (self);
+}
+
+static void
+on_empty_list_widget_add_tasks_cb (AdwStatusPage *empty_list_widget,
+ GtdTaskListView *self)
+{
+ gtk_stack_set_visible_child_name (self->main_stack, "task-list");
+}
+
+static void
+on_new_task_row_entered_cb (GtdTaskListView *self,
+ GtdNewTaskRow *row)
+{
+ set_active_row (self, NULL);
+}
+
+static void
+on_new_task_row_exited_cb (GtdTaskListView *self,
+ GtdNewTaskRow *row)
+{
+}
+
+static GtkWidget*
+create_row_for_task_cb (gpointer item,
+ gpointer user_data)
+{
+ GtdTaskListView *self;
+ GtkWidget *row;
+
+ self = GTD_TASK_LIST_VIEW (user_data);
+
+ set_active_row (self, NULL);
+
+ if (GTD_IS_TASK (item))
+ {
+ row = gtd_task_row_new (item, self->renderer);
+
+ gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row), self->show_list_name);
+ gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row), self->show_due_date);
+
+ g_signal_connect_swapped (row, "enter", G_CALLBACK (on_task_row_entered_cb), self);
+ g_signal_connect_swapped (row, "exit", G_CALLBACK (on_task_row_exited_cb), self);
+
+ g_signal_connect (row, "remove-task", G_CALLBACK (on_remove_task_row_cb), self);
+ }
+ else
+ {
+ row = gtd_new_task_row_new ();
+
+ g_signal_connect_swapped (row, "enter", G_CALLBACK (on_new_task_row_entered_cb), self);
+ g_signal_connect_swapped (row, "exit", G_CALLBACK (on_new_task_row_exited_cb), self);
+ }
+
+ g_hash_table_insert (self->task_to_row, item, row);
+
+ return row;
+}
+
+static gboolean
+scroll_to_bottom_cb (gpointer data)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (data);
+ GtkWidget *widget;
+ GtkRoot *root;
+
+ widget = GTK_WIDGET (self);
+ root = gtk_widget_get_root (widget);
+
+ if (!root)
+ return G_SOURCE_CONTINUE;
+
+ self->scroll_to_bottom_handler_id = 0;
+
+ /*
+ * Only focus the new task row if the current list is visible,
+ * and the focused widget isn't inside this list view.
+ */
+ if (gtk_widget_get_visible (widget) &&
+ gtk_widget_get_child_visible (widget) &&
+ gtk_widget_get_mapped (widget) &&
+ !gtk_widget_is_ancestor (gtk_window_get_focus (GTK_WINDOW (root)), widget))
+ {
+ GtkWidget *new_task_row;
+ gboolean ignored;
+
+ new_task_row = gtk_widget_get_last_child (GTK_WIDGET (self->listbox));
+ gtk_widget_grab_focus (new_task_row);
+ g_signal_emit_by_name (self->scrolled_window, "scroll-child", GTK_SCROLL_END, FALSE, &ignored);
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+on_task_removed_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+
+ gtd_provider_remove_task_finish (GTD_PROVIDER (source), result, &error);
+
+ if (error)
+ g_warning ("Error removing task list: %s", error->message);
+}
+
+static void
+on_clear_completed_tasks_activated_cb (GSimpleAction *simple,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ GtdTaskListView *self;
+ GListModel *model;
+ guint i;
+
+ self = GTD_TASK_LIST_VIEW (user_data);
+ model = self->model;
+
+ for (i = 0; i < g_list_model_get_n_items (model); i++)
+ {
+ g_autoptr (GtdTask) task = g_list_model_get_item (model, i);
+
+ if (!gtd_task_get_complete (task))
+ continue;
+
+ gtd_provider_remove_task (gtd_task_get_provider (task),
+ task,
+ NULL,
+ on_task_removed_cb,
+ self);
+ }
+}
+
+static void
+on_remove_task_action_cb (GtdNotification *notification,
+ gpointer user_data)
+{
+ RemoveTaskData *data = user_data;
+
+ gtd_provider_remove_task (gtd_task_get_provider (data->task),
+ data->task,
+ NULL,
+ on_task_removed_cb,
+ data->view);
+
+ g_clear_pointer (&data, g_free);
+}
+
+static void
+on_undo_remove_task_action_cb (GtdNotification *notification,
+ gpointer user_data)
+{
+ RemoveTaskData *data;
+ GtdTaskList *list;
+
+ data = user_data;
+
+ /*
+ * Re-add task to the list. This will emit GListModel:items-changed (since
+ * GtdTaskList implements GListModel) and the row will be added back.
+ */
+ list = gtd_task_get_list (data->task);
+ gtd_task_list_add_task (list, data->task);
+
+ g_free (data);
+}
+
+static void
+on_remove_task_row_cb (GtdTaskRow *row,
+ GtdTaskListView *self)
+{
+ GtdManager *manager = gtd_manager_get_default ();
+ g_autofree gchar *text = NULL;
+ GtdNotification *notification;
+ RemoveTaskData *data;
+ GtdTaskList *list;
+ GtdTask *task;
+
+ task = gtd_task_row_get_task (row);
+
+ text = g_strdup_printf (_("Task <b>%s</b> removed"), gtd_task_get_title (task));
+
+ data = g_new0 (RemoveTaskData, 1);
+ data->view = self;
+ data->task = task;
+
+ /* Remove task from the list */
+ list = gtd_task_get_list (task);
+ gtd_task_list_remove_task (list, task);
+
+ /* Notify about the removal */
+ notification = gtd_notification_new (text);
+
+ gtd_notification_set_dismissal_action (notification,
+ (GtdNotificationActionFunc) on_remove_task_action_cb,
+ data);
+
+ gtd_notification_set_secondary_action (notification,
+ _("Undo"),
+ (GtdNotificationActionFunc) on_undo_remove_task_action_cb,
+ data);
+
+ gtd_manager_send_notification (manager, notification);
+
+ /* Clear the active row */
+ set_active_row (self, NULL);
+}
+
+static void
+on_task_row_entered_cb (GtdTaskListView *self,
+ GtdTaskRow *row)
+{
+ set_active_row (self, row);
+}
+
+static void
+on_task_row_exited_cb (GtdTaskListView *self,
+ GtdTaskRow *row)
+{
+ if (row == self->active_row)
+ set_active_row (self, NULL);
+}
+
+static void
+on_listbox_row_activated_cb (GtkListBox *listbox,
+ GtkListBoxRow *row,
+ GtdTaskListView *self)
+{
+ GtdTaskRow *task_row;
+
+ GTD_ENTRY;
+
+ if (!GTD_IS_TASK_ROW (row))
+ GTD_RETURN ();
+
+ task_row = GTD_TASK_ROW (row);
+
+ /* Toggle the row */
+ if (gtd_task_row_get_active (task_row))
+ set_active_row (self, NULL);
+ else
+ set_active_row (self, task_row);
+
+ GTD_EXIT;
+}
+
+
+/*
+ * Custom sorting functions
+ */
+
+static void
+internal_header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ GtdTaskListView *self)
+{
+ GtkWidget *header;
+ GtdTask *row_task;
+ GtdTask *before_task;
+
+ if (!self->header_func)
+ return;
+
+ row_task = before_task = NULL;
+
+ if (!GTD_IS_TASK_ROW (row))
+ return;
+
+ if (row)
+ row_task = gtd_task_row_get_task (GTD_TASK_ROW (row));
+
+ if (before)
+ before_task = gtd_task_row_get_task (GTD_TASK_ROW (before));
+
+ header = self->header_func (row_task, before_task, self->header_user_data);
+
+ if (header)
+ {
+ GtkWidget *real_header = gtd_widget_new ();
+ gtk_widget_insert_before (header, real_header, NULL);
+
+ header = real_header;
+ }
+
+ gtk_list_box_row_set_header (row, header);
+}
+
+
+/*
+ * Drag n' Drop functions
+ */
+
+static GtkListBoxRow*
+get_drop_row_at_y (GtdTaskListView *self,
+ gdouble y)
+{
+ GtkAllocation row_allocation;
+ GtkListBoxRow *hovered_row;
+ GtkListBoxRow *task_row;
+ GtkListBoxRow *drop_row;
+
+ hovered_row = gtk_list_box_get_row_at_y (self->listbox, y);
+
+ /* Small optimization when hovering the first row */
+ if (gtk_list_box_row_get_index (hovered_row) == 0)
+ return GTD_IS_TASK_ROW (hovered_row) ? hovered_row : NULL;
+
+ drop_row = NULL;
+ task_row = hovered_row;
+
+ gtk_widget_get_allocation (GTK_WIDGET (hovered_row), &row_allocation);
+
+ /*
+ * If the pointer if in the top part of the row, move the DnD row to
+ * the previous row.
+ */
+ if (y < row_allocation.y + row_allocation.height / 2)
+ {
+ GtkWidget *aux;
+
+ /* Search for a valid task row */
+ for (aux = gtk_widget_get_prev_sibling (GTK_WIDGET (hovered_row));
+ aux;
+ aux = gtk_widget_get_prev_sibling (aux))
+ {
+ /* Skip DnD, New task and hidden rows */
+ if (!gtk_widget_get_visible (aux))
+ continue;
+
+ drop_row = GTK_LIST_BOX_ROW (aux);
+ break;
+ }
+ }
+ else
+ {
+ drop_row = task_row;
+ }
+
+ return GTD_IS_TASK_ROW (drop_row) ? drop_row : NULL;
+}
+
+static inline gboolean
+scroll_to_dnd (gpointer user_data)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (user_data);
+ GtkAdjustment *vadjustment;
+ gint value;
+
+ vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window));
+ value = gtk_adjustment_get_value (vadjustment) + (self->scroll_up ? -6 : 6);
+
+ gtk_adjustment_set_value (vadjustment,
+ CLAMP (value, 0, gtk_adjustment_get_upper (vadjustment)));
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+check_dnd_scroll (GtdTaskListView *self,
+ gboolean should_cancel,
+ gdouble y)
+{
+ gdouble current_y, height;
+
+ if (should_cancel)
+ {
+ if (self->scroll_timeout_id > 0)
+ {
+ g_source_remove (self->scroll_timeout_id);
+ self->scroll_timeout_id = 0;
+ }
+
+ return;
+ }
+
+ height = gtk_widget_get_allocated_height (self->scrolled_window);
+ gtk_widget_translate_coordinates (GTK_WIDGET (self->listbox),
+ self->scrolled_window,
+ 0, y,
+ NULL, &current_y);
+
+ if (current_y < DND_SCROLL_OFFSET || current_y > height - DND_SCROLL_OFFSET)
+ {
+ if (self->scroll_timeout_id > 0)
+ return;
+
+ /* Start the autoscroll */
+ self->scroll_up = current_y < DND_SCROLL_OFFSET;
+ self->scroll_timeout_id = g_timeout_add (25,
+ scroll_to_dnd,
+ self);
+ }
+ else
+ {
+ if (self->scroll_timeout_id == 0)
+ return;
+
+ /* Cancel the autoscroll */
+ g_source_remove (self->scroll_timeout_id);
+ self->scroll_timeout_id = 0;
+ }
+}
+
+static GdkDragAction
+on_drop_target_drag_enter_cb (GtkDropTarget *drop_target,
+ gdouble x,
+ gdouble y,
+ GtdTaskListView *self)
+{
+ GTD_ENTRY;
+
+ gtk_list_box_drag_highlight_row (self->listbox, gtk_list_box_get_row_at_y (self->listbox, y));
+
+ GTD_RETURN (GDK_ACTION_MOVE);
+}
+
+static void
+on_drop_target_drag_leave_cb (GtkDropTarget *drop_target,
+ GtdTaskListView *self)
+{
+ GTD_ENTRY;
+
+ gtk_list_box_drag_unhighlight_row (self->listbox);
+ check_dnd_scroll (self, TRUE, -1);
+
+ GTD_EXIT;
+}
+
+static GdkDragAction
+on_drop_target_drag_motion_cb (GtkDropTarget *drop_target,
+ gdouble x,
+ gdouble y,
+ GtdTaskListView *self)
+{
+ GdkDrop *drop;
+ GdkDrag *drag;
+
+ GTD_ENTRY;
+
+ /* Clear the currently active row */
+ set_active_row (self, NULL);
+
+ drop = gtk_drop_target_get_current_drop (drop_target);
+ drag = gdk_drop_get_drag (drop);
+
+ if (!drag)
+ {
+ g_info ("Only dragging task rows is supported");
+ GTD_GOTO (fail);
+ }
+
+ gtk_list_box_drag_highlight_row (self->listbox, gtk_list_box_get_row_at_y (self->listbox, y));
+ check_dnd_scroll (self, FALSE, y);
+ GTD_RETURN (GDK_ACTION_MOVE);
+
+fail:
+ GTD_RETURN (0);
+}
+
+static gboolean
+on_drop_target_drag_drop_cb (GtkDropTarget *drop_target,
+ const GValue *value,
+ gdouble x,
+ gdouble y,
+ GtdTaskListView *self)
+{
+ GtkListBoxRow *drop_row;
+ GtkWidget *row;
+ GtdTask *hovered_task;
+ GtdTask *source_task;
+ GdkDrop *drop;
+ GdkDrag *drag;
+ gint64 current_position;
+ gint64 new_position;
+
+ GTD_ENTRY;
+
+ drop = gtk_drop_target_get_current_drop (drop_target);
+ drag = gdk_drop_get_drag (drop);
+
+ if (!drag)
+ {
+ g_info ("Only dragging task rows is supported");
+ GTD_RETURN (FALSE);
+ }
+
+ gtk_list_box_drag_unhighlight_row (self->listbox);
+
+ source_task = g_value_get_object (value);
+ g_assert (source_task != NULL);
+
+ /*
+ * When the drag operation began, the source row was hidden. Now is the time
+ * to show it again.
+ */
+ row = g_hash_table_lookup (self->task_to_row, source_task);
+ gtk_widget_show (row);
+
+ drop_row = get_drop_row_at_y (self, y);
+ if (!drop_row)
+ {
+ check_dnd_scroll (self, TRUE, -1);
+ GTD_RETURN (FALSE);
+ }
+
+ hovered_task = gtd_task_row_get_task (GTD_TASK_ROW (drop_row));
+ new_position = gtd_task_get_position (hovered_task);
+ current_position = gtd_task_get_position (source_task);
+
+ GTD_TRACE_MSG ("Dropping task %p at %ld", source_task, new_position);
+
+ if (new_position != current_position)
+ {
+ gtd_task_list_move_task_to_position (GTD_TASK_LIST (self->model),
+ source_task,
+ new_position);
+ }
+
+ check_dnd_scroll (self, TRUE, -1);
+
+ GTD_RETURN (TRUE);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_list_view_finalize (GObject *object)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
+
+ g_clear_handle_id (&self->scroll_to_bottom_handler_id, g_source_remove);
+ g_clear_pointer (&self->task_to_row, g_hash_table_destroy);
+ g_clear_pointer (&self->default_date, g_date_time_unref);
+ g_clear_object (&self->incomplete_tasks_model);
+ g_clear_object (&self->renderer);
+ g_clear_object (&self->model);
+
+ G_OBJECT_CLASS (gtd_task_list_view_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_list_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_SHOW_DUE_DATE:
+ g_value_set_boolean (value, self->show_due_date);
+ break;
+
+ case PROP_SHOW_LIST_NAME:
+ g_value_set_boolean (value, self->show_list_name);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_list_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_SHOW_DUE_DATE:
+ gtd_task_list_view_set_show_due_date (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SHOW_LIST_NAME:
+ gtd_task_list_view_set_show_list_name (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_list_view_constructed (GObject *object)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (object);
+ GdkTexture *texture = gdk_texture_new_from_resource ("/org/gnome/todo/ui/assets/all-done.svg");
+ G_OBJECT_CLASS (gtd_task_list_view_parent_class)->constructed (object);
+
+ /* action_group */
+ self->action_group = G_ACTION_GROUP (g_simple_action_group_new ());
+
+ adw_status_page_set_paintable (self->empty_list_widget, GDK_PAINTABLE (texture));
+
+ g_action_map_add_action_entries (G_ACTION_MAP (self->action_group),
+ gtd_task_list_view_entries,
+ G_N_ELEMENTS (gtd_task_list_view_entries),
+ object);
+}
+
+
+/*
+ * GtkWidget overrides
+ */
+
+static void
+gtd_task_list_view_map (GtkWidget *widget)
+{
+ GtdTaskListView *self = GTD_TASK_LIST_VIEW (widget);
+ GtkRoot *root;
+
+
+ update_empty_state (self);
+
+ GTK_WIDGET_CLASS (gtd_task_list_view_parent_class)->map (widget);
+
+ root = gtk_widget_get_root (widget);
+
+ /* Clear previously added "list" actions */
+ gtk_widget_insert_action_group (GTK_WIDGET (root), "list", NULL);
+
+ /* Add this instance's action group */
+ gtk_widget_insert_action_group (GTK_WIDGET (root), "list", self->action_group);
+}
+
+static void
+gtd_task_list_view_class_init (GtdTaskListViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gtd_task_list_view_finalize;
+ object_class->constructed = gtd_task_list_view_constructed;
+ object_class->get_property = gtd_task_list_view_get_property;
+ object_class->set_property = gtd_task_list_view_set_property;
+
+ widget_class->map = gtd_task_list_view_map;
+
+ g_type_ensure (GTD_TYPE_EDIT_PANE);
+ g_type_ensure (GTD_TYPE_NEW_TASK_ROW);
+ g_type_ensure (GTD_TYPE_TASK_ROW);
+
+ /**
+ * GtdTaskListView::show-list-name:
+ *
+ * Whether the task rows should show the list name.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_SHOW_LIST_NAME,
+ g_param_spec_boolean ("show-list-name",
+ "Whether task rows show the list name",
+ "Whether task rows show the list name at the end of the row",
+ FALSE,
+ G_PARAM_READWRITE));
+
+ /**
+ * GtdTaskListView::show-due-date:
+ *
+ * Whether due dates of the tasks are shown.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_SHOW_DUE_DATE,
+ g_param_spec_boolean ("show-due-date",
+ "Whether due dates are shown",
+ "Whether due dates of the tasks are visible or not",
+ TRUE,
+ G_PARAM_READWRITE));
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-list-view.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, due_date_sizegroup);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, empty_list_widget);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, listbox);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, main_stack);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, tasklist_name_sizegroup);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskListView, scrolled_window);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_empty_list_widget_add_tasks_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_entered_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_new_task_row_exited_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_task_row_entered_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_task_row_exited_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "tasklistview");
+}
+
+static void
+gtd_task_list_view_init (GtdTaskListView *self)
+{
+ GtkDropTarget *target;
+
+ self->task_to_row = g_hash_table_new (NULL, NULL);
+
+ self->can_toggle = TRUE;
+ self->show_due_date = TRUE;
+ self->show_due_date = TRUE;
+
+ 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_drop_target_drag_drop_cb), self);
+ g_signal_connect (target, "enter", G_CALLBACK (on_drop_target_drag_enter_cb), self);
+ g_signal_connect (target, "leave", G_CALLBACK (on_drop_target_drag_leave_cb), self);
+ g_signal_connect (target, "motion", G_CALLBACK (on_drop_target_drag_motion_cb), self);
+
+ gtk_widget_add_controller (GTK_WIDGET (self->listbox), GTK_EVENT_CONTROLLER (target));
+
+ self->renderer = gtd_markdown_renderer_new ();
+
+ self->view_model = gtd_task_list_view_model_new ();
+ gtk_list_box_bind_model (self->listbox,
+ G_LIST_MODEL (self->view_model),
+ create_row_for_task_cb,
+ self,
+ NULL);
+}
+
+/**
+ * gtd_task_list_view_new:
+ *
+ * Creates a new #GtdTaskListView
+ *
+ * Returns: (transfer full): a newly allocated #GtdTaskListView
+ */
+GtkWidget*
+gtd_task_list_view_new (void)
+{
+ return g_object_new (GTD_TYPE_TASK_LIST_VIEW, NULL);
+}
+
+/**
+ * gtd_task_list_view_get_model:
+ * @view: a #GtdTaskListView
+ *
+ * Retrieves the #GtdTaskList from @view, or %NULL if none was set.
+ *
+ * Returns: (transfer none): the #GListModel of @view, or %NULL is
+ * none was set.
+ */
+GListModel*
+gtd_task_list_view_get_model (GtdTaskListView *view)
+{
+ g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), NULL);
+
+ return view->model;
+}
+
+/**
+ * gtd_task_list_view_set_model:
+ * @view: a #GtdTaskListView
+ * @model: a #GListModel
+ *
+ * Sets the internal #GListModel of @view. The model must have
+ * its element GType as @GtdTask.
+ */
+void
+gtd_task_list_view_set_model (GtdTaskListView *view,
+ GListModel *model)
+{
+ g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view));
+ g_return_if_fail (G_IS_LIST_MODEL (model));
+
+ if (view->model == model)
+ return;
+
+ view->model = model;
+
+ gtd_task_list_view_model_set_model (view->view_model, model);
+ schedule_scroll_to_bottom (view);
+ update_incomplete_tasks_model (view);
+ update_empty_state (view);
+}
+
+/**
+ * gtd_task_list_view_get_show_list_name:
+ * @view: a #GtdTaskListView
+ *
+ * Whether @view shows the tasks' list names.
+ *
+ * Returns: %TRUE if @view show the tasks' list names, %FALSE otherwise
+ */
+gboolean
+gtd_task_list_view_get_show_list_name (GtdTaskListView *view)
+{
+ g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (view), FALSE);
+
+ return view->show_list_name;
+}
+
+/**
+ * gtd_task_list_view_set_show_list_name:
+ * @view: a #GtdTaskListView
+ * @show_list_name: %TRUE to show list names, %FALSE to hide it
+ *
+ * Whether @view should should it's tasks' list name.
+ */
+void
+gtd_task_list_view_set_show_list_name (GtdTaskListView *view,
+ gboolean show_list_name)
+{
+ g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view));
+
+ if (view->show_list_name != show_list_name)
+ {
+ GtkWidget *child;
+
+ view->show_list_name = show_list_name;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child));
+
+ if (!GTD_IS_TASK_ROW (row_child))
+ continue;
+
+ gtd_task_row_set_list_name_visible (GTD_TASK_ROW (row_child), show_list_name);
+ }
+
+ g_object_notify (G_OBJECT (view), "show-list-name");
+ }
+}
+
+/**
+ * gtd_task_list_view_get_show_due_date:
+ * @self: a #GtdTaskListView
+ *
+ * Retrieves whether the @self is showing the due dates of the tasks
+ * or not.
+ *
+ * Returns: %TRUE if due dates are visible, %FALSE otherwise.
+ */
+gboolean
+gtd_task_list_view_get_show_due_date (GtdTaskListView *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), FALSE);
+
+ return self->show_due_date;
+}
+
+/**
+ * gtd_task_list_view_set_show_due_date:
+ * @self: a #GtdTaskListView
+ * @show_due_date: %TRUE to show due dates, %FALSE otherwise
+ *
+ * Sets whether @self shows the due dates of the tasks or not.
+ */
+void
+gtd_task_list_view_set_show_due_date (GtdTaskListView *self,
+ gboolean show_due_date)
+{
+ GtkWidget *child;
+
+ g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self));
+
+ if (self->show_due_date == show_due_date)
+ return;
+
+ self->show_due_date = show_due_date;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ GtkWidget *row_child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (child));
+
+ if (!GTD_IS_TASK_ROW (row_child))
+ continue;
+
+ gtd_task_row_set_due_date_visible (GTD_TASK_ROW (row_child), show_due_date);
+ }
+
+ g_object_notify (G_OBJECT (self), "show-due-date");
+}
+
+/**
+ * gtd_task_list_view_set_header_func:
+ * @view: a #GtdTaskListView
+ * @func: (closure user_data) (scope call) (nullable): the header function
+ * @user_data: data passed to @func
+ *
+ * Sets @func as the header function of @view. You can safely call
+ * %gtk_list_box_row_set_header from within @func.
+ *
+ * Do not unref nor free any of the passed data.
+ */
+void
+gtd_task_list_view_set_header_func (GtdTaskListView *view,
+ GtdTaskListViewHeaderFunc func,
+ gpointer user_data)
+{
+ g_return_if_fail (GTD_IS_TASK_LIST_VIEW (view));
+
+ if (func)
+ {
+ view->header_func = func;
+ view->header_user_data = user_data;
+
+ gtk_list_box_set_header_func (view->listbox,
+ (GtkListBoxUpdateHeaderFunc) internal_header_func,
+ view,
+ NULL);
+ }
+ else
+ {
+ view->header_func = NULL;
+ view->header_user_data = NULL;
+
+ gtk_list_box_set_header_func (view->listbox,
+ NULL,
+ NULL,
+ NULL);
+ }
+}
+
+/**
+ * gtd_task_list_view_get_default_date:
+ * @self: a #GtdTaskListView
+ *
+ * Retrieves the current default date which new tasks are set to.
+ *
+ * Returns: (nullable): a #GDateTime, or %NULL
+ */
+GDateTime*
+gtd_task_list_view_get_default_date (GtdTaskListView *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK_LIST_VIEW (self), NULL);
+
+ return self->default_date;
+}
+
+/**
+ * gtd_task_list_view_set_default_date:
+ * @self: a #GtdTaskListView
+ * @default_date: (nullable): the default_date, or %NULL
+ *
+ * Sets the current default date.
+ */
+void
+gtd_task_list_view_set_default_date (GtdTaskListView *self,
+ GDateTime *default_date)
+{
+ g_return_if_fail (GTD_IS_TASK_LIST_VIEW (self));
+
+ if (self->default_date == default_date)
+ return;
+
+ g_clear_pointer (&self->default_date, g_date_time_unref);
+ self->default_date = default_date ? g_date_time_ref (default_date) : NULL;
+}
+
diff --git a/src/gui/gtd-task-list-view.h b/src/gui/gtd-task-list-view.h
new file mode 100644
index 0000000..bb863dc
--- /dev/null
+++ b/src/gui/gtd-task-list-view.h
@@ -0,0 +1,73 @@
+/* gtd-task-list-view.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_VIEW_H
+#define GTD_TASK_LIST_VIEW_H
+
+#include "gtd-types.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TASK_LIST_VIEW (gtd_task_list_view_get_type())
+G_DECLARE_FINAL_TYPE (GtdTaskListView, gtd_task_list_view, GTD, TASK_LIST_VIEW, GtkBox)
+
+/**
+ * GtdTaskListViewHeaderFunc:
+ * @task: the #GtdTask that @row represents
+ * @previous_task: the #GtdTask that @before represents
+ * @user_data: (closure): user data
+ *
+ * The header function called on every task.
+ *
+ * Returns: (transfer full)(nullable): a #GtkWidget, or %NULL.
+ */
+typedef GtkWidget* (*GtdTaskListViewHeaderFunc) (GtdTask *task,
+ GtdTask *previous_task,
+ gpointer user_data);
+
+GtkWidget* gtd_task_list_view_new (void);
+
+GListModel* gtd_task_list_view_get_model (GtdTaskListView *view);
+
+void gtd_task_list_view_set_model (GtdTaskListView *view,
+ GListModel *model);
+
+gboolean gtd_task_list_view_get_show_list_name (GtdTaskListView *view);
+
+void gtd_task_list_view_set_show_list_name (GtdTaskListView *view,
+ gboolean show_list_name);
+
+gboolean gtd_task_list_view_get_show_due_date (GtdTaskListView *self);
+
+void gtd_task_list_view_set_show_due_date (GtdTaskListView *self,
+ gboolean show_due_date);
+
+void gtd_task_list_view_set_header_func (GtdTaskListView *view,
+ GtdTaskListViewHeaderFunc func,
+ gpointer user_data);
+
+GDateTime* gtd_task_list_view_get_default_date (GtdTaskListView *self);
+
+void gtd_task_list_view_set_default_date (GtdTaskListView *self,
+ GDateTime *default_date);
+
+G_END_DECLS
+
+#endif /* GTD_TASK_LIST_VIEW_H */
diff --git a/src/gui/gtd-task-list-view.ui b/src/gui/gtd-task-list-view.ui
new file mode 100644
index 0000000..b283753
--- /dev/null
+++ b/src/gui/gtd-task-list-view.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtdTaskListView" parent="GtkBox">
+ <property name="vexpand">1</property>
+ <property name="orientation">vertical</property>
+ <style>
+ <class name="view" />
+ </style>
+
+ <!-- Main stack -->
+ <child>
+ <object class="GtkStack" id="main_stack">
+ <property name="hexpand">true</property>
+ <property name="vexpand">true</property>
+ <property name="transition-type">crossfade</property>
+
+ <!-- Task list page -->
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">task-list</property>
+ <property name="child">
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="can_focus">1</property>
+ <property name="hexpand">1</property>
+ <property name="vexpand">1</property>
+ <property name="min-content-height">320</property>
+ <property name="hscrollbar-policy">never</property>
+ <child>
+ <object class="GtdWidget">
+ <property name="hexpand">1</property>
+ <property name="vexpand">1</property>
+ <property name="halign">center</property>
+ <property name="layout-manager">
+ <object class="GtdMaxSizeLayout">
+ <property name="max-width">700</property>
+ </object>
+ </property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">64</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="hexpand">1</property>
+ <property name="selection_mode">none</property>
+ <signal name="row-activated" handler="on_listbox_row_activated_cb" object="GtdTaskListView" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+
+ </child>
+ </object>
+
+ </property>
+ </object>
+ </child>
+
+ <!-- Empty list widget -->
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">empty-list</property>
+ <property name="child">
+ <object class="AdwStatusPage" id="empty_list_widget">
+ <property name="title">Tasks Will Appear Here</property>
+ <property name="icon-name">all-done</property>
+ <layout>
+ <property name="measure">true</property>
+ </layout>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="halign">center</property>
+ <property name="margin-top">24</property>
+ <property name="label">Add Tasks...</property>
+ <signal name="clicked" handler="on_empty_list_widget_add_tasks_cb" object="GtdTaskListView" swapped="no" />
+ <style>
+ <class name="suggested-action"/>
+ <class name="pill"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+ <object class="GtkSizeGroup" id="tasklist_name_sizegroup"/>
+ <object class="GtkSizeGroup" id="due_date_sizegroup"/>
+</interface>
diff --git a/src/gui/gtd-task-row.c b/src/gui/gtd-task-row.c
new file mode 100644
index 0000000..dd43d2b
--- /dev/null
+++ b/src/gui/gtd-task-row.c
@@ -0,0 +1,839 @@
+/* gtd-task-row.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 "GtdTaskRow"
+
+#include "gtd-debug.h"
+#include "gtd-edit-pane.h"
+#include "gtd-manager.h"
+#include "gtd-markdown-renderer.h"
+#include "gtd-provider.h"
+#include "gtd-star-widget.h"
+#include "gtd-task-row.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-task-list-view.h"
+#include "gtd-utils-private.h"
+#include "gtd-widget.h"
+
+#include <glib/gi18n.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+
+struct _GtdTaskRow
+{
+ GtkListBoxRow parent;
+
+ /*<private>*/
+ GtkWidget *content_box;
+ GtkWidget *main_box;
+
+ GtkWidget *done_check;
+ GtkWidget *edit_panel_revealer;
+ GtkWidget *header_event_box;
+ GtdStarWidget *star_widget;
+ GtkWidget *title_entry;
+
+ /* task widgets */
+ GtkLabel *task_date_label;
+ GtkLabel *task_list_label;
+
+ /* dnd widgets */
+ GtkWidget *dnd_box;
+ GtkWidget *dnd_icon;
+ gint clicked_x;
+ gint clicked_y;
+
+ /* data */
+ GtdTask *task;
+
+ GtdEditPane *edit_pane;
+
+ GtdMarkdownRenderer *renderer;
+ GPtrArray *bindings;
+
+ gboolean active;
+ gboolean changed;
+};
+
+#define PRIORITY_ICON_SIZE 8
+
+static void on_star_widget_activated_cb (GtdStarWidget *star_widget,
+ GParamSpec *pspec,
+ GtdTaskRow *self);
+
+G_DEFINE_TYPE (GtdTaskRow, gtd_task_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum
+{
+ ENTER,
+ EXIT,
+ REMOVE_TASK,
+ NUM_SIGNALS
+};
+
+enum
+{
+ PROP_0,
+ PROP_RENDERER,
+ PROP_TASK,
+ LAST_PROP
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+static gboolean
+date_to_label_binding_cb (GBinding *binding,
+ const GValue *from_value,
+ GValue *to_value,
+ gpointer user_data)
+{
+ g_autofree gchar *new_label = NULL;
+ GDateTime *dt;
+
+ g_return_val_if_fail (GTD_IS_TASK_ROW (user_data), FALSE);
+
+ dt = g_value_get_boxed (from_value);
+
+ if (dt)
+ {
+ g_autoptr (GDateTime) today = g_date_time_new_now_local ();
+
+ if (g_date_time_get_year (dt) == g_date_time_get_year (today) &&
+ g_date_time_get_month (dt) == g_date_time_get_month (today))
+ {
+ if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today))
+ {
+ new_label = g_strdup (_("Today"));
+ }
+ else if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today) + 1)
+ {
+ new_label = g_strdup (_("Tomorrow"));
+ }
+ else if (g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (today) - 1)
+ {
+ new_label = g_strdup (_("Yesterday"));
+ }
+ else if (g_date_time_get_day_of_year (dt) > g_date_time_get_day_of_month (today) &&
+ g_date_time_get_day_of_year (dt) < g_date_time_get_day_of_month (today) + 7)
+ {
+ new_label = g_date_time_format (dt, "%A");
+ }
+ else
+ {
+ new_label = g_date_time_format (dt, "%x");
+ }
+ }
+ else
+ {
+ new_label = g_date_time_format (dt, "%x");
+ }
+ }
+ else
+ {
+ new_label = g_strdup ("");
+ }
+
+ g_value_set_string (to_value, new_label);
+
+ return TRUE;
+}
+
+static GtkWidget*
+create_transient_row (GtdTaskRow *self)
+{
+ GtdTaskRow *new_row;
+
+ new_row = GTD_TASK_ROW (gtd_task_row_new (self->task, self->renderer));
+
+ gtk_widget_set_size_request (GTK_WIDGET (new_row),
+ gtk_widget_get_allocated_width (GTK_WIDGET (self)),
+ -1);
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (new_row->edit_panel_revealer), self->active);
+
+ gtk_widget_add_css_class (GTK_WIDGET (new_row), "background");
+ gtk_widget_set_opacity (GTK_WIDGET (new_row), 0.66);
+
+ return GTK_WIDGET (new_row);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_task_updated_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr (GError) error = NULL;
+
+ gtd_provider_update_task_finish (GTD_PROVIDER (object), result, &error);
+
+ if (error)
+ {
+ g_warning ("Error updating task: %s", error->message);
+ return;
+ }
+}
+
+static void
+on_remove_task_cb (GtdEditPane *edit_panel,
+ GtdTask *task,
+ GtdTaskRow *self)
+{
+ g_signal_emit (self, signals[REMOVE_TASK], 0);
+}
+
+static void
+on_button_press_event_cb (GtkGestureClick *gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ GtdTaskRow *self)
+{
+ GtkWidget *widget;
+ gdouble real_x;
+ gdouble real_y;
+
+ widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture));
+
+ gtk_widget_translate_coordinates (widget,
+ GTK_WIDGET (self),
+ x,
+ y,
+ &real_x,
+ &real_y);
+
+ self->clicked_x = real_x;
+ self->clicked_y = real_y;
+
+ GTD_TRACE_MSG ("GtkGestureClick:pressed received from a %s at %.1lf,%.1lf (%.1lf,%.1lf)",
+ G_OBJECT_TYPE_NAME (widget),
+ x,
+ y,
+ real_x,
+ real_y);
+}
+
+static GdkContentProvider*
+on_drag_prepare_cb (GtkDragSource *source,
+ gdouble x,
+ gdouble y,
+ GtdTaskRow *self)
+{
+ GTD_ENTRY;
+ GTD_RETURN (gdk_content_provider_new_typed (GTD_TYPE_TASK, self->task));
+}
+
+static void
+on_drag_begin_cb (GtkDragSource *source,
+ GdkDrag *drag,
+ GtdTaskRow *self)
+{
+ GtkWidget *drag_icon;
+ GtkWidget *new_row;
+ GtkWidget *widget;
+
+ GTD_ENTRY;
+
+ widget = GTK_WIDGET (self);
+
+ gtk_widget_set_cursor_from_name (widget, "grabbing");
+
+ new_row = create_transient_row (self);
+ drag_icon = gtk_drag_icon_get_for_drag (drag);
+ gtk_drag_icon_set_child (GTK_DRAG_ICON (drag_icon), new_row);
+ gdk_drag_set_hotspot (drag, self->clicked_x, self->clicked_y);
+
+ gtk_widget_hide (widget);
+
+ GTD_EXIT;
+}
+
+static gboolean
+on_drag_cancelled_cb (GtkDragSource *source,
+ GdkDrag *drag,
+ GdkDragCancelReason result,
+ GtdTaskRow *self)
+{
+ GTD_ENTRY;
+
+ gtk_widget_set_cursor_from_name (GTK_WIDGET (self), NULL);
+ gtk_widget_show (GTK_WIDGET (self));
+
+ GTD_RETURN (FALSE);
+}
+
+static void
+on_complete_check_toggled_cb (GtkCheckButton *button,
+ GtdTaskRow *self)
+{
+ GTD_ENTRY;
+
+ g_assert (GTD_IS_TASK (self->task));
+
+ gtd_task_row_set_active (self, FALSE);
+
+ gtd_task_set_complete (self->task, gtk_check_button_get_active (button));
+ gtd_provider_update_task (gtd_task_get_provider (self->task),
+ self->task,
+ NULL,
+ on_task_updated_cb,
+ self);
+
+ GTD_EXIT;
+}
+
+static void
+on_complete_changed_cb (GtdTaskRow *self,
+ GParamSpec *pspec,
+ GtdTask *task)
+{
+ gboolean complete;
+
+ complete = gtd_task_get_complete (task);
+
+ if (complete)
+ gtk_widget_add_css_class (GTK_WIDGET (self), "complete");
+ else
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "complete");
+
+ /* Update the toggle button as well */
+ g_signal_handlers_block_by_func (self->done_check, on_complete_check_toggled_cb, self);
+ gtk_check_button_set_active (GTK_CHECK_BUTTON (self->done_check), complete);
+ g_signal_handlers_unblock_by_func (self->done_check, on_complete_check_toggled_cb, self);
+}
+
+static void
+on_task_important_changed_cb (GtdTask *task,
+ GParamSpec *pspec,
+ GtdTaskRow *self)
+{
+ g_signal_handlers_block_by_func (self->star_widget, on_star_widget_activated_cb, self);
+
+ gtd_star_widget_set_active (self->star_widget, gtd_task_get_important (task));
+
+ g_signal_handlers_unblock_by_func (self->star_widget, on_star_widget_activated_cb, self);
+}
+
+static void
+on_star_widget_activated_cb (GtdStarWidget *star_widget,
+ GParamSpec *pspec,
+ GtdTaskRow *self)
+{
+ g_signal_handlers_block_by_func (self->task, on_task_important_changed_cb, self);
+
+ gtd_task_set_important (self->task, gtd_star_widget_get_active (star_widget));
+ gtd_provider_update_task (gtd_task_get_provider (self->task),
+ self->task,
+ NULL,
+ on_task_updated_cb,
+ self);
+
+ g_signal_handlers_unblock_by_func (self->task, on_task_important_changed_cb, self);
+}
+
+static gboolean
+on_key_pressed_cb (GtkEventControllerKey *controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType modifiers,
+ GtdTaskRow *self)
+{
+ GTD_ENTRY;
+
+ /* Exit when pressing Esc without modifiers */
+ if (keyval == GDK_KEY_Escape && !(modifiers & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)))
+ {
+ GTD_TRACE_MSG ("Escape pressed, closing task…");
+ gtd_task_row_set_active (self, FALSE);
+ }
+
+ GTD_RETURN (GDK_EVENT_PROPAGATE);
+}
+
+static void
+on_task_changed_cb (GtdTaskRow *self)
+{
+ g_debug ("Task changed");
+
+ self->changed = TRUE;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_row_finalize (GObject *object)
+{
+ GtdTaskRow *self = GTD_TASK_ROW (object);
+
+ if (self->changed)
+ {
+ if (self->task)
+ {
+ gtd_provider_update_task (gtd_task_get_provider (self->task),
+ self->task,
+ NULL,
+ on_task_updated_cb,
+ self);
+ }
+ self->changed = FALSE;
+ }
+
+ g_clear_object (&self->task);
+
+ G_OBJECT_CLASS (gtd_task_row_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_row_dispose (GObject *object)
+{
+ GtdTaskRow *self;
+ GtdTask *task;
+
+ self = GTD_TASK_ROW (object);
+ task = self->task;
+
+ g_clear_pointer (&self->bindings, g_ptr_array_unref);
+
+ if (task)
+ g_signal_handlers_disconnect_by_func (task, on_complete_changed_cb, self);
+
+ G_OBJECT_CLASS (gtd_task_row_parent_class)->dispose (object);
+}
+
+static void
+gtd_task_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskRow *self = GTD_TASK_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_RENDERER:
+ g_value_set_object (value, self->renderer);
+ break;
+
+ case PROP_TASK:
+ g_value_set_object (value, self->task);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskRow *self = GTD_TASK_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_RENDERER:
+ self->renderer = g_value_get_object (value);
+ break;
+
+ case PROP_TASK:
+ gtd_task_row_set_task (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_row_class_init (GtdTaskRowClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gtd_task_row_dispose;
+ object_class->finalize = gtd_task_row_finalize;
+ object_class->get_property = gtd_task_row_get_property;
+ object_class->set_property = gtd_task_row_set_property;
+
+ /**
+ * GtdTaskRow::renderer:
+ *
+ * The internal markdown renderer.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_RENDERER,
+ g_param_spec_object ("renderer",
+ "Renderer",
+ "Renderer",
+ GTD_TYPE_MARKDOWN_RENDERER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdTaskRow::task:
+ *
+ * The task that this row represents, or %NULL.
+ */
+ g_object_class_install_property (
+ object_class,
+ PROP_TASK,
+ g_param_spec_object ("task",
+ "Task of the row",
+ "The task that this row represents",
+ GTD_TYPE_TASK,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdTaskRow::enter:
+ *
+ * Emitted when the row is focused and in the editing state.
+ */
+ signals[ENTER] = g_signal_new ("enter",
+ GTD_TYPE_TASK_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdTaskRow::exit:
+ *
+ * Emitted when the row is unfocused and leaves the editing state.
+ */
+ signals[EXIT] = g_signal_new ("exit",
+ GTD_TYPE_TASK_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdTaskRow::remove-task:
+ *
+ * Emitted when the user wants to delete the task represented by this row.
+ */
+ signals[REMOVE_TASK] = g_signal_new ("remove-task",
+ GTD_TYPE_TASK_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-task-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, content_box);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, dnd_box);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, dnd_icon);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, done_check);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, edit_panel_revealer);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, header_event_box);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, main_box);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, star_widget);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, task_date_label);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, task_list_label);
+ gtk_widget_class_bind_template_child (widget_class, GtdTaskRow, title_entry);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_button_press_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_complete_check_toggled_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_key_pressed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_remove_task_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_task_changed_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "taskrow");
+}
+
+static void
+gtd_task_row_init (GtdTaskRow *self)
+{
+ GtkDragSource *drag_source;
+
+ self->bindings = g_ptr_array_new_with_free_func ((GDestroyNotify) g_binding_unbind);
+ self->active = FALSE;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ drag_source = gtk_drag_source_new ();
+ gtk_drag_source_set_actions (drag_source, GDK_ACTION_MOVE);
+
+ g_signal_connect (drag_source, "prepare", G_CALLBACK (on_drag_prepare_cb), self);
+ g_signal_connect (drag_source, "drag-begin", G_CALLBACK (on_drag_begin_cb), self);
+ g_signal_connect (drag_source, "drag-cancel", G_CALLBACK (on_drag_cancelled_cb), self);
+
+ gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag_source));
+
+ gtk_widget_set_cursor_from_name (self->dnd_icon, "grab");
+ gtk_widget_set_cursor_from_name (self->header_event_box, "pointer");
+}
+
+GtkWidget*
+gtd_task_row_new (GtdTask *task,
+ GtdMarkdownRenderer *renderer)
+{
+ return g_object_new (GTD_TYPE_TASK_ROW,
+ "task", task,
+ "renderer", renderer,
+ NULL);
+}
+
+void
+gtd_task_row_set_task (GtdTaskRow *self,
+ GtdTask *task)
+{
+ GBinding *binding;
+ GtdTask *old_task;
+
+ g_return_if_fail (GTD_IS_TASK_ROW (self));
+
+ old_task = self->task;
+
+ if (old_task)
+ {
+ g_signal_handlers_disconnect_by_func (old_task, on_complete_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (old_task, on_task_important_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (old_task, on_star_widget_activated_cb, self);
+ g_ptr_array_set_size (self->bindings, 0);
+ }
+
+ g_set_object (&self->task, task);
+
+ if (task)
+ {
+ gtk_label_set_label (self->task_list_label,
+ gtd_task_list_get_name (gtd_task_get_list (task)));
+
+ g_signal_handlers_block_by_func (self->title_entry, on_task_changed_cb, self);
+ g_signal_handlers_block_by_func (self->done_check, on_complete_check_toggled_cb, self);
+
+ binding = g_object_bind_property (task,
+ "loading",
+ self,
+ "sensitive",
+ G_BINDING_DEFAULT |
+ G_BINDING_INVERT_BOOLEAN |
+ G_BINDING_SYNC_CREATE);
+ g_ptr_array_add (self->bindings, binding);
+
+ binding = g_object_bind_property (task,
+ "title",
+ self->title_entry,
+ "text",
+ G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+ g_ptr_array_add (self->bindings, binding);
+
+ binding = g_object_bind_property_full (task,
+ "due-date",
+ self->task_date_label,
+ "label",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE,
+ date_to_label_binding_cb,
+ NULL,
+ self,
+ NULL);
+ g_ptr_array_add (self->bindings, binding);
+
+ on_complete_changed_cb (self, NULL, task);
+ g_signal_connect_object (task,
+ "notify::complete",
+ G_CALLBACK (on_complete_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ on_task_important_changed_cb (task, NULL, self);
+ g_signal_connect_object (task,
+ "notify::important",
+ G_CALLBACK (on_task_important_changed_cb),
+ self,
+ 0);
+
+ g_signal_connect_object (self->star_widget,
+ "notify::active",
+ G_CALLBACK (on_star_widget_activated_cb),
+ self,
+ 0);
+
+ g_signal_handlers_unblock_by_func (self->done_check, on_complete_check_toggled_cb, self);
+ g_signal_handlers_unblock_by_func (self->title_entry, on_task_changed_cb, self);
+ }
+}
+
+/**
+ * gtd_task_row_get_task:
+ * @row: a #GtdTaskRow
+ *
+ * Retrieves the #GtdTask that @row manages, or %NULL if none
+ * is set.
+ *
+ * Returns: (transfer none): the internal task of @row
+ */
+GtdTask*
+gtd_task_row_get_task (GtdTaskRow *row)
+{
+ g_return_val_if_fail (GTD_IS_TASK_ROW (row), NULL);
+
+ return row->task;
+}
+
+/**
+ * gtd_task_row_set_list_name_visible:
+ * @row: a #GtdTaskRow
+ * @show_list_name: %TRUE to show the list name, %FALSE to hide it
+ *
+ * Sets @row's list name label visibility to @show_list_name.
+ */
+void
+gtd_task_row_set_list_name_visible (GtdTaskRow *row,
+ gboolean show_list_name)
+{
+ g_return_if_fail (GTD_IS_TASK_ROW (row));
+
+ gtk_widget_set_visible (GTK_WIDGET (row->task_list_label), show_list_name);
+}
+
+/**
+ * gtd_task_row_set_due_date_visible:
+ * @row: a #GtdTaskRow
+ * @show_due_date: %TRUE to show the due, %FALSE to hide it
+ *
+ * Sets @row's due date label visibility to @show_due_date.
+ */
+void
+gtd_task_row_set_due_date_visible (GtdTaskRow *row,
+ gboolean show_due_date)
+{
+ g_return_if_fail (GTD_IS_TASK_ROW (row));
+
+ gtk_widget_set_visible (GTK_WIDGET (row->task_date_label), show_due_date);
+}
+
+gboolean
+gtd_task_row_get_active (GtdTaskRow *self)
+{
+ g_return_val_if_fail (GTD_IS_TASK_ROW (self), FALSE);
+
+ return self->active;
+}
+
+void
+gtd_task_row_set_active (GtdTaskRow *self,
+ gboolean active)
+{
+ GDateTime *dt;
+
+ g_return_if_fail (GTD_IS_TASK_ROW (self));
+
+ if (self->active == active)
+ return;
+
+ self->active = active;
+
+ /* Create or destroy the edit panel */
+ if (active && !self->edit_pane)
+ {
+ GTD_TRACE_MSG ("Creating edit pane");
+
+ self->edit_pane = GTD_EDIT_PANE (gtd_edit_pane_new ());
+ gtd_edit_pane_set_markdown_renderer (self->edit_pane, self->renderer);
+ gtd_edit_pane_set_task (self->edit_pane, self->task);
+
+ gtk_revealer_set_child (GTK_REVEALER (self->edit_panel_revealer), GTK_WIDGET (self->edit_pane));
+ gtk_widget_show (GTK_WIDGET (self->edit_pane));
+
+ g_signal_connect_swapped (self->edit_pane, "changed", G_CALLBACK (on_task_changed_cb), self);
+ g_signal_connect (self->edit_pane, "remove-task", G_CALLBACK (on_remove_task_cb), self);
+ }
+ else if (!active && self->edit_pane)
+ {
+ /* For some reason, setting the due date before closing the Edit Row has
+ * the potential to cause the Task List to update, causing the active task row
+ * to become corrupted, and crash Endeavour. My guess is this is an upstream
+ * behaviour, so work around it by setting the task due date during close */
+ dt = gtd_edit_pane_get_new_date_time (self->edit_pane);
+
+ if (gtd_task_get_due_date (self->task) != dt) {
+ gtd_task_set_due_date (self->task,
+ gtd_edit_pane_get_new_date_time (self->edit_pane));
+ }
+
+ GTD_TRACE_MSG ("Destroying edit pane");
+
+ gtk_revealer_set_child (GTK_REVEALER (self->edit_panel_revealer), NULL);
+ self->edit_pane = NULL;
+ }
+
+ /* And reveal or hide it */
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->edit_panel_revealer), active);
+
+ /* Toggle title editability */
+ gtk_editable_set_editable (GTK_EDITABLE (self->title_entry), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->title_entry), active);
+
+ /* Save the task if it is not being loaded */
+ if (!active && !gtd_object_get_loading (GTD_OBJECT (self->task)) && self->changed)
+ {
+ g_debug ("Saving task…");
+
+ gtd_provider_update_task (gtd_task_get_provider (self->task),
+ self->task,
+ NULL,
+ on_task_updated_cb,
+ self);
+ self->changed = FALSE;
+ }
+
+ if (active) {
+ gtk_widget_add_css_class (GTK_WIDGET (self), "card");
+ } else {
+ gtk_widget_remove_css_class (GTK_WIDGET (self), "card");
+ }
+
+ g_signal_emit (self, active ? signals[ENTER] : signals[EXIT], 0);
+}
+
+void
+gtd_task_row_set_sizegroups (GtdTaskRow *self,
+ GtkSizeGroup *name_group,
+ GtkSizeGroup *date_group)
+{
+ gtk_size_group_add_widget (name_group, GTK_WIDGET (self->task_list_label));
+ gtk_size_group_add_widget (name_group, GTK_WIDGET (self->task_date_label));
+}
diff --git a/src/gui/gtd-task-row.h b/src/gui/gtd-task-row.h
new file mode 100644
index 0000000..111fec3
--- /dev/null
+++ b/src/gui/gtd-task-row.h
@@ -0,0 +1,54 @@
+/* gtd-task-row.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_ROW_H
+#define GTD_TASK_ROW_H
+
+#include "endeavour.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TASK_ROW (gtd_task_row_get_type())
+G_DECLARE_FINAL_TYPE (GtdTaskRow, gtd_task_row, GTD, TASK_ROW, GtkListBoxRow)
+
+GtkWidget* gtd_task_row_new (GtdTask *task,
+ GtdMarkdownRenderer *renderer);
+
+void gtd_task_row_set_task (GtdTaskRow *self,
+ GtdTask *task);
+
+GtdTask* gtd_task_row_get_task (GtdTaskRow *row);
+
+void gtd_task_row_set_list_name_visible (GtdTaskRow *row,
+ gboolean show_list_name);
+
+void gtd_task_row_set_due_date_visible (GtdTaskRow *row,
+ gboolean show_due_date);
+
+gboolean gtd_task_row_get_active (GtdTaskRow *self);
+
+void gtd_task_row_set_active (GtdTaskRow *self,
+ gboolean active);
+
+void gtd_task_row_set_sizegroups (GtdTaskRow *self,
+ GtkSizeGroup *name_group,
+ GtkSizeGroup *date_group);
+
+G_END_DECLS
+
+#endif /* GTD_TASK_ROW_H */
diff --git a/src/gui/gtd-task-row.ui b/src/gui/gtd-task-row.ui
new file mode 100644
index 0000000..601464b
--- /dev/null
+++ b/src/gui/gtd-task-row.ui
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdTaskRow" parent="GtkListBoxRow">
+ <property name="hexpand">true</property>
+ <child>
+ <object class="GtkEventControllerKey">
+ <property name="propagation-phase">capture</property>
+ <signal name="key-pressed" handler="on_key_pressed_cb" object="GtdTaskRow" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="main_box">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="content_box">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-end">6</property>
+ <property name="margin-start">5</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="height-request">32</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="dnd_box"/>
+ </child>
+ <child>
+ <object class="GtkImage" id="dnd_icon">
+ <property name="icon-name">drag-handle-symbolic</property>
+ <property name="pixel-size">16</property>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">capture</property>
+ <signal name="pressed" handler="on_button_press_event_cb" object="GtdTaskRow" swapped="no"/>
+ </object>
+ </child>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="done_check">
+ <property name="can_focus">1</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">1</property>
+ <property name="margin-start">6</property>
+ <signal name="toggled" handler="on_complete_check_toggled_cb" object="GtdTaskRow" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="header_event_box">
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">capture</property>
+ <signal name="pressed" handler="on_button_press_event_cb" object="GtdTaskRow" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="hexpand">1</property>
+ <child>
+ <object class="GtkText" id="title_entry">
+ <property name="width-chars">5</property>
+ <property name="max-width-chars">72</property>
+ <property name="propagate-text-width">1</property>
+ <property name="editable">false</property>
+ <property name="sensitive">false</property>
+ <signal name="activate" handler="on_task_changed_cb" object="GtdTaskRow" swapped="yes"/>
+ <signal name="notify::text" handler="on_task_changed_cb" object="GtdTaskRow" swapped="yes"/>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="task_date_label">
+ <property name="xalign">1.0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="task_list_label">
+ <property name="visible">0</property>
+ <property name="xalign">1.0</property>
+ <property name="max_width_chars">18</property>
+ <property name="ellipsize">middle</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+
+ <!-- Star Widget -->
+ <child>
+ <object class="GtkBox">
+ <property name="margin-start">6</property>
+ <child>
+ <object class="GtdStarWidget" id="star_widget">
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id="edit_panel_revealer">
+ <property name="transition_type">slide-up</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/gui/gtd-widget.c b/src/gui/gtd-widget.c
new file mode 100644
index 0000000..e5bece4
--- /dev/null
+++ b/src/gui/gtd-widget.c
@@ -0,0 +1,1668 @@
+/* gtd-widget.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-animatable.h"
+#include "gtd-bin-layout.h"
+#include "gtd-debug.h"
+#include "gtd-animation-enums.h"
+#include "gtd-interval.h"
+#include "gtd-timeline.h"
+#include "gtd-property-transition.h"
+
+#include <graphene-gobject.h>
+#include <gobject/gvaluecollector.h>
+
+enum
+{
+ X,
+ Y,
+ Z,
+};
+
+typedef struct
+{
+ guint easing_duration;
+ guint easing_delay;
+ GtdEaseMode easing_mode;
+} AnimationState;
+
+typedef struct
+{
+ struct {
+ GHashTable *transitions;
+ GArray *states;
+ AnimationState *current_state;
+ } animation;
+
+ graphene_point3d_t pivot_point;
+ gfloat rotation[3];
+ gfloat scale[3];
+ graphene_point3d_t translation;
+
+ GskTransform *cached_transform;
+} GtdWidgetPrivate;
+
+static void set_animatable_property (GtdWidget *self,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec);
+
+static void gtd_animatable_iface_init (GtdAnimatableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdWidget, gtd_widget, GTK_TYPE_WIDGET,
+ G_ADD_PRIVATE (GtdWidget)
+ G_IMPLEMENT_INTERFACE (GTD_TYPE_ANIMATABLE, gtd_animatable_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_PIVOT_POINT,
+ PROP_ROTATION_X,
+ PROP_ROTATION_Y,
+ PROP_ROTATION_Z,
+ PROP_SCALE_X,
+ PROP_SCALE_Y,
+ PROP_SCALE_Z,
+ PROP_TRANSLATION_X,
+ PROP_TRANSLATION_Y,
+ PROP_TRANSLATION_Z,
+ N_PROPS
+};
+
+enum
+{
+ TRANSITION_STOPPED,
+ TRANSITIONS_COMPLETED,
+ NUM_SIGNALS
+};
+
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+static GParamSpec *properties [N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+typedef struct
+{
+ GtdWidget *widget;
+ GtdTransition *transition;
+ gchar *name;
+ gulong completed_id;
+} TransitionClosure;
+
+static void
+transition_closure_free (gpointer data)
+{
+ if (G_LIKELY (data != NULL))
+ {
+ TransitionClosure *closure = data;
+ GtdTimeline *timeline;
+
+ timeline = GTD_TIMELINE (closure->transition);
+
+ /* we disconnect the signal handler before stopping the timeline,
+ * so that we don't end up inside on_transition_stopped() from
+ * a call to g_hash_table_remove().
+ */
+ g_clear_signal_handler (&closure->completed_id, closure->transition);
+
+ if (gtd_timeline_is_playing (timeline))
+ gtd_timeline_stop (timeline);
+
+ g_object_unref (closure->transition);
+
+ g_free (closure->name);
+
+ g_slice_free (TransitionClosure, closure);
+ }
+}
+
+static void
+on_transition_stopped_cb (GtdTransition *transition,
+ gboolean is_finished,
+ TransitionClosure *closure)
+{
+ GtdWidget *self = closure->widget;
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+ GQuark t_quark;
+ gchar *t_name;
+
+ if (closure->name == NULL)
+ return;
+
+ /* we need copies because we emit the signal after the
+ * TransitionClosure data structure has been freed
+ */
+ t_quark = g_quark_from_string (closure->name);
+ t_name = g_strdup (closure->name);
+
+ if (gtd_transition_get_remove_on_complete (transition))
+ {
+ /* this is safe, because the timeline has now stopped,
+ * so we won't recurse; the reference on the Animatable
+ * will be dropped by the ::stopped signal closure in
+ * GtdTransition, which is RUN_LAST, and thus will
+ * be called after this handler
+ */
+ g_hash_table_remove (priv->animation.transitions, closure->name);
+ }
+
+ /* we emit the ::transition-stopped after removing the
+ * transition, so that we can chain up new transitions
+ * without interfering with the one that just finished
+ */
+ g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, is_finished);
+
+ g_free (t_name);
+
+ /* if it's the last transition then we clean up */
+ if (g_hash_table_size (priv->animation.transitions) == 0)
+ {
+ g_hash_table_unref (priv->animation.transitions);
+ priv->animation.transitions = NULL;
+
+ GTD_TRACE_MSG ("[animation] Transitions for '%p' completed", self);
+
+ g_signal_emit (self, signals[TRANSITIONS_COMPLETED], 0);
+ }
+}
+
+static void
+add_transition_to_widget (GtdWidget *self,
+ const gchar *name,
+ GtdTransition *transition)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+ TransitionClosure *closure;
+ GtdTimeline *timeline;
+
+ GTD_ENTRY;
+
+ if (!priv->animation.transitions)
+ {
+ priv->animation.transitions = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ NULL,
+ transition_closure_free);
+ }
+
+ if (g_hash_table_lookup (priv->animation.transitions, name) != NULL)
+ {
+ g_warning ("A transition with name '%s' already exists for the widget '%p'",
+ name,
+ self);
+ GTD_RETURN ();
+ }
+
+ gtd_transition_set_animatable (transition, GTD_ANIMATABLE (self));
+
+ timeline = GTD_TIMELINE (transition);
+
+ closure = g_slice_new (TransitionClosure);
+ closure->widget = self;
+ closure->transition = g_object_ref (transition);
+ closure->name = g_strdup (name);
+ closure->completed_id = g_signal_connect (timeline,
+ "stopped",
+ G_CALLBACK (on_transition_stopped_cb),
+ closure);
+
+ GTD_TRACE_MSG ("[animation] Adding transition '%s' [%p] to widget %p",
+ closure->name,
+ closure->transition,
+ self);
+
+ g_hash_table_insert (priv->animation.transitions, closure->name, closure);
+ gtd_timeline_start (timeline);
+
+ GTD_EXIT;
+}
+
+static gboolean
+should_skip_implicit_transition (GtdWidget *self,
+ GParamSpec *pspec)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+
+ /* if the easing state has a non-zero duration we always want an
+ * implicit transition to occur
+ */
+ if (priv->animation.current_state->easing_duration == 0)
+ return TRUE;
+
+ /* if the widget is not mapped and is not part of a branch of the scene
+ * graph that is being cloned, then we always skip implicit transitions
+ * on the account of the fact that the widget is not going to be visible
+ * when those transitions happen
+ */
+ if (!gtk_widget_get_mapped (GTK_WIDGET (self)))
+ return TRUE;
+
+ return FALSE;
+}
+
+static GtdTransition*
+create_transition (GtdWidget *self,
+ GParamSpec *pspec,
+ ...)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+ g_autofree gchar *error = NULL;
+ g_auto (GValue) initial = G_VALUE_INIT;
+ g_auto (GValue) final = G_VALUE_INIT;
+ TransitionClosure *closure;
+ GtdTimeline *timeline;
+ GtdInterval *interval;
+ GtdTransition *res = NULL;
+ va_list var_args;
+ GType ptype;
+
+ g_assert (pspec != NULL);
+
+ if (!priv->animation.transitions)
+ {
+ priv->animation.transitions = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ NULL,
+ transition_closure_free);
+ }
+
+ va_start (var_args, pspec);
+
+ ptype = G_PARAM_SPEC_VALUE_TYPE (pspec);
+
+ G_VALUE_COLLECT_INIT (&initial, ptype, var_args, 0, &error);
+ if (error != NULL)
+ {
+ g_critical ("%s: %s", G_STRLOC, error);
+ goto out;
+ }
+
+ G_VALUE_COLLECT_INIT (&final, ptype, var_args, 0, &error);
+ if (error != NULL)
+ {
+ g_critical ("%s: %s", G_STRLOC, error);
+ goto out;
+ }
+
+ if (should_skip_implicit_transition (self, pspec))
+ {
+ GTD_TRACE_MSG ("[animation] Skipping implicit transition for '%p::%s'",
+ self,
+ pspec->name);
+
+ /* remove a transition, if one exists */
+ gtd_widget_remove_transition (self, pspec->name);
+
+ /* we don't go through the Animatable interface because we
+ * already know we got here through an animatable property.
+ */
+ set_animatable_property (self, pspec->param_id, &final, pspec);
+
+ goto out;
+ }
+
+ closure = g_hash_table_lookup (priv->animation.transitions, pspec->name);
+ if (closure == NULL)
+ {
+ res = gtd_property_transition_new (pspec->name);
+
+ gtd_transition_set_remove_on_complete (res, TRUE);
+
+ interval = gtd_interval_new_with_values (ptype, &initial, &final);
+ gtd_transition_set_interval (res, interval);
+
+ timeline = GTD_TIMELINE (res);
+ gtd_timeline_set_delay (timeline, priv->animation.current_state->easing_delay);
+ gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration);
+ gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode);
+
+ /* this will start the transition as well */
+ add_transition_to_widget (self, pspec->name, res);
+
+ /* the widget now owns the transition */
+ g_object_unref (res);
+ }
+ else
+ {
+ GtdEaseMode cur_mode;
+ guint cur_duration;
+
+ GTD_TRACE_MSG ("[animation] Existing transition for %p:%s",
+ self,
+ pspec->name);
+
+ timeline = GTD_TIMELINE (closure->transition);
+
+ cur_duration = gtd_timeline_get_duration (timeline);
+ if (cur_duration != priv->animation.current_state->easing_duration)
+ gtd_timeline_set_duration (timeline, priv->animation.current_state->easing_duration);
+
+ cur_mode = gtd_timeline_get_progress_mode (timeline);
+ if (cur_mode != priv->animation.current_state->easing_mode)
+ gtd_timeline_set_progress_mode (timeline, priv->animation.current_state->easing_mode);
+
+ gtd_timeline_rewind (timeline);
+
+ interval = gtd_transition_get_interval (closure->transition);
+ gtd_interval_set_initial_value (interval, &initial);
+ gtd_interval_set_final_value (interval, &final);
+
+ res = closure->transition;
+ }
+
+out:
+ va_end (var_args);
+
+ return res;
+}
+
+static void
+invalidate_cached_transform (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+
+ g_clear_pointer (&priv->cached_transform, gsk_transform_unref);
+}
+
+static void
+calculate_transform (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+ graphene_point3d_t pivot;
+ GskTransform *transform;
+ gboolean pivot_is_zero;
+ gint height;
+ gint width;
+
+ transform = NULL;
+ width = gtk_widget_get_width (GTK_WIDGET (self));
+ height = gtk_widget_get_height (GTK_WIDGET (self));
+
+ /* Pivot point */
+ pivot_is_zero = graphene_point3d_equal (&priv->pivot_point, graphene_point3d_zero ());
+ pivot = GRAPHENE_POINT3D_INIT (width * priv->pivot_point.x,
+ height * priv->pivot_point.y,
+ priv->pivot_point.z);
+ if (!pivot_is_zero)
+ transform = gsk_transform_translate_3d (transform, &pivot);
+
+ /* Perspective */
+ transform = gsk_transform_perspective (transform, 2 * MAX (width, height));
+
+ /* Translation */
+ if (G_APPROX_VALUE (priv->translation.z, 0.f, FLT_EPSILON))
+ {
+ transform = gsk_transform_translate (transform,
+ &GRAPHENE_POINT_INIT (priv->translation.x,
+ priv->translation.y));
+ }
+ else
+ {
+ transform = gsk_transform_translate_3d (transform, &priv->translation);
+ }
+
+ /* Scale */
+ if (G_APPROX_VALUE (priv->scale[Z], 1.f, FLT_EPSILON))
+ transform = gsk_transform_scale (transform, priv->scale[X], priv->scale[Y]);
+ else
+ transform = gsk_transform_scale_3d (transform, priv->scale[X], priv->scale[Y], priv->scale[Z]);
+
+ /* Rotation */
+ transform = gsk_transform_rotate_3d (transform, priv->rotation[X], graphene_vec3_x_axis ());
+ transform = gsk_transform_rotate_3d (transform, priv->rotation[Y], graphene_vec3_y_axis ());
+ transform = gsk_transform_rotate_3d (transform, priv->rotation[Z], graphene_vec3_z_axis ());
+
+ /* Rollback pivot point */
+ if (!pivot_is_zero)
+ transform = gsk_transform_translate_3d (transform,
+ &GRAPHENE_POINT3D_INIT (-pivot.x,
+ -pivot.y,
+ -pivot.z));
+
+ priv->cached_transform = transform;
+}
+
+static void
+set_rotation_internal (GtdWidget *self,
+ gfloat rotation_x,
+ gfloat rotation_y,
+ gfloat rotation_z)
+{
+ GtdWidgetPrivate *priv;
+ gboolean changed[3];
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON);
+ changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON);
+ changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON);
+
+ if (!changed[X] && !changed[Y] && !changed[Z])
+ GTD_RETURN ();
+
+ invalidate_cached_transform (self);
+
+ priv->rotation[X] = rotation_x;
+ priv->rotation[Y] = rotation_y;
+ priv->rotation[Z] = rotation_z;
+
+ if (changed[X])
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_X]);
+
+ if (changed[Y])
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Y]);
+
+ if (changed[Z])
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ROTATION_Z]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ GTD_EXIT;
+}
+
+static void
+set_scale_internal (GtdWidget *self,
+ gfloat scale_x,
+ gfloat scale_y,
+ gfloat scale_z)
+{
+ GtdWidgetPrivate *priv;
+ gboolean changed[3];
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON);
+ changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON);
+ changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON);
+
+ if (!changed[X] && !changed[Y] && !changed[Z])
+ GTD_RETURN ();
+
+ invalidate_cached_transform (self);
+
+ priv->scale[X] = scale_x;
+ priv->scale[Y] = scale_y;
+ priv->scale[Z] = scale_z;
+
+ if (changed[X])
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_X]);
+
+ if (changed[Y])
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Y]);
+
+ if (changed[Z])
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SCALE_Z]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ GTD_EXIT;
+}
+
+static void
+set_translation_internal (GtdWidget *self,
+ gfloat translation_x,
+ gfloat translation_y,
+ gfloat translation_z)
+{
+ graphene_point3d_t old_translation, translation;
+ GtdWidgetPrivate *priv;
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+ translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z);
+
+ if (graphene_point3d_equal (&priv->translation, &translation))
+ GTD_RETURN ();
+
+ old_translation = priv->translation;
+
+ invalidate_cached_transform (self);
+ priv->translation = translation;
+
+ if (!G_APPROX_VALUE (old_translation.x, translation.x, FLT_EPSILON))
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_X]);
+
+ if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON))
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Y]);
+
+ if (!G_APPROX_VALUE (old_translation.y, translation.y, FLT_EPSILON))
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSLATION_Z]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ GTD_EXIT;
+}
+
+static void
+set_animatable_property (GtdWidget *self,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+ GObject *object = G_OBJECT (self);
+
+ g_object_freeze_notify (object);
+
+ switch (prop_id)
+ {
+ case PROP_ROTATION_X:
+ set_rotation_internal (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]);
+ break;
+
+ case PROP_ROTATION_Y:
+ set_rotation_internal (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]);
+ break;
+
+ case PROP_ROTATION_Z:
+ set_rotation_internal (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value));
+ break;
+
+ case PROP_SCALE_X:
+ set_scale_internal (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]);
+ break;
+
+ case PROP_SCALE_Y:
+ set_scale_internal (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]);
+ break;
+
+ case PROP_SCALE_Z:
+ set_scale_internal (self, priv->scale[X], priv->scale[Y], g_value_get_float (value));
+ break;
+
+ case PROP_TRANSLATION_X:
+ set_translation_internal (self, g_value_get_float (value), priv->translation.y, priv->translation.z);
+ break;
+
+ case PROP_TRANSLATION_Y:
+ set_translation_internal (self, priv->translation.x, g_value_get_float (value), priv->translation.z);
+ break;
+
+ case PROP_TRANSLATION_Z:
+ set_translation_internal (self, priv->translation.x, priv->translation.y, g_value_get_float (value));
+ break;
+
+ default:
+ g_object_set_property (object, pspec->name, value);
+ break;
+ }
+
+ g_object_thaw_notify (object);
+}
+
+/*
+ * GtdAnimatable interface
+ */
+
+static GParamSpec *
+gtd_widget_find_property (GtdAnimatable *animatable,
+ const gchar *property_name)
+{
+ return g_object_class_find_property (G_OBJECT_GET_CLASS (animatable), property_name);
+}
+
+static void
+gtd_widget_get_initial_state (GtdAnimatable *animatable,
+ const gchar *property_name,
+ GValue *initial)
+{
+ g_object_get_property (G_OBJECT (animatable), property_name, initial);
+}
+
+static void
+gtd_widget_set_final_state (GtdAnimatable *animatable,
+ const gchar *property_name,
+ const GValue *final)
+{
+ GObjectClass *obj_class = G_OBJECT_GET_CLASS (animatable);
+ GParamSpec *pspec;
+
+ pspec = g_object_class_find_property (obj_class, property_name);
+
+ if (pspec)
+ set_animatable_property (GTD_WIDGET (animatable), pspec->param_id, final, pspec);
+}
+
+static GtdWidget*
+gtd_widget_get_widget (GtdAnimatable *animatable)
+{
+ return GTD_WIDGET (animatable);
+}
+
+static void
+gtd_animatable_iface_init (GtdAnimatableInterface *iface)
+{
+ iface->find_property = gtd_widget_find_property;
+ iface->get_initial_state = gtd_widget_get_initial_state;
+ iface->set_final_state = gtd_widget_set_final_state;
+ iface->get_widget = gtd_widget_get_widget;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_widget_dispose (GObject *object)
+{
+ GtdWidget *self = GTD_WIDGET (object);
+ GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (object));
+
+ while (child)
+ {
+ GtkWidget *next = gtk_widget_get_next_sibling (child);
+
+ gtk_widget_unparent (child);
+ child = next;
+ }
+
+ invalidate_cached_transform (self);
+ gtd_widget_remove_all_transitions (self);
+
+ G_OBJECT_CLASS (gtd_widget_parent_class)->dispose (object);
+}
+
+static void
+gtd_widget_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdWidget *self = GTD_WIDGET (object);
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_PIVOT_POINT:
+ g_value_set_boxed (value, &priv->pivot_point);
+ break;
+
+ case PROP_ROTATION_X:
+ g_value_set_float (value, priv->rotation[X]);
+ break;
+
+ case PROP_ROTATION_Y:
+ g_value_set_float (value, priv->rotation[Y]);
+ break;
+
+ case PROP_ROTATION_Z:
+ g_value_set_float (value, priv->rotation[Z]);
+ break;
+
+ case PROP_SCALE_X:
+ g_value_set_float (value, priv->scale[X]);
+ break;
+
+ case PROP_SCALE_Y:
+ g_value_set_float (value, priv->scale[Y]);
+ break;
+
+ case PROP_SCALE_Z:
+ g_value_set_float (value, priv->scale[Z]);
+ break;
+
+ case PROP_TRANSLATION_X:
+ g_value_set_float (value, priv->translation.x);
+ break;
+
+ case PROP_TRANSLATION_Y:
+ g_value_set_float (value, priv->translation.y);
+ break;
+
+ case PROP_TRANSLATION_Z:
+ g_value_set_float (value, priv->translation.z);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_widget_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdWidget *self = GTD_WIDGET (object);
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_PIVOT_POINT:
+ gtd_widget_set_pivot_point (self, g_value_get_boxed (value));
+ break;
+
+ case PROP_ROTATION_X:
+ gtd_widget_set_rotation (self, g_value_get_float (value), priv->rotation[Y], priv->rotation[Z]);
+ break;
+
+ case PROP_ROTATION_Y:
+ gtd_widget_set_rotation (self, priv->rotation[X], g_value_get_float (value), priv->rotation[Z]);
+ break;
+
+ case PROP_ROTATION_Z:
+ gtd_widget_set_rotation (self, priv->rotation[X], priv->rotation[Y], g_value_get_float (value));
+ break;
+
+ case PROP_SCALE_X:
+ gtd_widget_set_scale (self, g_value_get_float (value), priv->scale[Y], priv->scale[Z]);
+ break;
+
+ case PROP_SCALE_Y:
+ gtd_widget_set_scale (self, priv->scale[X], g_value_get_float (value), priv->scale[Z]);
+ break;
+
+ case PROP_SCALE_Z:
+ gtd_widget_set_scale (self, priv->scale[X], priv->scale[Y], g_value_get_float (value));
+ break;
+
+ case PROP_TRANSLATION_X:
+ gtd_widget_set_translation (self, g_value_get_float (value), priv->translation.y, priv->translation.z);
+ break;
+
+ case PROP_TRANSLATION_Y:
+ gtd_widget_set_translation (self, priv->translation.x, g_value_get_float (value), priv->translation.z);
+ break;
+
+ case PROP_TRANSLATION_Z:
+ gtd_widget_set_translation (self, priv->translation.x, priv->translation.y, g_value_get_float (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_widget_class_init (GtdWidgetClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gtd_widget_dispose;
+ object_class->get_property = gtd_widget_get_property;
+ object_class->set_property = gtd_widget_set_property;
+
+ /**
+ * GtdWidget:
+ */
+ properties[PROP_PIVOT_POINT] = g_param_spec_boxed ("pivot-point",
+ "Pivot point",
+ "Pivot point",
+ GRAPHENE_TYPE_POINT3D,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:rotation-x
+ */
+ properties[PROP_ROTATION_X] = g_param_spec_float ("rotation-x",
+ "Rotation in the X axis",
+ "Rotation in the X axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 0.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:rotation-y
+ */
+ properties[PROP_ROTATION_Y] = g_param_spec_float ("rotation-y",
+ "Rotation in the Y axis",
+ "Rotation in the Y axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 0.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:rotation-z
+ */
+ properties[PROP_ROTATION_Z] = g_param_spec_float ("rotation-z",
+ "Rotation in the Z axis",
+ "Rotation in the Z axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 0.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:scale-x
+ */
+ properties[PROP_SCALE_X] = g_param_spec_float ("scale-x",
+ "Scale in the X axis",
+ "Scale in the X axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 1.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:scale-y
+ */
+ properties[PROP_SCALE_Y] = g_param_spec_float ("scale-y",
+ "Scale in the Y axis",
+ "Scale in the Y axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 1.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:scale-z
+ */
+ properties[PROP_SCALE_Z] = g_param_spec_float ("scale-z",
+ "Scale in the Z axis",
+ "Scale in the Z axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 1.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:translation-x
+ */
+ properties[PROP_TRANSLATION_X] = g_param_spec_float ("translation-x",
+ "Translation in the X axis",
+ "Translation in the X axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 0.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:translation-y
+ */
+ properties[PROP_TRANSLATION_Y] = g_param_spec_float ("translation-y",
+ "Translation in the Y axis",
+ "Translation in the Y axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 0.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GtdWidget:translation-z
+ */
+ properties[PROP_TRANSLATION_Z] = g_param_spec_float ("translation-z",
+ "Translation in the Z axis",
+ "Translation in the Z axis",
+ -G_MAXFLOAT,
+ G_MAXFLOAT,
+ 0.f,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * GtdWidget::transitions-completed:
+ * @actor: a #GtdWidget
+ *
+ * The ::transitions-completed signal is emitted once all transitions
+ * involving @actor are complete.
+ *
+ * Since: 1.10
+ */
+ signals[TRANSITIONS_COMPLETED] =
+ g_signal_new ("transitions-completed",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * GtdWidget::transition-stopped:
+ * @actor: a #GtdWidget
+ * @name: the name of the transition
+ * @is_finished: whether the transition was finished, or stopped
+ *
+ * The ::transition-stopped signal is emitted once a transition
+ * is stopped; a transition is stopped once it reached its total
+ * duration (including eventual repeats), it has been stopped
+ * using gtd_timeline_stop(), or it has been removed from the
+ * transitions applied on @actor, using gtd_actor_remove_transition().
+ *
+ * Since: 1.12
+ */
+ signals[TRANSITION_STOPPED] =
+ g_signal_new ("transition-stopped",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE |
+ G_SIGNAL_NO_HOOKS | G_SIGNAL_DETAILED,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_STRING,
+ G_TYPE_BOOLEAN);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTD_TYPE_BIN_LAYOUT);
+}
+
+static void
+gtd_widget_init (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv = gtd_widget_get_instance_private (self);
+
+ priv->scale[X] = 1.f;
+ priv->scale[Y] = 1.f;
+ priv->scale[Z] = 1.f;
+
+ priv->pivot_point = GRAPHENE_POINT3D_INIT (0.5, 0.5, 0.f);
+
+ gtd_widget_save_easing_state (self);
+ gtd_widget_set_easing_duration (self, 0);
+}
+
+GtkWidget*
+gtd_widget_new (void)
+{
+ return g_object_new (GTD_TYPE_WIDGET, NULL);
+}
+
+/**
+ */
+void
+gtd_widget_get_pivot_point (GtdWidget *self,
+ graphene_point3d_t *out_pivot_point)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+ g_return_if_fail (out_pivot_point != NULL);
+
+ priv = gtd_widget_get_instance_private (self);
+ *out_pivot_point = priv->pivot_point;
+}
+
+/**
+ */
+void
+gtd_widget_set_pivot_point (GtdWidget *self,
+ const graphene_point3d_t *pivot_point)
+{
+ GtdWidgetPrivate *priv;
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+ g_return_if_fail (pivot_point != NULL);
+ g_return_if_fail (pivot_point->x >= 0.f && pivot_point->x <= 1.0);
+ g_return_if_fail (pivot_point->y >= 0.f && pivot_point->y <= 1.0);
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (graphene_point3d_equal (&priv->pivot_point, pivot_point))
+ GTD_RETURN ();
+
+ invalidate_cached_transform (self);
+ priv->pivot_point = *pivot_point;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ GTD_EXIT;
+}
+
+/**
+ */
+void
+gtd_widget_get_rotation (GtdWidget *self,
+ gfloat *rotation_x,
+ gfloat *rotation_y,
+ gfloat *rotation_z)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (rotation_x)
+ *rotation_x = priv->rotation[X];
+
+ if (rotation_y)
+ *rotation_y = priv->rotation[Y];
+
+ if (rotation_z)
+ *rotation_z = priv->rotation[Z];
+}
+
+/**
+ */
+void
+gtd_widget_set_rotation (GtdWidget *self,
+ gfloat rotation_x,
+ gfloat rotation_y,
+ gfloat rotation_z)
+{
+ GtdWidgetPrivate *priv;
+ gboolean changed[3];
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ changed[X] = !G_APPROX_VALUE (priv->rotation[X], rotation_x, FLT_EPSILON);
+ changed[Y] = !G_APPROX_VALUE (priv->rotation[Y], rotation_y, FLT_EPSILON);
+ changed[Z] = !G_APPROX_VALUE (priv->rotation[Z], rotation_z, FLT_EPSILON);
+
+ if (!changed[X] && !changed[Y] && !changed[Z])
+ GTD_RETURN ();
+
+ if (changed[X])
+ create_transition (self, properties[PROP_ROTATION_X], priv->rotation[X], rotation_x);
+
+ if (changed[Y])
+ create_transition (self, properties[PROP_ROTATION_Y], priv->rotation[Y], rotation_y);
+
+ if (changed[Z])
+ create_transition (self, properties[PROP_ROTATION_Z], priv->rotation[Z], rotation_z);
+
+ GTD_EXIT;
+}
+
+/**
+ */
+void
+gtd_widget_get_scale (GtdWidget *self,
+ gfloat *scale_x,
+ gfloat *scale_y,
+ gfloat *scale_z)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (scale_x)
+ *scale_x = priv->scale[0];
+
+ if (scale_y)
+ *scale_y = priv->scale[1];
+
+ if (scale_z)
+ *scale_z = priv->scale[2];
+}
+
+/**
+ */
+void
+gtd_widget_set_scale (GtdWidget *self,
+ gfloat scale_x,
+ gfloat scale_y,
+ gfloat scale_z)
+{
+ GtdWidgetPrivate *priv;
+ gboolean changed[3];
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ changed[X] = !G_APPROX_VALUE (priv->scale[X], scale_x, FLT_EPSILON);
+ changed[Y] = !G_APPROX_VALUE (priv->scale[Y], scale_y, FLT_EPSILON);
+ changed[Z] = !G_APPROX_VALUE (priv->scale[Z], scale_z, FLT_EPSILON);
+
+ if (!changed[X] && !changed[Y] && !changed[Z])
+ GTD_RETURN ();
+
+ if (changed[X])
+ create_transition (self, properties[PROP_SCALE_X], priv->scale[X], scale_x);
+
+ if (changed[Y])
+ create_transition (self, properties[PROP_SCALE_Y], priv->scale[Y], scale_y);
+
+ if (changed[Z])
+ create_transition (self, properties[PROP_SCALE_Z], priv->scale[Z], scale_z);
+
+ GTD_EXIT;
+}
+
+/**
+ */
+void
+gtd_widget_get_translation (GtdWidget *self,
+ gfloat *translation_x,
+ gfloat *translation_y,
+ gfloat *translation_z)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (translation_x)
+ *translation_x = priv->translation.x;
+
+ if (translation_y)
+ *translation_y = priv->translation.y;
+
+ if (translation_z)
+ *translation_z = priv->translation.z;
+}
+
+/**
+ */
+void
+gtd_widget_set_translation (GtdWidget *self,
+ gfloat translation_x,
+ gfloat translation_y,
+ gfloat translation_z)
+{
+ graphene_point3d_t translation;
+ GtdWidgetPrivate *priv;
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+ translation = GRAPHENE_POINT3D_INIT (translation_x, translation_y, translation_z);
+
+ if (graphene_point3d_equal (&priv->translation, &translation))
+ GTD_RETURN ();
+
+ if (!G_APPROX_VALUE (priv->translation.x, translation.x, FLT_EPSILON))
+ create_transition (self, properties[PROP_TRANSLATION_X], priv->translation.x, translation_x);
+
+ if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON))
+ create_transition (self, properties[PROP_TRANSLATION_Y], priv->translation.y, translation_y);
+
+ if (!G_APPROX_VALUE (priv->translation.y, translation.y, FLT_EPSILON))
+ create_transition (self, properties[PROP_TRANSLATION_Z], priv->translation.z, translation_z);
+
+ GTD_EXIT;
+}
+
+/**
+ */
+GskTransform*
+gtd_widget_apply_transform (GtdWidget *self,
+ GskTransform *transform)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_WIDGET (self), NULL);
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (!priv->cached_transform)
+ calculate_transform (self);
+
+ if (!transform)
+ return gsk_transform_ref (priv->cached_transform);
+
+ return gsk_transform_transform (transform, priv->cached_transform);
+}
+
+/**
+ * gtd_widget_add_transition:
+ * @self: a #GtdWidget
+ * @name: the name of the transition to add
+ * @transition: the #GtdTransition to add
+ *
+ * Adds a @transition to the #GtdWidget's list of animations.
+ *
+ * The @name string is a per-widget unique identifier of the @transition: only
+ * one #GtdTransition can be associated to the specified @name.
+ *
+ * The @transition will be started once added.
+ *
+ * This function will take a reference on the @transition.
+ *
+ * This function is usually called implicitly when modifying an animatable
+ * property.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_add_transition (GtdWidget *self,
+ const gchar *name,
+ GtdTransition *transition)
+{
+ g_return_if_fail (GTD_IS_WIDGET (self));
+ g_return_if_fail (name != NULL);
+ g_return_if_fail (GTD_IS_TRANSITION (transition));
+
+ add_transition_to_widget (self, name, transition);
+}
+
+/**
+ * gtd_widget_remove_transition:
+ * @self: a #GtdWidget
+ * @name: the name of the transition to remove
+ *
+ * Removes the transition stored inside a #GtdWidget using @name
+ * identifier.
+ *
+ * If the transition is currently in progress, it will be stopped.
+ *
+ * This function releases the reference acquired when the transition
+ * was added to the #GtdWidget.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_remove_transition (GtdWidget *self,
+ const gchar *name)
+{
+ GtdWidgetPrivate *priv;
+ TransitionClosure *closure;
+ gboolean was_playing;
+ GQuark t_quark;
+ gchar *t_name;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+ g_return_if_fail (name != NULL);
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.transitions == NULL)
+ return;
+
+ closure = g_hash_table_lookup (priv->animation.transitions, name);
+ if (closure == NULL)
+ return;
+
+ was_playing =
+ gtd_timeline_is_playing (GTD_TIMELINE (closure->transition));
+ t_quark = g_quark_from_string (closure->name);
+ t_name = g_strdup (closure->name);
+
+ g_hash_table_remove (priv->animation.transitions, name);
+
+ /* we want to maintain the invariant that ::transition-stopped is
+ * emitted after the transition has been removed, to allow replacing
+ * or chaining; removing the transition from the hash table will
+ * stop it, but transition_closure_free() will disconnect the signal
+ * handler we install in add_transition_internal(), to avoid loops
+ * or segfaults.
+ *
+ * since we know already that a transition will stop once it's removed
+ * from an widget, we can simply emit the ::transition-stopped here
+ * ourselves, if the timeline was playing (if it wasn't, then the
+ * signal was already emitted at least once).
+ */
+ if (was_playing)
+ g_signal_emit (self, signals[TRANSITION_STOPPED], t_quark, t_name, FALSE);
+
+ g_free (t_name);
+}
+
+/**
+ * gtd_widget_remove_all_transitions:
+ * @self: a #GtdWidget
+ *
+ * Removes all transitions associated to @self.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_remove_all_transitions (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+ if (priv->animation.transitions == NULL)
+ return;
+
+ g_hash_table_remove_all (priv->animation.transitions);
+}
+
+/**
+ * gtd_widget_set_easing_duration:
+ * @self: a #GtdWidget
+ * @msecs: the duration of the easing, or %NULL
+ *
+ * Sets the duration of the tweening for animatable properties
+ * of @self for the current easing state.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_set_easing_duration (GtdWidget *self,
+ guint msecs)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.current_state == NULL)
+ {
+ g_warning ("You must call gtd_widget_save_easing_state() prior "
+ "to calling gtd_widget_set_easing_duration().");
+ return;
+ }
+
+ if (priv->animation.current_state->easing_duration != msecs)
+ priv->animation.current_state->easing_duration = msecs;
+}
+
+/**
+ * gtd_widget_get_easing_duration:
+ * @self: a #GtdWidget
+ *
+ * Retrieves the duration of the tweening for animatable
+ * properties of @self for the current easing state.
+ *
+ * Return value: the duration of the tweening, in milliseconds
+ *
+ * Since: 1.10
+ */
+guint
+gtd_widget_get_easing_duration (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_WIDGET (self), 0);
+
+ priv = gtd_widget_get_instance_private (self);
+ if (priv->animation.current_state != NULL)
+ return priv->animation.current_state->easing_duration;
+
+ return 0;
+}
+
+/**
+ * gtd_widget_set_easing_mode:
+ * @self: a #GtdWidget
+ * @mode: an easing mode, excluding %GTD_CUSTOM_MODE
+ *
+ * Sets the easing mode for the tweening of animatable properties
+ * of @self.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_set_easing_mode (GtdWidget *self,
+ GtdEaseMode mode)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+ g_return_if_fail (mode != GTD_CUSTOM_MODE);
+ g_return_if_fail (mode < GTD_EASE_LAST);
+
+ priv = gtd_widget_get_instance_private (self);
+ if (priv->animation.current_state == NULL)
+ {
+ g_warning ("You must call gtd_widget_save_easing_state() prior "
+ "to calling gtd_widget_set_easing_mode().");
+ return;
+ }
+
+ if (priv->animation.current_state->easing_mode != mode)
+ priv->animation.current_state->easing_mode = mode;
+}
+
+/**
+ * gtd_widget_get_easing_mode:
+ * @self: a #GtdWidget
+ *
+ * Retrieves the easing mode for the tweening of animatable properties
+ * of @self for the current easing state.
+ *
+ * Return value: an easing mode
+ *
+ * Since: 1.10
+ */
+GtdEaseMode
+gtd_widget_get_easing_mode (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_WIDGET (self), GTD_EASE_OUT_CUBIC);
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.current_state != NULL)
+ return priv->animation.current_state->easing_mode;
+
+ return GTD_EASE_OUT_CUBIC;
+}
+
+/**
+ * gtd_widget_set_easing_delay:
+ * @self: a #GtdWidget
+ * @msecs: the delay before the start of the tweening, in milliseconds
+ *
+ * Sets the delay that should be applied before tweening animatable
+ * properties.
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_set_easing_delay (GtdWidget *self,
+ guint msecs)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.current_state == NULL)
+ {
+ g_warning ("You must call gtd_widget_save_easing_state() prior "
+ "to calling gtd_widget_set_easing_delay().");
+ return;
+ }
+
+ if (priv->animation.current_state->easing_delay != msecs)
+ priv->animation.current_state->easing_delay = msecs;
+}
+
+/**
+ * gtd_widget_get_easing_delay:
+ * @self: a #GtdWidget
+ *
+ * Retrieves the delay that should be applied when tweening animatable
+ * properties.
+ *
+ * Return value: a delay, in milliseconds
+ *
+ * Since: 1.10
+ */
+guint
+gtd_widget_get_easing_delay (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_WIDGET (self), 0);
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.current_state != NULL)
+ return priv->animation.current_state->easing_delay;
+
+ return 0;
+}
+
+/**
+ * gtd_widget_get_transition:
+ * @self: a #GtdWidget
+ * @name: the name of the transition
+ *
+ * Retrieves the #GtdTransition of a #GtdWidget by using the
+ * transition @name.
+ *
+ * Transitions created for animatable properties use the name of the
+ * property itself, for instance the code below:
+ *
+ * |[<!-- language="C" -->
+ * gtd_widget_set_easing_duration (widget, 1000);
+ * gtd_widget_set_rotation_angle (widget, GTD_Y_AXIS, 360.0);
+ *
+ * transition = gtd_widget_get_transition (widget, "rotation-angle-y");
+ * g_signal_connect (transition, "stopped",
+ * G_CALLBACK (on_transition_stopped),
+ * widget);
+ * ]|
+ *
+ * will call the `on_transition_stopped` callback when the transition
+ * is finished.
+ *
+ * If you just want to get notifications of the completion of a transition,
+ * you should use the #GtdWidget::transition-stopped signal, using the
+ * transition name as the signal detail.
+ *
+ * Return value: (transfer none): a #GtdTransition, or %NULL is none
+ * was found to match the passed name; the returned instance is owned
+ * by Gtd and it should not be freed
+ */
+GtdTransition *
+gtd_widget_get_transition (GtdWidget *self,
+ const gchar *name)
+{
+ TransitionClosure *closure;
+ GtdWidgetPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_WIDGET (self), NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ priv = gtd_widget_get_instance_private (self);
+ if (priv->animation.transitions == NULL)
+ return NULL;
+
+ closure = g_hash_table_lookup (priv->animation.transitions, name);
+ if (closure == NULL)
+ return NULL;
+
+ return closure->transition;
+}
+
+/**
+ * gtd_widget_has_transitions: (skip)
+ */
+gboolean
+gtd_widget_has_transitions (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_val_if_fail (GTD_IS_WIDGET (self), FALSE);
+
+ priv = gtd_widget_get_instance_private (self);
+ if (priv->animation.transitions == NULL)
+ return FALSE;
+
+ return g_hash_table_size (priv->animation.transitions) > 0;
+}
+
+/**
+ * gtd_widget_save_easing_state:
+ * @self: a #GtdWidget
+ *
+ * Saves the current easing state for animatable properties, and creates
+ * a new state with the default values for easing mode and duration.
+ *
+ * New transitions created after calling this function will inherit the
+ * duration, easing mode, and delay of the new easing state; this also
+ * applies to transitions modified in flight.
+ */
+void
+gtd_widget_save_easing_state (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+ AnimationState new_state;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.states == NULL)
+ priv->animation.states = g_array_new (FALSE, FALSE, sizeof (AnimationState));
+
+ new_state.easing_mode = GTD_EASE_OUT_CUBIC;
+ new_state.easing_duration = 250;
+ new_state.easing_delay = 0;
+
+ g_array_append_val (priv->animation.states, new_state);
+
+ priv->animation.current_state = &g_array_index (priv->animation.states,
+ AnimationState,
+ priv->animation.states->len - 1);
+}
+
+/**
+ * gtd_widget_restore_easing_state:
+ * @self: a #GtdWidget
+ *
+ * Restores the easing state as it was prior to a call to
+ * gtd_widget_save_easing_state().
+ *
+ * Since: 1.10
+ */
+void
+gtd_widget_restore_easing_state (GtdWidget *self)
+{
+ GtdWidgetPrivate *priv;
+
+ g_return_if_fail (GTD_IS_WIDGET (self));
+
+ priv = gtd_widget_get_instance_private (self);
+
+ if (priv->animation.states == NULL)
+ {
+ g_critical ("The function gtd_widget_restore_easing_state() has "
+ "been called without a previous call to "
+ "gtd_widget_save_easing_state().");
+ return;
+ }
+
+ g_array_remove_index (priv->animation.states, priv->animation.states->len - 1);
+
+ if (priv->animation.states->len > 0)
+ priv->animation.current_state = &g_array_index (priv->animation.states, AnimationState, priv->animation.states->len - 1);
+ else
+ {
+ g_array_unref (priv->animation.states);
+ priv->animation.states = NULL;
+ priv->animation.current_state = NULL;
+ }
+}
diff --git a/src/gui/gtd-widget.h b/src/gui/gtd-widget.h
new file mode 100644
index 0000000..c7b4ad2
--- /dev/null
+++ b/src/gui/gtd-widget.h
@@ -0,0 +1,110 @@
+/* gtd-'gtd-widget.c',.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 <gtk/gtk.h>
+
+#include "gtd-animation-enums.h"
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_WIDGET (gtd_widget_get_type ())
+G_DECLARE_DERIVABLE_TYPE (GtdWidget, gtd_widget, GTD, WIDGET, GtkWidget)
+
+struct _GtdWidgetClass
+{
+ GtkWidgetClass parent;
+};
+
+GtkWidget* gtd_widget_new (void);
+
+void gtd_widget_get_pivot_point (GtdWidget *self,
+ graphene_point3d_t *out_pivot_point);
+
+void gtd_widget_set_pivot_point (GtdWidget *self,
+ const graphene_point3d_t *pivot_point);
+
+void gtd_widget_get_rotation (GtdWidget *self,
+ gfloat *rotation_x,
+ gfloat *rotation_y,
+ gfloat *rotation_z);
+
+void gtd_widget_set_rotation (GtdWidget *self,
+ gfloat rotation_x,
+ gfloat rotation_y,
+ gfloat rotation_z);
+
+void gtd_widget_get_scale (GtdWidget *self,
+ gfloat *scale_x,
+ gfloat *scale_y,
+ gfloat *scale_z);
+
+void gtd_widget_set_scale (GtdWidget *self,
+ gfloat scale_x,
+ gfloat scale_y,
+ gfloat scale_z);
+
+void gtd_widget_get_translation (GtdWidget *self,
+ gfloat *translation_x,
+ gfloat *translation_y,
+ gfloat *translation_z);
+
+void gtd_widget_set_translation (GtdWidget *self,
+ gfloat translation_x,
+ gfloat translation_y,
+ gfloat translation_z);
+
+GskTransform* gtd_widget_apply_transform (GtdWidget *self,
+ GskTransform *transform);
+
+void gtd_widget_save_easing_state (GtdWidget *self);
+
+void gtd_widget_restore_easing_state (GtdWidget *self);
+
+void gtd_widget_set_easing_mode (GtdWidget *self,
+ GtdEaseMode mode);
+
+GtdEaseMode gtd_widget_get_easing_mode (GtdWidget *self);
+
+void gtd_widget_set_easing_duration (GtdWidget *self,
+ guint msecs);
+
+guint gtd_widget_get_easing_duration (GtdWidget *self);
+
+void gtd_widget_set_easing_delay (GtdWidget *self,
+ guint msecs);
+
+guint gtd_widget_get_easing_delay (GtdWidget *self);
+
+GtdTransition* gtd_widget_get_transition (GtdWidget *self,
+ const gchar *name);
+
+void gtd_widget_add_transition (GtdWidget *self,
+ const gchar *name,
+ GtdTransition *transition);
+
+void gtd_widget_remove_transition (GtdWidget *self,
+ const gchar *name);
+
+void gtd_widget_remove_all_transitions (GtdWidget *self);
+
+G_END_DECLS
diff --git a/src/gui/gtd-window.c b/src/gui/gtd-window.c
new file mode 100644
index 0000000..029280c
--- /dev/null
+++ b/src/gui/gtd-window.c
@@ -0,0 +1,413 @@
+/* gtd-window.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 "GtdWindow"
+
+#include "config.h"
+
+#include "gtd-activatable.h"
+#include "gtd-application.h"
+#include "gtd-debug.h"
+#include "gtd-task-list-view.h"
+#include "gtd-manager.h"
+#include "gtd-manager-protected.h"
+#include "gtd-menu-button.h"
+#include "gtd-notification.h"
+#include "gtd-omni-area.h"
+#include "gtd-plugin-manager.h"
+#include "gtd-provider.h"
+#include "gtd-panel.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-window.h"
+#include "gtd-workspace.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+/**
+ * SECTION:gtd-window
+ * @short_description:main window
+ * @title:GtdWindow
+ * @stability:Unstable
+ *
+ * The #GtdWindow is the main application window of Endeavour. Objects should
+ * use this class to change between selection and normal mode and
+ * fine-tune the headerbar.
+ */
+
+struct _GtdWindow
+{
+ AdwApplicationWindow application;
+
+ GtkStack *stack;
+
+ GtdWorkspace *current_workspace;
+ GListStore *workspaces;
+ GVariant *parameters;
+
+ PeasExtensionSet *workspaces_set;
+};
+
+enum
+{
+ PROP_0,
+ PROP_CURRENT_WORKSPACE,
+ N_PROPS,
+};
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+G_DEFINE_TYPE (GtdWindow, gtd_window, ADW_TYPE_APPLICATION_WINDOW)
+
+static gint compare_workspaced_func (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data);
+
+static void
+setup_development_build (GtdWindow *self)
+{
+ g_message (_("This is a development build of Endeavour. You may experience errors, wrong behaviors, "
+ "and data loss."));
+
+ gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
+}
+
+static gboolean
+is_development_build (void)
+{
+#ifdef DEVELOPMENT_BUILD
+ return TRUE;
+#else
+ return FALSE;
+#endif
+}
+
+static void
+load_geometry (GtdWindow *self)
+{
+ GSettings *settings;
+ GtkWindow *window;
+ gboolean maximized;
+ gint height;
+ gint width;
+
+ window = GTK_WINDOW (self);
+ settings = gtd_manager_get_settings (gtd_manager_get_default ());
+
+ maximized = g_settings_get_boolean (settings, "window-maximized");
+ g_settings_get (settings, "window-size", "(ii)", &width, &height);
+
+ gtk_window_set_default_size (window, width, height);
+
+ if (maximized)
+ gtk_window_maximize (window);
+}
+
+static void
+add_workspace (GtdWindow *self,
+ GtdWorkspace *workspace)
+{
+ const gchar *workspace_id;
+
+ workspace_id = gtd_workspace_get_id (workspace);
+
+ gtk_stack_add_named (self->stack, GTK_WIDGET (workspace), workspace_id);
+ g_list_store_insert_sorted (self->workspaces, workspace, compare_workspaced_func, self);
+}
+
+static void
+remove_workspace (GtdWindow *self,
+ GtdWorkspace *workspace)
+{
+ guint position;
+
+ if (!g_list_store_find (self->workspaces, workspace, &position))
+ return;
+
+ gtk_stack_remove (self->stack, GTK_WIDGET (workspace));
+ g_list_store_remove (self->workspaces, position);
+}
+
+
+/*
+ * Callbacks
+ */
+
+
+static void
+on_action_activate_workspace_activated_cb (GSimpleAction *simple,
+ GVariant *state,
+ gpointer user_data)
+{
+ g_autofree gchar *workspace_id = NULL;
+ GtdWindow *self;
+
+ self = GTD_WINDOW (user_data);
+ g_variant_get (state,
+ "(sv)",
+ &workspace_id,
+ &self->parameters,
+ NULL);
+
+ gtk_stack_set_visible_child_name (self->stack, workspace_id);
+}
+
+static gint
+compare_workspaced_func (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ gint a_priority;
+ gint b_priority;
+
+ a_priority = gtd_workspace_get_priority ((GtdWorkspace *)a);
+ b_priority = gtd_workspace_get_priority ((GtdWorkspace *)b);
+
+ return b_priority - a_priority;
+}
+
+static void
+on_stack_visible_child_cb (GtkStack *stack,
+ GParamSpec *pspec,
+ GtdWindow *self)
+{
+ g_autoptr (GVariant) parameters = NULL;
+ g_autoptr (GIcon) workspace_icon = NULL;
+ GtdWorkspace *new_workspace;
+
+ GTD_ENTRY;
+
+ if (self->current_workspace)
+ gtd_workspace_deactivate (self->current_workspace);
+
+ new_workspace = GTD_WORKSPACE (gtk_stack_get_visible_child (stack));
+ self->current_workspace = new_workspace;
+
+ if (!new_workspace)
+ GTD_GOTO (out);
+
+ parameters = g_steal_pointer (&self->parameters);
+ gtd_workspace_activate (new_workspace, parameters);
+
+out:
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CURRENT_WORKSPACE]);
+ GTD_EXIT;
+}
+
+static void
+on_workspace_added_cb (PeasExtensionSet *extension_set,
+ PeasPluginInfo *plugin_info,
+ GtdWorkspace *workspace,
+ GtdWindow *self)
+{
+ GTD_ENTRY;
+
+ add_workspace (self, g_object_ref_sink (workspace));
+
+ GTD_EXIT;
+}
+
+static void
+on_workspace_removed_cb (PeasExtensionSet *extension_set,
+ PeasPluginInfo *plugin_info,
+ GtdWorkspace *workspace,
+ GtdWindow *self)
+{
+ GTD_ENTRY;
+
+ remove_workspace (self, workspace);
+
+ GTD_EXIT;
+}
+
+
+/*
+ * GtkWindow overrides
+ */
+
+static void
+gtd_window_unmap (GtkWidget *widget)
+{
+ GSettings *settings;
+ GtkWindow *window;
+ gboolean maximized;
+
+ window = GTK_WINDOW (widget);
+ settings = gtd_manager_get_settings (gtd_manager_get_default ());
+ maximized = gtk_window_is_maximized (window);
+
+ g_settings_set_boolean (settings, "window-maximized", maximized);
+
+ if (!maximized)
+ {
+ gint height;
+ gint width;
+
+ gtk_window_get_default_size (window, &width, &height);
+ g_settings_set (settings, "window-size", "(ii)", width, height);
+ }
+
+ GTK_WIDGET_CLASS (gtd_window_parent_class)->unmap (widget);
+}
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_window_dispose (GObject *object)
+{
+ GtdWindow *self = GTD_WINDOW (object);
+
+ g_clear_object (&self->workspaces_set);
+
+ G_OBJECT_CLASS (gtd_window_parent_class)->dispose (object);
+}
+
+static void
+gtd_window_finalize (GObject *object)
+{
+ GtdWindow *self = GTD_WINDOW (object);
+
+ g_clear_object (&self->workspaces);
+
+ G_OBJECT_CLASS (gtd_window_parent_class)->finalize (object);
+}
+
+static void
+gtd_window_constructed (GObject *object)
+{
+ GtdWindow *self;
+
+ self = GTD_WINDOW (object);
+
+ G_OBJECT_CLASS (gtd_window_parent_class)->constructed (object);
+
+ /* Load stored size */
+ load_geometry (GTD_WINDOW (object));
+
+ /* Workspaces */
+ self->workspaces_set = peas_extension_set_new (peas_engine_get_default (),
+ GTD_TYPE_WORKSPACE,
+ NULL);
+
+ peas_extension_set_foreach (self->workspaces_set,
+ (PeasExtensionSetForeachFunc) on_workspace_added_cb,
+ self);
+
+ g_object_connect (self->workspaces_set,
+ "signal::extension-added", G_CALLBACK (on_workspace_added_cb), self,
+ "signal::extension-removed", G_CALLBACK (on_workspace_removed_cb), self,
+ NULL);
+}
+
+static void
+gtd_window_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdWindow *self = (GtdWindow *) object;
+
+ switch (prop_id)
+ {
+ case PROP_CURRENT_WORKSPACE:
+ g_value_set_object (value, self->current_workspace);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_window_class_init (GtdWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gtd_window_dispose;
+ object_class->finalize = gtd_window_finalize;
+ object_class->constructed = gtd_window_constructed;
+ object_class->get_property = gtd_window_get_property;
+
+ widget_class->unmap = gtd_window_unmap;
+
+ properties[PROP_CURRENT_WORKSPACE] = g_param_spec_object ("current-workspace",
+ "Current workspace",
+ "Current workspace",
+ GTD_TYPE_WORKSPACE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ g_type_ensure (GTD_TYPE_MENU_BUTTON);
+ g_type_ensure (GTD_TYPE_OMNI_AREA);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/todo/ui/gtd-window.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GtdWindow, stack);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_stack_visible_child_cb);
+}
+
+static void
+gtd_window_init (GtdWindow *self)
+{
+ static const GActionEntry entries[] = {
+ { "activate-workspace", on_action_activate_workspace_activated_cb, "(sv)" },
+ };
+
+ g_action_map_add_action_entries (G_ACTION_MAP (self),
+ entries,
+ G_N_ELEMENTS (entries),
+ self);
+
+ self->workspaces = g_list_store_new (GTD_TYPE_WORKSPACE);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* Development build */
+ if (is_development_build ())
+ setup_development_build (self);
+}
+
+GtkWidget*
+gtd_window_new (GtdApplication *application)
+{
+ return g_object_new (GTD_TYPE_WINDOW,
+ "application", application,
+ NULL);
+}
+
+/**
+ * gtd_window_get_current_workspace:
+ * @self: a #GtdWindow
+ *
+ * Retrieves the currently active workspace
+ *
+ * Returns: (transfer full): a #GtdWorkspace
+ */
+GtdWorkspace*
+gtd_window_get_current_workspace (GtdWindow *self)
+{
+ g_return_val_if_fail (GTD_IS_WINDOW (self), NULL);
+
+ return self->current_workspace;
+}
diff --git a/src/gui/gtd-window.h b/src/gui/gtd-window.h
new file mode 100644
index 0000000..3cfe0d8
--- /dev/null
+++ b/src/gui/gtd-window.h
@@ -0,0 +1,43 @@
+/* gtd-window.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_WINDOW_H
+#define GTD_WINDOW_H
+
+#include "gtd-types.h"
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_WINDOW (gtd_window_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdWindow, gtd_window, GTD, WINDOW, AdwApplicationWindow)
+
+GtkWidget* gtd_window_new (GtdApplication *application);
+
+void gtd_window_embed_widget_in_header (GtdWindow *self,
+ GtkWidget *widget,
+ GtkPositionType position);
+
+GtdWorkspace* gtd_window_get_current_workspace (GtdWindow *self);
+
+G_END_DECLS
+
+#endif /* GTD_WINDOW_H */
diff --git a/src/gui/gtd-window.ui b/src/gui/gtd-window.ui
new file mode 100644
index 0000000..147054b
--- /dev/null
+++ b/src/gui/gtd-window.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GtdWindow" parent="AdwApplicationWindow">
+ <property name="default_width">800</property>
+ <property name="default_height">600</property>
+
+ <child>
+ <object class="GtkShortcutController">
+ <property name="name">Main Window Shortcuts</property>
+ <property name="scope">global</property>
+ </object>
+ </child>
+
+ <style>
+ <class name="org-gnome-Todo"/>
+ </style>
+
+ <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="GtdWindow" swapped="no"/>
+ <style>
+ <class name="background"/>
+ </style>
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/src/gui/gtd-workspace.c b/src/gui/gtd-workspace.c
new file mode 100644
index 0000000..ad1b1c0
--- /dev/null
+++ b/src/gui/gtd-workspace.c
@@ -0,0 +1,158 @@
+/* gtd-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 "gtd-workspace.h"
+
+G_DEFINE_INTERFACE (GtdWorkspace, gtd_workspace, GTK_TYPE_WIDGET)
+
+static void
+gtd_workspace_default_init (GtdWorkspaceInterface *iface)
+{
+ /**
+ * GtdWorkspace::icon:
+ *
+ * The icon of the panel.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_object ("icon",
+ "Icon of the workspace",
+ "The icon of the workspace",
+ G_TYPE_ICON,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GtdWorkspace::title:
+ *
+ * The user-visible title of the workspace.
+ */
+ g_object_interface_install_property (iface,
+ g_param_spec_string ("title",
+ "The title of the workspace",
+ "The title of the workspace",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * gtd_workspace_get_id:
+ * @self: a #GtdWorkspace
+ *
+ * Retrieves the id of @self. It is mandatory to implement
+ * this.
+ *
+ * Returns: the id of @self
+ */
+const gchar*
+gtd_workspace_get_id (GtdWorkspace *self)
+{
+ g_return_val_if_fail (GTD_IS_WORKSPACE (self), NULL);
+ g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_id, NULL);
+
+ return GTD_WORKSPACE_GET_IFACE (self)->get_id (self);
+}
+
+/**
+ * gtd_workspace_get_title:
+ * @self: a #GtdWorkspace
+ *
+ * Retrieves the title of @self. It is mandatory to implement
+ * this.
+ *
+ * Returns: the title of @self
+ */
+const gchar*
+gtd_workspace_get_title (GtdWorkspace *self)
+{
+ g_return_val_if_fail (GTD_IS_WORKSPACE (self), NULL);
+ g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_title, NULL);
+
+ return GTD_WORKSPACE_GET_IFACE (self)->get_title (self);
+}
+
+/**
+ * gtd_workspace_get_priority:
+ * @self: a #GtdWorkspace
+ *
+ * Retrieves the priority of @self. It is mandatory to implement
+ * this.
+ *
+ * Returns: the priority of @self
+ */
+gint
+gtd_workspace_get_priority (GtdWorkspace *self)
+{
+ g_return_val_if_fail (GTD_IS_WORKSPACE (self), 0);
+ g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_priority, 0);
+
+ return GTD_WORKSPACE_GET_IFACE (self)->get_priority (self);
+}
+
+/**
+ * gtd_workspace_get_icon:
+ * @self: a #GtdWorkspace
+ *
+ * Retrieves the icon of @self. It is mandatory to implement
+ * this.
+ *
+ * Returns: (transfer full): a #GIcon
+ */
+GIcon*
+gtd_workspace_get_icon (GtdWorkspace *self)
+{
+ g_return_val_if_fail (GTD_IS_WORKSPACE (self), NULL);
+ g_return_val_if_fail (GTD_WORKSPACE_GET_IFACE (self)->get_icon, NULL);
+
+ return GTD_WORKSPACE_GET_IFACE (self)->get_icon (self);
+}
+
+/**
+ * gtd_workspace_activate:
+ * @self: a #GtdWorkspace
+ * @parameters: (nullable): workspace-specific parameters
+ *
+ * Activates @self. This happens when the workspace
+ * becomes the active workspace in the main window.
+ */
+void
+gtd_workspace_activate (GtdWorkspace *self,
+ GVariant *parameters)
+{
+ g_return_if_fail (GTD_IS_WORKSPACE (self));
+
+ if (GTD_WORKSPACE_GET_IFACE (self)->activate)
+ GTD_WORKSPACE_GET_IFACE (self)->activate (self, parameters);
+}
+
+/**
+ * gtd_workspace_deactivate:
+ * @self: a #GtdWorkspace
+ *
+ * Deactivates @self. This happens when the workspace
+ * is switched away in the main window.
+ */
+void
+gtd_workspace_deactivate (GtdWorkspace *self)
+{
+ g_return_if_fail (GTD_IS_WORKSPACE (self));
+
+ if (GTD_WORKSPACE_GET_IFACE (self)->deactivate)
+ GTD_WORKSPACE_GET_IFACE (self)->deactivate (self);
+}
+
diff --git a/src/gui/gtd-workspace.h b/src/gui/gtd-workspace.h
new file mode 100644
index 0000000..d5bbaa5
--- /dev/null
+++ b/src/gui/gtd-workspace.h
@@ -0,0 +1,61 @@
+/* gtd-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>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_WORKSPACE (gtd_workspace_get_type ())
+G_DECLARE_INTERFACE (GtdWorkspace, gtd_workspace, GTD, WORKSPACE, GtkWidget)
+
+struct _GtdWorkspaceInterface
+{
+ GTypeInterface parent;
+
+ const gchar* (*get_id) (GtdWorkspace *self);
+
+ const gchar* (*get_title) (GtdWorkspace *self);
+
+ gint (*get_priority) (GtdWorkspace *self);
+
+ GIcon* (*get_icon) (GtdWorkspace *self);
+
+ void (*activate) (GtdWorkspace *self,
+ GVariant *parameters);
+
+ void (*deactivate) (GtdWorkspace *self);
+};
+
+const gchar* gtd_workspace_get_id (GtdWorkspace *self);
+
+const gchar* gtd_workspace_get_title (GtdWorkspace *self);
+
+gint gtd_workspace_get_priority (GtdWorkspace *self);
+
+GIcon* gtd_workspace_get_icon (GtdWorkspace *self);
+
+void gtd_workspace_activate (GtdWorkspace *self,
+ GVariant *parameters);
+
+void gtd_workspace_deactivate (GtdWorkspace *self);
+
+G_END_DECLS
diff --git a/src/gui/gui.gresource.xml b/src/gui/gui.gresource.xml
new file mode 100644
index 0000000..746902c
--- /dev/null
+++ b/src/gui/gui.gresource.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/todo/ui">
+ <file compressed="true" preprocess="xml-stripblanks">gtd-edit-pane.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-initial-setup-window.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-new-task-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-omni-area.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-provider-popover.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-provider-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-provider-selector.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-task-list-popover.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-task-list-view.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-task-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">gtd-window.ui</file>
+
+ <!-- Assets -->
+ <file>assets/all-done.svg</file>
+ </gresource>
+
+ <!-- GTK -->
+ <gresource prefix="/org/gnome/todo/gtk">
+ <file compressed="true" preprocess="xml-stripblanks" alias="help-overlay.ui">shortcuts-dialog.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">menus.ui</file>
+ </gresource>
+</gresources>
diff --git a/src/gui/menus.ui b/src/gui/menus.ui
new file mode 100644
index 0000000..cfd12e8
--- /dev/null
+++ b/src/gui/menus.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <menu id="primary-menu">
+
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">_Help</attribute>
+ <attribute name="action">app.help</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
+ <attribute name="action">win.show-help-overlay</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_About Endeavour</attribute>
+ <attribute name="action">app.about</attribute>
+ </item>
+ </section>
+
+ </menu>
+</interface>
diff --git a/src/gui/meson.build b/src/gui/meson.build
new file mode 100644
index 0000000..5e67748
--- /dev/null
+++ b/src/gui/meson.build
@@ -0,0 +1,6 @@
+sources += gnome.compile_resources(
+ 'gtd-gui-resources',
+ 'gui.gresource.xml',
+ c_name: 'gtd_gui',
+ export: true,
+)
diff --git a/src/gui/shortcuts-dialog.ui b/src/gui/shortcuts-dialog.ui
new file mode 100644
index 0000000..38a1a85
--- /dev/null
+++ b/src/gui/shortcuts-dialog.ui
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <object class="GtkShortcutsWindow" id="help_overlay">
+ <property name="modal">True</property>
+
+ <child>
+ <object class="GtkShortcutsSection">
+ <property name="visible">True</property>
+ <property name="section-name">shortcuts</property>
+ <property name="max-height">10</property>
+
+ <!-- General shortcuts -->
+ <child>
+ <object class="GtkShortcutsGroup">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">General</property>
+
+ <child>
+ <object class="GtkShortcutsShortcut">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">Quit</property>
+ <property name="accelerator">&lt;Control&gt;Q</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkShortcutsShortcut">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">Help</property>
+ <property name="accelerator">F1</property>
+ </object>
+ </child>
+
+
+ <child>
+ <object class="GtkShortcutsShortcut">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">Move to the panel/view up</property>
+ <property name="accelerator">&lt;Control&gt;Page_Up</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkShortcutsShortcut">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">Move to the panel/view up</property>
+ <property name="accelerator">&lt;Alt&gt;Up</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkShortcutsShortcut">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">Move to the panel/view below</property>
+ <property name="accelerator">&lt;Control&gt;Page_Down</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkShortcutsShortcut">
+ <property name="visible">True</property>
+ <property name="title" translatable="yes" context="shortcut window">Move to the panel/view below</property>
+ <property name="accelerator">&lt;Alt&gt;Down</property>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..c25123e
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,44 @@
+/*
+ * main.c
+ * Copyright (C) 2015-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ *
+ * endeavour 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.
+ *
+ * endeavour 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 "gtd-application.h"
+#include "gtd-utils-private.h"
+
+#include <config.h>
+#include <glib/gi18n.h>
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ g_autoptr (GtdApplication) app = NULL;
+
+ gtd_ensure_types ();
+
+ bindtextdomain (GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ g_set_application_name (_("Endeavour"));
+
+ app = gtd_application_new ();
+ g_application_set_default (G_APPLICATION (app));
+
+ return g_application_run (G_APPLICATION (app), argc, argv);
+}
+
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..57119c6
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,339 @@
+sources = []
+
+#######
+# VCS #
+#######
+
+configure_file(output: 'config.h', configuration: config_h)
+
+vcs_tag = vcs_tag(
+ input: 'gtd-vcs.h.in',
+ output: 'gtd-vcs.h'
+)
+
+vcs_identifier_h = declare_dependency(sources: vcs_tag)
+
+endeavour_deps += vcs_identifier_h
+
+#########
+# Enums #
+#########
+
+enum_headers = files(
+ join_paths('animation', 'gtd-animation-enums.h'),
+)
+
+enum_types = 'gtd-enum-types'
+
+gtd_enum_types = gnome.mkenums(
+ enum_types,
+ sources: enum_headers,
+ c_template: enum_types + '.c.template',
+ h_template: enum_types + '.h.template'
+)
+
+sources += gtd_enum_types
+
+###########
+# Plugins #
+###########
+
+incs = include_directories(
+ 'animation',
+ 'core',
+ 'gui',
+ 'models',
+)
+
+###########
+# Subdirs #
+###########
+
+subdir('gui')
+subdir('plugins')
+
+################
+# Dependencies #
+################
+
+gtd_deps = endeavour_deps + [
+ plugins_dep,
+]
+
+################
+# Header files #
+################
+
+headers = files(
+ 'animation/gtd-animatable.h',
+ 'animation/gtd-animation-utils.h',
+ 'animation/gtd-easing.h',
+ 'animation/gtd-interval.h',
+ 'animation/gtd-keyframe-transition.h',
+ 'animation/gtd-property-transition.h',
+ 'animation/gtd-timeline.h',
+ 'animation/gtd-transition.h',
+ 'core/gtd-activatable.h',
+ 'core/gtd-clock.h',
+ 'core/gtd-manager.h',
+ 'core/gtd-notification.h',
+ 'core/gtd-object.h',
+ 'core/gtd-provider.h',
+ 'core/gtd-task.h',
+ 'core/gtd-task-list.h',
+ 'gui/gtd-bin-layout.h',
+ 'gui/gtd-max-size-layout.h',
+ 'gui/gtd-menu-button.h',
+ 'gui/gtd-omni-area.h',
+ 'gui/gtd-omni-area-addin.h',
+ 'gui/gtd-panel.h',
+ 'gui/gtd-provider-popover.h',
+ 'gui/gtd-star-widget.h',
+ 'gui/gtd-task-list-view.h',
+ 'gui/gtd-widget.h',
+ 'gui/gtd-window.h',
+ 'gui/gtd-workspace.h',
+ 'models/gtd-list-model-filter.h',
+ 'models/gtd-list-store.h',
+ 'gtd-types.h',
+ 'gtd-utils.h',
+ 'endeavour.h'
+)
+
+install_headers(headers, subdir: meson.project_name())
+
+
+################
+# Source files #
+################
+
+sources += files(
+ 'animation/gtd-animatable.c',
+ 'animation/gtd-animation-utils.c',
+ 'animation/gtd-easing.c',
+ 'animation/gtd-interval.c',
+ 'animation/gtd-keyframe-transition.c',
+ 'animation/gtd-property-transition.c',
+ 'animation/gtd-timeline.c',
+ 'animation/gtd-transition.c',
+ 'core/gtd-activatable.c',
+ 'core/gtd-clock.c',
+ 'core/gtd-log.c',
+ 'core/gtd-manager.c',
+ 'core/gtd-notification.c',
+ 'core/gtd-object.c',
+ 'core/gtd-plugin-manager.c',
+ 'core/gtd-provider.c',
+ 'core/gtd-task.c',
+ 'core/gtd-task-list.c',
+ 'gui/gtd-bin-layout.c',
+ 'gui/gtd-panel.c',
+ 'gui/gtd-workspace.c',
+ 'gui/gtd-provider-popover.c',
+ 'gui/gtd-provider-row.c',
+ 'gui/gtd-provider-selector.c',
+ 'gui/gtd-edit-pane.c',
+ 'gui/gtd-markdown-renderer.c',
+ 'gui/gtd-new-task-row.c',
+ 'gui/gtd-task-list-popover.c',
+ 'gui/gtd-task-list-view.c',
+ 'gui/gtd-task-row.c',
+ 'gui/gtd-color-button.c',
+ 'gui/gtd-menu-button.c',
+ 'gui/gtd-star-widget.c',
+ 'gui/gtd-application.c',
+ 'gui/gtd-initial-setup-window.c',
+ 'gui/gtd-max-size-layout.c',
+ 'gui/gtd-omni-area.c',
+ 'gui/gtd-omni-area-addin.c',
+ 'gui/gtd-widget.c',
+ 'gui/gtd-window.c',
+ 'models/gtd-list-model-filter.c',
+ 'models/gtd-list-model-sort.c',
+ 'models/gtd-list-store.c',
+ 'models/gtd-task-list-view-model.c',
+ 'models/gtd-task-model.c',
+ 'gtd-utils.c',
+ 'main.c'
+)
+
+
+#############
+# Resources #
+#############
+
+sources += compile_schemas
+
+sources += gnome.compile_resources(
+ 'gtd-resources',
+ 'todo.gresource.xml',
+ c_name: 'todo',
+ export: true,
+)
+
+sources += gnome.compile_resources(
+ 'gtd-icon-resources',
+ join_paths(icons_dir, 'icons.gresource.xml'),
+ source_dir: [ icons_dir ],
+ c_name: 'todo_icons',
+ export: true,
+)
+
+cflags = [
+ '-DPACKAGE_DATA_DIR="@0@"'.format(endeavour_pkgdatadir),
+ '-DPACKAGE_LIB_DIR="@0@"'.format(endeavour_pkglibdir),
+ '-DPACKAGE_LOCALE_DIR="@0@"'.format(endeavour_localedir),
+ '-DPACKAGE_SRC_DIR="@0@"'.format(meson.current_source_dir()),
+ '-DUI_DATA_DIR="@0@"'.format(join_paths(endeavour_pkgdatadir, 'style'))
+]
+
+ldflags = [ '-Wl,--export-dynamic' ]
+
+
+#########
+# Debug #
+#########
+
+debug_conf = configuration_data()
+debug_conf.set('BUGREPORT_URL', 'https://gitlab.gnome.org/World/Endeavour/issues/new')
+debug_conf.set10('ENABLE_TRACING', endeavour_tracing)
+
+
+sources += configure_file(
+ input: 'gtd-debug.h.in',
+ output: 'gtd-debug.h',
+ configuration: debug_conf,
+)
+
+
+##############
+# endeavour #
+##############
+
+endeavour = executable(
+ meson.project_name(),
+ sources,
+ include_directories: incs,
+ dependencies: [ vcs_identifier_h, gtd_deps ],
+ c_args: cflags,
+ link_args: ldflags,
+ install: true,
+ install_dir: endeavour_bindir
+)
+
+
+###################
+# Private library #
+###################
+
+libgtd = shared_library(
+ 'gtd',
+ sources: sources,
+ version: libversion,
+ soversion: soversion,
+ include_directories: incs,
+ dependencies: gtd_deps,
+ c_args: cflags
+)
+
+libgtd_dep = declare_dependency(
+ link_with: libgtd,
+ dependencies: gtd_deps
+)
+
+
+##############
+# pkg-config #
+##############
+
+pkg.generate(
+ libraries: libgtd,
+ version: endeavour_version,
+ name: 'Endeavour',
+ description: 'Header and path for Endeavour Plugins',
+ filebase: meson.project_name(),
+ subdirs: meson.project_name(),
+ variables: 'exec_prefix=' + endeavour_libexecdir,
+ install_dir: join_paths(endeavour_libdir, 'pkgconfig')
+)
+
+
+#########################
+# GObject-Introspection #
+#########################
+
+if get_option('introspection')
+ gir_sources = files(
+ 'core/gtd-activatable.c',
+ 'core/gtd-activatable.h',
+ 'core/gtd-clock.c',
+ 'core/gtd-clock.h',
+ 'core/gtd-manager.c',
+ 'core/gtd-manager.h',
+ 'core/gtd-notification.c',
+ 'core/gtd-notification.h',
+ 'core/gtd-object.c',
+ 'core/gtd-object.h',
+ 'core/gtd-provider.c',
+ 'core/gtd-provider.h',
+ 'core/gtd-task.c',
+ 'core/gtd-task.h',
+ 'core/gtd-task-list.c',
+ 'core/gtd-task-list.h',
+ 'gui/gtd-bin-layout.c',
+ 'gui/gtd-bin-layout.h',
+ 'gui/gtd-max-size-layout.c',
+ 'gui/gtd-max-size-layout.h',
+ 'gui/gtd-menu-button.c',
+ 'gui/gtd-menu-button.h',
+ 'gui/gtd-omni-area.c',
+ 'gui/gtd-omni-area.h',
+ 'gui/gtd-omni-area-addin.c',
+ 'gui/gtd-omni-area-addin.h',
+ 'gui/gtd-panel.c',
+ 'gui/gtd-panel.h',
+ 'gui/gtd-provider-popover.c',
+ 'gui/gtd-provider-popover.h',
+ 'gui/gtd-star-widget.c',
+ 'gui/gtd-star-widget.h',
+ 'gui/gtd-task-list-view.c',
+ 'gui/gtd-task-list-view.h',
+ 'gui/gtd-widget.c',
+ 'gui/gtd-widget.h',
+ 'gui/gtd-window.c',
+ 'gui/gtd-window.h',
+ 'gui/gtd-workspace.c',
+ 'gui/gtd-workspace.h',
+ 'models/gtd-list-model-filter.c',
+ 'models/gtd-list-model-filter.h',
+ 'models/gtd-list-store.c',
+ 'models/gtd-list-store.h',
+ 'gtd-types.h',
+ )
+
+ gir_incs = [
+ 'Gio-2.0',
+ 'GObject-2.0',
+ 'Gtk-4.0'
+ ]
+
+ gir_extra_args = '--warn-all'
+
+ gir_dir = join_paths(endeavour_datadir, '@0@-@1@'.format('gir', endeavour_gir_version))
+ typelib_dir = join_paths(endeavour_libdir, '@0@-@1@'.format('girepository', endeavour_gir_version))
+
+ gnome.generate_gir(
+ libgtd,
+ sources: gir_sources,
+ namespace: endeavour_gir_namespace,
+ nsversion: endeavour_gir_version,
+ identifier_prefix: endeavour_gir_namespace,
+ symbol_prefix: endeavour_gir_namespace.to_lower(),
+ includes: gir_incs,
+ include_directories: incs,
+ extra_args: gir_extra_args,
+ install: true,
+ install_dir_gir: gir_dir,
+ install_dir_typelib: typelib_dir,
+ )
+endif
diff --git a/src/models/gtd-list-model-filter.c b/src/models/gtd-list-model-filter.c
new file mode 100644
index 0000000..632c227
--- /dev/null
+++ b/src/models/gtd-list-model-filter.c
@@ -0,0 +1,569 @@
+/* gtd-list-model-filter.c
+ *
+ * Copyright (C) 2016 Christian Hergert <christian@hergert.me>
+ *
+ * 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 "GtdListModelFilter"
+
+#include "gtd-debug.h"
+#include "gtd-list-model-filter.h"
+#include "gtd-task-list.h"
+
+typedef struct
+{
+ GSequenceIter *child_iter;
+ GSequenceIter *filter_iter;
+} GtdListModelFilterItem;
+
+typedef struct
+{
+ /* The list we are filtering */
+ GListModel *child_model;
+
+ /*
+ * Both sequences point to the same GtdListModelFilterItem which
+ * contains cross-referencing stable GSequenceIter pointers.
+ * The child_seq is considered the "owner" and used to release
+ * allocated resources.
+ */
+ GSequence *child_seq;
+ GSequence *filter_seq;
+
+ /*
+ * Typical set of callback/closure/free function pointers and data.
+ * Called for child items to determine visibility state.
+ */
+ GtdListModelFilterFunc filter_func;
+ gpointer filter_func_data;
+ GDestroyNotify filter_func_data_destroy;
+
+ /* cache */
+ gint64 length;
+ gint64 last_position;
+ GSequenceIter *last_iter;
+
+ /*
+ * If set, we will not emit items-changed. This is useful during
+ * invalidation so that we can do a single emission for all items
+ * that have changed.
+ */
+ gboolean supress_items_changed : 1;
+} GtdListModelFilterPrivate;
+
+struct _GtdListModelFilter
+{
+ GObject parent_instance;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GtdListModelFilter, gtd_list_model_filter, G_TYPE_OBJECT, 0,
+ G_ADD_PRIVATE (GtdListModelFilter)
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
+ list_model_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_CHILD_MODEL,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signal_id;
+
+static void
+gtd_list_model_filter_item_free (gpointer data)
+{
+ GtdListModelFilterItem *item = data;
+
+ g_clear_pointer (&item->filter_iter, g_sequence_remove);
+ item->child_iter = NULL;
+ g_slice_free (GtdListModelFilterItem, item);
+}
+
+static gboolean
+gtd_list_model_filter_default_filter_func (GObject *item,
+ gpointer user_data)
+{
+ return TRUE;
+}
+
+/*
+ * Locates the next item in the filter sequence starting from
+ * the cross-reference found at @iter. If none are found, the
+ * end_iter for the filter sequence is returned.
+ *
+ * This returns an iter in the filter_sequence, not the child_seq.
+ *
+ * Returns: a #GSequenceIter from the filter sequence.
+ */
+static GSequenceIter *
+find_next_visible_filter_iter (GtdListModelFilter *self,
+ GSequenceIter *iter)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+ g_assert (iter != NULL);
+
+ for (; !g_sequence_iter_is_end (iter); iter = g_sequence_iter_next (iter))
+ {
+ GtdListModelFilterItem *item = g_sequence_get (iter);
+
+ g_assert (item->child_iter == iter);
+ g_assert (item->filter_iter == NULL ||
+ g_sequence_iter_get_sequence (item->filter_iter) == priv->filter_seq);
+
+ if (item->filter_iter != NULL)
+ return item->filter_iter;
+ }
+
+ return g_sequence_get_end_iter (priv->filter_seq);
+}
+
+static void
+invalidate_cache (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ GTD_TRACE_MSG ("Invalidating cache");
+
+ priv->last_iter = NULL;
+ priv->last_position = -1u;
+}
+
+static void
+emit_items_changed (GtdListModelFilter *self,
+ guint position,
+ guint n_removed,
+ guint n_added)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ if (position <= priv->last_position)
+ invalidate_cache (self);
+
+ priv->length -= n_removed;
+ priv->length += n_added;
+
+ GTD_TRACE_MSG ("Emitting items-changed(%u, %u, %u)", position, n_removed, n_added);
+
+ g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added);
+}
+
+static void
+child_model_items_changed (GtdListModelFilter *self,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GListModel *child_model)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+ gboolean unblocked;
+ guint i;
+
+ GTD_ENTRY;
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+ g_assert (G_IS_LIST_MODEL (child_model));
+ g_assert (priv->child_model == child_model);
+ g_assert (position <= (guint)g_sequence_get_length (priv->child_seq));
+ g_assert ((g_sequence_get_length (priv->child_seq) - n_removed + n_added) ==
+ g_list_model_get_n_items (child_model));
+
+ GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added);
+
+ unblocked = !priv->supress_items_changed;
+
+ if (n_removed > 0)
+ {
+ GSequenceIter *iter = g_sequence_get_iter_at_pos (priv->child_seq, position);
+ gint64 first_position = -1;
+ guint count = 0;
+
+ g_assert (!g_sequence_iter_is_end (iter));
+
+ /* Small shortcut when all items are removed */
+ if (n_removed == (guint)g_sequence_get_length (priv->child_seq))
+ {
+ g_sequence_remove_range (g_sequence_get_begin_iter (priv->child_seq),
+ g_sequence_get_end_iter (priv->child_seq));
+ g_assert (g_sequence_is_empty (priv->child_seq));
+ g_assert (g_sequence_is_empty (priv->filter_seq));
+
+ if (unblocked)
+ emit_items_changed (self, 0, priv->length, 0);
+
+ GTD_TRACE_MSG ("Removed all items");
+
+ GTD_GOTO (add_new_items);
+ }
+
+ for (i = 0; i < n_removed; i++)
+ {
+ GSequenceIter *to_remove = iter;
+ GtdListModelFilterItem *item = g_sequence_get (iter);
+
+ g_assert (item != NULL);
+ g_assert (item->child_iter == iter);
+ g_assert (item->filter_iter == NULL ||
+ g_sequence_iter_get_sequence (item->filter_iter) == priv->filter_seq);
+
+ /* If this is visible, we need to notify about removal */
+ if (unblocked && item->filter_iter != NULL)
+ {
+ if (first_position < 0)
+ first_position = g_sequence_iter_get_position (item->filter_iter);
+
+ count++;
+ }
+
+ /* Fetch the next while the iter is still valid */
+ iter = g_sequence_iter_next (iter);
+
+ /* Cascades into also removing from filter_seq. */
+ g_sequence_remove (to_remove);
+ }
+
+ GTD_TRACE_MSG ("Removed %u items", count);
+
+ if (unblocked && first_position >= 0)
+ emit_items_changed (self, first_position, count, 0);
+ }
+
+add_new_items:
+
+ if (n_added > 0)
+ {
+ GSequenceIter *iter = g_sequence_get_iter_at_pos (priv->child_seq, position);
+ GSequenceIter *filter_iter = find_next_visible_filter_iter (self, iter);
+ guint filter_position = g_sequence_iter_get_position (filter_iter);
+ guint count = 0;
+
+ /* Walk backwards to insert items into the filter list so that
+ * we can use the same filter_position for each items-changed
+ * signal emission.
+ */
+ for (i = position + n_added; i > position; i--)
+ {
+ GtdListModelFilterItem *item;
+ g_autoptr (GObject) instance = NULL;
+
+ item = g_slice_new0 (GtdListModelFilterItem);
+ item->filter_iter = NULL;
+ item->child_iter = g_sequence_insert_before (iter, item);
+
+ instance = g_list_model_get_item (child_model, i - 1);
+ g_assert (G_IS_OBJECT (instance));
+
+ /* Check if this item is visible */
+ if (priv->filter_func (instance, priv->filter_func_data))
+ {
+ item->filter_iter = g_sequence_insert_before (filter_iter, item);
+ count++;
+
+ /* Use this in the future for relative positioning */
+ filter_iter = item->filter_iter;
+ }
+
+ /* Insert next item before this */
+ iter = item->child_iter;
+ }
+
+ GTD_TRACE_MSG ("Added %u items (%u were filtered)", count, n_added - count);
+
+ if (unblocked && count)
+ emit_items_changed (self, filter_position, 0, count);
+ }
+
+ g_assert ((guint)g_sequence_get_length (priv->child_seq) == g_list_model_get_n_items (child_model));
+
+ GTD_EXIT;
+}
+
+static void
+gtd_list_model_filter_finalize (GObject *object)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)object;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_clear_pointer (&priv->child_seq, g_sequence_free);
+ g_clear_pointer (&priv->filter_seq, g_sequence_free);
+
+ if (priv->filter_func_data_destroy)
+ {
+ g_clear_pointer (&priv->filter_func_data, priv->filter_func_data_destroy);
+ priv->filter_func_data_destroy = NULL;
+ }
+
+ g_clear_object (&priv->child_model);
+
+ G_OBJECT_CLASS (gtd_list_model_filter_parent_class)->finalize (object);
+}
+
+static void
+gtd_list_model_filter_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListModelFilter *self = GTD_LIST_MODEL_FILTER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CHILD_MODEL:
+ g_value_set_object (value, gtd_list_model_filter_get_child_model (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_list_model_filter_class_init (GtdListModelFilterClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_list_model_filter_finalize;
+ object_class->get_property = gtd_list_model_filter_get_property;
+
+ properties [PROP_CHILD_MODEL] =
+ g_param_spec_object ("child-model",
+ "Child Model",
+ "The child model being filtered.",
+ G_TYPE_LIST_MODEL,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ signal_id = g_signal_lookup ("items-changed", GTD_TYPE_LIST_MODEL_FILTER);
+}
+
+static void
+gtd_list_model_filter_init (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ priv->filter_func = gtd_list_model_filter_default_filter_func;
+ priv->child_seq = g_sequence_new (gtd_list_model_filter_item_free);
+ priv->filter_seq = g_sequence_new (NULL);
+ priv->last_position = -1;
+}
+
+static GType
+gtd_list_model_filter_get_item_type (GListModel *model)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)model;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+
+ return g_list_model_get_item_type (priv->child_model);
+}
+
+static guint
+gtd_list_model_filter_get_n_items (GListModel *model)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)model;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+ g_assert (priv->filter_seq != NULL);
+
+ return priv->length;
+}
+
+static gpointer
+gtd_list_model_filter_get_item (GListModel *model,
+ guint position)
+{
+ GtdListModelFilter *self = (GtdListModelFilter *)model;
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+ GtdListModelFilterItem *item;
+ GSequenceIter *iter;
+ guint child_position;
+
+ g_assert (GTD_IS_LIST_MODEL_FILTER (self));
+
+ iter = NULL;
+
+ if (priv->last_position != -1)
+ {
+ if (priv->last_position == position + 1)
+ iter = g_sequence_iter_prev (priv->last_iter);
+ else if (priv->last_position == position - 1)
+ iter = g_sequence_iter_next (priv->last_iter);
+ else if (priv->last_position == position)
+ iter = priv->last_iter;
+ }
+
+ if (!iter)
+ iter = g_sequence_get_iter_at_pos (priv->filter_seq, position);
+
+ if (g_sequence_iter_is_end (iter))
+ return NULL;
+
+ item = g_sequence_get (iter);
+ g_assert (item != NULL);
+ g_assert (item->filter_iter == iter);
+ g_assert (item->child_iter != NULL);
+ g_assert (g_sequence_iter_get_sequence (item->child_iter) == priv->child_seq);
+
+ child_position = g_sequence_iter_get_position (item->child_iter);
+
+ return g_list_model_get_item (priv->child_model, child_position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtd_list_model_filter_get_item_type;
+ iface->get_n_items = gtd_list_model_filter_get_n_items;
+ iface->get_item = gtd_list_model_filter_get_item;
+}
+
+GtdListModelFilter *
+gtd_list_model_filter_new (GListModel *child_model)
+{
+ GtdListModelFilter *ret;
+ GtdListModelFilterPrivate *priv;
+
+ g_return_val_if_fail (G_IS_LIST_MODEL (child_model), NULL);
+
+ ret = g_object_new (GTD_TYPE_LIST_MODEL_FILTER, NULL);
+ priv = gtd_list_model_filter_get_instance_private (ret);
+ priv->child_model = g_object_ref (child_model);
+
+ g_signal_connect_object (child_model,
+ "items-changed",
+ G_CALLBACK (child_model_items_changed),
+ ret,
+ G_CONNECT_SWAPPED);
+
+ gtd_list_model_filter_invalidate (ret);
+
+ return ret;
+}
+
+/**
+ * gtd_list_model_filter_get_child_model:
+ * @self: A #GtdListModelFilter
+ *
+ * Gets the child model that is being filtered.
+ *
+ * Returns: (transfer none): A #GListModel.
+ */
+GListModel *
+gtd_list_model_filter_get_child_model (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_return_val_if_fail (GTD_IS_LIST_MODEL_FILTER (self), NULL);
+
+ return priv->child_model;
+}
+
+void
+gtd_list_model_filter_invalidate (GtdListModelFilter *self)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+ guint n_items;
+
+ GTD_ENTRY;
+
+ g_return_if_fail (GTD_IS_LIST_MODEL_FILTER (self));
+
+ /* We block emission while in invalidate so that we can use
+ * a single larger items-changed rather lots of small emissions.
+ */
+ priv->supress_items_changed = TRUE;
+
+ /* First determine how many items we need to synthesize as a removal */
+ n_items = g_sequence_get_length (priv->filter_seq);
+
+ /*
+ * If we have a child store, we want to rebuild our list of items
+ * from scratch, so just remove everything.
+ */
+ if (!g_sequence_is_empty (priv->child_seq))
+ g_sequence_remove_range (g_sequence_get_begin_iter (priv->child_seq),
+ g_sequence_get_end_iter (priv->child_seq));
+
+ g_assert (g_sequence_is_empty (priv->child_seq));
+ g_assert (g_sequence_is_empty (priv->filter_seq));
+ g_assert (!priv->child_model || G_IS_LIST_MODEL (priv->child_model));
+
+ /*
+ * Now add the new items by synthesizing the addition of all the
+ * items in the list.
+ */
+ if (priv->child_model != NULL)
+ {
+ guint child_n_items;
+
+ /*
+ * Now add all the items as one shot to our list so that
+ * we get populate our sequence and filter sequence.
+ */
+ child_n_items = g_list_model_get_n_items (priv->child_model);
+ child_model_items_changed (self, 0, 0, child_n_items, priv->child_model);
+
+ g_assert ((guint)g_sequence_get_length (priv->child_seq) == child_n_items);
+ g_assert ((guint)g_sequence_get_length (priv->filter_seq) <= child_n_items);
+ }
+
+ priv->supress_items_changed = FALSE;
+
+ /* Now that we've updated our sequences, notify of all the changes
+ * as a single series of updates to the consumers.
+ */
+ if (n_items > 0 || !g_sequence_is_empty (priv->filter_seq))
+ emit_items_changed (self, 0, n_items, g_sequence_get_length (priv->filter_seq));
+
+ GTD_EXIT;
+}
+
+void
+gtd_list_model_filter_set_filter_func (GtdListModelFilter *self,
+ GtdListModelFilterFunc filter_func,
+ gpointer filter_func_data,
+ GDestroyNotify filter_func_data_destroy)
+{
+ GtdListModelFilterPrivate *priv = gtd_list_model_filter_get_instance_private (self);
+
+ g_return_if_fail (GTD_IS_LIST_MODEL_FILTER (self));
+ g_return_if_fail (filter_func || (!filter_func_data && !filter_func_data_destroy));
+
+ if (priv->filter_func_data_destroy != NULL)
+ g_clear_pointer (&priv->filter_func_data, priv->filter_func_data_destroy);
+
+ if (filter_func != NULL)
+ {
+ priv->filter_func = filter_func;
+ priv->filter_func_data = filter_func_data;
+ priv->filter_func_data_destroy = filter_func_data_destroy;
+ }
+ else
+ {
+ priv->filter_func = gtd_list_model_filter_default_filter_func;
+ priv->filter_func_data = NULL;
+ priv->filter_func_data_destroy = NULL;
+ }
+
+ gtd_list_model_filter_invalidate (self);
+}
diff --git a/src/models/gtd-list-model-filter.h b/src/models/gtd-list-model-filter.h
new file mode 100644
index 0000000..f5c2a42
--- /dev/null
+++ b/src/models/gtd-list-model-filter.h
@@ -0,0 +1,44 @@
+/* gtd-list-model-filter.h
+ *
+ * Copyright © 2016 Christian Hergert <christian@hergert.me>
+ * 2018 Georges Basile Stavracas Neto <gbsneto@gnome.org>
+ *
+ * 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 <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_LIST_MODEL_FILTER (gtd_list_model_filter_get_type())
+
+typedef gboolean (*GtdListModelFilterFunc) (GObject *object,
+ gpointer user_data);
+
+G_DECLARE_FINAL_TYPE (GtdListModelFilter, gtd_list_model_filter, GTD, LIST_MODEL_FILTER, GObject)
+
+GtdListModelFilter* gtd_list_model_filter_new (GListModel *child_model);
+
+GListModel* gtd_list_model_filter_get_child_model (GtdListModelFilter *self);
+
+void gtd_list_model_filter_invalidate (GtdListModelFilter *self);
+
+void gtd_list_model_filter_set_filter_func (GtdListModelFilter *self,
+ GtdListModelFilterFunc filter_func,
+ gpointer filter_func_data,
+ GDestroyNotify filter_func_data_destroy);
+
+G_END_DECLS
diff --git a/src/models/gtd-list-model-sort.c b/src/models/gtd-list-model-sort.c
new file mode 100644
index 0000000..dedbb6d
--- /dev/null
+++ b/src/models/gtd-list-model-sort.c
@@ -0,0 +1,500 @@
+/* gtd-list-model-sort.c
+ *
+ * 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
+ */
+
+#define G_LOG_DOMAIN "GtdListModelSort"
+
+#include "gtd-debug.h"
+#include "gtd-list-model-sort.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+
+struct _GtdListModelSort
+{
+ GObject parent;
+
+ GListModel *child_model;
+
+ GSequence *child_seq;
+ GSequence *sort_seq;
+
+ GtdListModelCompareFunc compare_func;
+ gpointer compare_func_data;
+ GDestroyNotify compare_func_data_destroy;
+
+ gboolean supress_items_changed : 1;
+
+ /* cache */
+ gint64 length;
+ gint64 last_position;
+ GSequenceIter *last_iter;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdListModelSort, gtd_list_model_sort, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_CHILD_MODEL,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gtd_list_model_sort_item_free (gpointer data)
+{
+ GSequenceIter *iter = data;
+
+ g_clear_pointer (&iter, g_sequence_remove);
+}
+
+static gint
+seq_compare_func (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ GtdListModelSort *self = (GtdListModelSort*) user_data;
+
+ return self->compare_func (G_OBJECT (a), G_OBJECT (b), self->compare_func_data);
+}
+
+static gint
+default_compare_func (GObject *a,
+ GObject *b,
+ gpointer user_data)
+{
+ return 0;
+}
+
+
+static void
+invalidate_cache (GtdListModelSort *self)
+{
+ GTD_TRACE_MSG ("Invalidating cache");
+
+ self->last_iter = NULL;
+ self->last_position = -1;
+}
+
+static void
+emit_items_changed (GtdListModelSort *self,
+ guint position,
+ guint n_removed,
+ guint n_added)
+{
+ if (position <= self->last_position)
+ invalidate_cache (self);
+
+ self->length -= n_removed;
+ self->length += n_added;
+
+ GTD_TRACE_MSG ("Emitting items-changed(%u, %u, %u)", position, n_removed, n_added);
+
+ g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added);
+}
+
+
+static void
+child_model_items_changed (GtdListModelSort *self,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GListModel *child_model)
+{
+ guint i;
+
+ GTD_ENTRY;
+
+ g_assert (GTD_IS_LIST_MODEL_SORT (self));
+ g_assert (G_IS_LIST_MODEL (child_model));
+ g_assert (self->child_model == child_model);
+ g_assert (position <= (guint)g_sequence_get_length (self->child_seq));
+ g_assert (g_sequence_get_length (self->child_seq) - n_removed + n_added == g_list_model_get_n_items (child_model));
+
+ GTD_TRACE_MSG ("Received items-changed(%u, %u, %u)", position, n_removed, n_added);
+
+ if (n_removed > 0)
+ {
+ GSequenceIter *iter;
+ gint64 previous_sort_position = -1;
+ gint64 first_position = -1;
+ gint64 counter = 0;
+
+ /* Small shortcut when all items are removed */
+ if (n_removed == (guint)g_sequence_get_length (self->child_seq))
+ {
+ g_sequence_remove_range (g_sequence_get_begin_iter (self->child_seq),
+ g_sequence_get_end_iter (self->child_seq));
+
+ g_assert (g_sequence_is_empty (self->child_seq));
+ g_assert (g_sequence_is_empty (self->sort_seq));
+
+ emit_items_changed (self, 0, n_removed, 0);
+
+ goto add_new_items;
+ }
+
+ iter = g_sequence_get_iter_at_pos (self->child_seq, position);
+ g_assert (!g_sequence_iter_is_end (iter));
+
+ for (i = 0; i < n_removed; i++)
+ {
+ GSequenceIter *sort_iter = g_sequence_get (iter);
+ GSequenceIter *to_remove = iter;
+ guint sort_position;
+
+ g_assert (g_sequence_iter_get_sequence (sort_iter) == self->sort_seq);
+
+ sort_position = g_sequence_iter_get_position (sort_iter);
+
+ /* Fetch the next while the iter is still valid */
+ iter = g_sequence_iter_next (iter);
+
+ /* Cascades into also removing from sort_seq. */
+ g_sequence_remove (to_remove);
+
+ if (first_position == -1)
+ first_position = sort_position;
+
+ /*
+ * This happens when the position in the sorted sequence is different
+ * from the position in the child sequence. We try to accumulate as many
+ * items-changed() signals as possible, but we can't do that when the
+ * order doesn't match.
+ */
+ if (previous_sort_position != -1 && sort_position != previous_sort_position + 1)
+ {
+ emit_items_changed (self, first_position, counter, 0);
+
+ first_position = sort_position;
+ counter = 0;
+ }
+
+ previous_sort_position = sort_position;
+ counter++;
+ }
+
+ /*
+ * Last items-changed() - if the child model is already sorted,
+ * only this one will be fired.
+ */
+ if (counter > 0)
+ emit_items_changed (self, first_position, counter, 0);
+ }
+
+add_new_items:
+
+ if (n_added > 0)
+ {
+ GSequenceIter *iter = g_sequence_get_iter_at_pos (self->child_seq, position);
+ gint64 previous_sort_position = -1;
+ gint64 first_position = -1;
+ gint64 counter = 0;
+
+ for (i = 0; i < n_added; i++)
+ {
+ g_autoptr (GObject) instance = NULL;
+ GSequenceIter *sort_iter;
+ guint new_sort_position;
+
+ instance = g_list_model_get_item (child_model, position + i);
+ g_assert (G_IS_OBJECT (instance));
+
+ sort_iter = g_sequence_insert_sorted (self->sort_seq,
+ g_steal_pointer (&instance),
+ seq_compare_func,
+ self);
+
+ new_sort_position = g_sequence_iter_get_position (sort_iter);
+
+ /* Insert next item before this */
+ iter = g_sequence_insert_before (iter, sort_iter);
+ iter = g_sequence_iter_next (iter);
+
+ if (first_position == -1)
+ first_position = new_sort_position;
+
+ /*
+ * This happens when the position in the sorted sequence is different
+ * from the position in the child sequence. We try to accumulate as many
+ * items-changed() signals as possible, but we can't do that when the
+ * order doesn't match.
+ */
+ if (previous_sort_position != -1 && new_sort_position != previous_sort_position + 1)
+ {
+ emit_items_changed (self, first_position, 0, counter);
+
+ first_position = new_sort_position;
+ counter = 0;
+ }
+
+ previous_sort_position = new_sort_position;
+ counter++;
+ }
+
+ /*
+ * Last items-changed() - if the child model is already sorted,
+ * only this one will be fired.
+ */
+ if (counter > 0)
+ emit_items_changed (self, first_position, 0, counter);
+ }
+
+ g_assert ((guint)g_sequence_get_length (self->child_seq) == g_list_model_get_n_items (child_model));
+ g_assert ((guint)g_sequence_get_length (self->sort_seq) == g_list_model_get_n_items (child_model));
+
+ GTD_EXIT;
+}
+
+
+/*
+ * GListModel iface
+ */
+
+static GType
+gtd_list_model_sort_get_item_type (GListModel *model)
+{
+ GtdListModelSort *self = (GtdListModelSort*) model;
+
+ g_assert (GTD_IS_LIST_MODEL_SORT (self));
+
+ return g_list_model_get_item_type (self->child_model);
+}
+
+static guint
+gtd_list_model_sort_get_n_items (GListModel *model)
+{
+ GtdListModelSort *self = (GtdListModelSort*) model;
+
+ g_assert (GTD_IS_LIST_MODEL_SORT (self));
+ g_assert (self->sort_seq != NULL);
+
+ return self->length;
+}
+
+static gpointer
+gtd_list_model_sort_get_item (GListModel *model,
+ guint position)
+{
+ GtdListModelSort *self;
+ GSequenceIter *iter;
+ gpointer item;
+
+ g_assert (GTD_IS_LIST_MODEL_SORT (model));
+
+ self = (GtdListModelSort*) model;
+ iter = NULL;
+
+ if (self->last_position != -1)
+ {
+ if (self->last_position == position + 1)
+ iter = g_sequence_iter_prev (self->last_iter);
+ else if (self->last_position == position - 1)
+ iter = g_sequence_iter_next (self->last_iter);
+ else if (self->last_position == position)
+ iter = self->last_iter;
+ }
+
+ if (!iter)
+ iter = g_sequence_get_iter_at_pos (self->sort_seq, position);
+
+ if (g_sequence_iter_is_end (iter))
+ return NULL;
+
+ self->last_iter = iter;
+ self->last_position = position;
+
+ item = g_sequence_get (iter);
+ g_assert (item != NULL);
+
+ return g_object_ref (G_OBJECT (item));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtd_list_model_sort_get_item_type;
+ iface->get_n_items = gtd_list_model_sort_get_n_items;
+ iface->get_item = gtd_list_model_sort_get_item;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_list_model_sort_finalize (GObject *object)
+{
+ GtdListModelSort *self = (GtdListModelSort*) object;
+
+ g_clear_pointer (&self->child_seq, g_sequence_free);
+ g_clear_pointer (&self->sort_seq, g_sequence_free);
+
+ if (self->compare_func_data_destroy)
+ {
+ g_clear_pointer (&self->compare_func_data, self->compare_func_data_destroy);
+ self->compare_func_data_destroy = NULL;
+ }
+
+ g_clear_object (&self->child_model);
+
+ G_OBJECT_CLASS (gtd_list_model_sort_parent_class)->finalize (object);
+}
+
+static void
+gtd_list_model_sort_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListModelSort *self = GTD_LIST_MODEL_SORT (object);
+
+ switch (prop_id)
+ {
+ case PROP_CHILD_MODEL:
+ g_value_set_object (value, gtd_list_model_sort_get_child_model (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_list_model_sort_class_init (GtdListModelSortClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_list_model_sort_finalize;
+ object_class->get_property = gtd_list_model_sort_get_property;
+
+ properties [PROP_CHILD_MODEL] = g_param_spec_object ("child-model",
+ "Child Model",
+ "The child model being sorted.",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtd_list_model_sort_init (GtdListModelSort *self)
+{
+ self->compare_func = default_compare_func;
+ self->child_seq = g_sequence_new (gtd_list_model_sort_item_free);
+ self->sort_seq = g_sequence_new (g_object_unref);
+ self->last_position = -1;
+}
+
+GtdListModelSort *
+gtd_list_model_sort_new (GListModel *child_model)
+{
+ GtdListModelSort *self;
+
+ g_return_val_if_fail (G_IS_LIST_MODEL (child_model), NULL);
+
+ self = g_object_new (GTD_TYPE_LIST_MODEL_SORT, NULL);
+ self->child_model = g_object_ref (child_model);
+
+ g_signal_connect_object (child_model,
+ "items-changed",
+ G_CALLBACK (child_model_items_changed),
+ self,
+ G_CONNECT_SWAPPED);
+
+ child_model_items_changed (self, 0, 0, g_list_model_get_n_items (child_model), child_model);
+
+ return self;
+}
+
+/**
+ * gtd_list_model_sort_get_child_model:
+ * @self: A #GtdListModelSort
+ *
+ * Gets the child model that is being sorted.
+ *
+ * Returns: (transfer none): A #GListModel.
+ */
+GListModel *
+gtd_list_model_sort_get_child_model (GtdListModelSort *self)
+{
+ g_return_val_if_fail (GTD_IS_LIST_MODEL_SORT (self), NULL);
+
+ return self->child_model;
+}
+
+void
+gtd_list_model_sort_invalidate (GtdListModelSort *self)
+{
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_MODEL_SORT (self));
+
+ /* First determine how many items we need to synthesize as a removal */
+ n_items = g_sequence_get_length (self->child_seq);
+
+ g_assert (G_IS_LIST_MODEL (self->child_model));
+
+ invalidate_cache (self);
+
+ g_sequence_sort (self->sort_seq, seq_compare_func, self);
+
+ g_assert ((guint)g_sequence_get_length (self->child_seq) == n_items);
+ g_assert ((guint)g_sequence_get_length (self->sort_seq) == n_items);
+
+ if (n_items > 0)
+ emit_items_changed (self, 0, n_items, n_items);
+}
+
+void
+gtd_list_model_sort_set_sort_func (GtdListModelSort *self,
+ GtdListModelCompareFunc compare_func,
+ gpointer compare_func_data,
+ GDestroyNotify compare_func_data_destroy)
+{
+ g_return_if_fail (GTD_IS_LIST_MODEL_SORT (self));
+ g_return_if_fail (compare_func || (!compare_func_data && !compare_func_data_destroy));
+
+ GTD_ENTRY;
+
+ if (self->compare_func_data_destroy != NULL)
+ g_clear_pointer (&self->compare_func_data, self->compare_func_data_destroy);
+
+ if (compare_func != NULL)
+ {
+ self->compare_func = compare_func;
+ self->compare_func_data = compare_func_data;
+ self->compare_func_data_destroy = compare_func_data_destroy;
+ }
+ else
+ {
+ self->compare_func = default_compare_func;
+ self->compare_func_data = NULL;
+ self->compare_func_data_destroy = NULL;
+ }
+
+ gtd_list_model_sort_invalidate (self);
+
+ GTD_EXIT;
+}
diff --git a/src/models/gtd-list-model-sort.h b/src/models/gtd-list-model-sort.h
new file mode 100644
index 0000000..c95db29
--- /dev/null
+++ b/src/models/gtd-list-model-sort.h
@@ -0,0 +1,46 @@
+/* gtd-list-model-sort.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 <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_LIST_MODEL_SORT (gtd_list_model_sort_get_type())
+
+typedef gboolean (*GtdListModelCompareFunc) (GObject *a,
+ GObject *b,
+ gpointer user_data);
+
+G_DECLARE_FINAL_TYPE (GtdListModelSort, gtd_list_model_sort, GTD, LIST_MODEL_SORT, GObject)
+
+GtdListModelSort* gtd_list_model_sort_new (GListModel *child_model);
+
+GListModel* gtd_list_model_sort_get_child_model (GtdListModelSort *self);
+
+void gtd_list_model_sort_invalidate (GtdListModelSort *self);
+
+void gtd_list_model_sort_set_sort_func (GtdListModelSort *self,
+ GtdListModelCompareFunc compare_func,
+ gpointer compare_func_data,
+ GDestroyNotify compare_func_data_destroy);
+
+G_END_DECLS
diff --git a/src/models/gtd-list-store.c b/src/models/gtd-list-store.c
new file mode 100644
index 0000000..bd30bcc
--- /dev/null
+++ b/src/models/gtd-list-store.c
@@ -0,0 +1,567 @@
+/* gtd-list-store.c
+ *
+ * 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
+ */
+
+#define G_LOG_DOMAIN "GtdListStore"
+
+#include "gtd-list-store.h"
+
+#include <gio/gio.h>
+
+/**
+ * SECTION:gtdliststore
+ * @title: GtdListStore
+ * @short_description: A simple implementation of #GListModel
+ * @include: gio/gio.h
+ *
+ * #GtdListStore is a simple implementation of #GListModel that stores all
+ * items in memory.
+ *
+ * It provides insertions, deletions, and lookups in logarithmic time
+ * with a fast path for the common case of iterating the list linearly.
+ */
+
+/**
+ * GtdListStore:
+ *
+ * #GtdListStore is an opaque data structure and can only be accessed
+ * using the following functions.
+ **/
+
+struct _GtdListStore
+{
+ GObject parent;
+
+ GType item_type;
+ GSequence *items;
+
+ GHashTable *item_to_iter;
+
+ /* cache */
+ gint64 length;
+ gint64 last_position;
+ GSequenceIter *last_iter;
+};
+
+enum
+{
+ PROP_0,
+ PROP_ITEM_TYPE,
+ N_PROPERTIES
+};
+
+static void gtd_list_store_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdListStore, gtd_list_store, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtd_list_store_iface_init));
+
+static void
+remove_item_from_sequence_cb (gpointer item,
+ gpointer user_data)
+{
+ GSequenceIter *it;
+ GtdListStore *store;
+
+ store = (GtdListStore *)user_data;
+ it = g_hash_table_lookup (store->item_to_iter, item);
+
+ g_hash_table_remove (store->item_to_iter, item);
+ g_sequence_remove (it);
+}
+
+static void
+gtd_list_store_items_changed (GtdListStore *store,
+ guint position,
+ guint removed,
+ guint added)
+{
+ /* check if the iter cache may have been invalidated */
+ if (position <= store->last_position)
+ {
+ store->last_iter = NULL;
+ store->last_position = -1;
+ }
+
+ store->length -= removed;
+ store->length += added;
+
+ g_list_model_items_changed (G_LIST_MODEL (store), position, removed, added);
+}
+
+static void
+gtd_list_store_dispose (GObject *object)
+{
+ GtdListStore *store = GTD_LIST_STORE (object);
+
+ g_clear_pointer (&store->item_to_iter, g_hash_table_destroy);
+ g_clear_pointer (&store->items, g_sequence_free);
+
+ G_OBJECT_CLASS (gtd_list_store_parent_class)->dispose (object);
+}
+
+static void
+gtd_list_store_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListStore *store = GTD_LIST_STORE (object);
+
+ switch (property_id)
+ {
+ case PROP_ITEM_TYPE:
+ g_value_set_gtype (value, store->item_type);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtd_list_store_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdListStore *store = GTD_LIST_STORE (object);
+
+ switch (property_id)
+ {
+ case PROP_ITEM_TYPE: /* construct-only */
+ store->item_type = g_value_get_gtype (value);
+ if (!g_type_is_a (store->item_type, G_TYPE_OBJECT))
+ g_critical ("GtdListStore cannot store items of type '%s'. Items must be GObjects.",
+ g_type_name (store->item_type));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ }
+}
+
+static void
+gtd_list_store_class_init (GtdListStoreClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = gtd_list_store_dispose;
+ object_class->get_property = gtd_list_store_get_property;
+ object_class->set_property = gtd_list_store_set_property;
+
+ /**
+ * GtdListStore:item-type:
+ *
+ * The type of items contained in this list store. Items must be
+ * subclasses of #GObject.
+ **/
+ g_object_class_install_property (object_class, PROP_ITEM_TYPE,
+ g_param_spec_gtype ("item-type", "", "", G_TYPE_OBJECT,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+}
+
+static GType
+gtd_list_store_get_item_type (GListModel *list)
+{
+ GtdListStore *store = GTD_LIST_STORE (list);
+
+ return store->item_type;
+}
+
+static guint
+gtd_list_store_get_n_items (GListModel *list)
+{
+ GtdListStore *store = GTD_LIST_STORE (list);
+
+ return store->length;
+}
+
+static gpointer
+gtd_list_store_get_item (GListModel *list,
+ guint position)
+{
+ GtdListStore *store = GTD_LIST_STORE (list);
+ GSequenceIter *it = NULL;
+
+ if (store->last_position != -1)
+ {
+ if (store->last_position == position + 1)
+ it = g_sequence_iter_prev (store->last_iter);
+ else if (store->last_position == position - 1)
+ it = g_sequence_iter_next (store->last_iter);
+ else if (store->last_position == position)
+ it = store->last_iter;
+ }
+
+ if (it == NULL)
+ it = g_sequence_get_iter_at_pos (store->items, position);
+
+ store->last_iter = it;
+ store->last_position = position;
+
+ if (g_sequence_iter_is_end (it))
+ return NULL;
+ else
+ return g_object_ref (g_sequence_get (it));
+}
+
+static void
+gtd_list_store_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = gtd_list_store_get_item_type;
+ iface->get_n_items = gtd_list_store_get_n_items;
+ iface->get_item = gtd_list_store_get_item;
+}
+
+static void
+gtd_list_store_init (GtdListStore *store)
+{
+ store->item_to_iter = g_hash_table_new (g_direct_hash, g_direct_equal);
+ store->items = g_sequence_new (g_object_unref);
+ store->last_position = -1;
+}
+
+/**
+ * gtd_list_store_new:
+ * @item_type: the #GType of items in the list
+ *
+ * Creates a new #GtdListStore with items of type @item_type. @item_type
+ * must be a subclass of #GObject.
+ *
+ * Returns: a new #GtdListStore
+ */
+GtdListStore *
+gtd_list_store_new (GType item_type)
+{
+ /* We only allow GObjects as item types right now. This might change
+ * in the future.
+ */
+ g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL);
+
+ return g_object_new (GTD_TYPE_LIST_STORE,
+ "item-type", item_type,
+ NULL);
+}
+
+/**
+ * gtd_list_store_insert:
+ * @store: a #GtdListStore
+ * @position: the position at which to insert the new item
+ * @item: (type GObject): the new item
+ *
+ * Inserts @item into @store at @position. @item must be of type
+ * #GtdListStore:item-type or derived from it. @position must be smaller
+ * than the length of the list, or equal to it to append.
+ *
+ * This function takes a ref on @item.
+ *
+ * Use gtd_list_store_splice() to insert multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_insert (GtdListStore *store,
+ guint position,
+ gpointer item)
+{
+ GSequenceIter *new_it;
+ GSequenceIter *it;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type));
+ g_return_if_fail (position <= (guint) g_sequence_get_length (store->items));
+
+ it = g_sequence_get_iter_at_pos (store->items, position);
+ new_it = g_sequence_insert_before (it, g_object_ref (item));
+
+ g_hash_table_insert (store->item_to_iter, item, new_it);
+
+ gtd_list_store_items_changed (store, position, 0, 1);
+}
+
+/**
+ * gtd_list_store_insert_sorted:
+ * @store: a #GtdListStore
+ * @item: (type GObject): the new item
+ * @compare_func: (scope call): pairwise comparison function for sorting
+ * @user_data: (closure): user data for @compare_func
+ *
+ * Inserts @item into @store at a position to be determined by the
+ * @compare_func.
+ *
+ * The list must already be sorted before calling this function or the
+ * result is undefined. Usually you would approach this by only ever
+ * inserting items by way of this function.
+ *
+ * This function takes a ref on @item.
+ *
+ * Returns: the position at which @item was inserted
+ */
+guint
+gtd_list_store_insert_sorted (GtdListStore *store,
+ gpointer item,
+ GCompareDataFunc compare_func,
+ gpointer user_data)
+{
+ GSequenceIter *it;
+ guint position;
+
+ g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0);
+ g_return_val_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type), 0);
+ g_return_val_if_fail (compare_func != NULL, 0);
+
+ it = g_sequence_insert_sorted (store->items, g_object_ref (item), compare_func, user_data);
+ position = g_sequence_iter_get_position (it);
+
+ g_hash_table_insert (store->item_to_iter, item, it);
+
+ gtd_list_store_items_changed (store, position, 0, 1);
+
+ return position;
+}
+
+/**
+ * gtd_list_store_sort:
+ * @store: a #GtdListStore
+ * @compare_func: (scope call): pairwise comparison function for sorting
+ * @user_data: (closure): user data for @compare_func
+ *
+ * Sort the items in @store according to @compare_func.
+ */
+void
+gtd_list_store_sort (GtdListStore *store,
+ GCompareDataFunc compare_func,
+ gpointer user_data)
+{
+ gint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (compare_func != NULL);
+
+ g_sequence_sort (store->items, compare_func, user_data);
+
+ n_items = g_sequence_get_length (store->items);
+ gtd_list_store_items_changed (store, 0, n_items, n_items);
+}
+
+/**
+ * gtd_list_store_append:
+ * @store: a #GtdListStore
+ * @item: (type GObject): the new item
+ *
+ * Appends @item to @store. @item must be of type #GtdListStore:item-type.
+ *
+ * This function takes a ref on @item.
+ *
+ * Use gtd_list_store_splice() to append multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_append (GtdListStore *store,
+ gpointer item)
+{
+ GSequenceIter *it;
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type));
+
+ n_items = g_sequence_get_length (store->items);
+ it = g_sequence_append (store->items, g_object_ref (item));
+
+ g_hash_table_insert (store->item_to_iter, item, it);
+
+ gtd_list_store_items_changed (store, n_items, 0, 1);
+}
+
+/**
+ * gtd_list_store_remove:
+ * @store: a #GtdListStore
+ * @item: the item that is to be removed
+ *
+ * Removes the item from @store.
+ *
+ * Use gtd_list_store_splice() to remove multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_remove (GtdListStore *store,
+ gpointer item)
+{
+ GSequenceIter *it;
+ guint position;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+
+ it = g_hash_table_lookup (store->item_to_iter, item);
+ if (!it)
+ return;
+
+ g_return_if_fail (!g_sequence_iter_is_end (it));
+
+ position = g_sequence_iter_get_position (it);
+
+ g_hash_table_remove (store->item_to_iter, item);
+ g_sequence_remove (it);
+ gtd_list_store_items_changed (store, position, 1, 0);
+}
+
+/**
+ * gtd_list_store_remove_at_position:
+ * @store: a #GtdListStore
+ * @position: the position of the item that is to be removed
+ *
+ * Removes the item from @store that is at @position. @position must be
+ * smaller than the current length of the list.
+ *
+ * Use gtd_list_store_splice() to remove multiple items at the same time
+ * efficiently.
+ */
+void
+gtd_list_store_remove_at_position (GtdListStore *store,
+ guint position)
+{
+ GSequenceIter *it;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+
+ it = g_sequence_get_iter_at_pos (store->items, position);
+ g_return_if_fail (!g_sequence_iter_is_end (it));
+
+ g_hash_table_remove (store->item_to_iter, g_sequence_get (it));
+ g_sequence_remove (it);
+ gtd_list_store_items_changed (store, position, 1, 0);
+}
+
+/**
+ * gtd_list_store_remove_all:
+ * @store: a #GtdListStore
+ *
+ * Removes all items from @store.
+ */
+void
+gtd_list_store_remove_all (GtdListStore *store)
+{
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+
+ n_items = g_sequence_get_length (store->items);
+ g_sequence_remove_range (g_sequence_get_begin_iter (store->items),
+ g_sequence_get_end_iter (store->items));
+ g_hash_table_remove_all (store->item_to_iter);
+
+ gtd_list_store_items_changed (store, 0, n_items, 0);
+}
+
+/**
+ * gtd_list_store_splice:
+ * @store: a #GtdListStore
+ * @position: the position at which to make the change
+ * @n_removals: the number of items to remove
+ * @additions: (array length=n_additions) (element-type GObject): the items to add
+ * @n_additions: the number of items to add
+ *
+ * Changes @store by removing @n_removals items and adding @n_additions
+ * items to it. @additions must contain @n_additions items of type
+ * #GtdListStore:item-type. %NULL is not permitted.
+ *
+ * This function is more efficient than gtd_list_store_insert() and
+ * gtd_list_store_remove(), because it only emits
+ * #GListModel::items-changed once for the change.
+ *
+ * This function takes a ref on each item in @additions.
+ *
+ * The parameters @position and @n_removals must be correct (ie:
+ * @position + @n_removals must be less than or equal to the length of
+ * the list at the time this function is called).
+ */
+void
+gtd_list_store_splice (GtdListStore *store,
+ guint position,
+ guint n_removals,
+ gpointer *additions,
+ guint n_additions)
+{
+ GSequenceIter *it;
+ guint n_items;
+
+ g_return_if_fail (GTD_IS_LIST_STORE (store));
+ g_return_if_fail (position + n_removals >= position); /* overflow */
+
+ n_items = g_sequence_get_length (store->items);
+ g_return_if_fail (position + n_removals <= n_items);
+
+ it = g_sequence_get_iter_at_pos (store->items, position);
+
+ if (n_removals)
+ {
+ GSequenceIter *end;
+
+ end = g_sequence_iter_move (it, n_removals);
+ g_sequence_foreach_range (it, end, remove_item_from_sequence_cb, store);
+
+ it = end;
+ }
+
+ if (n_additions)
+ {
+ guint i;
+
+ for (i = 0; i < n_additions; i++)
+ {
+ if G_UNLIKELY (!g_type_is_a (G_OBJECT_TYPE (additions[i]), store->item_type))
+ {
+ g_critical ("%s: item %d is a %s instead of a %s. GtdListStore is now in an undefined state.",
+ G_STRFUNC, i, G_OBJECT_TYPE_NAME (additions[i]), g_type_name (store->item_type));
+ return;
+ }
+
+ it = g_sequence_insert_before (it, g_object_ref (additions[i]));
+ g_hash_table_insert (store->item_to_iter, additions[i], it);
+
+ it = g_sequence_iter_next (it);
+ }
+ }
+
+ gtd_list_store_items_changed (store, position, n_removals, n_additions);
+}
+
+/**
+ * gtd_list_store_get_item_position:
+ * @store: a #GtdListStore
+ * @item: the item to retrieve the position
+ *
+ * Retrieves the position of @items inside @store. It is a programming
+ * error to pass an @item that is not contained in @store.
+ *
+ * Returns: the position of @item in @store.
+ */
+guint
+gtd_list_store_get_item_position (GtdListStore *store,
+ gpointer item)
+{
+ GSequenceIter *iter;
+
+ g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0);
+
+ iter = g_hash_table_lookup (store->item_to_iter, item);
+ g_assert (iter != NULL);
+
+ return g_sequence_iter_get_position (iter);
+}
diff --git a/src/models/gtd-list-store.h b/src/models/gtd-list-store.h
new file mode 100644
index 0000000..84e8882
--- /dev/null
+++ b/src/models/gtd-list-store.h
@@ -0,0 +1,66 @@
+/* gtd-list-store.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-object.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_LIST_STORE (gtd_list_store_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdListStore, gtd_list_store, GTD, LIST_STORE, GObject)
+
+GtdListStore* gtd_list_store_new (GType item_type);
+
+void gtd_list_store_insert (GtdListStore *store,
+ guint position,
+ gpointer item);
+
+guint gtd_list_store_insert_sorted (GtdListStore *store,
+ gpointer item,
+ GCompareDataFunc compare_func,
+ gpointer user_data);
+
+void gtd_list_store_sort (GtdListStore *store,
+ GCompareDataFunc compare_func,
+ gpointer user_data);
+
+void gtd_list_store_append (GtdListStore *store,
+ gpointer item);
+
+void gtd_list_store_remove (GtdListStore *store,
+ gpointer item);
+
+void gtd_list_store_remove_at_position (GtdListStore *store,
+ guint position);
+
+void gtd_list_store_remove_all (GtdListStore *store);
+
+void gtd_list_store_splice (GtdListStore *store,
+ guint position,
+ guint n_removals,
+ gpointer *additions,
+ guint n_additions);
+
+guint gtd_list_store_get_item_position (GtdListStore *store,
+ gpointer item);
+
+G_END_DECLS
diff --git a/src/models/gtd-task-list-view-model.c b/src/models/gtd-task-list-view-model.c
new file mode 100644
index 0000000..d569d29
--- /dev/null
+++ b/src/models/gtd-task-list-view-model.c
@@ -0,0 +1,216 @@
+/* gtd-task-list-view-model.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-task-list.h"
+#include "gtd-task-list-view-model.h"
+
+
+/*
+ * Sentinel
+ */
+
+struct _GtdSentinel
+{
+ GObject parent_instance;
+};
+
+G_DEFINE_TYPE (GtdSentinel, gtd_sentinel, G_TYPE_OBJECT);
+
+static void
+gtd_sentinel_init (GtdSentinel *self)
+{
+}
+
+static void
+gtd_sentinel_class_init (GtdSentinelClass *klass)
+{
+}
+
+
+
+struct _GtdTaskListViewModel
+{
+ GObject parent_instance;
+
+ GtdSentinel *sentinel;
+
+ GListModel *model;
+ guint n_items;
+};
+
+static void g_list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdTaskListViewModel, gtd_task_list_view_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init))
+
+
+/*
+ * Auxiliary methods
+ */
+
+static void
+update_n_items (GtdTaskListViewModel *self,
+ guint position,
+ guint removed,
+ guint added)
+{
+ self->n_items = self->n_items - removed + added;
+ g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_model_items_changed_cb (GListModel *model,
+ guint position,
+ guint removed,
+ guint added,
+ GtdTaskListViewModel *self)
+{
+ update_n_items (self, position, removed, added);
+}
+
+
+/*
+ * GListModel interface
+ */
+
+static guint
+gtd_task_list_view_model_get_n_items (GListModel *model)
+{
+ GtdTaskListViewModel *self = (GtdTaskListViewModel *)model;
+
+ return self->n_items + 1;
+}
+
+static GType
+gtd_task_list_view_model_get_item_type (GListModel *model)
+{
+ return G_TYPE_OBJECT;
+}
+
+static gpointer
+gtd_task_list_view_model_get_item (GListModel *model,
+ guint position)
+{
+ GtdTaskListViewModel *self = (GtdTaskListViewModel *)model;
+
+ if (gtd_task_list_view_model_is_sentinel (self, position))
+ return g_object_ref (self->sentinel);
+
+ return g_list_model_get_item (self->model, position);
+}
+
+static void
+g_list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_n_items = gtd_task_list_view_model_get_n_items;
+ iface->get_item_type = gtd_task_list_view_model_get_item_type;
+ iface->get_item = gtd_task_list_view_model_get_item;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_list_view_model_finalize (GObject *object)
+{
+ GtdTaskListViewModel *self = (GtdTaskListViewModel *)object;
+
+ g_clear_object (&self->model);
+ g_clear_object (&self->sentinel);
+
+ G_OBJECT_CLASS (gtd_task_list_view_model_parent_class)->finalize (object);
+}
+
+static void
+gtd_task_list_view_model_class_init (GtdTaskListViewModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_task_list_view_model_finalize;
+}
+
+static void
+gtd_task_list_view_model_init (GtdTaskListViewModel *self)
+{
+ self->sentinel = g_object_new (GTD_TYPE_SENTINEL, NULL);
+}
+
+GtdTaskListViewModel *
+gtd_task_list_view_model_new (void)
+{
+ return g_object_new (GTD_TYPE_TASK_LIST_VIEW_MODEL, NULL);
+}
+
+GListModel *
+gtd_task_list_view_model_get_model (GtdTaskListViewModel *self)
+{
+ return self->model;
+}
+
+void
+gtd_task_list_view_model_set_model (GtdTaskListViewModel *self,
+ GListModel *model)
+{
+ guint old_n_items = 0;
+ guint new_n_items = 0;
+
+ if (G_UNLIKELY (self->model == model))
+ return;
+
+ old_n_items = self->n_items;
+
+ if (self->model)
+ {
+ g_signal_handlers_disconnect_by_func (self->model,
+ on_model_items_changed_cb,
+ self);
+ }
+
+ g_clear_object (&self->model);
+
+ if (model)
+ {
+ self->model = g_object_ref (model);
+
+ g_signal_connect_object (model,
+ "items-changed",
+ G_CALLBACK (on_model_items_changed_cb),
+ self,
+ 0);
+
+ new_n_items = g_list_model_get_n_items (model);
+ }
+
+ update_n_items (self, 0, old_n_items, new_n_items);
+}
+
+gboolean
+gtd_task_list_view_model_is_sentinel (GtdTaskListViewModel *self,
+ guint position)
+{
+ return position == self->n_items;
+}
diff --git a/src/models/gtd-task-list-view-model.h b/src/models/gtd-task-list-view-model.h
new file mode 100644
index 0000000..7323f3a
--- /dev/null
+++ b/src/models/gtd-task-list-view-model.h
@@ -0,0 +1,43 @@
+/* gtd-task-list-view-model.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 <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_SENTINEL (gtd_sentinel_get_type ())
+G_DECLARE_FINAL_TYPE (GtdSentinel, gtd_sentinel, GTD, SENTINEL, GObject)
+
+#define GTD_TYPE_TASK_LIST_VIEW_MODEL (gtd_task_list_view_model_get_type())
+G_DECLARE_FINAL_TYPE (GtdTaskListViewModel, gtd_task_list_view_model, GTD, TASK_LIST_VIEW_MODEL, GObject)
+
+GtdTaskListViewModel* gtd_task_list_view_model_new (void);
+
+GListModel* gtd_task_list_view_model_get_model (GtdTaskListViewModel *self);
+
+void gtd_task_list_view_model_set_model (GtdTaskListViewModel *self,
+ GListModel *model);
+
+gboolean gtd_task_list_view_model_is_sentinel (GtdTaskListViewModel *self,
+ guint position);
+
+G_END_DECLS
diff --git a/src/models/gtd-task-model-private.h b/src/models/gtd-task-model-private.h
new file mode 100644
index 0000000..7bc1c7d
--- /dev/null
+++ b/src/models/gtd-task-model-private.h
@@ -0,0 +1,29 @@
+/* gtd-task-model-private.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-task-model.h"
+
+G_BEGIN_DECLS
+
+GtdTaskModel* _gtd_task_model_new (GtdManager *manager);
+
+G_END_DECLS
diff --git a/src/models/gtd-task-model.c b/src/models/gtd-task-model.c
new file mode 100644
index 0000000..62984e7
--- /dev/null
+++ b/src/models/gtd-task-model.c
@@ -0,0 +1,216 @@
+/* gtd-task-model.c
+ *
+ * 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
+ */
+
+#define G_LOG_DOMAIN "GtdTaskModel"
+
+#include "gtd-debug.h"
+#include "gtd-list-store.h"
+#include "gtd-manager.h"
+#include "gtd-task.h"
+#include "gtd-task-list.h"
+#include "gtd-task-model.h"
+#include "gtd-task-model-private.h"
+
+struct _GtdTaskModel
+{
+ GObject parent;
+
+ GtkFlattenListModel *model;
+
+ guint number_of_tasks;
+
+ GtdManager *manager;
+};
+
+static void g_list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GtdTaskModel, gtd_task_model, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_iface_init))
+
+enum
+{
+ PROP_0,
+ PROP_MANAGER,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_model_items_changed_cb (GListModel *model,
+ guint position,
+ guint n_removed,
+ guint n_added,
+ GtdTaskModel *self)
+{
+ GTD_TRACE_MSG ("Child model changed with position=%u, n_removed=%u, n_added=%u", position, n_removed, n_added);
+
+ g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added);
+}
+
+
+/*
+ * GListModel iface
+ */
+
+static gpointer
+gtd_task_model_get_item (GListModel *model,
+ guint position)
+{
+ GtdTaskModel *self = (GtdTaskModel*) model;
+
+ return g_list_model_get_item (G_LIST_MODEL (self->model), position);
+}
+
+static guint
+gtd_task_model_get_n_items (GListModel *model)
+{
+ GtdTaskModel *self = (GtdTaskModel*) model;
+
+ return g_list_model_get_n_items (G_LIST_MODEL (self->model));
+}
+
+static GType
+gtd_task_model_get_item_type (GListModel *model)
+{
+ return GTD_TYPE_TASK;
+}
+
+static void
+g_list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item = gtd_task_model_get_item;
+ iface->get_n_items = gtd_task_model_get_n_items;
+ iface->get_item_type = gtd_task_model_get_item_type;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gtd_task_model_finalize (GObject *object)
+{
+ GtdTaskModel *self = (GtdTaskModel *)object;
+
+ g_clear_object (&self->manager);
+ g_clear_object (&self->model);
+
+ G_OBJECT_CLASS (gtd_task_model_parent_class)->finalize (object);
+}
+
+
+static void
+gtd_task_model_constructed (GObject *object)
+{
+ GtdTaskModel *self = (GtdTaskModel *)object;
+ GListModel *model;
+
+ g_assert (self->manager != NULL);
+
+ model = gtd_manager_get_task_lists_model (self->manager);
+
+ g_signal_connect_object (self->model,
+ "items-changed",
+ G_CALLBACK (on_model_items_changed_cb),
+ self,
+ 0);
+ gtk_flatten_list_model_set_model (self->model, model);
+
+ G_OBJECT_CLASS (gtd_task_model_parent_class)->constructed (object);
+}
+
+static void
+gtd_task_model_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskModel *self = GTD_TASK_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_value_set_object (value, self->manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_model_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GtdTaskModel *self = GTD_TASK_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_assert (self->manager == NULL);
+ self->manager = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gtd_task_model_class_init (GtdTaskModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gtd_task_model_finalize;
+ object_class->constructed = gtd_task_model_constructed;
+ object_class->get_property = gtd_task_model_get_property;
+ object_class->set_property = gtd_task_model_set_property;
+
+ properties[PROP_MANAGER] = g_param_spec_object ("manager",
+ "Manager",
+ "Manager",
+ GTD_TYPE_MANAGER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gtd_task_model_init (GtdTaskModel *self)
+{
+ self->model = gtk_flatten_list_model_new (NULL);
+}
+
+GtdTaskModel*
+_gtd_task_model_new (GtdManager *manager)
+{
+ return g_object_new (GTD_TYPE_TASK_MODEL,
+ "manager", manager,
+ NULL);
+}
diff --git a/src/models/gtd-task-model.h b/src/models/gtd-task-model.h
new file mode 100644
index 0000000..2dc7aee
--- /dev/null
+++ b/src/models/gtd-task-model.h
@@ -0,0 +1,33 @@
+/* gtd-task-model.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 <gio/gio.h>
+
+#include "gtd-types.h"
+
+G_BEGIN_DECLS
+
+#define GTD_TYPE_TASK_MODEL (gtd_task_model_get_type())
+
+G_DECLARE_FINAL_TYPE (GtdTaskModel, gtd_task_model, GTD, TASK_MODEL, GObject)
+
+G_END_DECLS
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 =
diff --git a/src/themes/_omniarea.css b/src/themes/_omniarea.css
new file mode 100644
index 0000000..1b575d1
--- /dev/null
+++ b/src/themes/_omniarea.css
@@ -0,0 +1,13 @@
+omniarea entry {
+ background-color: mix(@theme_bg_color, @content_view_bg, 0.25);
+ color: @theme_fg_color;
+}
+omniarea:hover entry,
+omniarea:active entry {
+ background-color: @content_view_bg;
+ color: @theme_fg_color;
+}
+
+omniarea:backdrop entry {
+ background-color: @theme_bg_color;
+}
diff --git a/src/themes/_taskrow.css b/src/themes/_taskrow.css
new file mode 100644
index 0000000..f3ccee4
--- /dev/null
+++ b/src/themes/_taskrow.css
@@ -0,0 +1,38 @@
+taskrow {
+ transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ border-radius: 12px;
+}
+
+taskrow:hover {
+ background-color: alpha(@theme_fg_color, 0.1);
+}
+
+taskrow:focus {
+ background-color: alpha(@theme_fg_color, 0.05);
+}
+
+taskrow.card:hover {
+ background-color: alpha(@theme_fg_color, 0.01);
+}
+
+taskrow:dir(ltr) { padding-left: 6px; }
+taskrow:dir(rtl) { padding-right: 6px; }
+
+/* task title entry */
+taskrow text {
+ border: solid 1px transparent;
+ background: none;
+ border-radius: 3px;
+}
+
+/* line-through complete tasks */
+taskrow.complete > * { opacity: 0.5; }
+taskrow.complete entry.title { text-decoration-line: line-through; }
+
+/* dnd row */
+taskrow box.dnd {
+ background: alpha(@theme_fg_color, 0.1);
+}
+
+/* new task row */
+newtaskrow entry { padding: 0 16px 0 14px; }
diff --git a/src/themes/_widgets.css b/src/themes/_widgets.css
new file mode 100644
index 0000000..0ee041c
--- /dev/null
+++ b/src/themes/_widgets.css
@@ -0,0 +1,27 @@
+/* Star Widget */
+
+@keyframes wiggle {
+ 12.5% { transform: rotate(15deg); }
+ 37.5% { transform: rotate(-15deg); }
+ 62.5% { transform: rotate(15deg); }
+ 87.5% { transform: rotate(-15deg); }
+}
+
+star {
+ color: alpha(@theme_fg_color, 0.1);
+ transition: color 400ms;
+}
+
+star:hover {
+ color: alpha(@theme_fg_color, 0.4);
+ transition: color 250ms;
+}
+
+star:checked {
+ color: @theme_selected_bg_color;
+ transition: color 250ms;
+
+ animation-name: wiggle;
+ animation-duration: 400ms;
+ animation-timing-function: linear;
+}
diff --git a/src/themes/style.css b/src/themes/style.css
new file mode 100644
index 0000000..a9096b3
--- /dev/null
+++ b/src/themes/style.css
@@ -0,0 +1,58 @@
+@import url("resource:///org/gnome/todo/themes/_omniarea.css");
+@import url("resource:///org/gnome/todo/themes/_taskrow.css");
+@import url("resource:///org/gnome/todo/themes/_widgets.css");
+
+tasklistview list:drop(active) {
+ border: none;
+}
+
+row.thumbnail {
+ border: solid 1px @borders;
+ box-shadow: 0px 0px 3px alpha(@borders, 0.8);
+ border-radius: 16px;
+}
+
+grid-item.thumbnail {
+ padding: 6px;
+ font-size: 10px;
+}
+
+grid-item image {
+ margin: 12px 24px;
+ font-size: 10px;
+ border: solid 1px @borders;
+ border-radius: 4px;
+ box-shadow: 0px 0px 4px alpha(@borders, 0.8);
+}
+
+grid-item {
+ background-color: transparent;
+}
+
+grid-item.light {
+ color: #000003;
+}
+
+grid-item.dark {
+ color: #eeeeec;
+}
+
+arrow-frame {
+ border: solid 1px;
+ border-bottom-width: 0px;
+ border-top-width: 0px;
+ background-color: @theme_bg_color;
+ border-color: @borders;
+}
+
+arrow-frame:dir(ltr) {
+ border-right-width: 0px;
+}
+
+arrow-frame:dir(rtl) {
+ border-left-width: 0px;
+}
+
+label.main-title {
+ font-size: 28px;
+}
diff --git a/src/todo.gresource.xml b/src/todo.gresource.xml
new file mode 100644
index 0000000..e9910f3
--- /dev/null
+++ b/src/todo.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/todo">
+ <file compressed="true" alias="style.css">themes/style.css</file>
+ <file compressed="true">themes/_omniarea.css</file>
+ <file compressed="true">themes/_taskrow.css</file>
+ <file compressed="true">themes/_widgets.css</file>
+ </gresource>
+</gresources>