summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-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
60 files changed, 12699 insertions, 0 deletions
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>