From dfc5a9b970deee295ceae10fd7e9792e9ff6c1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Iv=C3=A1n?= Date: Mon, 29 Jul 2024 20:33:18 -0600 Subject: [PATCH] drop-overlay: Introduce GcalDropOverlay A new widget to perform drag and drop operations, for example, dropping ICS files to the GcalWindow. It allows to overlay a child for visual feedback when a DnD operation is done. either in the main window or in the import dialog, depending on whether none could be imported, or just some of them. It is based on Loupe's DragOverlay widget. Closes #1257 --- src/gui/gcal-drop-overlay.c | 184 ++++++++++++++++++++ src/gui/gcal-drop-overlay.h | 36 ++++ src/gui/gcal-window.c | 198 +++++++++++++++++++++ src/gui/gcal-window.ui | 338 +++++++++++++++++++----------------- src/gui/meson.build | 1 + src/theme/style.css | 21 +++ 6 files changed, 622 insertions(+), 156 deletions(-) create mode 100644 src/gui/gcal-drop-overlay.c create mode 100644 src/gui/gcal-drop-overlay.h diff --git a/src/gui/gcal-drop-overlay.c b/src/gui/gcal-drop-overlay.c new file mode 100644 index 00000000..fa9a6809 --- /dev/null +++ b/src/gui/gcal-drop-overlay.c @@ -0,0 +1,184 @@ +/* gcal-drop-overlay.c + * + * Copyright 2024 Diego Iván + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// A drop overlay widget, based on Loupe's LpDragOverlay +// https://gitlab.gnome.org/GNOME/loupe/-/blob/692997ff0ee10ea187469c9da328010e1733dd6f/src/widgets/drag_overlay.rs + +#define G_LOG_DOMAIN "GcalDropOverlay" + +#include "gcal-debug.h" +#include "gcal-drop-overlay.h" + +struct _GcalDropOverlay +{ + AdwBin parent; + + GtkOverlay *overlay; + GtkRevealer *revealer; + GtkDropTarget *drop_target; +}; + +static void on_current_drop_notify_cb (GcalDropOverlay *self); + +G_DEFINE_TYPE (GcalDropOverlay, gcal_drop_overlay, ADW_TYPE_BIN) + +enum +{ + PROP_0, + PROP_CHILD, + PROP_OVERLAYED, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +static void +on_current_drop_notify_cb (GcalDropOverlay *self) +{ + GdkDrop *current_drop; + + GCAL_ENTRY; + + current_drop = gtk_drop_target_get_current_drop (self->drop_target); + + if (current_drop != NULL) + { + gtk_revealer_set_reveal_child (self->revealer, TRUE); + gtk_widget_add_css_class (gtk_overlay_get_child (self->overlay), "blurred"); + } + else + { + gtk_revealer_set_reveal_child (self->revealer, FALSE); + gtk_widget_remove_css_class (gtk_overlay_get_child (self->overlay), "blurred"); + } + + gtk_revealer_set_reveal_child (self->revealer, current_drop != NULL); + + GCAL_EXIT; +} + +static void +gcal_drop_overlay_dispose (GObject *object) +{ + GcalDropOverlay *self = GCAL_DROP_OVERLAY (object); + + g_clear_object (&self->drop_target); + + G_OBJECT_CLASS (gcal_drop_overlay_parent_class)->dispose (object); +} + +static void +gcal_drop_overlay_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalDropOverlay *self = GCAL_DROP_OVERLAY (object); + + switch (prop_id) + { + case PROP_CHILD: + g_value_set_object (value, gtk_overlay_get_child (self->overlay)); + break; + + case PROP_OVERLAYED: + g_value_set_object (value, gtk_revealer_get_child (self->revealer)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_drop_overlay_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalDropOverlay *self = GCAL_DROP_OVERLAY (object); + + switch (prop_id) + { + case PROP_CHILD: + gtk_overlay_set_child (self->overlay, g_value_get_object (value)); + break; + + case PROP_OVERLAYED: + gtk_revealer_set_child (self->revealer, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_drop_overlay_class_init (GcalDropOverlayClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gcal_drop_overlay_dispose; + object_class->get_property = gcal_drop_overlay_get_property; + object_class->set_property = gcal_drop_overlay_set_property; + + properties[PROP_CHILD] = g_param_spec_object ("child", NULL, NULL, + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + properties[PROP_OVERLAYED] = g_param_spec_object ("overlayed", NULL, NULL, + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gcal_drop_overlay_init (GcalDropOverlay *self) +{ + self->overlay = GTK_OVERLAY (gtk_overlay_new ()); + self->revealer = GTK_REVEALER (gtk_revealer_new ()); + + gtk_overlay_add_overlay (self->overlay, GTK_WIDGET (self->revealer)); + gtk_widget_set_can_target (GTK_WIDGET (self->revealer), FALSE); + gtk_revealer_set_transition_type (self->revealer, GTK_REVEALER_TRANSITION_TYPE_CROSSFADE); + gtk_revealer_set_reveal_child (self->revealer, FALSE); + + adw_bin_set_child (ADW_BIN (self), GTK_WIDGET (self->overlay)); +} + +void +gcal_drop_overlay_set_drop_target (GcalDropOverlay *self, + GtkDropTarget *drop_target) +{ + GCAL_ENTRY; + + g_return_if_fail (GCAL_IS_DROP_OVERLAY (self)); + g_return_if_fail (GTK_IS_DROP_TARGET (drop_target)); + + g_set_object (&self->drop_target, drop_target); + + g_signal_connect_object (self->drop_target, + "notify::current-drop", + G_CALLBACK (on_current_drop_notify_cb), + self, + G_CONNECT_SWAPPED); + GCAL_EXIT; +} diff --git a/src/gui/gcal-drop-overlay.h b/src/gui/gcal-drop-overlay.h new file mode 100644 index 00000000..01e04585 --- /dev/null +++ b/src/gui/gcal-drop-overlay.h @@ -0,0 +1,36 @@ +/* gcal-drop-overlay.h + * + * Copyright 2024 Diego Iván M.E + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gcal-context.h" +#include "gcal-event.h" + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_DROP_OVERLAY (gcal_drop_overlay_get_type()) +G_DECLARE_FINAL_TYPE (GcalDropOverlay, gcal_drop_overlay, GCAL, DROP_OVERLAY, AdwBin) + +void gcal_drop_overlay_set_drop_target (GcalDropOverlay *self, + GtkDropTarget *drop_target); + +G_END_DECLS diff --git a/src/gui/gcal-window.c b/src/gui/gcal-window.c index 45c055c1..ddbbab17 100644 --- a/src/gui/gcal-window.c +++ b/src/gui/gcal-window.c @@ -25,6 +25,7 @@ #include "config.h" #include "gcal-date-chooser.h" #include "gcal-debug.h" +#include "gcal-drop-overlay.h" #include "gcal-event-editor-dialog.h" #include "gcal-event-widget.h" #include "gcal-context.h" @@ -90,6 +91,12 @@ typedef struct gchar *uuid; } OpenEditDialogData; +typedef struct +{ + GList *ics_files; + GList *invalid_files; +} FileFilterResult; + struct _GcalWindow { AdwApplicationWindow parent; @@ -158,6 +165,11 @@ struct _GcalWindow /* Window states */ gboolean in_key_press; + + /* File drop */ + GcalDropOverlay *drop_overlay; + AdwStatusPage *drop_status_page; + GtkDropTarget *drop_target; }; enum @@ -340,6 +352,86 @@ content_title (GcalWindow *self, return g_strdup (_("Month")); } +static inline void +free_file_filter_result (FileFilterResult *self) +{ + g_list_free_full (self->ics_files, g_object_unref); + g_list_free_full (self->invalid_files, g_object_unref); +} + +static inline gboolean +is_ics_file (GFile *file) +{ + g_autoptr (GFileInfo) file_info = NULL; + g_autoptr (GError) error = NULL; + + file_info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + NULL, + &error); + + if (error) + return FALSE; + + return g_strcmp0 (g_file_info_get_content_type (file_info), "text/calendar") == 0; +} + +static inline void +free_file_slist_full (GSList *files) +{ + g_slist_free_full (files, g_object_unref); +} + +static void +filter_ics_files_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + FileFilterResult *result; + GSList *files; + + files = task_data; + result = g_new0 (FileFilterResult, 1); + + for (GSList *iter = files; iter != NULL; iter = iter->next) + { + GFile *file = G_FILE (iter->data); + + if (is_ics_file (file)) + result->ics_files = g_list_prepend (result->ics_files, g_object_ref (file)); + else + result->invalid_files = g_list_prepend (result->invalid_files, g_object_ref (file)); + } + + g_task_return_pointer (task, result, NULL); +} + +static FileFilterResult* +filter_ics_files_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), NULL); + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +filter_ics_files (GcalWindow *self, + GSList *files, + GAsyncReadyCallback callback) +{ + g_autoslist (GFile) files_copy = NULL; + g_autoptr (GTask) task = NULL; + + files_copy = g_slist_copy_deep (files, (GCopyFunc) g_object_ref, NULL); + + task = g_task_new (NULL, NULL, callback, self); + g_task_set_task_data (task, g_steal_pointer (&files_copy), (GDestroyNotify) free_file_slist_full); + g_task_set_source_tag (task, filter_ics_files); + g_task_run_in_thread (task, filter_ics_files_thread); +} + /* * Callbacks */ @@ -763,6 +855,99 @@ on_event_editor_dialog_remove_event_cb (GcalEventEditorDialog *edit_dialog, GCAL_EXIT; } +static gboolean +on_drop_target_accept_cb (GtkDropTarget *target, + GdkDrop *drop, + gpointer user_data) +{ + GdkDrag *drag; + GdkContentFormats *formats; + + drag = gdk_drop_get_drag (drop); + formats = gdk_drop_get_formats (drop); + + // If drag is NULL, it means we are not receiving the from an external application/window, + // but from internal sources, i.e, dragging events + gboolean is_external = drag == NULL; + return is_external && gdk_content_formats_contain_gtype (formats, GDK_TYPE_FILE_LIST); +} + +static void +on_ics_files_filtered_cb (GObject *source_object, + GAsyncResult *result, + gpointer data) +{ + g_autoptr (GError) error = NULL; + g_autofree gchar* toast_title = NULL; + AdwToast *error_toast = NULL; + GcalWindow *self; + FileFilterResult *filter_result; + + GCAL_ENTRY; + + self = GCAL_WINDOW (data); + filter_result = filter_ics_files_finish (result, &error); + + if (error) + return; + + // Creates an error toast if it's needed + if (filter_result->invalid_files) + { + if (filter_result->invalid_files->next) + { + toast_title = g_strdup(_("Could not import some files: Not ICS files")); + } + else + { + g_autofree gchar *basename = g_file_get_basename (filter_result->invalid_files->data); + toast_title = g_strdup_printf (_("Could not import “%s”: Not an ICS file"), basename); + } + error_toast = adw_toast_new (toast_title); + } + + if (filter_result->ics_files) + { + g_clear_pointer (&self->import_dialog, gtk_widget_unparent); + + self->import_dialog = gcal_import_dialog_new_for_file_list (self->context, filter_result->ics_files); + adw_dialog_present (ADW_DIALOG (self->import_dialog), GTK_WIDGET (self)); + + g_object_add_weak_pointer (G_OBJECT (self->import_dialog), (gpointer *)&self->import_dialog); + + if (error_toast) + gcal_import_dialog_add_toast (GCAL_IMPORT_DIALOG (self->import_dialog), error_toast); + } + else + { + adw_toast_overlay_add_toast (self->overlay, error_toast); + } + + g_clear_pointer (&filter_result, free_file_filter_result); + GCAL_EXIT; +} + +static gboolean +on_drop_target_drop_cb (GtkDropTarget *target, + const GValue *value, + gdouble x, + gdouble y, + gpointer user_data) +{ + g_autoptr (GSList) files = NULL; + GdkFileList *file_list; + GcalWindow *self; + + GCAL_ENTRY; + + self = GCAL_WINDOW (user_data); + file_list = g_value_get_boxed (value); + files = gdk_file_list_get_files (file_list); + filter_ics_files (self, files, on_ics_files_filtered_cb); + + GCAL_RETURN (TRUE); +} + static gboolean schedule_open_edit_dialog_by_uuid (OpenEditDialogData *edit_dialog_data) { @@ -1023,6 +1208,7 @@ gcal_window_class_init (GcalWindowClass *klass) g_type_ensure (GCAL_TYPE_TOOLBAR_END); g_type_ensure (GCAL_TYPE_WEATHER_SETTINGS); g_type_ensure (GCAL_TYPE_WEEK_VIEW); + g_type_ensure (GCAL_TYPE_DROP_OVERLAY); object_class = G_OBJECT_CLASS (klass); object_class->finalize = gcal_window_finalize; @@ -1085,6 +1271,8 @@ gcal_window_class_init (GcalWindowClass *klass) gtk_widget_class_bind_template_child (widget_class, GcalWindow, views_switcher); gtk_widget_class_bind_template_child (widget_class, GcalWindow, weather_settings); gtk_widget_class_bind_template_child (widget_class, GcalWindow, week_view); + gtk_widget_class_bind_template_child (widget_class, GcalWindow, drop_overlay); + gtk_widget_class_bind_template_child (widget_class, GcalWindow, drop_target); gtk_widget_class_bind_template_callback (widget_class, view_changed); @@ -1099,6 +1287,10 @@ gcal_window_class_init (GcalWindowClass *klass) /* Edit dialog related */ gtk_widget_class_bind_template_callback (widget_class, on_event_editor_dialog_remove_event_cb); + + /* Drop Target related */ + gtk_widget_class_bind_template_callback (widget_class, on_drop_target_accept_cb); + gtk_widget_class_bind_template_callback (widget_class, on_drop_target_drop_cb); } static void @@ -1135,6 +1327,12 @@ gcal_window_init (GcalWindow *self) gtk_widget_add_css_class (GTK_WIDGET (self), "devel"); gtk_widget_set_parent (self->quick_add_popover, GTK_WIDGET (self)); + + GType drop_types[] = { GDK_TYPE_FILE_LIST }; + gtk_drop_target_set_gtypes (self->drop_target, drop_types, + G_N_ELEMENTS(drop_types)); + + gcal_drop_overlay_set_drop_target(self->drop_overlay, self->drop_target); } /** diff --git a/src/gui/gcal-window.ui b/src/gui/gcal-window.ui index e9fe0857..c78f646d 100644 --- a/src/gui/gcal-window.ui +++ b/src/gui/gcal-window.ui @@ -117,174 +117,200 @@ - - - - - Calendar - - - - - vertical - - - - - - - - - - - - open-menu-symbolic - win_menu - Main Menu - True - - - - - - - - False - False - - - - - - - - - - - - True - - - - - - - - - False - - - win.today - - - _Today - True - calendar-today-symbolic - True - - - - - - - - - - - + + + + Drop Event Files to Import + calendar-month-symbolic + + + + + + copy + capture + + + + + + + + + Calendar + + + + + vertical - - - - - - - - - - - - - - - - - win.today - - - _Today - True - calendar-today-symbolic - True - - - - - - - views_stack - wide - - - - - - - - - - - True - True - vertical - - - True - True - - - - week - _Week - True - calendar-week-symbolic + + + + + + + + + + open-menu-symbolic + win_menu + Main Menu + True + + + + + + + + False + False + + + + + + + + + + + + True + + + + + + + + + False + + + win.today - - - - + + _Today + True + calendar-today-symbolic + True - - - month - _Month - True - calendar-month-symbolic - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + win.today + + + _Today + True + calendar-today-symbolic + True + + + + + + + views_stack + wide + + + + + + + + + + + + True + True + vertical + + + True + True + + + + + week + _Week + True + calendar-week-symbolic + + + + + + + + + + + + month + _Month + True + calendar-month-symbolic + + + + + + + + + + + + + + + + + @@ -314,7 +340,7 @@ - +
@@ -346,11 +372,11 @@ menu - + - + diff --git a/src/gui/meson.build b/src/gui/meson.build index 9faf7573..55599ef3 100644 --- a/src/gui/meson.build +++ b/src/gui/meson.build @@ -16,6 +16,7 @@ built_sources += gnome.compile_resources( sources += files( 'gcal-application.c', 'gcal-calendar-button.c', + 'gcal-drop-overlay.c', 'gcal-event-popover.c', 'gcal-event-widget.c', 'gcal-meeting-row.c', diff --git a/src/theme/style.css b/src/theme/style.css index 18da6ec0..90e2401c 100644 --- a/src/theme/style.css +++ b/src/theme/style.css @@ -483,3 +483,24 @@ menubutton stack > box { syncindicator { padding: 6px; } + +/* + * Styles for the drop overlay. + * From Amberol https://gitlab.gnome.org/World/amberol/-/blob/0dd6250ffd294b14532c428dcb76ae42086a5923/src/gtk/style.css + */ + +/* Removes the default style */ +.drop-target:drop(active) { + box-shadow: none; +} + +/* Custom style for the drop overlay area */ +.drop-overlay-area { + padding: 32px; + color: var(--accent-fg-color); + background-color: alpha(var(--accent-bg-color), 0.5); +} + +.blurred { + filter: blur(6px); +}