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); +}