diff options
Diffstat (limited to 'src/gui')
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, ¤t_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"><Control>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"><Control>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"><Alt>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"><Control>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"><Alt>Down</property> + </object> + </child> + + </object> + </child> + + </object> + </child> + </object> +</interface> |
