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:
@@ -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
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
<file compressed="true">gcal-summary-section.ui</file>
|
||||
<file compressed="true">gcal-time-zone-dialog.ui</file>
|
||||
<file compressed="true">gcal-time-zone-dialog-row.ui</file>
|
||||
<file compressed="true">gcal-attendees-section.ui</file>
|
||||
<file compressed="true">gcal-attendee-row.ui</file>
|
||||
<file compressed="true">gcal-attendee-details-page.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
@@ -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 <Gtk.FilterListModel>.n-items;
|
||||
|
||||
ListBox list_accepted {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Adw.PreferencesGroup {
|
||||
title: _("Tentative");
|
||||
visible: bind template.model-tentative as <Gtk.FilterListModel>.n-items;
|
||||
|
||||
ListBox list_tentative {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Adw.PreferencesGroup {
|
||||
title: _("Declined");
|
||||
visible: bind template.model-declined as <Gtk.FilterListModel>.n-items;
|
||||
|
||||
ListBox list_declined {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Adw.PreferencesGroup {
|
||||
title: _("Delegated");
|
||||
visible: bind template.model-delegated as <Gtk.FilterListModel>.n-items;
|
||||
|
||||
ListBox list_delegated {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Adw.PreferencesGroup {
|
||||
title: _("Not responded");
|
||||
visible: bind template.model-not-responded as <Gtk.FilterListModel>.n-items;
|
||||
|
||||
ListBox list_not_responded {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"boxed-list",
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef GCAL_ATTENDEE_DETAILS_PAGE_H
|
||||
#define GCAL_ATTENDEE_DETAILS_PAGE_H
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#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
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/* gcal-attendee-row.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-row.h"
|
||||
#include "gcal-enums.h"
|
||||
#include "gcal-event-attendee.h"
|
||||
#include "gcal-utils.h"
|
||||
|
||||
#include <adwaita.h>
|
||||
#include <gdk/gdk.h>
|
||||
#include <glib-object.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <libintl.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/* gcal-attendee-row.h
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#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
|
||||
@@ -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 <string>;
|
||||
tooltip-text: _("Number of participants");
|
||||
|
||||
styles [
|
||||
"participants-count-label",
|
||||
]
|
||||
}
|
||||
|
||||
[suffix]
|
||||
Image open_details {
|
||||
icon-name: "go-next-symbolic";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/* gcal-attendees-section.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-attendees-section.h"
|
||||
|
||||
#include <adwaita.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <libintl.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/* gcal-attendees-section.h
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <glib.h>
|
||||
|
||||
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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <libecal/libecal.h>
|
||||
#include <adwaita.h>
|
||||
#include <glib/gi18n.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <libecal/libecal.h>
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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__ */
|
||||
|
||||
Reference in New Issue
Block a user