/* gcal-new-calendar-page.c * * Copyright 2019 Georges Basile Stavracas Neto * * 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 */ #define G_LOG_DOMAIN "GcalNewCalendarPage" #include "config.h" #include #include "gcal-context.h" #include "gcal-calendar-management-page.h" #include "gcal-debug.h" #include "gcal-new-calendar-page.h" #include "gcal-source-discoverer.h" #include "gcal-utils.h" #define ENTRY_PROGRESS_TIMEOUT 100 // ms typedef enum { ENTRY_STATE_EMPTY, ENTRY_STATE_VALIDATING, ENTRY_STATE_VALID, ENTRY_STATE_INVALID } EntryState; struct _GcalNewCalendarPage { AdwNavigationPage parent; GtkWidget *add_button; GtkEntry *calendar_address_entry; EntryState calendar_address_entry_state; AdwMessageDialog *credentials_dialog; GtkEntry *credentials_password_entry; GtkEntry *credentials_user_entry; GtkColorDialogButton *local_calendar_color_button; AdwEntryRow *local_calendar_name_row; GtkWidget *web_sources_listbox; GtkWidget *web_sources_revealer; guint calendar_address_id; GPtrArray *remote_sources; guint validate_url_resource_id; GCancellable *cancellable; ESource *local_source; GcalContext *context; }; static gboolean pulse_web_entry (gpointer data); static void sources_discovered_cb (GObject *source_object, GAsyncResult *result, gpointer user_data); static void cancel_validation_discover (GcalNewCalendarPage *self); static void gcal_calendar_management_page_iface_init (GcalCalendarManagementPageInterface *iface); G_DEFINE_TYPE_WITH_CODE (GcalNewCalendarPage, gcal_new_calendar_page, ADW_TYPE_NAVIGATION_PAGE, G_IMPLEMENT_INTERFACE (GCAL_TYPE_CALENDAR_MANAGEMENT_PAGE, gcal_calendar_management_page_iface_init)) enum { PROP_0, PROP_CONTEXT, N_PROPS }; /* * Auxiliary methods */ static gchar* calendar_path_to_name_suggestion (GFile *file) { g_autofree gchar *unencoded_basename = NULL; g_autofree gchar *basename = NULL; gchar *ext; guint i; const gchar* import_file_extensions[] = { ".ical", ".ics", ".ifb", ".icalendar", ".vcs" }; g_return_val_if_fail (G_IS_FILE (file), NULL); unencoded_basename = g_file_get_basename (file); basename = g_filename_display_name (unencoded_basename); ext = strrchr (basename, '.'); if (!ext) return NULL; for (i = 0; i < G_N_ELEMENTS(import_file_extensions); i++) { if (g_ascii_strcasecmp (import_file_extensions[i], ext) == 0) { *ext = '\0'; break; } } g_strdelimit (basename, "-_", ' '); return g_steal_pointer (&basename); } static void update_add_button (GcalNewCalendarPage *self) { g_autofree gchar *add_button_label = NULL; gboolean valid; uint32_t n_calendars; valid = (self->local_source != NULL || self->remote_sources != NULL) && self->calendar_address_entry_state != ENTRY_STATE_VALIDATING && self->calendar_address_entry_state != ENTRY_STATE_INVALID; gtk_widget_set_sensitive (self->add_button, valid); n_calendars = 0; if (self->remote_sources) n_calendars += self->remote_sources->len; else if (self->local_source) n_calendars++; if (n_calendars > 0) { add_button_label = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "Add Calendar", "Add %1$u Calendars", n_calendars), n_calendars); } else { add_button_label = g_strdup (C_("button", "Add Calendar")); } gtk_button_set_label (GTK_BUTTON (self->add_button), add_button_label); } static void update_local_source (GcalNewCalendarPage *self) { g_autofree gchar *calendar_name = NULL; g_clear_object (&self->local_source); calendar_name = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->local_calendar_name_row))); calendar_name = g_strstrip (calendar_name); if (calendar_name && g_utf8_strlen (calendar_name, -1) > 0) { g_autofree gchar *color_string = NULL; ESourceExtension *ext; const GdkRGBA *color; ESource *source; color = gtk_color_dialog_button_get_rgba (self->local_calendar_color_button); color_string = gdk_rgba_to_string (color); /* Create the new source and add the needed extensions */ source = e_source_new (NULL, NULL, NULL); e_source_set_parent (source, "local-stub"); e_source_set_display_name (source, calendar_name); ext = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "local"); e_source_selectable_set_color (E_SOURCE_SELECTABLE (ext), color_string); e_source_backend_set_backend_name (E_SOURCE_BACKEND (ext), "local"); self->local_source = source; } update_add_button (self); } static void toggle_url_entry_pulsing (GcalNewCalendarPage *self, gboolean pulsing) { if (pulsing && self->calendar_address_id == 0) { gtk_entry_set_progress_fraction (self->calendar_address_entry, 0.1); self->calendar_address_id = g_timeout_add (ENTRY_PROGRESS_TIMEOUT, pulse_web_entry, self); } else if (!pulsing && self->calendar_address_id != 0) { gtk_entry_set_progress_fraction (self->calendar_address_entry, 0.0); g_clear_handle_id (&self->calendar_address_id, g_source_remove); } } static void update_url_entry_state (GcalNewCalendarPage *self, EntryState state, const gchar *error_message) { self->calendar_address_entry_state = state; if (state == ENTRY_STATE_INVALID) { gtk_widget_add_css_class (GTK_WIDGET (self->calendar_address_entry), "error"); if (error_message) { g_autofree gchar *multiline_message = NULL; g_auto(GStrv) split_message = NULL; split_message = g_strsplit (error_message, ": ", -1); multiline_message = g_strjoinv (":\n", split_message); gtk_entry_set_icon_from_icon_name (self->calendar_address_entry, GTK_ENTRY_ICON_SECONDARY, "dialog-error"); gtk_entry_set_icon_tooltip_text (self->calendar_address_entry, GTK_ENTRY_ICON_SECONDARY, multiline_message); } } else { gtk_widget_remove_css_class (GTK_WIDGET (self->calendar_address_entry), "error"); gtk_entry_set_icon_from_icon_name (self->calendar_address_entry, GTK_ENTRY_ICON_SECONDARY, NULL); gtk_entry_set_icon_tooltip_text (self->calendar_address_entry, GTK_ENTRY_ICON_SECONDARY, NULL); } update_add_button (self); toggle_url_entry_pulsing (self, state == ENTRY_STATE_VALIDATING); } static void discover_sources (GcalNewCalendarPage *self) { const gchar *username; const gchar *password; const gchar *url; GCAL_ENTRY; self->validate_url_resource_id = 0; url = gtk_editable_get_text (GTK_EDITABLE (self->calendar_address_entry)); username = gtk_editable_get_text (GTK_EDITABLE (self->credentials_user_entry)); password = gtk_editable_get_text (GTK_EDITABLE (self->credentials_password_entry)); gcal_discover_sources_from_uri (url, username, password, self->cancellable, sources_discovered_cb, self); update_url_entry_state (self, ENTRY_STATE_VALIDATING, NULL); GCAL_EXIT; } /* * Callbacks */ static gboolean pulse_web_entry (gpointer data) { GcalNewCalendarPage *self = data; gtk_entry_progress_pulse (self->calendar_address_entry); return G_SOURCE_CONTINUE; } static void sources_discovered_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GcalNewCalendarPage *self; g_autoptr (GPtrArray) sources = NULL; g_autoptr (GError) error = NULL; GCAL_ENTRY; self = GCAL_NEW_CALENDAR_PAGE (user_data); sources = gcal_discover_sources_from_uri_finish (result, &error); if (error) { if (g_error_matches (error, GCAL_SOURCE_DISCOVERER_ERROR, GCAL_SOURCE_DISCOVERER_ERROR_UNAUTHORIZED)) { GtkRoot *root; g_debug ("Unauthorized, asking for credentials..."); root = gtk_widget_get_root (GTK_WIDGET (self)); gtk_window_set_transient_for (GTK_WINDOW (self->credentials_dialog), GTK_WINDOW (root)); gtk_window_present (GTK_WINDOW (self->credentials_dialog)); } /* * If the operation was cancelled by the user, no error message * or error style class will be applied */ else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_warning ("Error finding sources: %s", error->message); update_url_entry_state (self, ENTRY_STATE_INVALID, error->message); } GCAL_RETURN (); } g_debug ("Found %u sources", sources->len); self->remote_sources = g_steal_pointer (&sources); update_url_entry_state (self, ENTRY_STATE_VALID, NULL); GCAL_EXIT; } static gboolean validate_url_cb (gpointer data) { GcalNewCalendarPage *self = data; g_autoptr (GUri) guri = NULL; GCAL_ENTRY; update_url_entry_state (self, ENTRY_STATE_VALIDATING, NULL); self->validate_url_resource_id = 0; guri = g_uri_parse (gtk_editable_get_text (GTK_EDITABLE (self->calendar_address_entry)), SOUP_HTTP_URI_FLAGS | G_URI_FLAGS_PARSE_RELAXED, NULL); if (guri != NULL && g_uri_get_scheme (guri) && g_uri_get_host (guri)) { discover_sources (self); } else { update_url_entry_state (self, ENTRY_STATE_INVALID, _("The URL you have entered appears to be invalid.")); g_debug ("Invalid URL passed"); } GCAL_RETURN (G_SOURCE_REMOVE); } static void on_add_button_clicked_cb (GtkWidget *button, GcalNewCalendarPage *self) { GcalManager *manager; manager = gcal_context_get_manager (self->context); /* Commit each new remote source */ if (self->remote_sources) { guint i; for (i = 0; i < self->remote_sources->len; i++) gcal_manager_save_source (manager, g_ptr_array_index (self->remote_sources, i)); g_clear_pointer (&self->remote_sources, g_ptr_array_unref); } else { gcal_manager_save_source (manager, self->local_source); } gcal_calendar_management_page_switch_page (GCAL_CALENDAR_MANAGEMENT_PAGE (self), "calendars", NULL); } static void on_calendar_address_activated_cb (GtkEntry *entry, GcalNewCalendarPage *self) { cancel_validation_discover (self); validate_url_cb (self); } static void on_credentials_dialog_response_cb (AdwMessageDialog *dialog, const gchar *response, GcalNewCalendarPage *self) { if (g_str_equal (response, "connect")) discover_sources (self); else if (g_str_equal (response, "cancel")) update_url_entry_state (self, ENTRY_STATE_INVALID, _("A valid user and password are required to connect to this calendar.")); gtk_editable_set_text (GTK_EDITABLE (self->credentials_user_entry), ""); gtk_editable_set_text (GTK_EDITABLE (self->credentials_password_entry), ""); } static void cancel_validation_discover (GcalNewCalendarPage *self) { if (self->calendar_address_id != 0) { gtk_entry_set_progress_fraction (self->calendar_address_entry, 0); g_clear_handle_id (&self->calendar_address_id, g_source_remove); } /* Cleanup previous remote sources */ g_clear_pointer (&self->remote_sources, g_ptr_array_unref); if (self->validate_url_resource_id != 0) g_clear_handle_id (&self->validate_url_resource_id, g_source_remove); /* Cancel a validation in case it is on-going */ if (self->calendar_address_entry_state == ENTRY_STATE_VALIDATING) { g_cancellable_cancel (self->cancellable); g_clear_object (&self->cancellable); self->cancellable = g_cancellable_new (); } } static void on_url_entry_text_changed_cb (GtkEntry *entry, GParamSpec *pspec, GcalNewCalendarPage *self) { const gchar *text; GCAL_ENTRY; text = gtk_editable_get_text (GTK_EDITABLE (entry)); cancel_validation_discover (self); if (text && g_utf8_strlen (text, -1) > 0) { /* * Only validate the text after the timeout, as we don't want to bother them * as soon as they type into the entry. */ self->validate_url_resource_id = g_timeout_add (750, validate_url_cb, self); } else { gtk_entry_set_progress_fraction (self->calendar_address_entry, 0); update_url_entry_state (self, ENTRY_STATE_EMPTY, NULL); } GCAL_EXIT; } static void on_local_calendar_name_row_text_changed_cb (AdwEntryRow *entry_row, GParamSpec *pspec, GcalNewCalendarPage *self) { update_local_source (self); } static void on_local_calendar_color_button_rgba_changed_cb (GtkColorChooser *chooser, GParamSpec *pspec, GcalNewCalendarPage *self) { update_local_source (self); } static void on_web_description_label_link_activated_cb (GtkLabel *label, gchar *uri, GcalNewCalendarPage *self) { GDBusConnection *connection = g_application_get_dbus_connection (g_application_get_default ()); gcal_utils_launch_gnome_settings (connection, "online-accounts", NULL); } /* * GcalCalendarManagementPage iface */ static void gcal_new_calendar_page_deactivate (GcalCalendarManagementPage *page) { GcalNewCalendarPage *self; GCAL_ENTRY; self = GCAL_NEW_CALENDAR_PAGE (page); g_clear_object (&self->local_source); g_clear_pointer (&self->remote_sources, g_ptr_array_unref); update_add_button (self); gtk_editable_set_text (GTK_EDITABLE (self->local_calendar_name_row), ""); gtk_editable_set_text (GTK_EDITABLE (self->calendar_address_entry), ""); toggle_url_entry_pulsing (self, FALSE); GCAL_EXIT; } static void gcal_calendar_management_page_iface_init (GcalCalendarManagementPageInterface *iface) { iface->deactivate = gcal_new_calendar_page_deactivate; } /* * GObject overrides */ static void gcal_new_calendar_page_dispose (GObject *object) { GcalNewCalendarPage *self = (GcalNewCalendarPage *)object; gtk_widget_dispose_template (GTK_WIDGET (self), GCAL_TYPE_NEW_CALENDAR_PAGE); G_OBJECT_CLASS (gcal_new_calendar_page_parent_class)->dispose (object); } static void gcal_new_calendar_page_finalize (GObject *object) { GcalNewCalendarPage *self = (GcalNewCalendarPage *)object; g_clear_handle_id (&self->calendar_address_id, g_source_remove); g_clear_handle_id (&self->validate_url_resource_id, g_source_remove); g_cancellable_cancel (self->cancellable); g_clear_object (&self->cancellable); g_clear_object (&self->context); G_OBJECT_CLASS (gcal_new_calendar_page_parent_class)->finalize (object); } static void gcal_new_calendar_page_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GcalNewCalendarPage *self = GCAL_NEW_CALENDAR_PAGE (object); switch (prop_id) { case PROP_CONTEXT: g_value_set_object (value, self->context); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gcal_new_calendar_page_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GcalNewCalendarPage *self = GCAL_NEW_CALENDAR_PAGE (object); switch (prop_id) { case PROP_CONTEXT: self->context = g_value_dup_object (value); g_assert (self->context != NULL); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void gcal_new_calendar_page_class_init (GcalNewCalendarPageClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->dispose = gcal_new_calendar_page_dispose; object_class->finalize = gcal_new_calendar_page_finalize; object_class->get_property = gcal_new_calendar_page_get_property; object_class->set_property = gcal_new_calendar_page_set_property; g_object_class_override_property (object_class, PROP_CONTEXT, "context"); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/calendar-management/gcal-new-calendar-page.ui"); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, add_button); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, calendar_address_entry); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, credentials_dialog); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, credentials_password_entry); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, credentials_user_entry); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, local_calendar_color_button); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, local_calendar_name_row); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, web_sources_listbox); gtk_widget_class_bind_template_child (widget_class, GcalNewCalendarPage, web_sources_revealer); gtk_widget_class_bind_template_callback (widget_class, on_add_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, on_calendar_address_activated_cb); gtk_widget_class_bind_template_callback (widget_class, on_credentials_dialog_response_cb); gtk_widget_class_bind_template_callback (widget_class, on_local_calendar_name_row_text_changed_cb); gtk_widget_class_bind_template_callback (widget_class, on_local_calendar_color_button_rgba_changed_cb); gtk_widget_class_bind_template_callback (widget_class, on_url_entry_text_changed_cb); gtk_widget_class_bind_template_callback (widget_class, on_web_description_label_link_activated_cb); } static void gcal_new_calendar_page_init (GcalNewCalendarPage *self) { self->cancellable = g_cancellable_new (); gtk_widget_init_template (GTK_WIDGET (self)); }