Store a list model of attendees inside the event editor dialog and use this model when displaying the details page. The attendees section (like other sections) gets injected with the event and gets another copy of the attendees from the event. Both the section and the details page can now avoid resetting themselves when the underlying data (event) doesn't change. Note: the details page uses a ListBox for the sake of the UI style, which can underperform with a lot of attendees (building the visual tree can be laggy). Part-of: <https://gitlab.gnome.org/GNOME/gnome-calendar/-/merge_requests/671>
421 lines
15 KiB
C
421 lines
15 KiB
C
/* 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-enum-types.h"
|
|
#include "gcal-enums.h"
|
|
#include "gcal-event-attendee.h"
|
|
|
|
#include <adwaita.h>
|
|
#include <gio/gio.h>
|
|
#include <glib-object.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
struct _GcalAttendeeDetailsPage
|
|
{
|
|
AdwNavigationPage parent_instance;
|
|
|
|
GListModel *attendees;
|
|
|
|
GtkFilterListModel *base_filtered_list;
|
|
GtkFilterListModel *filter_list_accepted;
|
|
GtkFilterListModel *filter_list_tentative;
|
|
GtkFilterListModel *filter_list_declined;
|
|
GtkFilterListModel *filter_list_delegated;
|
|
GtkFilterListModel *filter_list_not_responded;
|
|
|
|
GtkCustomFilter *base_filter;
|
|
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;
|
|
|
|
GcalEventAttendeeTypeFilterFlags attendee_type_filter_flags;
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_MODEL_ACCEPTED = 1,
|
|
PROP_MODEL_TENTATIVE,
|
|
PROP_MODEL_DECLINED,
|
|
PROP_MODEL_DELEGATED,
|
|
PROP_MODEL_NOT_RESPONDED,
|
|
PROP_ATTENDEE_TYPE,
|
|
N_PROPS
|
|
};
|
|
|
|
static GParamSpec *properties[N_PROPS] = { NULL, };
|
|
|
|
G_DEFINE_FINAL_TYPE (GcalAttendeeDetailsPage, gcal_attendee_details_page, ADW_TYPE_NAVIGATION_PAGE)
|
|
|
|
static void
|
|
set_type_filter_flags (GcalAttendeeDetailsPage *self,
|
|
GcalEventAttendeeTypeFilterFlags flags)
|
|
{
|
|
self->attendee_type_filter_flags = flags;
|
|
gtk_filter_changed (GTK_FILTER (self->base_filter), GTK_FILTER_CHANGE_DIFFERENT);
|
|
}
|
|
|
|
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_object (&page->base_filter);
|
|
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->base_filtered_list = NULL;
|
|
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
|
|
base_filter_cb (GcalEventAttendee *attendee,
|
|
gpointer data)
|
|
{
|
|
GcalAttendeeDetailsPage *self = GCAL_ATTENDEE_DETAILS_PAGE (data);
|
|
GcalEventAttendeeType type = gcal_event_attendee_get_attendee_type (attendee);
|
|
|
|
switch (self->attendee_type_filter_flags)
|
|
{
|
|
case GCAL_EVENT_ATTENDEE_TYPE_FILTER_PERSON:
|
|
return type == GCAL_EVENT_ATTENDEE_TYPE_INDIVIDUAL || type == GCAL_EVENT_ATTENDEE_TYPE_GROUP;
|
|
case GCAL_EVENT_ATTENDEE_TYPE_FILTER_RESOURCE:
|
|
return type == GCAL_EVENT_ATTENDEE_TYPE_RESOURCE || type == GCAL_EVENT_ATTENDEE_TYPE_ROOM;
|
|
case GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE:
|
|
default:
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
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->base_filter = gtk_custom_filter_new ((GtkCustomFilterFunc) base_filter_cb, self, NULL);
|
|
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->base_filtered_list =
|
|
gtk_filter_list_model_new (self->attendees,
|
|
GTK_FILTER (self->base_filter));
|
|
|
|
self->filter_list_accepted =
|
|
gtk_filter_list_model_new (G_LIST_MODEL (self->base_filtered_list),
|
|
GTK_FILTER (self->accepted_filter));
|
|
|
|
self->filter_list_tentative =
|
|
gtk_filter_list_model_new (G_LIST_MODEL (self->base_filtered_list),
|
|
GTK_FILTER (self->tentative_filter));
|
|
|
|
self->filter_list_declined =
|
|
gtk_filter_list_model_new (G_LIST_MODEL (self->base_filtered_list),
|
|
GTK_FILTER (self->declined_filter));
|
|
|
|
self->filter_list_delegated =
|
|
gtk_filter_list_model_new (G_LIST_MODEL (self->base_filtered_list),
|
|
GTK_FILTER (self->delegated_filter));
|
|
|
|
self->filter_list_not_responded =
|
|
gtk_filter_list_model_new (G_LIST_MODEL (self->base_filtered_list),
|
|
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;
|
|
case PROP_ATTENDEE_TYPE:
|
|
g_value_set_flags (value, self->attendee_type_filter_flags);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gcal_attendee_details_page_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GcalAttendeeDetailsPage *self = GCAL_ATTENDEE_DETAILS_PAGE (object);
|
|
|
|
switch (property_id)
|
|
{
|
|
case PROP_ATTENDEE_TYPE:
|
|
set_type_filter_flags (self, g_value_get_flags (value));
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gcal_attendee_details_page_init (GcalAttendeeDetailsPage *instance)
|
|
{
|
|
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (instance));
|
|
|
|
instance->attendee_type_filter_flags = GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE;
|
|
|
|
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;
|
|
object_class->set_property = gcal_attendee_details_page_set_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);
|
|
|
|
/**
|
|
* GcalAttendeeDetailsPage:attendee-type:
|
|
*
|
|
* Used for the base filter to filter the attendees based on
|
|
* attendee type (aka CUTYPE).
|
|
* GCAL_EVENT_ATTENDEE_TYPE_NONE is used to clear the filter.
|
|
*/
|
|
properties[PROP_ATTENDEE_TYPE] =
|
|
g_param_spec_flags ("attendee-type-filter-flags", NULL, NULL,
|
|
GCAL_TYPE_EVENT_ATTENDEE_TYPE_FILTER_FLAGS,
|
|
GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE,
|
|
G_PARAM_READWRITE | 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_attendees:
|
|
* @self: a #GcalAttendeeDetailsPage instance.
|
|
* @attendees: a borrowed list of #GcalEventAttendee to display.
|
|
*
|
|
* Sets the attendees for display. This list is owned by the
|
|
* event editor dialog. Do not free here.
|
|
*/
|
|
void
|
|
gcal_attendee_details_page_set_attendees (GcalAttendeeDetailsPage *self,
|
|
GListModel *attendees)
|
|
{
|
|
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
|
|
|
|
if (self->attendees == attendees)
|
|
return;
|
|
|
|
self->attendees = attendees;
|
|
gtk_filter_list_model_set_model (self->base_filtered_list, self->attendees);
|
|
}
|
|
|
|
/**
|
|
* gcal_attendee_details_page_set_type_filter:
|
|
* @self: a #GcalAttendeeDetailsPage instance.
|
|
* @flags: flags of type #GcalEventAttendeeTypeFilterFlags.
|
|
*
|
|
* Sets the type of attendees to use as the base filter on the
|
|
* list of attendees. E.g. only people or only resources, or both
|
|
* are filtered before grouping by response is performed.
|
|
*/
|
|
void
|
|
gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *self,
|
|
GcalEventAttendeeTypeFilterFlags flags)
|
|
{
|
|
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
|
|
set_type_filter_flags (self, flags);
|
|
}
|