event-editor: Add attendees section and details page

UX/UI designed by Philipp Sauberzweig (@PhilippSauberz).

The event editor dialog is now a NavigationView and the participants
details open in a separate page. Group attendees/participants in
AdwPreferencesGroups and bind groups'
ListBox widgets to backing filter models. These are just "views" into a
single GListStore containing all the event attendees.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-calendar/-/merge_requests/671>
This commit is contained in:
mindonwarp
2026-01-21 19:37:52 +00:00
committed by Marge Bot
parent 884855ee0b
commit ba3ca061ce
17 changed files with 1495 additions and 83 deletions
@@ -0,0 +1,353 @@
/* gcal-attendee-details-page.c
*
* Copyright (C) 2025 Alen Galinec <mind.on.warp@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#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 <adwaita.h>
#include <gio/gio.h>
#include <glib-object.h>
#include <gtk/gtk.h>
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);
}
}