diff --git a/po/POTFILES.in b/po/POTFILES.in index a11aa923..3a9d9fc5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -17,6 +17,12 @@ src/gui/common/gcal-calendar-row.c src/gui/common/gcal-date-chooser.c src/gui/event-editor/gcal-alarm-row.blp src/gui/event-editor/gcal-alarm-row.c +src/gui/event-editor/gcal-attendee-row.blp +src/gui/event-editor/gcal-attendee-row.c +src/gui/event-editor/gcal-attendee-details-page.blp +src/gui/event-editor/gcal-attendee-details-page.c +src/gui/event-editor/gcal-attendees-section.blp +src/gui/event-editor/gcal-attendees-section.c src/gui/event-editor/gcal-date-chooser-row.blp src/gui/event-editor/gcal-date-time-chooser.blp src/gui/event-editor/gcal-date-time-chooser.c diff --git a/src/gui/event-editor/event-editor.gresource.xml b/src/gui/event-editor/event-editor.gresource.xml index e32da094..92ed8298 100644 --- a/src/gui/event-editor/event-editor.gresource.xml +++ b/src/gui/event-editor/event-editor.gresource.xml @@ -13,5 +13,8 @@ gcal-summary-section.ui gcal-time-zone-dialog.ui gcal-time-zone-dialog-row.ui + gcal-attendees-section.ui + gcal-attendee-row.ui + gcal-attendee-details-page.ui diff --git a/src/gui/event-editor/gcal-attendee-details-page.blp b/src/gui/event-editor/gcal-attendee-details-page.blp new file mode 100644 index 00000000..1826007b --- /dev/null +++ b/src/gui/event-editor/gcal-attendee-details-page.blp @@ -0,0 +1,81 @@ +using Gtk 4.0; +using Adw 1; + +template $GcalAttendeeDetailsPage: Adw.NavigationPage { + title: _("Participants"); + + child: Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-end-title-buttons: false; + show-start-title-buttons: true; + } + + content: Adw.PreferencesPage { + Adw.PreferencesGroup { + title: _("Accepted"); + visible: bind template.model-accepted as .n-items; + + ListBox list_accepted { + selection-mode: none; + + styles [ + "boxed-list", + ] + } + } + + Adw.PreferencesGroup { + title: _("Tentative"); + visible: bind template.model-tentative as .n-items; + + ListBox list_tentative { + selection-mode: none; + + styles [ + "boxed-list", + ] + } + } + + Adw.PreferencesGroup { + title: _("Declined"); + visible: bind template.model-declined as .n-items; + + ListBox list_declined { + selection-mode: none; + + styles [ + "boxed-list", + ] + } + } + + Adw.PreferencesGroup { + title: _("Delegated"); + visible: bind template.model-delegated as .n-items; + + ListBox list_delegated { + selection-mode: none; + + styles [ + "boxed-list", + ] + } + } + + Adw.PreferencesGroup { + title: _("Not responded"); + visible: bind template.model-not-responded as .n-items; + + ListBox list_not_responded { + selection-mode: none; + + styles [ + "boxed-list", + ] + } + } + }; + }; +} diff --git a/src/gui/event-editor/gcal-attendee-details-page.c b/src/gui/event-editor/gcal-attendee-details-page.c new file mode 100644 index 00000000..effbce3b --- /dev/null +++ b/src/gui/event-editor/gcal-attendee-details-page.c @@ -0,0 +1,353 @@ +/* gcal-attendee-details-page.c + * + * Copyright (C) 2025 Alen Galinec + * + * 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 . + */ + +#include "gcal-attendee-details-page.h" +#include "gcal-attendee-row.h" +#include "gcal-enums.h" +#include "gcal-event-attendee.h" +#include "gcal-event.h" + +#include +#include +#include +#include + +struct _GcalAttendeeDetailsPage +{ + AdwNavigationPage parent_instance; + + GcalEvent *event; + GListStore *attendees; + + GtkFilterListModel *filter_list_accepted; + GtkFilterListModel *filter_list_tentative; + GtkFilterListModel *filter_list_declined; + GtkFilterListModel *filter_list_delegated; + GtkFilterListModel *filter_list_not_responded; + + GtkCustomFilter *accepted_filter; + GtkCustomFilter *tentative_filter; + GtkCustomFilter *declined_filter; + GtkCustomFilter *delegated_filter; + GtkCustomFilter *not_responded_filter; + + GtkWidget *list_accepted; + GtkWidget *list_tentative; + GtkWidget *list_declined; + GtkWidget *list_delegated; + GtkWidget *list_not_responded; +}; + +enum +{ + PROP_MODEL_ACCEPTED = 1, + PROP_MODEL_TENTATIVE, + PROP_MODEL_DECLINED, + PROP_MODEL_DELEGATED, + PROP_MODEL_NOT_RESPONDED, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS] = { NULL, }; + +G_DEFINE_FINAL_TYPE (GcalAttendeeDetailsPage, gcal_attendee_details_page, ADW_TYPE_NAVIGATION_PAGE) + +static void +gcal_attendee_details_page_clear_list (GcalAttendeeDetailsPage *self) +{ + g_return_if_fail (GCAL_IS_ATTENDEE_DETAILS_PAGE (self)); + + g_list_store_remove_all (self->attendees); +} + +static GtkWidget * +create_attendee_row_widget (void *attendee, + void *user_data) +{ + return gcal_attendee_row_new (attendee); +} + +static void +gcal_attendee_details_page_finalize (GObject *object) +{ + GcalAttendeeDetailsPage *page = (GcalAttendeeDetailsPage *) object; + + g_clear_pointer (&page->attendees, g_list_store_remove_all); + + g_clear_object (&page->accepted_filter); + g_clear_object (&page->tentative_filter); + g_clear_object (&page->declined_filter); + g_clear_object (&page->delegated_filter); + g_clear_object (&page->not_responded_filter); + + /* g_clear_pointer on these would try to free self->attendees again */ + page->filter_list_accepted = NULL; + page->filter_list_tentative = NULL; + page->filter_list_declined = NULL; + page->filter_list_delegated = NULL; + page->filter_list_not_responded = NULL; +} + +static gboolean +custom_accepted_filter_cb (GcalEventAttendee *attendee, + gpointer data) +{ + return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_ACCEPTED; +} + +static gboolean +custom_tentative_filter_cb (GcalEventAttendee *attendee, + gpointer data) +{ + return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_TENTATIVE; +} + +static gboolean +custom_declined_filter_cb (GcalEventAttendee *attendee, + gpointer data) +{ + return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_DECLINED; +} + +static gboolean +custom_delegated_filter_cb (GcalEventAttendee *attendee, + gpointer data) +{ + return gcal_event_attendee_get_part_status (attendee) == GCAL_EVENT_ATTENDEE_PART_DELEGATED; +} + +static gboolean +custom_not_responded_filter_cb (GcalEventAttendee *attendee, + gpointer data) +{ + switch (gcal_event_attendee_get_part_status (attendee)) + { + case GCAL_EVENT_ATTENDEE_PART_NONE: + case GCAL_EVENT_ATTENDEE_PART_NEEDS_ACTION: + return TRUE; + default: + return FALSE; + } +} + +static void +gcal_attendee_details_page_init_filters (GcalAttendeeDetailsPage *self) +{ + self->accepted_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_accepted_filter_cb, NULL, NULL); + self->tentative_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_tentative_filter_cb, NULL, NULL); + self->declined_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_declined_filter_cb, NULL, NULL); + self->delegated_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_delegated_filter_cb, NULL, NULL); + self->not_responded_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) custom_not_responded_filter_cb, NULL, NULL); +} + +static void +gcal_attendee_details_page_init_filter_models (GcalAttendeeDetailsPage *self) +{ + self->filter_list_accepted = + gtk_filter_list_model_new (G_LIST_MODEL (self->attendees), + GTK_FILTER (self->accepted_filter)); + + self->filter_list_tentative = + gtk_filter_list_model_new (G_LIST_MODEL (self->attendees), + GTK_FILTER (self->tentative_filter)); + + self->filter_list_declined = + gtk_filter_list_model_new (G_LIST_MODEL (self->attendees), + GTK_FILTER (self->declined_filter)); + + self->filter_list_delegated = + gtk_filter_list_model_new (G_LIST_MODEL (self->attendees), + GTK_FILTER (self->delegated_filter)); + + self->filter_list_not_responded = + gtk_filter_list_model_new (G_LIST_MODEL (self->attendees), + GTK_FILTER (self->not_responded_filter)); +} + +static void +gcal_attendee_details_page_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GcalAttendeeDetailsPage *self = GCAL_ATTENDEE_DETAILS_PAGE (object); + + switch (property_id) + { + case PROP_MODEL_ACCEPTED: + g_value_set_object (value, self->filter_list_accepted); + break; + case PROP_MODEL_TENTATIVE: + g_value_set_object (value, self->filter_list_tentative); + break; + case PROP_MODEL_DECLINED: + g_value_set_object (value, self->filter_list_declined); + break; + case PROP_MODEL_DELEGATED: + g_value_set_object (value, self->filter_list_delegated); + break; + case PROP_MODEL_NOT_RESPONDED: + g_value_set_object (value, self->filter_list_not_responded); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gcal_attendee_details_page_init (GcalAttendeeDetailsPage *instance) +{ + g_return_if_fail (GCAL_IS_ATTENDEE_DETAILS_PAGE (instance)); + + instance->attendees = g_list_store_new (GCAL_TYPE_EVENT_ATTENDEE); + gcal_attendee_details_page_init_filters (instance); + gcal_attendee_details_page_init_filter_models (instance); + + gtk_widget_init_template (GTK_WIDGET (instance)); + + gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_accepted), + G_LIST_MODEL (instance->filter_list_accepted), + create_attendee_row_widget, + NULL, NULL); + + gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_tentative), + G_LIST_MODEL (instance->filter_list_tentative), + create_attendee_row_widget, + NULL, NULL); + + gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_declined), + G_LIST_MODEL (instance->filter_list_declined), + create_attendee_row_widget, + NULL, NULL); + + gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_delegated), + G_LIST_MODEL (instance->filter_list_delegated), + create_attendee_row_widget, + NULL, NULL); + + gtk_list_box_bind_model (GTK_LIST_BOX (instance->list_not_responded), + G_LIST_MODEL (instance->filter_list_not_responded), + create_attendee_row_widget, + NULL, NULL); +} + +static void +gcal_attendee_details_page_class_init (GcalAttendeeDetailsPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + /* object */ + + object_class->finalize = gcal_attendee_details_page_finalize; + object_class->get_property = gcal_attendee_details_page_get_property; + + /** + * GcalAttendeeDetailsPage:model-accepted: + * + * Binding model for the list of attendees with "accepted" response. + */ + properties[PROP_MODEL_ACCEPTED] = + g_param_spec_object ("model-accepted", NULL, NULL, + GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeeDetailsPage:model-tentative: + * + * Binding model for the list of attendees with "tentative" (aka maybe) response. + */ + properties[PROP_MODEL_TENTATIVE] = + g_param_spec_object ("model-tentative", NULL, NULL, + GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeeDetailsPage:model-declined: + * + * Binding model for the list of attendees with "declined" response. + */ + properties[PROP_MODEL_DECLINED] = + g_param_spec_object ("model-declined", NULL, NULL, + GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeeDetailsPage:model-delegated: + * + * Binding model for the list of attendees with "delegated" response. + * Lists the attendees who delegated their attendance to someone else. + */ + properties[PROP_MODEL_DELEGATED] = + g_param_spec_object ("model-delegated", NULL, NULL, + GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeeDetailsPage:model-not-responded: + * + * Binding model for the list of attendees who haven't responded yet + * or thei response type is unknown. + */ + properties[PROP_MODEL_NOT_RESPONDED] = + g_param_spec_object ("model-not-responded", NULL, NULL, + GTK_TYPE_FILTER_LIST_MODEL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /* widget */ + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendee-details-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_accepted); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_tentative); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_declined); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_delegated); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeDetailsPage, list_not_responded); +} + +/** + * gcal_attendee_details_page_set_event: + * @self: a #GcalAttendeeDetailsPage instance. + * @event: a #GcalEvent to use for this page. + * + * Sets the event for this page in a similar fashion to the #GcalEventEditorSection. + * Because this page is a reusable widget, the old event reference is replaced and + * the contents of this widget are reset while properly cleaning up resources. + */ +void +gcal_attendee_details_page_set_event (GcalAttendeeDetailsPage *self, + GcalEvent *event) +{ + g_return_if_fail (GCAL_IS_ATTENDEE_DETAILS_PAGE (self)); + + if (self->event == event) + return; + + gcal_attendee_details_page_clear_list (self); + + g_set_object (&self->event, event); + + if (self->event == NULL) + return; + + g_autoptr (GSList) attendees = NULL; + g_autoptr (GSList) iter = NULL; + + attendees = gcal_event_get_attendees (self->event); + for (iter = attendees; iter != NULL; iter = iter->next) + { + g_list_store_append (self->attendees, iter->data); + } +} diff --git a/src/gui/event-editor/gcal-attendee-details-page.h b/src/gui/event-editor/gcal-attendee-details-page.h new file mode 100644 index 00000000..423cdac5 --- /dev/null +++ b/src/gui/event-editor/gcal-attendee-details-page.h @@ -0,0 +1,36 @@ +/* gcal-attendee-details-page.c + * + * Copyright (C) 2025 Alen Galinec + * + * 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 . + */ + +#ifndef GCAL_ATTENDEE_DETAILS_PAGE_H +#define GCAL_ATTENDEE_DETAILS_PAGE_H + +#include + +#include "gcal-event.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_ATTENDEE_DETAILS_PAGE (gcal_attendee_details_page_get_type ()) +G_DECLARE_FINAL_TYPE (GcalAttendeeDetailsPage, gcal_attendee_details_page, GCAL, ATTENDEE_DETAILS_PAGE, AdwNavigationPage) + +void gcal_attendee_details_page_set_event (GcalAttendeeDetailsPage *self, + GcalEvent *event); + +G_END_DECLS + +#endif diff --git a/src/gui/event-editor/gcal-attendee-row.blp b/src/gui/event-editor/gcal-attendee-row.blp new file mode 100644 index 00000000..eff499b4 --- /dev/null +++ b/src/gui/event-editor/gcal-attendee-row.blp @@ -0,0 +1,32 @@ +using Gtk 4.0; +using Adw 1; + +template $GcalAttendeeRow: Adw.ActionRow { + GestureClick gesture_click { + button: 3; + } + + [suffix] + Box suffixes { + orientation: horizontal; + + Label role { + styles [ + "dimmed", + ] + } + } +} + +PopoverMenu context_menu { + menu-model: context_menu_model; + has-arrow: false; + halign: start; +} + +menu context_menu_model { + item { + label: _("Copy email"); + action: "attendee-row.copy-email"; + } +} diff --git a/src/gui/event-editor/gcal-attendee-row.c b/src/gui/event-editor/gcal-attendee-row.c new file mode 100644 index 00000000..6b76a665 --- /dev/null +++ b/src/gui/event-editor/gcal-attendee-row.c @@ -0,0 +1,278 @@ +/* gcal-attendee-row.c + * + * Copyright (C) 2025 Alen Galinec + * + * 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 . + */ + +#include "gcal-attendee-row.h" +#include "gcal-enums.h" +#include "gcal-event-attendee.h" +#include "gcal-utils.h" + +#include +#include +#include +#include +#include + +struct _GcalAttendeeRow +{ + AdwActionRow parent_instance; + + GcalEventAttendee *attendee; + + GtkWidget *role; + GtkWidget *context_menu; + GtkGestureClick *gesture_click; + + GSimpleActionGroup *action_group; +}; + +G_DEFINE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, ADW_TYPE_ACTION_ROW); + +enum +{ + PROP_0, + PROP_ATTENDEE, + N_PROPS, +}; + +static GParamSpec *properties[N_PROPS]; + +/* Aux functions */ + +static void +on_copy_email_cb (GSimpleAction *action, + GVariant *param, + gpointer user_data) +{ + GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (user_data); + + GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (self)); + GdkClipboard *clipboard = gdk_display_get_clipboard (display); + + g_autofree const gchar *email = + gcal_get_email_from_mailto_uri (gcal_event_attendee_get_uri (self->attendee)); + + gdk_clipboard_set_text (clipboard, email); +} + +static void +show_context_menu (GcalAttendeeRow *self, + guint npress, + double x, + double y) +{ + g_return_if_fail (GCAL_IS_ATTENDEE_ROW (self)); + + const GdkRectangle position = { .x = x, .y = y }; + gtk_popover_set_pointing_to (GTK_POPOVER (self->context_menu), &position); + gtk_popover_popup (GTK_POPOVER (self->context_menu)); +} + +static void +setup_row_title_and_subtitle (GcalAttendeeRow *self) +{ + g_autoptr (GString) title = NULL; + g_autoptr (GString) subtitle = NULL; + + /* this is the initial line for both cases */ + title = g_string_new (gcal_event_attendee_get_name (self->attendee)); + g_autofree const gchar *delegator_email = gcal_get_email_from_mailto_uri (gcal_event_attendee_get_uri (self->attendee)); + + if (gcal_event_attendee_get_part_status (self->attendee) != GCAL_EVENT_ATTENDEE_PART_DELEGATED) + { + /* show subtitle only when the name != email */ + if (g_strcmp0 (gcal_event_attendee_get_name (self->attendee), delegator_email) != 0) + subtitle = g_string_new (delegator_email ? delegator_email : ""); + } + else + { + /* append the delegator email to the title */ + if (delegator_email) + g_string_append_printf (title, " (%s)", delegator_email); + + g_autofree const gchar *delegated_to = gcal_get_email_from_mailto_uri (gcal_event_attendee_get_delegated_to (self->attendee)); + subtitle = g_string_new (gettext ("Delegated to: ")); + g_string_append (subtitle, delegated_to); + } + + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self), title->str); + adw_action_row_set_subtitle (ADW_ACTION_ROW (self), subtitle ? subtitle->str : NULL); +} + +static void +setup_row_content (GcalAttendeeRow *self) +{ + setup_row_title_and_subtitle (self); + + /* set role */ + if (gcal_event_attendee_get_role (self->attendee) == GCAL_EVENT_ATTENDEE_ROLE_OPTIONAL) + { + gtk_label_set_label (GTK_LABEL (self->role), gettext ("optional")); + } + else + { + /* other types don't need extra visual hints */ + gtk_label_set_label (GTK_LABEL (self->role), NULL); + } +} + +static void +gcal_attendee_row_finalize (GObject *object) +{ + GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object); + + g_clear_object (&self->gesture_click); + g_clear_object (&self->action_group); + + G_OBJECT_CLASS (gcal_attendee_row_parent_class)->finalize (object); +} + +static void +gcal_attendee_row_dispose (GObject *object) +{ + GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object); + g_clear_pointer (&self->context_menu, gtk_widget_unparent); + + G_OBJECT_CLASS (gcal_attendee_row_parent_class)->dispose (object); +} + +static void +gcal_attendee_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object); + + switch (property_id) + { + case PROP_ATTENDEE: + g_assert (self->attendee == NULL); + self->attendee = g_value_get_object (value); + setup_row_content (self); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gcal_attendee_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GcalAttendeeRow *self = GCAL_ATTENDEE_ROW (object); + + switch (property_id) + { + case PROP_ATTENDEE: + g_value_set_object (value, self->attendee); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gcal_attendee_row_class_init (GcalAttendeeRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = gcal_attendee_row_set_property; + object_class->get_property = gcal_attendee_row_get_property; + object_class->dispose = gcal_attendee_row_dispose; + object_class->finalize = gcal_attendee_row_finalize; + + /** + * GcalAttendeeRow:attendee: + * + * A reference to a #GcalEventAttendee for which this row is displayed. + */ + properties[PROP_ATTENDEE] = + g_param_spec_object ("attendee", NULL, NULL, + GCAL_TYPE_EVENT_ATTENDEE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendee-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, context_menu); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, gesture_click); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, role); +} + +static void +gcal_attendee_row_init (GcalAttendeeRow *instance) +{ + static const GActionEntry actions[] = { + { "copy-email", on_copy_email_cb } + }; + + gtk_widget_init_template (GTK_WIDGET (instance)); + + gtk_widget_set_parent (GTK_WIDGET (instance->context_menu), GTK_WIDGET (instance)); + + instance->action_group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (instance->action_group), + actions, + G_N_ELEMENTS (actions), + instance); + + gtk_widget_insert_action_group (instance->context_menu, + "attendee-row", + G_ACTION_GROUP (instance->action_group)); + + g_signal_connect_swapped (instance->gesture_click, "pressed", G_CALLBACK (show_context_menu), instance); +} + + +/** + * gcal_attendee_row_new: + * @attendee: an instance of #GcalEventAttendee to display. + * + * Creates a new #GcalAttendeeRow widget. + * + * Returns: a new #GcalAttendeeRow widget instance. + */ +GtkWidget * +gcal_attendee_row_new (GcalEventAttendee *attendee) +{ + g_return_val_if_fail (GCAL_IS_EVENT_ATTENDEE (attendee), NULL); + + return g_object_new (GCAL_TYPE_ATTENDEE_ROW, + "attendee", attendee, + NULL); +} + +/** + * gcal_attendee_row_get_attendee: + * @self: a #GcalAttendeeRow + * + * Returns the internally stored reference to a #GcalEventAttendee. + * + * Returns: (transfer none) (nullable): the contained #GcalEventAttendee. + */ +GcalEventAttendee * +gcal_attendee_row_get_attendee (GcalAttendeeRow *self) +{ + g_return_val_if_fail (GCAL_IS_ATTENDEE_ROW (self), NULL); + + return self->attendee; +} diff --git a/src/gui/event-editor/gcal-attendee-row.h b/src/gui/event-editor/gcal-attendee-row.h new file mode 100644 index 00000000..5ebe0cf5 --- /dev/null +++ b/src/gui/event-editor/gcal-attendee-row.h @@ -0,0 +1,33 @@ +/* gcal-attendee-row.h + * + * Copyright (C) 2025 Alen Galinec + * + * 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 . + */ + +#pragma once + +#include + +#include "gcal-event-attendee.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_ATTENDEE_ROW (gcal_attendee_row_get_type ()) +G_DECLARE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, GCAL, ATTENDEE_ROW, AdwActionRow) + +GtkWidget *gcal_attendee_row_new (GcalEventAttendee *attendee); +GcalEventAttendee *gcal_attendee_row_get_attendee (GcalAttendeeRow *self); + +G_END_DECLS diff --git a/src/gui/event-editor/gcal-attendees-section.blp b/src/gui/event-editor/gcal-attendees-section.blp new file mode 100644 index 00000000..e4bfdf22 --- /dev/null +++ b/src/gui/event-editor/gcal-attendees-section.blp @@ -0,0 +1,52 @@ +using Gtk 4.0; +using Adw 1; + +template $GcalAttendeesSection: Gtk.Box { + orientation: vertical; + hexpand: true; + valign: start; + + ListBox listbox { + halign: fill; + valign: fill; + selection-mode: none; + + styles [ + "boxed-list", + ] + + Adw.ActionRow organizer_row { + title: _("Organizer"); + + styles [ + "property", + ] + } + + Adw.ActionRow summary_row { + title: _("Participants"); + activatable-widget: open_details; + activated => $on_participant_summary_row_activated_cb(template) not-swapped; + + styles [ + "property", + ] + + [suffix] + Label { + valign: center; + label: bind template.num-participants as ; + tooltip-text: _("Number of participants"); + + styles [ + "participants-count-label", + ] + } + + [suffix] + Image open_details { + icon-name: "go-next-symbolic"; + } + } + } +} diff --git a/src/gui/event-editor/gcal-attendees-section.c b/src/gui/event-editor/gcal-attendees-section.c new file mode 100644 index 00000000..f955d55d --- /dev/null +++ b/src/gui/event-editor/gcal-attendees-section.c @@ -0,0 +1,412 @@ +/* gcal-attendees-section.c + * + * Copyright (C) 2025 Alen Galinec + * + * 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 . + */ + +#include "gcal-attendees-section.h" + +#include +#include +#include + +#include "gcal-context.h" +#include "gcal-enums.h" +#include "gcal-event-attendee.h" +#include "gcal-event-editor-section.h" +#include "gcal-event-organizer.h" +#include "gcal-event.h" +#include "gcal-utils.h" + +struct _GcalAttendeesSection +{ + GtkBox parent_instance; + + GcalContext *context; + GcalEvent *event; + + AdwActionRow *organizer_row; + AdwActionRow *summary_row; + + /* participation summary */ + guint num_accepted; + guint num_declined; + guint num_tentative; + guint num_not_responded; + guint num_delegated; +}; + +static void gcal_event_editor_section_init_iface (GcalEventEditorSectionInterface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (GcalAttendeesSection, gcal_attendees_section, GTK_TYPE_BOX, + G_IMPLEMENT_INTERFACE (GCAL_TYPE_EVENT_EDITOR_SECTION, gcal_event_editor_section_init_iface)) + +enum +{ + PROP_0, + PROP_CONTEXT, + PROP_NUM_ACCEPTED, + PROP_NUM_DECLINED, + PROP_NUM_TENTATIVE, + PROP_NUM_DELEGATED, + PROP_NUM_UNKNOWN, + PROP_NUM_TOTAL, + N_PROPS +}; + +enum +{ + SUMMARY_ROW_ACTIVATED, + N_SIGNALS, +}; + +static guint signals[N_SIGNALS] = { 0, }; +static GParamSpec *properties[N_PROPS] = { NULL, }; + +static void +reset_section (GcalAttendeesSection *self, + GcalEvent *event) +{ + if (event == self->event) + return; + + g_set_object (&self->event, event); + + adw_action_row_set_subtitle (self->organizer_row, NULL); + adw_action_row_set_subtitle (self->summary_row, NULL); + + self->num_accepted = 0; + self->num_declined = 0; + self->num_tentative = 0; + self->num_not_responded = 0; + self->num_delegated = 0; +} + +static inline void +concat_subtitle_part (GStrvBuilder *builder, + guint value, + const gchar *text) +{ + if (value <= 0) + return; + + g_strv_builder_take (builder, g_strdup_printf ("%d %s", value, text)); +} + +static gchar* +build_summary_subtitle (GcalAttendeesSection *self) +{ + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + + concat_subtitle_part (builder, self->num_accepted, ngettext ("accepted", "accepted", self->num_accepted)); + concat_subtitle_part (builder, self->num_tentative, ngettext ("maybe", "maybe", self->num_tentative)); + concat_subtitle_part (builder, self->num_declined, ngettext ("declined", "declined", self->num_declined)); + concat_subtitle_part (builder, self->num_delegated, ngettext ("delegated", "delegated", self->num_delegated)); + concat_subtitle_part (builder, self->num_not_responded, ngettext ("not responded", "not responded", self->num_not_responded)); + + g_auto (GStrv) parts = g_strv_builder_end (builder); + + gchar *result = g_strjoinv (", ", parts); + + return result; +} + +static void +gcal_attendees_section_notify (GcalAttendeesSection *self) +{ + for (guint i = 1; i < N_PROPS; ++i) + g_object_notify_by_pspec (G_OBJECT (self), properties[i]); +} + +static void +gcal_attendees_section_set_event (GcalEventEditorSection *section, + GcalEvent *event, + GcalEventEditorFlags flags) +{ + GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (section); + GcalEventOrganizer *organizer; + g_autoptr (GSList) attendees = NULL; + + reset_section (self, event); + + if (self->event == NULL) + return; + + /* set organizer */ + organizer = gcal_event_get_organizer (self->event); + if (organizer) + { + g_autofree const gchar *email = gcal_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer)); + g_autofree const gchar *name_with_email = g_strdup_printf ("%s (%s)", gcal_event_organizer_get_name (organizer), email); + + adw_action_row_set_subtitle (self->organizer_row, name_with_email); + } + + /* set attendees and summarize attendance status */ + attendees = gcal_event_get_attendees (self->event); + for (GSList *l = attendees; l != NULL; l = l->next) + { + g_autoptr (GcalEventAttendee) attendee = GCAL_EVENT_ATTENDEE (l->data); + + switch (gcal_event_attendee_get_part_status (attendee)) + { + case GCAL_EVENT_ATTENDEE_PART_ACCEPTED: + self->num_accepted++; + break; + + case GCAL_EVENT_ATTENDEE_PART_DECLINED: + self->num_declined++; + break; + + case GCAL_EVENT_ATTENDEE_PART_TENTATIVE: + self->num_tentative++; + break; + + case GCAL_EVENT_ATTENDEE_PART_DELEGATED: + self->num_delegated++; + break; + + default: + self->num_not_responded++; + break; + } + } + + adw_action_row_set_subtitle (self->summary_row, build_summary_subtitle (self)); + + /* notify properties changed */ + gcal_attendees_section_notify (self); +} + +static void +gcal_attendees_section_apply (GcalEventEditorSection *section) +{ + /* read-only */ +} + +static gboolean +gcal_attendees_section_changed (GcalEventEditorSection *section) +{ + /* This section is read-only for now */ + return FALSE; +} + +static void +gcal_event_editor_section_init_iface (GcalEventEditorSectionInterface *iface) +{ + iface->set_event = gcal_attendees_section_set_event; + iface->apply = gcal_attendees_section_apply; + iface->changed = gcal_attendees_section_changed; +} + +static guint +gcal_attendees_section_get_num_participants (GcalAttendeesSection *self) +{ + g_return_val_if_fail (GCAL_IS_ATTENDEES_SECTION (self), 0); + + return self->num_accepted + self->num_declined + self->num_tentative + self->num_delegated + self->num_not_responded; +} + +static void +on_participant_summary_row_activated_cb (AdwActionRow *summary_row, + GcalAttendeesSection *self) +{ + g_signal_emit (self, signals[SUMMARY_ROW_ACTIVATED], 0); +} + +static void +gcal_attendees_section_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (object); + + switch (property_id) + { + case PROP_CONTEXT: + g_value_set_object (value, self->context); + break; + + case PROP_NUM_ACCEPTED: + g_value_set_uint (value, self->num_accepted); + break; + + case PROP_NUM_DECLINED: + g_value_set_uint (value, self->num_declined); + break; + + case PROP_NUM_TENTATIVE: + g_value_set_uint (value, self->num_tentative); + break; + + case PROP_NUM_DELEGATED: + g_value_set_uint (value, self->num_delegated); + break; + + case PROP_NUM_UNKNOWN: + g_value_set_uint (value, self->num_not_responded); + break; + + case PROP_NUM_TOTAL: + g_value_set_uint (value, gcal_attendees_section_get_num_participants (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gcal_attendees_section_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (object); + + switch (property_id) + { + case PROP_CONTEXT: + g_assert (self->context == NULL); + self->context = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gcal_attendees_section_finalize (GObject *object) +{ + GcalAttendeesSection *self = (GcalAttendeesSection *) object; + + g_clear_object (&self->context); + g_clear_object (&self->event); + + G_OBJECT_CLASS (gcal_attendees_section_parent_class)->finalize (object); +} + +static void +gcal_attendees_section_init (GcalAttendeesSection *instance) +{ + gtk_widget_init_template (GTK_WIDGET (instance)); +} + +static void +gcal_attendees_section_class_init (GcalAttendeesSectionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gcal_attendees_section_finalize; + object_class->get_property = gcal_attendees_section_get_property; + object_class->set_property = gcal_attendees_section_set_property; + + /** + * GcalAttendeesSection:context: + * + * The #GcalContext instance. Required by the interface. + */ + properties[PROP_CONTEXT] = + g_param_spec_object ("context", NULL, NULL, + GCAL_TYPE_CONTEXT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeesSection:num-accepted: + * + * Gets the number of attendees who accepted. + */ + properties[PROP_NUM_ACCEPTED] = + g_param_spec_uint ("num-accepted", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeesSection:num-declined: + * + * Gets the number of attendees who declined. + */ + properties[PROP_NUM_DECLINED] = + g_param_spec_uint ("num-declined", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeesSection:num-tentative: + * + * Gets the number of attendees who "maybe" will attend. + */ + properties[PROP_NUM_TENTATIVE] = + g_param_spec_uint ("num-tentative", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeesSection:num-unknown: + * + * Gets the number of attendees with an unknown response. + */ + properties[PROP_NUM_UNKNOWN] = + g_param_spec_uint ("num-unknown", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeesSection: num-delegated: + * + * Gets the number of attendees who delegated their attendance to someone else. + */ + properties[PROP_NUM_DELEGATED] = + g_param_spec_uint ("num-delegated", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * GcalAttendeesSection:num-participants: + * + * Gets the total number of attendees/participants. + */ + properties[PROP_NUM_TOTAL] = + g_param_spec_uint ("num-participants", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + /** + * GcalAttendeesSection::summary-row-activated: + * + * This signal is emitted when the summary row is activated. + */ + signals[SUMMARY_ROW_ACTIVATED] = + g_signal_new ("summary-row-activated", + GCAL_TYPE_ATTENDEES_SECTION, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendees-section.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, organizer_row); + gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, summary_row); + + gtk_widget_class_bind_template_callback (widget_class, on_participant_summary_row_activated_cb); +} diff --git a/src/gui/event-editor/gcal-attendees-section.h b/src/gui/event-editor/gcal-attendees-section.h new file mode 100644 index 00000000..ca78b0f4 --- /dev/null +++ b/src/gui/event-editor/gcal-attendees-section.h @@ -0,0 +1,29 @@ +/* gcal-attendees-section.h + * + * Copyright (C) 2025 Alen Galinec + * + * 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 . + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_ATTENDEES_SECTION (gcal_attendees_section_get_type ()) +G_DECLARE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL, ATTENDEES_SECTION, GtkBox) + +G_END_DECLS diff --git a/src/gui/event-editor/gcal-event-editor-dialog.blp b/src/gui/event-editor/gcal-event-editor-dialog.blp index 23e8e9ca..aa7f8572 100644 --- a/src/gui/event-editor/gcal-event-editor-dialog.blp +++ b/src/gui/event-editor/gcal-event-editor-dialog.blp @@ -5,89 +5,102 @@ template $GcalEventEditorDialog: Adw.Dialog { content-width: 700; content-height: 1000; - Adw.ToolbarView { - [top] - Adw.HeaderBar { - show-start-title-buttons: false; - show-end-title-buttons: false; + Adw.NavigationView nav_view { + Adw.NavigationPage main_page { + Adw.ToolbarView { + [top] + Adw.HeaderBar { + show-start-title-buttons: false; + show-end-title-buttons: false; - Button cancel_button { - use-underline: true; - label: _("_Cancel"); - clicked => $on_cancel_button_clicked_cb(template) not-swapped; - } - - [end] - Button done_button { - use-underline: true; - label: _("_Done"); - action-name: "event-editor.save"; - - styles [ - "suggested-action", - ] - } - } - - content: Box { - orientation: vertical; - - Adw.Banner { - title: _("This event is in a read-only calendar. It cannot be edited or deleted."); - revealed: bind template.writable inverted; - } - - Adw.PreferencesPage { - vexpand: true; - - Adw.PreferencesGroup { - $GcalSummarySection summary_section { - sensitive: bind template.writable; - } - } - - Adw.PreferencesGroup { - $GcalCalendarComboRow calendar_combo_row { - sensitive: bind template.writable; - } - } - - Adw.PreferencesGroup { - $GcalScheduleSection schedule_section { - sensitive: bind template.writable; - } - } - - Adw.PreferencesGroup { - title: _("Reminders"); - - $GcalRemindersSection reminders_section { - sensitive: bind template.writable; - } - } - - Adw.PreferencesGroup { - title: _("Notes"); - - $GcalNotesSection notes_section { - sensitive: bind template.writable; - vexpand: true; - } - } - - Adw.PreferencesGroup delete_group { - Adw.ButtonRow { - activated => $on_delete_row_activated_cb(template) not-swapped; - title: _("_Delete Event"); + Button cancel_button { use-underline: true; + label: _("_Cancel"); + clicked => $on_cancel_button_clicked_cb(template) not-swapped; + } + + [end] + Button done_button { + use-underline: true; + label: _("_Done"); + action-name: "event-editor.save"; styles [ - "destructive-action", + "suggested-action", ] } } + + content: Box { + orientation: vertical; + + Adw.Banner { + title: _("This event is in a read-only calendar. It cannot be edited or deleted."); + revealed: bind template.writable inverted; + } + + Adw.PreferencesPage { + vexpand: true; + + Adw.PreferencesGroup { + $GcalSummarySection summary_section { + sensitive: bind template.writable; + } + } + + Adw.PreferencesGroup { + $GcalCalendarComboRow calendar_combo_row { + sensitive: bind template.writable; + } + } + + Adw.PreferencesGroup { + $GcalScheduleSection schedule_section { + sensitive: bind template.writable; + } + } + + Adw.PreferencesGroup participants_group { + $GcalAttendeesSection attendees_section { + sensitive: bind template.writable; + summary-row-activated => $on_attendees_summary_activated_cb(template) not-swapped; + } + } + + Adw.PreferencesGroup { + title: _("Reminders"); + + $GcalRemindersSection reminders_section { + sensitive: bind template.writable; + } + } + + Adw.PreferencesGroup { + title: _("Notes"); + + $GcalNotesSection notes_section { + sensitive: bind template.writable; + vexpand: true; + } + } + + Adw.PreferencesGroup delete_group { + Adw.ButtonRow { + activated => $on_delete_row_activated_cb(template) not-swapped; + title: _("_Delete Event"); + use-underline: true; + + styles [ + "destructive-action", + ] + } + } + } + }; } - }; + } + + $GcalAttendeeDetailsPage attendee_details_page {} } ShortcutController { diff --git a/src/gui/event-editor/gcal-event-editor-dialog.c b/src/gui/event-editor/gcal-event-editor-dialog.c index a7ef425e..8137b4b6 100644 --- a/src/gui/event-editor/gcal-event-editor-dialog.c +++ b/src/gui/event-editor/gcal-event-editor-dialog.c @@ -19,22 +19,24 @@ #define G_LOG_DOMAIN "GcalEventEditorDialog" -#include "gcal-alarm-row.h" +#include "gcal-attendee-details-page.h" +#include "gcal-attendees-section.h" #include "gcal-calendar-combo-row.h" #include "gcal-context.h" #include "gcal-debug.h" #include "gcal-event-editor-dialog.h" #include "gcal-event-editor-section.h" -#include "gcal-utils.h" #include "gcal-event.h" #include "gcal-notes-section.h" #include "gcal-reminders-section.h" #include "gcal-schedule-section.h" #include "gcal-summary-section.h" +#include "gcal-utils.h" -#include +#include #include #include +#include /** * SECTION:gcal-event-editor-dialog @@ -60,9 +62,14 @@ struct _GcalEventEditorDialog GcalEventEditorSection *schedule_section; GtkWidget *source_label; GcalEventEditorSection *summary_section; + GcalEventEditorSection *attendees_section; + GtkWidget *participants_group; + GtkWidget *nav_view; + GtkWidget *main_page; + GtkWidget *attendee_details_page; - GcalEventEditorSection *sections[4]; + GcalEventEditorSection *sections[5]; GMenu *sources_menu; @@ -302,6 +309,19 @@ out: * Callbacks */ +static void +on_attendees_summary_activated_cb (GcalAttendeesSection *section, + GcalEventEditorDialog *self) +{ + g_return_if_fail (GCAL_IS_EVENT_EDITOR_DIALOG (self)); + + gcal_attendee_details_page_set_event (GCAL_ATTENDEE_DETAILS_PAGE (self->attendee_details_page), + self->event); + + adw_navigation_view_push (ADW_NAVIGATION_VIEW (self->nav_view), + ADW_NAVIGATION_PAGE (self->attendee_details_page)); +} + static void on_event_editor_save_action_activated_cb (GSimpleAction *action, GVariant *param, @@ -522,6 +542,8 @@ gcal_event_editor_dialog_class_init (GcalEventEditorDialogClass *klass) g_type_ensure (GCAL_TYPE_REMINDERS_SECTION); g_type_ensure (GCAL_TYPE_SCHEDULE_SECTION); g_type_ensure (GCAL_TYPE_SUMMARY_SECTION); + g_type_ensure (GCAL_TYPE_ATTENDEES_SECTION); + g_type_ensure (GCAL_TYPE_ATTENDEE_DETAILS_PAGE); object_class->finalize = gcal_event_editor_dialog_finalize; object_class->get_property = gcal_event_editor_dialog_get_property; @@ -581,11 +603,16 @@ gcal_event_editor_dialog_class_init (GcalEventEditorDialogClass *klass) gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, reminders_section); gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, schedule_section); gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, summary_section); - + gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, attendees_section); + gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, participants_group); + gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, nav_view); + gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, main_page); + gtk_widget_class_bind_template_child (widget_class, GcalEventEditorDialog, attendee_details_page); /* callbacks */ gtk_widget_class_bind_template_callback (widget_class, on_cancel_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, on_delete_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, on_attendees_summary_activated_cb); } static void @@ -608,6 +635,7 @@ gcal_event_editor_dialog_init (GcalEventEditorDialog *self) self->sections[i++] = self->reminders_section; self->sections[i++] = self->schedule_section; self->sections[i++] = self->summary_section; + self->sections[i++] = self->attendees_section; for (i = 0; i < G_N_ELEMENTS (self->sections); i++) g_object_bind_property (self, "context", self->sections[i], "context", G_BINDING_DEFAULT); @@ -666,11 +694,11 @@ gcal_event_editor_dialog_set_event (GcalEventEditorDialog *self, /* dialog's title */ if (new_event) - adw_dialog_set_title (ADW_DIALOG (self), _("New Event")); + adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (self->main_page), _("New Event")); else if (gcal_calendar_is_read_only (calendar)) - adw_dialog_set_title (ADW_DIALOG (self), _("Event Details")); + adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (self->main_page), _("Event Details")); else - adw_dialog_set_title (ADW_DIALOG (self), _("Edit Event")); + adw_navigation_page_set_title (ADW_NAVIGATION_PAGE (self->main_page), _("Edit Event")); g_list_store_remove_all (self->read_only_calendar_model); if (gcal_calendar_is_read_only (calendar)) @@ -682,6 +710,8 @@ gcal_event_editor_dialog_set_event (GcalEventEditorDialog *self, self->recurrence_changed = FALSE; set_writable (self, !gcal_calendar_is_read_only (calendar)); + gboolean has_participants = gcal_event_get_organizer (self->event) != NULL; + gtk_widget_set_visible (GTK_WIDGET (self->participants_group), has_participants); gtk_widget_set_visible (self->cancel_button, self->writable); diff --git a/src/gui/event-editor/meson.build b/src/gui/event-editor/meson.build index 52d2c3d1..7032c132 100644 --- a/src/gui/event-editor/meson.build +++ b/src/gui/event-editor/meson.build @@ -16,6 +16,9 @@ blueprints = custom_target( 'gcal-time-chooser-row.blp', 'gcal-time-zone-dialog-row.blp', 'gcal-time-zone-dialog.blp', + 'gcal-attendees-section.blp', + 'gcal-attendee-row.blp', + 'gcal-attendee-details-page.blp', ), output: '.', command: [ @@ -49,4 +52,7 @@ sources += files( 'gcal-summary-section.c', 'gcal-time-zone-dialog.c', 'gcal-time-zone-dialog-row.c', + 'gcal-attendees-section.c', + 'gcal-attendee-row.c', + 'gcal-attendee-details-page.c', ) diff --git a/src/theme/style.css b/src/theme/style.css index c753cc58..0ecb9c1b 100644 --- a/src/theme/style.css +++ b/src/theme/style.css @@ -491,3 +491,10 @@ syncindicator { .blurred { filter: blur(6px); } + +/* Participants section of the event editor dialog */ +.participants-count-label { + border-radius: 12px; + background-color: color-mix(in srgb, currentColor 10%, transparent); + padding: 6px 9px; +} diff --git a/src/utils/gcal-utils.c b/src/utils/gcal-utils.c index bc21d743..f1bde2e4 100644 --- a/src/utils/gcal-utils.c +++ b/src/utils/gcal-utils.c @@ -1409,3 +1409,43 @@ gcal_create_writable_calendars_model (GcalManager *manager) return G_LIST_MODEL (g_steal_pointer (&filter_model)); } +/** + * gcal_get_email_from_mailto_uri: + * @mailto_uri: the original mailto string. + * + * Tries to strip the "mailto:" part of the incoming string. + * The incoming original URI should be in the form: "mailto:email.address@host.org" + * if it is coming from a valid iCal event. + * + * If the incoming string is malformed, i.e. contains more than one occurrence + * of the ":" (colon) character, the original string is returned. + * This may or may not be desirable. + * + * Returns: (transfer full) (nullable): The URI without the "mailto:" part. + */ +const gchar * +gcal_get_email_from_mailto_uri (const gchar *mailto_uri) +{ + if (mailto_uri == NULL) + return NULL; + + GStrv uri_pieces = g_strsplit (mailto_uri, ":", 0); + + if (g_strv_length (uri_pieces) != 2) + { + g_strfreev (uri_pieces); + g_warning ("Malformed organizer URI (value): %s", mailto_uri); + + /* note: There was a crash here! Copying is important + * because the original string may be owned by someone else. + * (The "mailto_uri" from GcalEventAttendee and GcalEventOrganizer are not transfered) */ + + /* return original string */ + return g_strdup (mailto_uri); + } + + gchar *address = g_strdup (uri_pieces[1]); + g_strfreev (uri_pieces); + + return address; +} diff --git a/src/utils/gcal-utils.h b/src/utils/gcal-utils.h index 70235d07..f867cf2e 100644 --- a/src/utils/gcal-utils.h +++ b/src/utils/gcal-utils.h @@ -136,4 +136,5 @@ SoupSession * gcal_create_soup_session (void); GListModel * gcal_create_writable_calendars_model (GcalManager *manager); +const gchar * gcal_get_email_from_mailto_uri (const gchar *mailto_uri); #endif /* __GCAL_UTILS_H__ */