diff --git a/build-aux/org.gnome.Calendar.json b/build-aux/org.gnome.Calendar.json index f19d23c3156d4953e768b83eafd72a42dc81baa2..a812c11f347908ff0bd288b3772447868c3455e8 100644 --- a/build-aux/org.gnome.Calendar.json +++ b/build-aux/org.gnome.Calendar.json @@ -166,6 +166,7 @@ { "name" : "gnome-calendar", "buildsystem" : "meson", + "run-tests": true, "sources" : [ { "type" : "dir", diff --git a/src/core/gcal-event.c b/src/core/gcal-event.c index 7f675123cb1b05cc5935f35a55e343fe1ecca58c..ea58686f36b987ef43627a191c7b6b6b44bc9ab9 100644 --- a/src/core/gcal-event.c +++ b/src/core/gcal-event.c @@ -1692,159 +1692,6 @@ gcal_event_get_recurrence (GcalEvent *self) return self->recurrence; } -/** - * gcal_event_get_original_timezones: - * @self: a #GcalEvent - * @out_start_timezone: (direction out)(nullable): the return location for the - * previously stored start date timezone - * @out_end_timezone: (direction out)(nullable): the return location for the - * previously stored end date timezone - * - * Retrieves the start and end date timezones previously stored by - * gcal_event_save_original_timezones(). - * - * If gcal_event_save_original_timezones() wasn't called before, - * it will use local timezones instead. - */ -void -gcal_event_get_original_timezones (GcalEvent *self, - GTimeZone **out_start_timezone, - GTimeZone **out_end_timezone) -{ - g_autoptr (GTimeZone) original_start_tz = NULL; - g_autoptr (GTimeZone) original_end_tz = NULL; - ICalComponent *icalcomp; - ICalProperty *property; - - g_return_if_fail (GCAL_IS_EVENT (self)); - g_assert (self->component != NULL); - - icalcomp = e_cal_component_get_icalcomponent (self->component); - - for (property = i_cal_component_get_first_property (icalcomp, I_CAL_X_PROPERTY); - property; - g_object_unref (property), property = i_cal_component_get_next_property (icalcomp, I_CAL_X_PROPERTY)) - { - const gchar *value; - - if (original_start_tz && original_end_tz) - { - g_object_unref (property); - break; - } - - if (g_strcmp0 (i_cal_property_get_x_name (property), "X-GNOME-CALENDAR-ORIGINAL-TZ-START") == 0) - { - value = i_cal_property_get_x (property); - original_start_tz = g_time_zone_new_identifier (value); - - GCAL_TRACE_MSG ("Found X-GNOME-CALENDAR-ORIGINAL-TZ-START=%s", value); - } - else if (g_strcmp0 (i_cal_property_get_x_name (property), "X-GNOME-CALENDAR-ORIGINAL-TZ-END") == 0) - { - value = i_cal_property_get_x (property); - original_end_tz = g_time_zone_new_identifier (value); - - GCAL_TRACE_MSG ("Found X-GNOME-CALENDAR-ORIGINAL-TZ-END=%s", value); - } - } - - if (!original_start_tz) - original_start_tz = g_time_zone_new_local (); - - if (!original_end_tz) - original_end_tz = g_time_zone_new_local (); - - g_assert (original_start_tz != NULL); - g_assert (original_end_tz != NULL); - - if (out_start_timezone) - *out_start_timezone = g_steal_pointer (&original_start_tz); - - if (out_end_timezone) - *out_end_timezone = g_steal_pointer (&original_end_tz); -} - -/** - * gcal_event_save_original_timezones: - * @self: a #GcalEvent - * - * Stores the current timezones of the start and end dates of @self - * into separate, GNOME Calendar specific fields. These fields can - * be used by gcal_event_get_original_timezones() to retrieve the - * previous timezones of the event later. - */ -void -gcal_event_save_original_timezones (GcalEvent *self) -{ - ICalComponent *icalcomp; - ICalProperty *property; - GTimeZone *tz; - gboolean has_original_start_tz; - gboolean has_original_end_tz; - - GCAL_ENTRY; - - g_return_if_fail (GCAL_IS_EVENT (self)); - g_assert (self->component != NULL); - - has_original_start_tz = FALSE; - has_original_end_tz = FALSE; - icalcomp = e_cal_component_get_icalcomponent (self->component); - - for (property = i_cal_component_get_first_property (icalcomp, I_CAL_X_PROPERTY); - property && (!has_original_start_tz || !has_original_end_tz); - g_object_unref (property), property = i_cal_component_get_next_property (icalcomp, I_CAL_X_PROPERTY)) - { - if (g_strcmp0 (i_cal_property_get_x_name (property), "X-GNOME-CALENDAR-ORIGINAL-TZ-START") == 0) - { - tz = g_date_time_get_timezone (self->dt_start); - i_cal_property_set_x (property, g_time_zone_get_identifier (tz)); - - GCAL_TRACE_MSG ("Reusing X-GNOME-CALENDAR-ORIGINAL-TZ-START property with %s", i_cal_property_get_x (property)); - - has_original_start_tz = TRUE; - } - else if (g_strcmp0 (i_cal_property_get_x_name (property), "X-GNOME-CALENDAR-ORIGINAL-TZ-END") == 0) - { - tz = g_date_time_get_timezone (self->dt_end); - i_cal_property_set_x (property, g_time_zone_get_identifier (tz)); - - GCAL_TRACE_MSG ("Reusing X-GNOME-CALENDAR-ORIGINAL-TZ-END property with %s", i_cal_property_get_x (property)); - - has_original_end_tz = TRUE; - } - } - - g_clear_object (&property); - - if (!has_original_start_tz) - { - tz = g_date_time_get_timezone (self->dt_start); - property = i_cal_property_new_x (g_time_zone_get_identifier (tz)); - i_cal_property_set_x_name (property, "X-GNOME-CALENDAR-ORIGINAL-TZ-START"); - - GCAL_TRACE_MSG ("Added new X-GNOME-CALENDAR-ORIGINAL-TZ-START property with %s", i_cal_property_get_x (property)); - - i_cal_component_take_property (icalcomp, property); - } - - if (!has_original_end_tz) - { - tz = g_date_time_get_timezone (self->dt_end); - property = i_cal_property_new_x (g_time_zone_get_identifier (tz)); - i_cal_property_set_x_name (property, "X-GNOME-CALENDAR-ORIGINAL-TZ-END"); - - GCAL_TRACE_MSG ("Added new X-GNOME-CALENDAR-ORIGINAL-TZ-END property with %s", i_cal_property_get_x (property)); - - i_cal_component_take_property (icalcomp, property); - } - - e_cal_component_commit_sequence (self->component); - - GCAL_EXIT; -} - /** * gcal_event_format_date: * @self: a #GcalEvent diff --git a/src/core/gcal-event.h b/src/core/gcal-event.h index 52fafe191bcd8e27d0f26ea847e963778bee2859..b9adbda6d584e3e5fc7dee1fb67041a3aa61e8e0 100644 --- a/src/core/gcal-event.h +++ b/src/core/gcal-event.h @@ -128,12 +128,6 @@ void gcal_event_set_recurrence (GcalEvent GcalRecurrence* gcal_event_get_recurrence (GcalEvent *self); -void gcal_event_save_original_timezones (GcalEvent *self); - -void gcal_event_get_original_timezones (GcalEvent *self, - GTimeZone **start_tz, - GTimeZone **end_tz); - gchar* gcal_event_format_date (GcalEvent *self); gboolean gcal_event_overlaps (GcalEvent *self, diff --git a/src/gui/event-editor/gcal-event-schedule.c b/src/gui/event-editor/gcal-event-schedule.c new file mode 100644 index 0000000000000000000000000000000000000000..ed4694dce96b86a7e2cf2680577fe296ae72f604 --- /dev/null +++ b/src/gui/event-editor/gcal-event-schedule.c @@ -0,0 +1,654 @@ +/* gcal-event-schedule.c - immutable struct for the date and recurrence values of an event + * + * Copyright 2025 Federico Mena Quintero + * + * 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GcalEventSchedule" + +#include "gcal-date-time-utils.h" +#include "gcal-debug.h" +#include "gcal-event.h" +#include "gcal-recurrence.h" +#include "gcal-event-schedule.h" + +GcalScheduleValues +gcal_schedule_values_copy (const GcalScheduleValues *values) +{ + GcalScheduleValues copy; + + copy.all_day = values->all_day; + 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_copy (values->recur) : NULL; + + return copy; +} + +static GcalEventSchedule * +gcal_event_schedule_copy (const GcalEventSchedule *values) +{ + GcalEventSchedule *copy = g_new0 (GcalEventSchedule, 1); + + copy->orig = gcal_schedule_values_copy (&values->orig); + copy->curr = gcal_schedule_values_copy (&values->curr); + copy->time_format = values->time_format; + + return copy; +} + +static 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); +} + +/** + * gcal_event_schedule_free(): + * + * Frees the contents of @values. + */ +void +gcal_event_schedule_free (GcalEventSchedule *values) +{ + gcal_schedule_values_free (&values->orig); + gcal_schedule_values_free (&values->curr); + g_free (values); +} + +GcalEventSchedule* +gcal_event_schedule_set_all_day (const GcalEventSchedule *values, gboolean all_day) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + if (all_day == values->curr.all_day) + { + return copy; + } + + if (all_day) + { + /* We are switching from a time-slot event to an all-day one. If we had + * + * date_start = $day1 + $time + * date_end = $day2 + $time + * + * we want to switch to + * + * date_start = $day1 + 00:00 (midnight) + * date_end = $day2 + 1 day + 00:00 (midnight of the next day) + */ + + GDateTime *start = values->curr.date_start; + GDateTime *end = values->curr.date_end; + + g_autoptr (GDateTime) new_start = g_date_time_new (g_date_time_get_timezone (start), + g_date_time_get_year (start), + g_date_time_get_month (start), + g_date_time_get_day_of_month (start), + 0, + 0, + 0.0); + + g_autoptr (GDateTime) tmp = g_date_time_new (g_date_time_get_timezone (end), + g_date_time_get_year (end), + g_date_time_get_month (end), + g_date_time_get_day_of_month (end), + 0, + 0, + 0.0); + g_autoptr (GDateTime) new_end = g_date_time_add_days (tmp, 1); + + gcal_set_date_time (©->curr.date_start, new_start); + gcal_set_date_time (©->curr.date_end, new_end); + } + else + { + /* We are switching from an all-day event to a time-slot one. In this case, + * we want to preserve the current dates, but restore the times of the original + * event. + */ + + GDateTime *start = values->curr.date_start; + GDateTime *end = values->curr.date_end; + + g_autoptr (GDateTime) new_start = g_date_time_new (g_date_time_get_timezone (start), + g_date_time_get_year (start), + g_date_time_get_month (start), + g_date_time_get_day_of_month (start), + g_date_time_get_hour (values->orig.date_start), + g_date_time_get_minute (values->orig.date_start), + g_date_time_get_seconds (values->orig.date_start)); + + g_autoptr (GDateTime) tmp = g_date_time_new (g_date_time_get_timezone (end), + g_date_time_get_year (end), + g_date_time_get_month (end), + g_date_time_get_day_of_month (end), + g_date_time_get_hour (values->orig.date_end), + g_date_time_get_minute (values->orig.date_end), + g_date_time_get_seconds (values->orig.date_end)); + + g_autoptr (GDateTime) new_end = g_date_time_add_days (tmp, -1); + + gcal_set_date_time (©->curr.date_start, new_start); + gcal_set_date_time (©->curr.date_end, new_end); + } + + copy->curr.all_day = all_day; + return copy; +} + +GcalEventSchedule * +gcal_event_schedule_set_time_format (const GcalEventSchedule *values, GcalTimeFormat time_format) +{ + GcalEventSchedule *copy = gcal_event_schedule_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. + */ +GcalEventSchedule * +gcal_event_schedule_set_start_date (const GcalEventSchedule *values, GDateTime *start) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + gcal_set_date_time (©->curr.date_start, start); + + if (g_date_time_compare (start, copy->curr.date_end) == 1) + { + gcal_set_date_time (©->curr.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. + */ +GcalEventSchedule * +gcal_event_schedule_set_end_date (const GcalEventSchedule *values, GDateTime *end) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + if (copy->curr.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 (©->curr.date_end, fake_end_date); + } + else + { + gcal_set_date_time (©->curr.date_end, end); + } + + if (g_date_time_compare (copy->curr.date_start, end) == 1) + { + gcal_set_date_time (©->curr.date_start, end); + } + + return copy; +} + +GcalEventSchedule * +gcal_event_schedule_set_start_date_time (const GcalEventSchedule *values, GDateTime *start) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + gcal_set_date_time (©->curr.date_start, start); + + if (g_date_time_compare (start, copy->curr.date_end) == 1) + { + GTimeZone *end_tz = g_date_time_get_timezone (copy->curr.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 (©->curr.date_end, new_end_in_end_tz); + } + + return copy; +} + +GcalEventSchedule * +gcal_event_schedule_set_end_date_time (const GcalEventSchedule *values, GDateTime *end) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + gcal_set_date_time (©->curr.date_end, end); + + if (g_date_time_compare (copy->curr.date_start, end) == 1) + { + GTimeZone *start_tz = g_date_time_get_timezone (copy->curr.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 (©->curr.date_start, new_start_in_start_tz); + } + + return copy; +} + +static GcalRecurrence * +recur_change_frequency (GcalRecurrence *old_recur, GcalRecurrenceFrequency frequency) +{ + if (frequency == GCAL_RECURRENCE_NO_REPEAT) + { + /* Invariant: GCAL_RECURRENCE_NO_REPEAT is reduced to a NULL recurrence */ + return NULL; + } + else + { + GcalRecurrence *new_recur; + + if (old_recur) + { + new_recur = gcal_recurrence_copy (old_recur); + } + else + { + new_recur = gcal_recurrence_new (); + } + + new_recur->frequency = frequency; + + return new_recur; + } +} + +static GcalRecurrence * +recur_change_limit_type (GcalRecurrence *old_recur, GcalRecurrenceLimitType limit_type, GDateTime *date_start) +{ + /* An old recurrence would already be present with something other than GCAL_RECURRENCE_NO_REPEAT */ + g_assert (old_recur != NULL); + g_assert (old_recur->frequency != GCAL_RECURRENCE_NO_REPEAT); + + GcalRecurrence *new_recur = gcal_recurrence_copy (old_recur); + + if (limit_type == old_recur->limit_type) + { + return new_recur; + } + else + { + new_recur->limit_type = limit_type; + + switch (limit_type) + { + case GCAL_RECURRENCE_FOREVER: + break; + + case GCAL_RECURRENCE_COUNT: + /* Other policies are possible. This is just "more than once". */ + new_recur->limit.count = 2; + break; + + case GCAL_RECURRENCE_UNTIL: + /* Again, other policies are possible. For now, leave the decision to the user. */ + g_assert (date_start != NULL); + new_recur->limit.until = g_date_time_ref (date_start); + break; + + default: + g_assert_not_reached (); + } + } + + return new_recur; +} + +GcalEventSchedule * +gcal_event_schedule_set_recur_frequency (const GcalEventSchedule *values, + GcalRecurrenceFrequency frequency) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + GcalRecurrence *new_recur = recur_change_frequency (copy->curr.recur, frequency); + g_clear_pointer (©->curr.recur, gcal_recurrence_unref); + copy->curr.recur = new_recur; + + return copy; +} + +GcalEventSchedule * +gcal_event_schedule_set_recur_limit_type (const GcalEventSchedule *values, + GcalRecurrenceLimitType limit_type) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + GcalRecurrence *new_recur = recur_change_limit_type (copy->curr.recur, limit_type, values->curr.date_start); + g_clear_pointer (©->curr.recur, gcal_recurrence_unref); + copy->curr.recur = new_recur; + + return copy; +} + +GcalEventSchedule * +gcal_event_schedule_set_recurrence_count (const GcalEventSchedule *values, + guint count) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + /* An old recurrence would already be present */ + g_assert (copy->curr.recur != NULL); + g_assert (copy->curr.recur->frequency != GCAL_RECURRENCE_NO_REPEAT); + g_assert (copy->curr.recur->limit_type == GCAL_RECURRENCE_COUNT); + + copy->curr.recur->limit.count = count; + + return copy; +} + +GcalEventSchedule * +gcal_event_schedule_set_recurrence_until (const GcalEventSchedule *values, + GDateTime *until) +{ + GcalEventSchedule *copy = gcal_event_schedule_copy (values); + + /* An old recurrence would already be present */ + g_assert (copy->curr.recur != NULL); + g_assert (copy->curr.recur->frequency != GCAL_RECURRENCE_NO_REPEAT); + g_assert (copy->curr.recur->limit_type == GCAL_RECURRENCE_UNTIL); + + gcal_set_date_time (©->curr.recur->limit.until, until); + + return copy; +} + +/** + * Extracts the values from @event that are needed to populate #GcalScheduleSection. + * + * Returns: a #GcalEventSchedule ready for use. Free it with + * gcal_event_schedule_free(). + */ +GcalEventSchedule * +gcal_event_schedule_from_event (GcalEvent *event, + GcalTimeFormat time_format) +{ + GcalScheduleValues values; + memset (&values, 0, sizeof (values)); + + if (event) + { + GcalRecurrence *recur = gcal_event_get_recurrence (event); + + values.all_day = gcal_event_get_all_day (event); + values.date_start = g_date_time_ref (gcal_event_get_date_start (event)); + values.date_end = g_date_time_ref (gcal_event_get_date_end (event)); + + if (recur) + { + values.recur = gcal_recurrence_copy (recur); + } + else + { + values.recur = NULL; + } + } + + GcalEventSchedule *section_values = g_new0 (GcalEventSchedule, 1); + + *section_values = (GcalEventSchedule) { + .orig = gcal_schedule_values_copy (&values), + .curr = values, + .time_format = time_format, + }; + + return section_values; +} + +/* Builds a new GcalscheduleSectgionValues from two ISO 8601 date-times; be sure to + * include your timezone if you need it. This function is just to be used from tests. + **/ +static GcalScheduleValues +values_with_date_times (const char *start, const char *end, gboolean all_day) +{ + 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 = { + .all_day = all_day, + .date_start = g_date_time_ref (start_date), + .date_end = g_date_time_ref (end_date), + .recur = NULL, + }; + + return values; +} + +/* This is just for writing tests */ +GcalEventSchedule * +gcal_event_schedule_with_date_times (const char *start, const char *end, gboolean all_day) +{ + GcalScheduleValues values = values_with_date_times (start, end, all_day); + + GcalEventSchedule *section_values = g_new0 (GcalEventSchedule, 1); + + *section_values = (GcalEventSchedule) { + .orig = gcal_schedule_values_copy (&values), + .curr = values, + .time_format = GCAL_TIME_FORMAT_24H, + }; + + return section_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 (GcalEventSchedule) values = gcal_event_schedule_with_date_times("20250303T10:00:00-06:00", + "20250303T11:00:00-06:00", + FALSE); + + g_autoptr (GDateTime) two_hours_later = g_date_time_new_from_iso8601 ("20250303T12:00:00-06:00", NULL); + + g_autoptr (GcalEventSchedule) new_values = gcal_event_schedule_set_start_date (values, two_hours_later); + g_assert (g_date_time_equal (new_values->curr.date_start, two_hours_later)); + g_assert (g_date_time_equal (new_values->curr.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 (GcalEventSchedule) values = gcal_event_schedule_with_date_times("20250303T10:00:00-06:00", + "20250303T11:00:00-06:00", + FALSE); + + g_autoptr (GDateTime) two_hours_earlier = g_date_time_new_from_iso8601 ("20250303T09:00:00-06:00", NULL); + + g_autoptr (GcalEventSchedule) new_values = gcal_event_schedule_set_end_date (values, two_hours_earlier); + g_assert (g_date_time_equal (new_values->curr.date_start, two_hours_earlier)); + g_assert (g_date_time_equal (new_values->curr.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 (GcalEventSchedule) values = gcal_event_schedule_with_date_times("20250303T10:00:00-06:00", + "20250303T11:30:00-05:00", + FALSE); + g_assert (g_date_time_compare (values->curr.date_start, values->curr.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 (GcalEventSchedule) new_values = gcal_event_schedule_set_start_date_time (values, new_start); + g_assert (g_date_time_equal (new_values->curr.date_start, new_start)); + + g_assert (g_date_time_equal (new_values->curr.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 (GcalEventSchedule) values = gcal_event_schedule_with_date_times("20250303T10:00:00-06:00", + "20250303T11:30:00-05:00", + FALSE); + g_assert (g_date_time_compare (values->curr.date_start, values->curr.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 (GcalEventSchedule) new_values = gcal_event_schedule_set_end_date_time (values, new_end); + g_assert (g_date_time_equal (new_values->curr.date_end, new_end)); + + g_assert (g_date_time_equal (new_values->curr.date_start, expected_start)); +} + +static void +test_43_switching_to_all_day_preserves_timezones (void) +{ + g_test_bug ("43"); + /* https://gitlab.gnome.org/GNOME/gnome-calendar/-/issues/43 + * + * Given an event that is not all-day, e.g. from $day1-12:00 to $day2-13:00, if one turns it to an + * all-day event, we want to switch the dates like this: + * + * start: $day1's 00:00 + * end: $day2's 24:00 (e.g. ($day2 + 1)'s 00:00) + * + * Also test that after that, changing the dates and turning off all-day restores the + * original times. + */ + + g_autoptr (GcalEventSchedule) values = gcal_event_schedule_with_date_times ("20250303T12:00:00-06:00", + "20250304T13:00:00-06:00", + FALSE); + + /* set all-day and check that this modified the times to midnight */ + + g_autoptr (GcalEventSchedule) all_day_values = gcal_event_schedule_set_all_day (values, TRUE); + + g_autoptr (GDateTime) expected_start_date = g_date_time_new_from_iso8601 ("20250303T00:00:00-06:00", NULL); + g_autoptr (GDateTime) expected_end_date = g_date_time_new_from_iso8601 ("20250305T00:00:00-06:00", NULL); + + g_assert (g_date_time_equal (all_day_values->curr.date_start, expected_start_date)); + g_assert (g_date_time_equal (all_day_values->curr.date_end, expected_end_date)); + + /* turn off all-day; check that times were restored */ + + g_autoptr (GcalEventSchedule) not_all_day_values = gcal_event_schedule_set_all_day (all_day_values, FALSE); + + g_autoptr (GDateTime) expected_start_date2 = g_date_time_new_from_iso8601 ("20250303T12:00:00-06:00", NULL); + g_autoptr (GDateTime) expected_end_date2 = g_date_time_new_from_iso8601 ("20250304T13:00:00-06:00", NULL); + + g_assert (g_date_time_equal (not_all_day_values->curr.date_start, expected_start_date2)); + g_assert (g_date_time_equal (not_all_day_values->curr.date_end, expected_end_date2)); +} + +static void +test_recur_changes_to_no_repeat (void) +{ + g_autoptr (GcalRecurrence) old_recur = gcal_recurrence_new (); + old_recur->frequency = GCAL_RECURRENCE_WEEKLY; + + g_autoptr (GcalRecurrence) new_recur = recur_change_frequency (old_recur, GCAL_RECURRENCE_NO_REPEAT); + g_assert_null (new_recur); +} + +static void +test_recur_changes_limit_type_to_count (void) +{ + g_autoptr (GcalRecurrence) old_recur = gcal_recurrence_new (); + old_recur->frequency = GCAL_RECURRENCE_WEEKLY; + + g_autoptr (GcalRecurrence) new_recur = recur_change_limit_type (old_recur, GCAL_RECURRENCE_COUNT, NULL); + g_assert_cmpint (new_recur->limit_type, ==, GCAL_RECURRENCE_COUNT); + g_assert_cmpint (new_recur->limit.count, ==, 2); +} + +static void +test_recur_changes_limit_type_to_until (void) +{ + g_autoptr (GcalRecurrence) old_recur = gcal_recurrence_new (); + old_recur->frequency = GCAL_RECURRENCE_WEEKLY; + + g_autoptr (GDateTime) date = g_date_time_new_from_iso8601 ("20250411T00:00:00-06:00", NULL); + + g_autoptr (GcalRecurrence) new_recur = recur_change_limit_type (old_recur, GCAL_RECURRENCE_UNTIL, date); + g_assert_cmpint (new_recur->limit_type, ==, GCAL_RECURRENCE_UNTIL); + g_assert (g_date_time_equal (new_recur->limit.until, date)); +} + +void +gcal_event_schedule_add_tests (void) +{ + g_test_add_func ("/event_editor/event_schedule/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/event_schedule/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/event_schedule/setting_start_datetime_preserves_end_timezone", + test_setting_start_datetime_preserves_end_timezone); + g_test_add_func ("/event_editor/event_schedule/setting_end_datetime_preserves_start_timezone", + test_setting_end_datetime_preserves_start_timezone); + g_test_add_func ("/event_editor/event_schedule/43_switching_to_all_day_preserves_timezones", + test_43_switching_to_all_day_preserves_timezones); + g_test_add_func ("/event_editor/event_schedule/recur_changes_to_no_repeat", + test_recur_changes_to_no_repeat); + g_test_add_func ("/event_editor/event_schedule/recur_changes_limit_type_to_count", + test_recur_changes_limit_type_to_count); + g_test_add_func ("/event_editor/event_schedule/recur_changes_limit_type_to_until", + test_recur_changes_limit_type_to_until); +} diff --git a/src/gui/event-editor/gcal-event-schedule.h b/src/gui/event-editor/gcal-event-schedule.h new file mode 100644 index 0000000000000000000000000000000000000000..a7a83c520afbaa29c0deb9e20ebef495e983b4dd --- /dev/null +++ b/src/gui/event-editor/gcal-event-schedule.h @@ -0,0 +1,94 @@ +/* gcal-event-schedule.h - immutable struct for the date and recurrence values of an event + * + * Copyright 2025 Federico Mena Quintero + * + * 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "gcal-enums.h" +#include "gcal-event.h" +#include "gcal-recurrence.h" + +G_BEGIN_DECLS + +/* Values from a GcalEvent that this widget can manipulate. + * + * We keep an immutable copy of the original event's values, and later generate + * a new GcalScheduleValues with the updated data from the widgetry. + */ +typedef struct +{ + gboolean all_day; + + GDateTime *date_start; + GDateTime *date_end; + + GcalRecurrence *recur; +} GcalScheduleValues; + +typedef struct +{ + /* original values from event */ + GcalScheduleValues orig; + + /* current values, as modified by actions from widgets */ + GcalScheduleValues curr; + + /* copied from GcalContext to avoid a dependency on it */ + GcalTimeFormat time_format; +} GcalEventSchedule; + +GcalScheduleValues gcal_schedule_values_copy (const GcalScheduleValues *values); + + +GcalEventSchedule *gcal_event_schedule_from_event (GcalEvent *event, + GcalTimeFormat time_format); +void gcal_event_schedule_free (GcalEventSchedule *values); + +GcalEventSchedule *gcal_event_schedule_set_all_day (const GcalEventSchedule *values, + gboolean all_day); +GcalEventSchedule *gcal_event_schedule_set_time_format (const GcalEventSchedule *values, + GcalTimeFormat time_format); +GcalEventSchedule *gcal_event_schedule_set_start_date (const GcalEventSchedule *values, + GDateTime *start); +GcalEventSchedule *gcal_event_schedule_set_end_date (const GcalEventSchedule *values, + GDateTime *end); +GcalEventSchedule *gcal_event_schedule_set_start_date_time (const GcalEventSchedule *values, + GDateTime *start); +GcalEventSchedule *gcal_event_schedule_set_end_date_time (const GcalEventSchedule *values, + GDateTime *end); +GcalEventSchedule *gcal_event_schedule_set_recur_frequency (const GcalEventSchedule *values, + GcalRecurrenceFrequency frequency); +GcalEventSchedule *gcal_event_schedule_set_recur_limit_type (const GcalEventSchedule *values, + GcalRecurrenceLimitType limit_type); +GcalEventSchedule *gcal_event_schedule_set_recurrence_count (const GcalEventSchedule *values, + guint count); +GcalEventSchedule *gcal_event_schedule_set_recurrence_until (const GcalEventSchedule *values, + GDateTime *until); + +GcalEventSchedule *gcal_event_schedule_with_date_times (const char *start, + const char *end, + gboolean all_day); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GcalEventSchedule, gcal_event_schedule_free); + +/* Tests */ + +void gcal_event_schedule_add_tests (void); + +G_END_DECLS diff --git a/src/gui/event-editor/gcal-schedule-section.blp b/src/gui/event-editor/gcal-schedule-section.blp index 8baf77ef981324ffc69535eaaab1bdc178482e60..fcd450f702ad3cd015c742390134224a6029e996 100644 --- a/src/gui/event-editor/gcal-schedule-section.blp +++ b/src/gui/event-editor/gcal-schedule-section.blp @@ -94,6 +94,7 @@ template $GcalScheduleSection: Box { numeric: true; adjustment: number_of_occurrences_adjustment; valign: center; + value-changed => $on_number_of_occurrences_changed_cb() swapped; } } @@ -105,6 +106,7 @@ template $GcalScheduleSection: Box { $GcalDateSelector until_date_selector { visible: false; valign: center; + notify::date => $on_until_date_changed_cb() swapped; } } } diff --git a/src/gui/event-editor/gcal-schedule-section.c b/src/gui/event-editor/gcal-schedule-section.c index d5f33498a0e70dfab3d8a5528b33e7f0f2f24255..b9bea1d10e91d9bcbf063745ba18ab633acfe66f 100644 --- a/src/gui/event-editor/gcal-schedule-section.c +++ b/src/gui/event-editor/gcal-schedule-section.c @@ -27,6 +27,7 @@ #include "gcal-debug.h" #include "gcal-event.h" #include "gcal-event-editor-section.h" +#include "gcal-event-schedule.h" #include "gcal-recurrence.h" #include "gcal-schedule-section.h" #include "gcal-utils.h" @@ -37,6 +38,8 @@ struct _GcalScheduleSection { GtkBox parent; + GcalEventSchedule *values; + AdwToggleGroup *schedule_type_toggle_group; AdwPreferencesGroup *start_date_group; GcalDateChooserRow *start_date_row; @@ -55,6 +58,41 @@ struct _GcalScheduleSection GcalEventEditorFlags flags; }; +/* Desired state of the widgets, computed from GcalEventSchedule. + * + * There is some subtle logic to decide how to update the widgets based on how + * each of them causes the GcalEventSchedule 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 + * GcalEventSchedule. See the comment in widget_state_from_values(). + */ + GDateTime *date_time_start; + GDateTime *date_time_end; + + struct { + gboolean duration_combo_visible; + gboolean number_of_occurrences_visible; + gboolean until_date_visible; + GcalRecurrenceFrequency frequency; + GcalRecurrenceLimitType limit_type; + guint limit_count; + GDateTime *limit_until; + } recurrence; +} 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, @@ -83,12 +121,119 @@ static void on_end_date_time_changed_cb (GtkWidget GParamSpec *pspec, GcalScheduleSection *self); +static void on_schedule_type_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + GcalScheduleSection *self); + +static void on_number_of_occurrences_changed_cb (GcalScheduleSection *self); +static void on_until_date_changed_cb (GcalScheduleSection *self); + +static WidgetState * +widget_state_from_values (const GcalEventSchedule *values) +{ + WidgetState *state = g_new0 (WidgetState, 1); + + state->schedule_type_all_day = values->curr.all_day; + state->date_widgets_visible = values->curr.all_day; + state->date_time_widgets_visible = !values->curr.all_day; + state->time_format = values->time_format; + + if (values->curr.date_start && values->curr.date_end) + { + state->date_time_start = g_date_time_ref (values->curr.date_start); + + if (values->curr.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 GcalEventSchedule 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->curr.date_end, -1); + } + else + { + state->date_time_end = g_date_time_ref (values->curr.date_end); + } + } + else + { + /* The event editor can be instantiated but not shown yet, and we may get a + * notification that the time_format changed. In that case, the event will not be + * set yet, and so the dates will not be set either - hence the test above for + * date_start and date_end not being NULL. Handle that by setting NULL dates for + * the widgets. + */ + state->date_time_start = NULL; + state->date_time_end = NULL; + } + + if (values->curr.recur && values->curr.recur->frequency != GCAL_RECURRENCE_NO_REPEAT) + { + state->recurrence.duration_combo_visible = TRUE; + state->recurrence.frequency = values->curr.recur->frequency; + state->recurrence.limit_type = values->curr.recur->limit_type; + + switch (values->curr.recur->limit_type) + { + case GCAL_RECURRENCE_COUNT: + state->recurrence.limit_count = values->curr.recur->limit.count; + state->recurrence.number_of_occurrences_visible = TRUE; + break; + + case GCAL_RECURRENCE_UNTIL: + state->recurrence.until_date_visible = TRUE; + state->recurrence.limit_until = g_date_time_ref (values->curr.recur->limit.until); + break; + + case GCAL_RECURRENCE_FOREVER: + state->recurrence.limit_count = 0; + break; + + default: + g_assert_not_reached (); + } + } + else + { + state->recurrence.duration_combo_visible = FALSE; + state->recurrence.number_of_occurrences_visible = FALSE; + state->recurrence.until_date_visible = FALSE; + state->recurrence.frequency = GCAL_RECURRENCE_NO_REPEAT; + state->recurrence.limit_type = GCAL_RECURRENCE_FOREVER; + state->recurrence.limit_count = 0; + state->recurrence.limit_until = NULL; + } + + 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); + + if (state->recurrence.limit_until) + { + g_date_time_unref (state->recurrence.limit_until); + } + + g_free (state); +} /* * Auxiliary methods */ -static inline gboolean +static gboolean all_day_selected (GcalScheduleSection *self) { const gchar *active = adw_toggle_group_get_active_name (self->schedule_type_toggle_group); @@ -128,71 +273,108 @@ remove_recurrence_properties (GcalEvent *event) e_cal_component_commit_sequence (comp); } - -/* - * Callbacks - */ - static void -on_schedule_type_changed_cb (GtkWidget *widget, - GParamSpec *pspec, - GcalScheduleSection *self) +update_widgets (GcalScheduleSection *self, + WidgetState *state) { - gboolean all_day = all_day_selected (self); + g_signal_handlers_block_by_func (self->schedule_type_toggle_group, on_schedule_type_changed_cb, self); + g_signal_handlers_block_by_func (self->number_of_occurrences_spin, on_number_of_occurrences_changed_cb, self); + g_signal_handlers_block_by_func (self->until_date_selector, on_until_date_changed_cb, self); + block_date_signals (self); - gtk_widget_set_visible (GTK_WIDGET (self->start_date_group), all_day); - gtk_widget_set_visible (GTK_WIDGET (self->end_date_group), all_day); - gtk_widget_set_visible (GTK_WIDGET (self->start_date_time_chooser), !all_day); - gtk_widget_set_visible (GTK_WIDGET (self->end_date_time_chooser), !all_day); + adw_toggle_group_set_active_name (self->schedule_type_toggle_group, + state->schedule_type_all_day ? "all-day" : "time-slot"); - block_date_signals (self); + 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); - if (all_day) + 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 */ + if (state->date_time_start) + { + gcal_date_chooser_row_set_date (self->start_date_row, state->date_time_start); + gcal_date_time_chooser_set_date_time (self->start_date_time_chooser, state->date_time_start); + } + + if (state->date_time_end) { - g_autoptr (GDateTime) start_local = NULL; - g_autoptr (GDateTime) end_local = NULL; + gcal_date_chooser_row_set_date (self->end_date_row, state->date_time_end); + gcal_date_time_chooser_set_date_time (self->end_date_time_chooser, state->date_time_end); + } - GDateTime *start = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser); - GDateTime *end = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser); + /* Recurrences */ + gtk_widget_set_visible (self->repeat_duration_combo, state->recurrence.duration_combo_visible); - start_local = g_date_time_to_local (start); - end_local = g_date_time_to_local (end); + adw_combo_row_set_selected (ADW_COMBO_ROW (self->repeat_combo), state->recurrence.frequency); + adw_combo_row_set_selected (ADW_COMBO_ROW (self->repeat_duration_combo), state->recurrence.limit_type); - gcal_date_chooser_row_set_date (self->start_date_row, start_local); - gcal_date_chooser_row_set_date (self->end_date_row, end_local); - gcal_date_time_chooser_set_date_time (self->start_date_time_chooser, start_local); - gcal_date_time_chooser_set_date_time (self->end_date_time_chooser, end_local); + gtk_widget_set_visible (self->until_date_selector, state->recurrence.until_date_visible); + if (state->recurrence.until_date_visible) + { + gcal_date_selector_set_date (GCAL_DATE_SELECTOR (self->until_date_selector), state->recurrence.limit_until); + } + + gtk_widget_set_visible (self->number_of_occurrences_spin, state->recurrence.number_of_occurrences_visible); + if (state->recurrence.number_of_occurrences_visible) + { + g_print ("update widgets: limit_count = %u\n", state->recurrence.limit_count); + gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->number_of_occurrences_spin), state->recurrence.limit_count); } unblock_date_signals (self); + g_signal_handlers_unblock_by_func (self->until_date_selector, on_until_date_changed_cb, self); + g_signal_handlers_unblock_by_func (self->number_of_occurrences_spin, on_number_of_occurrences_changed_cb, self); + g_signal_handlers_unblock_by_func (self->schedule_type_toggle_group, on_schedule_type_changed_cb, self); } +/* Recomputes and sets the widget state. Assumes self->values has already been updated. */ static void -on_start_date_changed_cb (GtkWidget *widget, - GParamSpec *pspec, - GcalScheduleSection *self) +refresh (GcalScheduleSection *self) { - GDateTime *start, *end; - - GCAL_ENTRY; - - block_date_signals (self); + g_autoptr (WidgetState) state = widget_state_from_values (self->values); + update_widgets (self, state); +} - start = gcal_date_chooser_row_get_date (self->start_date_row); - end = gcal_date_chooser_row_get_date (self->end_date_row); +/* 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_event_schedule (GcalScheduleSection *self, + GcalEventSchedule *values) +{ + gcal_event_schedule_free (self->values); + self->values = values; - if (g_date_time_compare (start, end) == 1) - { - gcal_date_chooser_row_set_date (self->end_date_row, start); - gcal_date_time_chooser_set_date (self->end_date_time_chooser, start); - } + refresh (self); +} - // Keep the date row and the date-time chooser in sync - gcal_date_time_chooser_set_date (self->start_date_time_chooser, start); +/* + * Callbacks + */ - unblock_date_signals (self); +static void +on_schedule_type_changed_cb (GtkWidget *widget, + GParamSpec *pspec, + GcalScheduleSection *self) +{ + gboolean all_day = all_day_selected (self); + GcalEventSchedule *updated = gcal_event_schedule_set_all_day (self->values, all_day); + update_from_event_schedule (self, updated); +} - GCAL_EXIT; +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); + GcalEventSchedule *updated = gcal_event_schedule_set_start_date (self->values, start); + update_from_event_schedule (self, updated); } static void @@ -200,27 +382,9 @@ on_end_date_changed_cb (GtkWidget *widget, GParamSpec *pspec, GcalScheduleSection *self) { - GDateTime *start, *end; - - GCAL_ENTRY; - - block_date_signals (self); - - start = gcal_date_chooser_row_get_date (self->start_date_row); - end = gcal_date_chooser_row_get_date (self->end_date_row); - - if (g_date_time_compare (start, end) == 1) - { - gcal_date_chooser_row_set_date (self->start_date_row, end); - gcal_date_time_chooser_set_date (self->start_date_time_chooser, end); - } - - // Keep the date row and the date-time chooser in sync - gcal_date_time_chooser_set_date (self->end_date_time_chooser, end); - - unblock_date_signals (self); - - GCAL_EXIT; + GDateTime *end = gcal_date_chooser_row_get_date (self->end_date_row); + GcalEventSchedule *updated = gcal_event_schedule_set_end_date (self->values, end); + update_from_event_schedule (self, updated); } static void @@ -228,27 +392,9 @@ on_start_date_time_changed_cb (GtkWidget *widget, GParamSpec *pspec, GcalScheduleSection *self) { - GDateTime *start, *end; - - GCAL_ENTRY; - - block_date_signals (self); - - start = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser); - end = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser); - - if (g_date_time_compare (start, end) == 1) - { - GTimeZone *end_tz = g_date_time_get_timezone (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_date_time_chooser_set_date_time (self->end_date_time_chooser, new_end_in_end_tz); - } - - unblock_date_signals (self); - - GCAL_EXIT; + GDateTime *start = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser); + GcalEventSchedule *updated = gcal_event_schedule_set_start_date_time (self->values, start); + update_from_event_schedule (self, updated); } static void @@ -256,27 +402,35 @@ on_end_date_time_changed_cb (GtkWidget *widget, GParamSpec *pspec, GcalScheduleSection *self) { - GDateTime *start, *end; - - GCAL_ENTRY; - - block_date_signals (self); + GDateTime *end = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser); + GcalEventSchedule *updated = gcal_event_schedule_set_end_date_time (self->values, end); + update_from_event_schedule (self, updated); +} - start = gcal_date_time_chooser_get_date_time (self->start_date_time_chooser); - end = gcal_date_time_chooser_get_date_time (self->end_date_time_chooser); +static void +on_number_of_occurrences_changed_cb (GcalScheduleSection *self) +{ + guint count = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (self->number_of_occurrences_spin)); + GcalEventSchedule *updated = gcal_event_schedule_set_recurrence_count (self->values, count); + update_from_event_schedule (self, updated); +} - if (g_date_time_compare (start, end) == 1) - { - GTimeZone *start_tz = g_date_time_get_timezone (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); +static void +on_until_date_changed_cb (GcalScheduleSection *self) +{ + GDateTime *until = gcal_date_selector_get_date (GCAL_DATE_SELECTOR (self->until_date_selector)); + GcalEventSchedule *updated = gcal_event_schedule_set_recurrence_until (self->values, until); + update_from_event_schedule (self, updated); +} - gcal_date_time_chooser_set_date_time (self->start_date_time_chooser, new_start_in_start_tz); - } +static GcalRecurrenceLimitType +get_recurence_limit_type (GcalScheduleSection *self) +{ + guint item = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_duration_combo)); - unblock_date_signals (self); + g_assert (item >= GCAL_RECURRENCE_FOREVER && item <= GCAL_RECURRENCE_UNTIL); - GCAL_EXIT; + return (GcalRecurrenceLimitType) item; } static void @@ -284,10 +438,19 @@ on_repeat_duration_changed_cb (GtkWidget *widget, GParamSpec *pspec, GcalScheduleSection *self) { - gint active = adw_combo_row_get_selected (ADW_COMBO_ROW (widget)); + GcalRecurrenceLimitType limit_type = get_recurence_limit_type (self); + GcalEventSchedule *updated = gcal_event_schedule_set_recur_limit_type (self->values, limit_type); + update_from_event_schedule (self, updated); +} + +static GcalRecurrenceFrequency +get_recurrence_frequency (GcalScheduleSection *self) +{ + guint item = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_combo)); + + g_assert (item >= GCAL_RECURRENCE_NO_REPEAT && item <= GCAL_RECURRENCE_OTHER); - gtk_widget_set_visible (self->number_of_occurrences_spin, active == 1); - gtk_widget_set_visible (self->until_date_selector, active == 2); + return (GcalRecurrenceFrequency) item; } static void @@ -295,23 +458,25 @@ 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); + GcalRecurrenceFrequency frequency = get_recurrence_frequency (self); + GcalEventSchedule *updated = gcal_event_schedule_set_recur_frequency (self->values, frequency); + update_from_event_schedule (self, updated); } static void on_time_format_changed_cb (GcalScheduleSection *self) { - GcalTimeFormat time_format = gcal_context_get_time_format (self->context); + if (self->event) + { + /* Apparently we can be notified that the time format changed, even when + * there has not been an event set yet. This breaks the code downstream. + */ + GcalTimeFormat time_format = gcal_context_get_time_format (self->context); + + GcalEventSchedule *updated = gcal_event_schedule_set_time_format (self->values, time_format); - gcal_date_time_chooser_set_time_format (self->start_date_time_chooser, time_format); - gcal_date_time_chooser_set_time_format (self->end_date_time_chooser, time_format); + update_from_event_schedule (self, updated); + } } @@ -335,17 +500,8 @@ gcal_schedule_section_set_event (GcalEventEditorSection *section, GcalEvent *event, GcalEventEditorFlags flags) { - g_autoptr (GDateTime) date_time_start_utc = NULL; - g_autoptr (GDateTime) date_time_end_utc = NULL; - g_autoptr (GDateTime) date_time_start_local = NULL; - g_autoptr (GDateTime) date_time_end_local = NULL; GcalScheduleSection *self; - GDateTime* date_time_start = NULL; - GDateTime* date_time_end = NULL; - GcalRecurrenceLimitType limit_type; - GcalRecurrenceFrequency frequency; - GcalRecurrence *recur; - gboolean all_day, new_event; + GcalTimeFormat time_format; GCAL_ENTRY; @@ -354,105 +510,43 @@ gcal_schedule_section_set_event (GcalEventEditorSection *section, g_set_object (&self->event, event); self->flags = flags; + time_format = gcal_context_get_time_format (self->context); + self->values = gcal_event_schedule_from_event (event, time_format); + if (!event) GCAL_RETURN (); - all_day = gcal_event_get_all_day (event); - new_event = flags & GCAL_EVENT_EDITOR_FLAG_NEW_EVENT; - - /* schedule type */ - adw_toggle_group_set_active_name (self->schedule_type_toggle_group, - all_day ? "all-day" : "time-slot"); - - gtk_widget_set_visible (GTK_WIDGET (self->start_date_group), all_day); - gtk_widget_set_visible (GTK_WIDGET (self->end_date_group), all_day); - gtk_widget_set_visible (GTK_WIDGET (self->start_date_time_chooser), !all_day); - gtk_widget_set_visible (GTK_WIDGET (self->end_date_time_chooser), !all_day); - - /* retrieve start and end date-times */ - date_time_start = gcal_event_get_date_start (event); - date_time_end = gcal_event_get_date_end (event); - /* - * This is subtracting what has been added in action_button_clicked (). - * See bug 769300. - */ - date_time_end = all_day ? g_date_time_add_days (date_time_end, -1) : date_time_end; - - date_time_start_utc = g_date_time_new_utc (g_date_time_get_year (date_time_start), - g_date_time_get_month (date_time_start), - g_date_time_get_day_of_month (date_time_start), - 0, 0, 0); - - date_time_end_utc = g_date_time_new_utc (g_date_time_get_year (date_time_end), - g_date_time_get_month (date_time_end), - g_date_time_get_day_of_month (date_time_end), - 0, 0, 0); - - date_time_start_local = g_date_time_new_local (g_date_time_get_year (date_time_start), - g_date_time_get_month (date_time_start), - g_date_time_get_day_of_month (date_time_start), - g_date_time_get_hour (date_time_start), - g_date_time_get_minute (date_time_start), - 0); - - date_time_end_local = g_date_time_new_local (g_date_time_get_year (date_time_end), - g_date_time_get_month (date_time_end), - g_date_time_get_day_of_month (date_time_end), - g_date_time_get_hour (date_time_end), - g_date_time_get_minute (date_time_end), - 0); - - block_date_signals (self); - - /* date */ - gcal_date_chooser_row_set_date (self->start_date_row, date_time_start_utc); - gcal_date_chooser_row_set_date (self->end_date_row, date_time_end_utc); - - /* date-time */ - if (new_event || all_day) - { - gcal_date_time_chooser_set_date_time (self->start_date_time_chooser, date_time_start_local); - gcal_date_time_chooser_set_date_time (self->end_date_time_chooser, date_time_end_local); - } - else - { - gcal_date_time_chooser_set_date_time (self->start_date_time_chooser, date_time_start); - gcal_date_time_chooser_set_date_time (self->end_date_time_chooser, date_time_end); - } - - unblock_date_signals (self); + refresh (self); - /* Recurrences */ - recur = gcal_event_get_recurrence (event); - frequency = recur ? recur->frequency : GCAL_RECURRENCE_NO_REPEAT; - limit_type = recur ? recur->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); + GCAL_EXIT; +} - 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); - } +static void +gcal_schedule_section_apply_to_event (GcalScheduleSection *self, + GcalEvent *event) +{ + GCAL_ENTRY; - switch (limit_type) + gcal_event_set_all_day (event, self->values->curr.all_day); + gcal_event_set_date_start (event, self->values->curr.date_start); + gcal_event_set_date_end (event, self->values->curr.date_end); + + /* Only apply the new recurrence if it's different from the old one. + * We don't unconditionally set the recurrence since remove_recurrence_properties() + * will actually remove all the rrules, not just *a* unique one. + * + * That is, here we allow for future support for more than one rrule + * in the event, given the current capabilities of gnome-calendar. + * + */ + if (!gcal_recurrence_is_equal (self->values->curr.recur, gcal_event_get_recurrence (event))) { - case GCAL_RECURRENCE_COUNT: - gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->number_of_occurrences_spin), recur->limit.count); - break; - - case GCAL_RECURRENCE_UNTIL: - gcal_date_selector_set_date (GCAL_DATE_SELECTOR (self->until_date_selector), recur->limit.until); - break; + remove_recurrence_properties (event); - case GCAL_RECURRENCE_FOREVER: - gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->number_of_occurrences_spin), 0); - break; + if (self->values->curr.recur) + { + gcal_event_set_recurrence (event, self->values->curr.recur); + } } GCAL_EXIT; @@ -461,149 +555,71 @@ gcal_schedule_section_set_event (GcalEventEditorSection *section, static void gcal_schedule_section_apply (GcalEventEditorSection *section) { - GDateTime *start_date, *end_date; - GcalRecurrenceFrequency freq; - GcalScheduleSection *self; - GcalRecurrence *old_recur; - gboolean all_day; + GcalScheduleSection *self = GCAL_SCHEDULE_SECTION (section); GCAL_ENTRY; - self = GCAL_SCHEDULE_SECTION (section); - - 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_schedule_section_apply_to_event (self, self->event); -#ifdef GCAL_ENABLE_TRACE - { - g_autofree gchar *start_dt_string = g_date_time_format (start_date, "%x %X %z"); - g_autofree gchar *end_dt_string = g_date_time_format (end_date, "%x %X %z"); + gcal_event_schedule_free (self->values); - g_debug ("New start date: %s", start_dt_string); - g_debug ("New end date: %s", end_dt_string); - } -#endif + GcalTimeFormat time_format = gcal_context_get_time_format (self->context); + self->values = gcal_event_schedule_from_event (self->event, time_format); - gcal_event_set_all_day (self->event, all_day); + GCAL_EXIT; +} - /* - * The end date for multi-day events is exclusive, so we bump it by a day. - * This fixes the discrepancy between the end day of the event and how it - * is displayed in the month view. See bug 769300. - */ - if (all_day) - { - GDateTime *fake_end_date = g_date_time_add_days (end_date, 1); +static gboolean +recurrence_changed (const GcalEventSchedule *values) +{ + return !gcal_recurrence_is_equal (values->orig.recur, values->curr.recur); +} - end_date = fake_end_date; - } +static gboolean +day_changed (const GcalEventSchedule *values) +{ + return (gcal_date_time_compare_date (values->curr.date_start, values->orig.date_start) < 0 || + gcal_date_time_compare_date (values->curr.date_end, values->orig.date_end) > 0); +} - gcal_event_set_date_start (self->event, start_date); - gcal_event_set_date_end (self->event, end_date); +static gboolean +values_changed (const GcalEventSchedule *values) +{ + const GcalScheduleValues *orig = &values->orig; + const GcalScheduleValues *curr = &values->curr; - /* Check Repeat popover and set recurrence-rules accordingly */ - old_recur = gcal_event_get_recurrence (self->event); - freq = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_combo)); + if (orig->all_day != curr->all_day) + return TRUE; - if (freq != GCAL_RECURRENCE_NO_REPEAT) - { - g_autoptr (GcalRecurrence) recur = NULL; + GTimeZone *orig_start_tz, *orig_end_tz, *start_tz, *end_tz; + orig_start_tz = g_date_time_get_timezone (orig->date_start); + orig_end_tz = g_date_time_get_timezone (orig->date_end); + start_tz = g_date_time_get_timezone (curr->date_start); + end_tz = g_date_time_get_timezone (curr->date_end); - recur = gcal_recurrence_new (); - recur->frequency = freq; - recur->limit_type = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_duration_combo)); + if (!g_date_time_equal (orig->date_start, curr->date_start)) + return TRUE; - 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)); + if (g_strcmp0 (g_time_zone_get_identifier (start_tz), g_time_zone_get_identifier (orig_start_tz)) != 0) + return TRUE; - /* Only apply the new recurrence if it's different from the old one */ - if (!gcal_recurrence_is_equal (old_recur, recur)) - { - /* Remove the previous recurrence... */ - remove_recurrence_properties (self->event); + if (!g_date_time_equal (orig->date_end, curr->date_end)) + return TRUE; - /* ... and set the new one */ - gcal_event_set_recurrence (self->event, recur); - } - } - else - { - /* When NO_REPEAT is set, make sure to remove the old recurrent */ - remove_recurrence_properties (self->event); - } + if (g_strcmp0 (g_time_zone_get_identifier (end_tz), g_time_zone_get_identifier (orig_end_tz)) != 0) + return TRUE; - GCAL_EXIT; + return recurrence_changed (values); } 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; + GcalScheduleSection *self = GCAL_SCHEDULE_SECTION (section); GCAL_ENTRY; - self = GCAL_SCHEDULE_SECTION (section); - all_day = all_day_selected (self); - was_all_day = gcal_event_get_all_day (self->event); - - /* 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 = gcal_event_get_date_start (self->event); - prev_end_date = gcal_event_get_date_end (self->event); - - 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, gcal_event_get_date_end (self->event))) - 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)); + GCAL_RETURN (values_changed (self->values)); } static void @@ -624,6 +640,7 @@ gcal_schedule_section_finalize (GObject *object) { GcalScheduleSection *self = (GcalScheduleSection *)object; + g_clear_pointer (&self->values, gcal_event_schedule_free); g_clear_object (&self->context); g_clear_object (&self->event); @@ -667,7 +684,6 @@ gcal_schedule_section_set_property (GObject *object, G_CALLBACK (on_time_format_changed_cb), self, G_CONNECT_SWAPPED); - on_time_format_changed_cb (self); break; default: @@ -709,6 +725,8 @@ gcal_schedule_section_class_init (GcalScheduleSectionClass *klass) 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); + gtk_widget_class_bind_template_callback (widget_class, on_number_of_occurrences_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, on_until_date_changed_cb); } static void @@ -725,49 +743,123 @@ gcal_schedule_section_init (GcalScheduleSection *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 && !gcal_event_get_recurrence (self->event)) - 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, gcal_event_get_recurrence (self->event))); + return recurrence_changed (self->values); } 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; + return day_changed (self->values); +} - all_day = all_day_selected (self); +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) + */ - 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); - } + g_autoptr (GcalEventSchedule) values = gcal_event_schedule_with_date_times ("20250303T00:00:00-06:00", + "20250304T00:00:00-06:00", + TRUE); + + /* 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); - GCAL_RETURN (gcal_date_time_compare_date (start_date, gcal_event_get_date_start (self->event)) < 0 || - gcal_date_time_compare_date (end_date, gcal_event_get_date_end (self->event)) > 0); + /* 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); + g_autoptr (GcalEventSchedule) new_values = gcal_event_schedule_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->curr.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); +} + +static void +test_turning_on_recurrence_count_turns_on_its_widget (void) +{ + /* Start with some configuration */ + g_autoptr (GcalEventSchedule) values = gcal_event_schedule_with_date_times("20250411T10:00:00-06:00", + "20250411T11:30:00-06:00", + FALSE); + + /* Turn on recurrence */ + g_autoptr (GcalEventSchedule) with_recur = + gcal_event_schedule_set_recur_frequency (values, GCAL_RECURRENCE_DAILY); + + /* Set it to "count" */ + g_autoptr (GcalEventSchedule) with_count = + gcal_event_schedule_set_recur_limit_type (with_recur, GCAL_RECURRENCE_COUNT); + + g_autoptr (WidgetState) state = widget_state_from_values (with_count); + g_assert_true (state->recurrence.duration_combo_visible); + g_assert_true (state->recurrence.number_of_occurrences_visible); + g_assert_false (state->recurrence.until_date_visible); +} + +static void +test_turning_on_recurrence_until_turns_on_its_widget (void) +{ + /* Start with some configuration */ + g_autoptr (GcalEventSchedule) values = gcal_event_schedule_with_date_times("20250411T10:00:00-06:00", + "20250411T11:30:00-06:00", + FALSE); + + /* Turn on recurrence */ + g_autoptr (GcalEventSchedule) with_recur = + gcal_event_schedule_set_recur_frequency (values, GCAL_RECURRENCE_DAILY); + + /* Set it to "count" */ + g_autoptr (GcalEventSchedule) with_until = + gcal_event_schedule_set_recur_limit_type (with_recur, GCAL_RECURRENCE_UNTIL); + + g_autoptr (WidgetState) state = widget_state_from_values (with_until); + g_assert_true (state->recurrence.duration_combo_visible); + g_assert_false (state->recurrence.number_of_occurrences_visible); + g_assert_true (state->recurrence.until_date_visible); +} + +void +gcal_schedule_section_add_tests (void) +{ + g_test_add_func ("/event_editor/schedule_section/all_day_displays_sensible_dates_and_roundtrips", + test_all_day_displays_sensible_dates_and_roundtrips); + g_test_add_func ("/event_editor/schedule_section/turning_on_recurrence_count_turns_on_its_widget", + test_turning_on_recurrence_count_turns_on_its_widget); + g_test_add_func ("/event_editor/schedule_section/turning_on_recurrence_until_turns_on_its_widget", + test_turning_on_recurrence_until_turns_on_its_widget); } diff --git a/src/gui/event-editor/gcal-schedule-section.h b/src/gui/event-editor/gcal-schedule-section.h index e817a33b3d77818fb68b22e602e687a2a5843ae1..e17f0be55bd76fbdfd944b599dc8fb1a5448c6af 100644 --- a/src/gui/event-editor/gcal-schedule-section.h +++ b/src/gui/event-editor/gcal-schedule-section.h @@ -23,6 +23,10 @@ #include #include +#include "gcal-enums.h" +#include "gcal-event.h" +#include "gcal-recurrence.h" + G_BEGIN_DECLS #define GCAL_TYPE_SCHEDULE_SECTION (gcal_schedule_section_get_type()) @@ -32,4 +36,9 @@ gboolean gcal_schedule_section_recurrence_changed (GcalScheduleSe gboolean gcal_schedule_section_day_changed (GcalScheduleSection *self); + +/* Tests */ + +void gcal_schedule_section_add_tests (void); + G_END_DECLS diff --git a/src/gui/event-editor/meson.build b/src/gui/event-editor/meson.build index 01ab836430db81109b86948cb6633e304877db4d..52d2c3d118929fd7848b079d07c61c96f393541b 100644 --- a/src/gui/event-editor/meson.build +++ b/src/gui/event-editor/meson.build @@ -39,6 +39,7 @@ sources += files( 'gcal-date-chooser-row.c', 'gcal-date-time-chooser.c', 'gcal-date-selector.c', + 'gcal-event-schedule.c', 'gcal-event-editor-dialog.c', 'gcal-event-editor-section.c', 'gcal-multi-choice.c', diff --git a/src/gui/gcal-event-widget.c b/src/gui/gcal-event-widget.c index fdc71caec6d9675607fb9002934b64a6506976f9..5d5bf997c227307cd6308811d8350a818e6a4f72 100644 --- a/src/gui/gcal-event-widget.c +++ b/src/gui/gcal-event-widget.c @@ -80,8 +80,6 @@ enum { PROP_0, PROP_CONTEXT, - PROP_DATE_END, - PROP_DATE_START, PROP_EVENT, PROP_TIMESTAMP_POLICY, PROP_ORIENTATION, @@ -666,14 +664,6 @@ gcal_event_widget_set_property (GObject *object, G_CONNECT_SWAPPED); break; - case PROP_DATE_END: - gcal_event_widget_set_date_end (self, g_value_get_boxed (value)); - break; - - case PROP_DATE_START: - gcal_event_widget_set_date_start (self, g_value_get_boxed (value)); - break; - case PROP_EVENT: gcal_event_widget_set_event_internal (self, g_value_get_object (value)); break; @@ -709,14 +699,6 @@ gcal_event_widget_get_property (GObject *object, g_value_set_object (value, self->context); break; - case PROP_DATE_END: - g_value_set_boxed (value, self->dt_end); - break; - - case PROP_DATE_START: - g_value_set_boxed (value, self->dt_start); - break; - case PROP_EVENT: g_value_set_object (value, self->event); break; @@ -788,37 +770,6 @@ gcal_event_widget_class_init (GcalEventWidgetClass *klass) "Context", GCAL_TYPE_CONTEXT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); - /** - * GcalEventWidget::date-end: - * - * The end date this widget represents. Notice that this may - * differ from the event's end date. For example, if the event - * spans more than one month and we're in Month View, the end - * date marks the last day this event widget is visible. - */ - g_object_class_install_property (object_class, - PROP_DATE_END, - g_param_spec_boxed ("date-end", - "End date", - "The end date of the widget", - G_TYPE_DATE_TIME, - G_PARAM_READWRITE)); - - /** - * GcalEventWidget::date-start: - * - * The start date this widget represents. Notice that this may - * differ from the event's start date. For example, if the event - * spans more than one month and we're in Month View, the start - * date marks the first day this event widget is visible. - */ - g_object_class_install_property (object_class, - PROP_DATE_START, - g_param_spec_boxed ("date-start", - "Start date", - "The start date of the widget", - G_TYPE_DATE_TIME, - G_PARAM_READWRITE)); /** * GcalEventWidget::event: @@ -965,6 +916,11 @@ gcal_event_widget_get_date_end (GcalEventWidget *self) * Sets the visible end date of this widget. This * may differ from the event's end date, but cannot * be after it. + * + * The end date for this widget may differ from the event's end + * date. For example, if the event spans more than one month and we're + * in Month View, the end date marks the last day this event widget is + * visible. */ void gcal_event_widget_set_date_end (GcalEventWidget *self, @@ -984,8 +940,6 @@ gcal_event_widget_set_date_end (GcalEventWidget *self, self->dt_end = g_date_time_ref (date_end); gcal_event_widget_update_style (self); - - g_object_notify (G_OBJECT (self), "date-end"); } } @@ -1009,11 +963,16 @@ gcal_event_widget_get_date_start (GcalEventWidget *self) /** * gcal_event_widget_set_date_start: * @self: a #GcalEventWidget - * @date_end: the start date of this widget + * @date_start: the start date of this widget * * Sets the visible start date of this widget. This * may differ from the event's start date, but cannot * be before it. + * + * The start date for this widget may differ from the event's start + * date. For example, if the event spans more than one month and we're + * in Month View, the start date marks the first day this event widget + * is visible. */ void gcal_event_widget_set_date_start (GcalEventWidget *self, @@ -1033,8 +992,6 @@ gcal_event_widget_set_date_start (GcalEventWidget *self, self->dt_start = g_date_time_ref (date_start); gcal_event_widget_update_style (self); - - g_object_notify (G_OBJECT (self), "date-start"); } } diff --git a/src/gui/gcal-tests.c b/src/gui/gcal-tests.c new file mode 100644 index 0000000000000000000000000000000000000000..acadea7f0c05caecc93e6314a52de255a920efa9 --- /dev/null +++ b/src/gui/gcal-tests.c @@ -0,0 +1,17 @@ +#include + +#include "gcal-tests.h" +#include "event-editor/gcal-event-schedule.h" +#include "event-editor/gcal-schedule-section.h" + +/* Adds all the tests for the internals of the GUI. + * + * To avoid exporting lots of little test functions from each source file, add a single + * gcal_foo_add_tests() that in turn adds unit tests for that file. + */ +void +gcal_tests_add_internals (void) +{ + gcal_event_schedule_add_tests (); + gcal_schedule_section_add_tests (); +} diff --git a/src/gui/gcal-tests.h b/src/gui/gcal-tests.h new file mode 100644 index 0000000000000000000000000000000000000000..346d13fc9034b627e22acab05a096825ec47f490 --- /dev/null +++ b/src/gui/gcal-tests.h @@ -0,0 +1,3 @@ +#pragma once + +void gcal_tests_add_internals (void); diff --git a/src/gui/meson.build b/src/gui/meson.build index 7fbebd2c25a06e950a22780b904ff3030c2fe8a0..aff98194f82ab391586ee3c1d87d69bb73e4fb43 100644 --- a/src/gui/meson.build +++ b/src/gui/meson.build @@ -52,6 +52,7 @@ sources += files( 'gcal-quick-add-popover.c', 'gcal-search-button.c', 'gcal-sync-indicator.c', + 'gcal-tests.c', 'gcal-weather-settings.c', 'gcal-window.c', ) diff --git a/tests/meson.build b/tests/meson.build index 29aff0a2049e466f24398b52604e0835ccd66534..682fff569cea3fae2981f26015fdce09d1a89c91 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -35,6 +35,7 @@ tests = [ 'daylight-saving', #'discoverer', # https://gitlab.gnome.org/GNOME/gnome-calendar/-/issues/1251 'event', + 'internals', 'range', 'range-tree', #'server', # https://gitlab.gnome.org/GNOME/gnome-calendar/-/issues/1251 diff --git a/tests/test-internals.c b/tests/test-internals.c new file mode 100644 index 0000000000000000000000000000000000000000..239e569c947ee002bfd72c112950fa1b8403b384 --- /dev/null +++ b/tests/test-internals.c @@ -0,0 +1,17 @@ +#include +#include + +#include "gui/gcal-tests.h" + +int +main (int argc, char **argv) +{ + g_setenv ("TZ", "UTC", TRUE); + + g_test_init (&argc, &argv, NULL); + g_test_bug_base ("https://gitlab.gnome.org/GNOME/gnome-calendar/-/issues/"); + + gcal_tests_add_internals (); + + return g_test_run (); +}