Files
federated-calendar/src/gui/event-editor/gcal-schedule-section.c
T

1094 lines
38 KiB
C

/* gcal-schedule-section.c
*
* Copyright 2020 Georges Basile Stavracas Neto <georges.stavracas@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/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "GcalScheduleSection"
#include "gcal-context.h"
#include "gcal-date-selector.h"
#include "gcal-date-chooser-row.h"
#include "gcal-date-time-chooser.h"
#include "gcal-debug.h"
#include "gcal-event.h"
#include "gcal-event-editor-section.h"
#include "gcal-recurrence.h"
#include "gcal-schedule-section.h"
#include "gcal-utils.h"
#include <glib/gi18n.h>
struct _GcalScheduleSection
{
GtkBox parent;
GcalScheduleValues *values;
AdwToggleGroup *schedule_type_toggle_group;
AdwPreferencesGroup *start_date_group;
GcalDateChooserRow *start_date_row;
AdwPreferencesGroup *end_date_group;
GcalDateChooserRow *end_date_row;
GcalDateTimeChooser *start_date_time_chooser;
GcalDateTimeChooser *end_date_time_chooser;
GtkWidget *number_of_occurrences_spin;
GtkWidget *repeat_combo;
GtkWidget *repeat_duration_combo;
GtkWidget *until_date_selector;
GcalContext *context;
GcalEvent *event;
GcalEventEditorFlags flags;
};
/* Desired state of the widgets, computed from GcalScheduleValues.
*
* There is some subtle logic to decide how to update the widgets based on how
* each of them causes the GcalScheduleValues to be modified. We encapsulate
* the desired state in this struct, so we can have tests for it.
*/
typedef struct
{
gboolean schedule_type_all_day;
gboolean date_widgets_visible;
gboolean date_time_widgets_visible;
GcalTimeFormat time_format;
/* Note that the displayed date/time may not correspond to the real data in the
* GcalScheduleValues. See the comment in widget_state_from_values().
*/
GDateTime *date_time_start;
GDateTime *date_time_end;
} WidgetState;
static void widget_state_free (WidgetState *state);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WidgetState, widget_state_free);
static void gcal_event_editor_section_iface_init (GcalEventEditorSectionInterface *iface);
G_DEFINE_TYPE_WITH_CODE (GcalScheduleSection, gcal_schedule_section, GTK_TYPE_BOX,
G_IMPLEMENT_INTERFACE (GCAL_TYPE_EVENT_EDITOR_SECTION, gcal_event_editor_section_iface_init))
enum
{
PROP_0,
PROP_CONTEXT,
N_PROPS
};
static void on_start_date_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self);
static void on_end_date_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self);
static void on_start_date_time_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self);
static void on_end_date_time_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self);
static void on_schedule_type_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self);
static GcalScheduleValues *
gcal_schedule_values_copy (const GcalScheduleValues *values)
{
GcalScheduleValues *copy = g_new0 (GcalScheduleValues, 1);
copy->all_day = values->all_day;
copy->orig_date_start = values->orig_date_start ? g_date_time_ref (values->orig_date_start) : NULL;
copy->orig_date_end = values->orig_date_end ? g_date_time_ref (values->orig_date_end) : NULL;
copy->date_start = values->date_start ? g_date_time_ref (values->date_start) : NULL;
copy->date_end = values->date_end ? g_date_time_ref (values->date_end) : NULL;
copy->recur = values->recur ? gcal_recurrence_ref (values->recur) : NULL;
copy->time_format = values->time_format;
return copy;
}
static GcalScheduleValues *
gcal_schedule_values_set_all_day (const GcalScheduleValues *values, gboolean all_day)
{
GcalScheduleValues *copy = gcal_schedule_values_copy (values);
copy->all_day = all_day;
return copy;
}
static GcalScheduleValues *
gcal_schedule_values_set_time_format (const GcalScheduleValues *values, GcalTimeFormat time_format)
{
GcalScheduleValues *copy = gcal_schedule_values_copy (values);
copy->time_format = time_format;
return copy;
}
/* The start_date_row widget has changed. We need to sync it to the values and
* adjust the other values based on it.
*/
static GcalScheduleValues *
gcal_schedule_values_set_start_date (const GcalScheduleValues *values, GDateTime *start)
{
GcalScheduleValues *copy = gcal_schedule_values_copy (values);
gcal_set_date_time (&copy->date_start, start);
if (g_date_time_compare (start, copy->date_end) == 1)
{
gcal_set_date_time (&copy->date_end, start);
}
return copy;
}
/* The end_date_row widget has changed. We need to sync it to the values and
* adjust the other values based on it.
*/
static GcalScheduleValues *
gcal_schedule_values_set_end_date (const GcalScheduleValues *values, GDateTime *end)
{
GcalScheduleValues *copy = gcal_schedule_values_copy (values);
if (copy->all_day)
{
/* See the comment "While in iCalendar..." in widget_state_from_values(). Here we
* take the human-readable end-date, and turn it into the appropriate value for
* iCalendar.
*/
g_autoptr (GDateTime) fake_end_date = g_date_time_add_days (end, 1);
gcal_set_date_time (&copy->date_end, fake_end_date);
}
else
{
gcal_set_date_time (&copy->date_end, end);
}
if (g_date_time_compare (copy->date_start, end) == 1)
{
gcal_set_date_time (&copy->date_start, end);
}
return copy;
}
static GcalScheduleValues *
gcal_schedule_values_set_start_date_time (const GcalScheduleValues *values, GDateTime *start)
{
GcalScheduleValues *copy = gcal_schedule_values_copy (values);
gcal_set_date_time (&copy->date_start, start);
if (g_date_time_compare (start, copy->date_end) == 1)
{
GTimeZone *end_tz = g_date_time_get_timezone (copy->date_end);
g_autoptr (GDateTime) new_end = g_date_time_add_hours (start, 1);
g_autoptr (GDateTime) new_end_in_end_tz = g_date_time_to_timezone (new_end, end_tz);
gcal_set_date_time (&copy->date_end, new_end_in_end_tz);
}
return copy;
}
static GcalScheduleValues *
gcal_schedule_values_set_end_date_time (const GcalScheduleValues *values, GDateTime *end)
{
GcalScheduleValues *copy = gcal_schedule_values_copy (values);
gcal_set_date_time (&copy->date_end, end);
if (g_date_time_compare (copy->date_start, end) == 1)
{
GTimeZone *start_tz = g_date_time_get_timezone (copy->date_start);
g_autoptr (GDateTime) new_start = g_date_time_add_hours (end, -1);
g_autoptr (GDateTime) new_start_in_start_tz = g_date_time_to_timezone (new_start, start_tz);
gcal_set_date_time (&copy->date_start, new_start_in_start_tz);
}
return copy;
}
static WidgetState *
widget_state_from_values (const GcalScheduleValues *values)
{
WidgetState *state = g_new0 (WidgetState, 1);
state->schedule_type_all_day = values->all_day;
state->date_widgets_visible = values->all_day;
state->date_time_widgets_visible = !values->all_day;
state->time_format = values->time_format;
g_assert (values->date_start);
state->date_time_start = g_date_time_ref (values->date_start);
if (values->all_day)
{
/* While in iCalendar a single-day all-day event goes from $day to $day+1, we don't
* want to show the user a single-day all-day event as starting on Feb 1 and ending
* on Feb 2, for example.
*
* So, we maintain the "real" data in GcalScheduleValues with the $day+1 as iCalendar wants,
* but for display purposes, we subtract a day from the end date to show the expected thing to the
* user. When the widget changes, we add the day back - see gcal_schedule_values_set_end_date().
*
* See https://bugzilla.gnome.org/show_bug.cgi?id=769300 for the original bug about this.
*
* Keep in sync with gcal_schedule_values_set_end_date().
*/
state->date_time_end = g_date_time_add_days (values->date_end, -1);
}
else
{
g_assert (values->date_end);
state->date_time_end = g_date_time_ref (values->date_end);
}
return state;
}
static void
widget_state_free (WidgetState *state)
{
g_date_time_unref (state->date_time_start);
g_date_time_unref (state->date_time_end);
g_free (state);
}
/*
* Auxiliary methods
*/
static inline gboolean
all_day_selected (GcalScheduleSection *self)
{
const gchar *active = adw_toggle_group_get_active_name (self->schedule_type_toggle_group);
return g_strcmp0 (active, "all-day") == 0;
}
static void
block_date_signals (GcalScheduleSection *self)
{
g_signal_handlers_block_by_func (self->start_date_row, on_start_date_changed_cb, self);
g_signal_handlers_block_by_func (self->end_date_row, on_end_date_changed_cb, self);
g_signal_handlers_block_by_func (self->start_date_time_chooser, on_start_date_time_changed_cb, self);
g_signal_handlers_block_by_func (self->end_date_time_chooser, on_end_date_time_changed_cb, self);
}
static void
unblock_date_signals (GcalScheduleSection *self)
{
g_signal_handlers_unblock_by_func (self->end_date_time_chooser, on_end_date_time_changed_cb, self);
g_signal_handlers_unblock_by_func (self->start_date_time_chooser, on_start_date_time_changed_cb, self);
g_signal_handlers_unblock_by_func (self->end_date_row, on_end_date_changed_cb, self);
g_signal_handlers_unblock_by_func (self->start_date_row, on_start_date_changed_cb, self);
}
static void
remove_recurrence_properties (GcalEvent *event)
{
ECalComponent *comp;
comp = gcal_event_get_component (event);
e_cal_component_set_recurid (comp, NULL);
e_cal_component_set_rrules (comp, NULL);
e_cal_component_commit_sequence (comp);
}
static void
update_widgets (GcalScheduleSection *self,
WidgetState *state)
{
g_signal_handlers_block_by_func (self->schedule_type_toggle_group, on_schedule_type_changed_cb, self);
block_date_signals (self);
adw_toggle_group_set_active_name (self->schedule_type_toggle_group,
state->schedule_type_all_day ? "all-day" : "time-slot");
gtk_widget_set_visible (GTK_WIDGET (self->start_date_group), state->date_widgets_visible);
gtk_widget_set_visible (GTK_WIDGET (self->end_date_group), state->date_widgets_visible);
gtk_widget_set_visible (GTK_WIDGET (self->start_date_time_chooser), state->date_time_widgets_visible);
gtk_widget_set_visible (GTK_WIDGET (self->end_date_time_chooser), state->date_time_widgets_visible);
gcal_date_time_chooser_set_time_format (self->start_date_time_chooser, state->time_format);
gcal_date_time_chooser_set_time_format (self->end_date_time_chooser, state->time_format);
/* date */
gcal_date_chooser_row_set_date (self->start_date_row, state->date_time_start);
gcal_date_chooser_row_set_date (self->end_date_row, state->date_time_end);
/* date-time */
gcal_date_time_chooser_set_date_time (self->start_date_time_chooser, state->date_time_start);
gcal_date_time_chooser_set_date_time (self->end_date_time_chooser, state->date_time_end);
unblock_date_signals (self);
g_signal_handlers_unblock_by_func (self->schedule_type_toggle_group, on_schedule_type_changed_cb, self);
}
/* Updates the widgets, and the current values, from the specified ones.
*
* This will free the current values and replace them with new ones.
*/
static void
update_from_values (GcalScheduleSection *self,
GcalScheduleValues *values)
{
g_autoptr (WidgetState) state = widget_state_from_values (values);
update_widgets (self, state);
gcal_schedule_values_free (self->values);
self->values = values;
}
/*
* Callbacks
*/
static void
on_schedule_type_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
gboolean all_day = all_day_selected (self);
GcalScheduleValues *updated = gcal_schedule_values_set_all_day (self->values, all_day);
update_from_values (self, updated);
}
static void
on_start_date_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
GDateTime *start = gcal_date_chooser_row_get_date (self->start_date_row);
GcalScheduleValues *updated = gcal_schedule_values_set_start_date (self->values, start);
update_from_values (self, updated);
}
static void
on_end_date_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
GDateTime *end = gcal_date_chooser_row_get_date (self->end_date_row);
GcalScheduleValues *updated = gcal_schedule_values_set_end_date (self->values, end);
update_from_values (self, updated);
}
static void
on_start_date_time_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
GDateTime *start = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser);
GcalScheduleValues *updated = gcal_schedule_values_set_start_date_time (self->values, start);
update_from_values (self, updated);
}
static void
on_end_date_time_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
GDateTime *end = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser);
GcalScheduleValues *updated = gcal_schedule_values_set_end_date_time (self->values, end);
update_from_values (self, updated);
}
static void
on_repeat_duration_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
gint active = adw_combo_row_get_selected (ADW_COMBO_ROW (widget));
gtk_widget_set_visible (self->number_of_occurrences_spin, active == 1);
gtk_widget_set_visible (self->until_date_selector, active == 2);
}
static void
on_repeat_type_changed_cb (GtkWidget *widget,
GParamSpec *pspec,
GcalScheduleSection *self)
{
GcalRecurrenceFrequency frequency;
frequency = adw_combo_row_get_selected (ADW_COMBO_ROW (widget));
if (frequency == GCAL_RECURRENCE_NO_REPEAT)
adw_combo_row_set_selected (ADW_COMBO_ROW (self->repeat_duration_combo), GCAL_RECURRENCE_FOREVER);
gtk_widget_set_visible (self->repeat_duration_combo, frequency != GCAL_RECURRENCE_NO_REPEAT);
}
static void
on_time_format_changed_cb (GcalScheduleSection *self)
{
GcalTimeFormat time_format = gcal_context_get_time_format (self->context);
GcalScheduleValues *updated = gcal_schedule_values_set_time_format (self->values, time_format);
WidgetState *state = widget_state_from_values (updated);
update_widgets (self, state);
widget_state_free (state);
}
/*
* GcalEventEditorSection interface
*/
/*
* Sets the event of the schedule section.
*
* If the event is not a new event, the event date start and date end timezones are kept and displayed to the user.
*
* If the event is a new event, the event date start and date end timezones are forgotten. The results of
* g_date_time_get_year, g_date_time_get_month, g_date_time_get_day_of_month, g_date_time_get_hour and
* g_date_time_get_minute are used for the date row and the date time chooser.
* The date row timezone is set to UTC (as all day events use the UTC timezone).
* The date time chooser timezone is set to local.
*/
static void
gcal_schedule_section_set_event (GcalEventEditorSection *section,
GcalEvent *event,
GcalEventEditorFlags flags)
{
GcalScheduleSection *self;
GcalRecurrenceLimitType limit_type;
GcalRecurrenceFrequency frequency;
GcalTimeFormat time_format;
GcalScheduleValues *values;
g_autoptr (WidgetState) state = NULL;
GCAL_ENTRY;
self = GCAL_SCHEDULE_SECTION (section);
g_set_object (&self->event, event);
self->flags = flags;
time_format = gcal_context_get_time_format (self->context);
self->values = gcal_schedule_values_from_event (event, time_format);
values = self->values; /* alias to avoid "self->values" everywhere */
if (!event)
GCAL_RETURN ();
state = widget_state_from_values (values);
update_widgets (self, state);
/* Recurrences */
if (values->recur)
{
frequency = values->recur->frequency;
limit_type = values->recur->limit_type;
}
else
{
frequency = GCAL_RECURRENCE_NO_REPEAT;
limit_type = GCAL_RECURRENCE_FOREVER;
}
adw_combo_row_set_selected (ADW_COMBO_ROW (self->repeat_combo), frequency);
adw_combo_row_set_selected (ADW_COMBO_ROW (self->repeat_duration_combo), limit_type);
if (frequency == GCAL_RECURRENCE_NO_REPEAT)
{
gtk_widget_set_visible (self->repeat_duration_combo, FALSE);
}
else
{
gtk_widget_set_visible (self->repeat_duration_combo, TRUE);
gtk_widget_set_visible (self->repeat_duration_combo, TRUE);
}
switch (limit_type)
{
case GCAL_RECURRENCE_COUNT:
gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->number_of_occurrences_spin), values->recur->limit.count);
break;
case GCAL_RECURRENCE_UNTIL:
gcal_date_selector_set_date (GCAL_DATE_SELECTOR (self->until_date_selector), values->recur->limit.until);
break;
case GCAL_RECURRENCE_FOREVER:
gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->number_of_occurrences_spin), 0);
break;
}
GCAL_EXIT;
}
static void
gcal_schedule_section_apply_to_event (GcalScheduleSection *self,
GcalEvent *event)
{
GcalRecurrenceFrequency freq;
GCAL_ENTRY;
gcal_event_set_date_start (event, self->values->date_start);
gcal_event_set_date_end (event, self->values->date_end);
/* Check Repeat popover and set recurrence-rules accordingly */
freq = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_combo));
if (freq != GCAL_RECURRENCE_NO_REPEAT)
{
g_autoptr (GcalRecurrence) recur = NULL;
recur = gcal_recurrence_new ();
recur->frequency = freq;
recur->limit_type = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_duration_combo));
if (recur->limit_type == GCAL_RECURRENCE_UNTIL)
recur->limit.until = g_date_time_ref (gcal_date_selector_get_date (GCAL_DATE_SELECTOR (self->until_date_selector)));
else if (recur->limit_type == GCAL_RECURRENCE_COUNT)
recur->limit.count = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->number_of_occurrences_spin));
/* Only apply the new recurrence if it's different from the old one */
if (!gcal_recurrence_is_equal (self->values->recur, recur))
{
/* Remove the previous recurrence... */
remove_recurrence_properties (event);
/* ... and set the new one */
gcal_event_set_recurrence (event, recur);
}
}
else
{
/* When NO_REPEAT is set, make sure to remove the old recurrent */
remove_recurrence_properties (event);
}
GCAL_EXIT;
}
static void
gcal_schedule_section_apply (GcalEventEditorSection *section)
{
GcalScheduleSection *self = GCAL_SCHEDULE_SECTION (section);
GCAL_ENTRY;
gcal_schedule_section_apply_to_event (self, self->event);
gcal_schedule_values_free (self->values);
GcalTimeFormat time_format = gcal_context_get_time_format (self->context);
self->values = gcal_schedule_values_from_event (self->event, time_format);
GCAL_EXIT;
}
static gboolean
gcal_schedule_section_changed (GcalEventEditorSection *section)
{
GcalScheduleSection *self;
GDateTime *start_date, *end_date, *prev_start_date, *prev_end_date;
GTimeZone *start_tz, *end_tz, *prev_start_tz, *prev_end_tz;
gboolean was_all_day;
gboolean all_day;
GCAL_ENTRY;
self = GCAL_SCHEDULE_SECTION (section);
all_day = all_day_selected (self);
was_all_day = self->values->all_day;
/* All day */
if (all_day != was_all_day)
GCAL_RETURN (TRUE);
if (!all_day)
{
start_date = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser);
end_date = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser);
}
else
{
start_date = gcal_date_chooser_row_get_date (self->start_date_row);
end_date = gcal_date_chooser_row_get_date (self->end_date_row);
}
prev_start_date = self->values->date_start;
prev_end_date = self->values->date_end;
start_tz = g_date_time_get_timezone (start_date);
end_tz = g_date_time_get_timezone (end_date);
prev_start_tz = g_date_time_get_timezone (prev_start_date);
prev_end_tz = g_date_time_get_timezone (prev_end_date);
/* Start date */
if (!g_date_time_equal (start_date, prev_start_date))
GCAL_RETURN (TRUE);
if (g_strcmp0 (g_time_zone_get_identifier (start_tz), g_time_zone_get_identifier (prev_start_tz)) != 0)
GCAL_RETURN (TRUE);
/* End date */
if (all_day)
{
GDateTime *fake_end_date = g_date_time_add_days (end_date, 1);
end_date = fake_end_date;
}
if (!g_date_time_equal (end_date, self->values->date_end))
GCAL_RETURN (TRUE);
if (g_strcmp0 (g_time_zone_get_identifier (end_tz), g_time_zone_get_identifier (prev_end_tz)) != 0)
GCAL_RETURN (TRUE);
/* Recurrency */
GCAL_RETURN (gcal_schedule_section_recurrence_changed (self));
}
static void
gcal_event_editor_section_iface_init (GcalEventEditorSectionInterface *iface)
{
iface->set_event = gcal_schedule_section_set_event;
iface->apply = gcal_schedule_section_apply;
iface->changed = gcal_schedule_section_changed;
}
/*
* GObject overrides
*/
static void
gcal_schedule_section_finalize (GObject *object)
{
GcalScheduleSection *self = (GcalScheduleSection *)object;
g_clear_pointer (&self->values, gcal_schedule_values_free);
g_clear_object (&self->context);
g_clear_object (&self->event);
G_OBJECT_CLASS (gcal_schedule_section_parent_class)->finalize (object);
}
static void
gcal_schedule_section_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GcalScheduleSection *self = GCAL_SCHEDULE_SECTION (object);
switch (prop_id)
{
case PROP_CONTEXT:
g_value_set_object (value, self->context);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gcal_schedule_section_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GcalScheduleSection *self = GCAL_SCHEDULE_SECTION (object);
switch (prop_id)
{
case PROP_CONTEXT:
g_assert (self->context == NULL);
self->context = g_value_dup_object (value);
g_signal_connect_object (self->context,
"notify::time-format",
G_CALLBACK (on_time_format_changed_cb),
self,
G_CONNECT_SWAPPED);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gcal_schedule_section_class_init (GcalScheduleSectionClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
g_type_ensure (GCAL_TYPE_DATE_TIME_CHOOSER);
object_class->finalize = gcal_schedule_section_finalize;
object_class->get_property = gcal_schedule_section_get_property;
object_class->set_property = gcal_schedule_section_set_property;
g_object_class_override_property (object_class, PROP_CONTEXT, "context");
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-schedule-section.ui");
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, schedule_type_toggle_group);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, start_date_group);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, start_date_row);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, end_date_group);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, end_date_row);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, start_date_time_chooser);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, end_date_time_chooser);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, number_of_occurrences_spin);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, repeat_combo);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, repeat_duration_combo);
gtk_widget_class_bind_template_child (widget_class, GcalScheduleSection, until_date_selector);
gtk_widget_class_bind_template_callback (widget_class, on_start_date_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, on_end_date_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, on_start_date_time_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, on_end_date_time_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, on_repeat_duration_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, on_repeat_type_changed_cb);
}
static void
gcal_schedule_section_init (GcalScheduleSection *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
g_signal_connect (self->schedule_type_toggle_group,
"notify::active",
G_CALLBACK (on_schedule_type_changed_cb),
self);
}
gboolean
gcal_schedule_section_recurrence_changed (GcalScheduleSection *self)
{
g_autoptr (GcalRecurrence) recurrence = NULL;
GcalRecurrenceFrequency freq;
g_return_val_if_fail (GCAL_IS_SCHEDULE_SECTION (self), FALSE);
freq = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_combo));
if (freq == GCAL_RECURRENCE_NO_REPEAT && !self->values->recur)
GCAL_RETURN (FALSE);
recurrence = gcal_recurrence_new ();
recurrence->frequency = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_combo));
recurrence->limit_type = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_duration_combo));
if (recurrence->limit_type == GCAL_RECURRENCE_UNTIL)
recurrence->limit.until = g_date_time_ref (gcal_date_selector_get_date (GCAL_DATE_SELECTOR (self->until_date_selector)));
else if (recurrence->limit_type == GCAL_RECURRENCE_COUNT)
recurrence->limit.count = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->number_of_occurrences_spin));
GCAL_RETURN (!gcal_recurrence_is_equal (recurrence, self->values->recur));
}
gboolean
gcal_schedule_section_day_changed (GcalScheduleSection *self)
{
GDateTime *start_date, *end_date;
gboolean all_day;
g_return_val_if_fail (GCAL_IS_SCHEDULE_SECTION (self), FALSE);
GCAL_ENTRY;
all_day = all_day_selected (self);
if (all_day)
{
start_date = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser);
end_date = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser);
}
else
{
start_date = gcal_date_chooser_row_get_date (self->start_date_row);
end_date = gcal_date_chooser_row_get_date (self->end_date_row);
}
GCAL_RETURN (gcal_date_time_compare_date (start_date, self->values->date_start) < 0 ||
gcal_date_time_compare_date (end_date, self->values->date_end) > 0);
}
/**
* Extracts the values from @event that are needed to populate #GcalScheduleSection.
*
* Returns: a #GcalscheduleValues ready for use. Free it with
* gcal_schedule_values_free().
*/
GcalScheduleValues *
gcal_schedule_values_from_event (GcalEvent *event,
GcalTimeFormat time_format)
{
GcalScheduleValues *values = g_new0 (GcalScheduleValues, 1);
if (event)
{
GcalRecurrence *recur = gcal_event_get_recurrence (event);
values->all_day = gcal_event_get_all_day (event);
values->orig_date_start = g_date_time_ref (gcal_event_get_date_start (event));
values->date_start = g_date_time_ref (values->orig_date_start);
values->orig_date_end = g_date_time_ref (gcal_event_get_date_end (event));
values->date_end = g_date_time_ref (values->orig_date_end);
if (recur)
{
values->recur = gcal_recurrence_ref (recur);
}
}
values->time_format = time_format;
return values;
}
/**
* Frees the contents of @values. Does not free the @values pointer itself;
* this structure is meant to be allocated on the stack.
*/
void
gcal_schedule_values_free (GcalScheduleValues *values)
{
values->all_day = FALSE;
g_clear_pointer (&values->date_start, g_date_time_unref);
g_clear_pointer (&values->date_end, g_date_time_unref);
g_clear_pointer (&values->recur, gcal_recurrence_unref);
g_free (values);
}
/* Builds a new Gcalschedulevalues from two ISO 8601 date-times; be sure to include your timezone if you need it */
static GcalScheduleValues *
values_with_date_times(const char *start, const char *end)
{
g_autoptr (GDateTime) start_date = g_date_time_new_from_iso8601 (start, NULL);
g_assert (start_date != NULL);
g_autoptr (GDateTime) end_date = g_date_time_new_from_iso8601 (end, NULL);
g_assert (end_date != NULL);
g_assert (g_date_time_compare (start_date, end_date) == -1);
GcalScheduleValues *values = g_new0 (GcalScheduleValues, 1);
*values = (GcalScheduleValues) {
.all_day = FALSE,
.orig_date_start = g_date_time_ref (start_date),
.orig_date_end = g_date_time_ref (end_date),
.date_start = g_date_time_ref (start_date),
.date_end = g_date_time_ref (end_date),
.recur = NULL,
.time_format = GCAL_TIME_FORMAT_24H,
};
return values;
}
static void
test_setting_start_date_after_end_date_resets_end_date (void)
{
/* We start with
* start = 10:00
* end = 11:00
*
* Then we set start to 12:00
*
* We want to test that end becomes 12:00 as well.
*/
g_autoptr (GcalScheduleValues) values = values_with_date_times("20250303T10:00:00-06:00",
"20250303T11:00:00-06:00");
g_autoptr (GDateTime) two_hours_later = g_date_time_new_from_iso8601 ("20250303T12:00:00-06:00", NULL);
g_autoptr (GcalScheduleValues) new_values = gcal_schedule_values_set_start_date (values, two_hours_later);
g_assert (g_date_time_equal (new_values->date_start, two_hours_later));
g_assert (g_date_time_equal (new_values->date_end, two_hours_later));
}
static void
test_setting_end_date_before_start_date_resets_start_date (void)
{
/* We start with
* start = 10:00
* end = 11:00
*
* Then we set end to 09:00
*
* We want to test that start becomes 09:00 as well.
*/
g_autoptr (GcalScheduleValues) values = values_with_date_times("20250303T10:00:00-06:00",
"20250303T11:00:00-06:00");
g_autoptr (GDateTime) two_hours_earlier = g_date_time_new_from_iso8601 ("20250303T09:00:00-06:00", NULL);
g_autoptr (GcalScheduleValues) new_values = gcal_schedule_values_set_end_date (values, two_hours_earlier);
g_assert (g_date_time_equal (new_values->date_start, two_hours_earlier));
g_assert (g_date_time_equal (new_values->date_end, two_hours_earlier));
}
static void
test_setting_start_datetime_preserves_end_timezone (void)
{
/* Start with
* start = 10:00, UTC-6
* end = 11:30, UTC-5 (note the different timezone; event is 30 minutes long)
*
* Set the start date to 11:30, UTC-6
*
* End date should be 13:30, UTC-5 (i.e. end_date was set to the same as start_date, but in a different tz)
*
* (imagine driving for one hour while crossing timezones)
*/
g_autoptr (GcalScheduleValues) values = values_with_date_times("20250303T10:00:00-06:00",
"20250303T11:30:00-05:00");
g_assert (g_date_time_compare (values->date_start, values->date_end) == -1);
g_autoptr (GDateTime) new_start = g_date_time_new_from_iso8601 ("20250303T11:30:00-06:00", NULL);
g_autoptr (GDateTime) expected_end = g_date_time_new_from_iso8601 ("20250303T13:30:00-05:00", NULL);
g_autoptr (GcalScheduleValues) new_values = gcal_schedule_values_set_start_date_time (values, new_start);
g_assert (g_date_time_equal (new_values->date_start, new_start));
g_assert (g_date_time_equal (new_values->date_end, expected_end));
}
static void
test_setting_end_datetime_preserves_start_timezone (void)
{
/* Start with
* start = 10:00, UTC-6
* end = 11:30, UTC-5 (note the different timezone; event is 30 minutes long)
*
* Set the end date to 09:30, UTC-5
*
* Start date should be 07:30, UTC-6 (set to one hour earlier than the end time, with the original start's tz)
*
* (imagine driving for one hour while crossing timezones)
*/
g_autoptr (GcalScheduleValues) values = values_with_date_times("20250303T10:00:00-06:00",
"20250303T11:30:00-05:00");
g_assert (g_date_time_compare (values->date_start, values->date_end) == -1);
g_autoptr (GDateTime) new_end = g_date_time_new_from_iso8601 ("20250303T09:30:00-05:00", NULL);
g_autoptr (GDateTime) expected_start = g_date_time_new_from_iso8601 ("20250303T07:30:00-06:00", NULL);
g_autoptr (GcalScheduleValues) new_values = gcal_schedule_values_set_end_date_time (values, new_end);
g_assert (g_date_time_equal (new_values->date_end, new_end));
g_assert (g_date_time_equal (new_values->date_start, expected_start));
}
static void
test_all_day_displays_sensible_dates_and_roundtrips (void)
{
/* Start with this, per iCalendar's convention for a single-day all-day event:
*
* start = midnight
* end = next midnight
* all_day = true
*
* Then, pass that on to the widgets, and check that they display the same date for start/end,
* since users see "all day, start=2025/03/03, end=2025/03/03" and think sure, a single all-day.
*
* Then, change the end date to one day later, to get a two-day event.
*
* Check that the values gets back end = (start + 2 days)
*/
g_autoptr (GDateTime) date = g_date_time_new_from_iso8601 ("20250303T00:00:00-06:00", NULL);
g_assert (date != NULL);
g_autoptr (GDateTime) next_day = g_date_time_new_from_iso8601 ("20250304T00:00:00-06:00", NULL);
g_assert (next_day != NULL);
g_autoptr (GcalScheduleValues) values = g_new0 (GcalScheduleValues, 1);
*values = (GcalScheduleValues) {
.all_day = TRUE,
.orig_date_start = g_date_time_ref (date),
.orig_date_end = g_date_time_ref (next_day),
.date_start = g_date_time_ref (date),
.date_end = g_date_time_ref (next_day),
.recur = NULL,
.time_format = GCAL_TIME_FORMAT_24H,
};
/* Compute the widget state from the values; check the displayed dates (should be the same) */
g_autoptr (WidgetState) state = widget_state_from_values (values);
g_assert_true (state->schedule_type_all_day);
g_assert_true (state->date_widgets_visible);
g_assert_false (state->date_time_widgets_visible);
g_assert_cmpint (g_date_time_get_year (state->date_time_start), ==, 2025);
g_assert_cmpint (g_date_time_get_month (state->date_time_start), ==, 3);
g_assert_cmpint (g_date_time_get_day_of_month (state->date_time_start), ==, 3);
g_assert_cmpint (g_date_time_get_year (state->date_time_end), ==, 2025);
g_assert_cmpint (g_date_time_get_month (state->date_time_end), ==, 3);
g_assert_cmpint (g_date_time_get_day_of_month (state->date_time_end), ==, 3);
/* Add a day to the end date */
g_autoptr (GDateTime) displayed_end_date_plus_one_day = g_date_time_new_from_iso8601 ("20250304T00:00:00-06:00", NULL);
g_assert (displayed_end_date_plus_one_day != NULL);
GcalScheduleValues *new_values = gcal_schedule_values_set_end_date (values, displayed_end_date_plus_one_day);
/* Check the real iCalendar value for the new date, should be different from the displayed value */
g_assert_cmpint (g_date_time_get_day_of_month (new_values->date_end), ==, 5);
/* Roundtrip back to widgets and check the displayed value */
g_autoptr (WidgetState) new_state = widget_state_from_values (new_values);
g_assert_cmpint (g_date_time_get_day_of_month (new_state->date_time_end), ==, 4);
}
void
gcal_schedule_section_add_tests (void)
{
g_test_add_func ("/event_editor/schedule_section/setting_start_date_after_end_date_resets_end_date",
test_setting_start_date_after_end_date_resets_end_date);
g_test_add_func ("/event_editor/schedule_section/setting_end_date_before_start_date_resets_start_date",
test_setting_end_date_before_start_date_resets_start_date);
g_test_add_func ("/event_editor/schedule_section/setting_start_datetime_preserves_end_timezone",
test_setting_start_datetime_preserves_end_timezone);
g_test_add_func ("/event_editor/schedule_section/setting_end_datetime_preserves_start_timezone",
test_setting_end_datetime_preserves_start_timezone);
g_test_add_func ("/event_editor/schedule_section/all_day_displays_sensible_dates_and_roundtrips",
test_all_day_displays_sensible_dates_and_roundtrips);
}