Part-of: <https://gitlab.gnome.org/GNOME/gnome-calendar/-/merge_requests/550>
1353 lines
49 KiB
C
1353 lines
49 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;
|
|
|
|
GcalScheduleSectionValues *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;
|
|
|
|
struct {
|
|
gboolean duration_combo_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,
|
|
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;
|
|
|
|
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_ref (values->recur) : NULL;
|
|
|
|
return copy;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_copy (const GcalScheduleSectionValues *values)
|
|
{
|
|
GcalScheduleSectionValues *copy = g_new0 (GcalScheduleSectionValues, 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_schedule_section_values_free():
|
|
*
|
|
* Frees the contents of @values.
|
|
*/
|
|
void
|
|
gcal_schedule_section_values_free (GcalScheduleSectionValues *values)
|
|
{
|
|
gcal_schedule_values_free (&values->orig);
|
|
gcal_schedule_values_free (&values->curr);
|
|
g_free (values);
|
|
}
|
|
|
|
static GcalScheduleSectionValues*
|
|
gcal_schedule_section_values_set_all_day (const GcalScheduleSectionValues *values, gboolean all_day)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_time_format (const GcalScheduleSectionValues *values, GcalTimeFormat time_format)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_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 GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_start_date (const GcalScheduleSectionValues *values, GDateTime *start)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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.
|
|
*/
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_end_date (const GcalScheduleSectionValues *values, GDateTime *end)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_start_date_time (const GcalScheduleSectionValues *values, GDateTime *start)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_end_date_time (const GcalScheduleSectionValues *values, GDateTime *end)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_recur_frequency (const GcalScheduleSectionValues *values,
|
|
GcalRecurrenceFrequency frequency)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_set_recur_limit_type (const GcalScheduleSectionValues *values,
|
|
GcalRecurrenceLimitType limit_type)
|
|
{
|
|
GcalScheduleSectionValues *copy = gcal_schedule_section_values_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;
|
|
}
|
|
|
|
static WidgetState *
|
|
widget_state_from_values (const GcalScheduleSectionValues *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 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->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;
|
|
break;
|
|
|
|
case GCAL_RECURRENCE_UNTIL:
|
|
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.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
|
|
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 */
|
|
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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
/* Recurrences */
|
|
gtk_widget_set_visible (self->repeat_duration_combo, state->recurrence.duration_combo_visible);
|
|
|
|
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);
|
|
|
|
gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->number_of_occurrences_spin), state->recurrence.limit_count);
|
|
|
|
if (state->recurrence.limit_until)
|
|
{
|
|
gcal_date_selector_set_date (GCAL_DATE_SELECTOR (self->until_date_selector), state->recurrence.limit_until);
|
|
}
|
|
|
|
unblock_date_signals (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
|
|
refresh (GcalScheduleSection *self)
|
|
{
|
|
g_autoptr (WidgetState) state = widget_state_from_values (self->values);
|
|
update_widgets (self, state);
|
|
}
|
|
|
|
/* 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_section_values (GcalScheduleSection *self,
|
|
GcalScheduleSectionValues *values)
|
|
{
|
|
gcal_schedule_section_values_free (self->values);
|
|
self->values = values;
|
|
|
|
refresh (self);
|
|
}
|
|
|
|
/*
|
|
* Callbacks
|
|
*/
|
|
|
|
static void
|
|
on_schedule_type_changed_cb (GtkWidget *widget,
|
|
GParamSpec *pspec,
|
|
GcalScheduleSection *self)
|
|
{
|
|
gboolean all_day = all_day_selected (self);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_all_day (self->values, all_day);
|
|
update_from_section_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);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_start_date (self->values, start);
|
|
update_from_section_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);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_end_date (self->values, end);
|
|
update_from_section_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);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_start_date_time (self->values, start);
|
|
update_from_section_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);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_end_date_time (self->values, end);
|
|
update_from_section_values (self, updated);
|
|
}
|
|
|
|
static GcalRecurrenceLimitType
|
|
get_recurence_limit_type (GcalScheduleSection *self)
|
|
{
|
|
guint item = adw_combo_row_get_selected (ADW_COMBO_ROW (self->repeat_duration_combo));
|
|
|
|
g_assert (item >= GCAL_RECURRENCE_FOREVER && item <= GCAL_RECURRENCE_UNTIL);
|
|
|
|
return (GcalRecurrenceLimitType) item;
|
|
}
|
|
|
|
static void
|
|
on_repeat_duration_changed_cb (GtkWidget *widget,
|
|
GParamSpec *pspec,
|
|
GcalScheduleSection *self)
|
|
{
|
|
GcalRecurrenceLimitType limit_type = get_recurence_limit_type (self);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_recur_limit_type (self->values, limit_type);
|
|
update_from_section_values (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);
|
|
|
|
return (GcalRecurrenceFrequency) item;
|
|
}
|
|
|
|
static void
|
|
on_repeat_type_changed_cb (GtkWidget *widget,
|
|
GParamSpec *pspec,
|
|
GcalScheduleSection *self)
|
|
{
|
|
GcalRecurrenceFrequency frequency = get_recurrence_frequency (self);
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_recur_frequency (self->values, frequency);
|
|
update_from_section_values (self, updated);
|
|
}
|
|
|
|
static void
|
|
on_time_format_changed_cb (GcalScheduleSection *self)
|
|
{
|
|
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);
|
|
|
|
GcalScheduleSectionValues *updated = gcal_schedule_section_values_set_time_format (self->values, time_format);
|
|
|
|
update_from_section_values (self, updated);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* 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;
|
|
GcalTimeFormat time_format;
|
|
|
|
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_section_values_from_event (event, time_format);
|
|
|
|
if (!event)
|
|
GCAL_RETURN ();
|
|
|
|
refresh (self);
|
|
|
|
GCAL_EXIT;
|
|
}
|
|
|
|
static void
|
|
gcal_schedule_section_apply_to_event (GcalScheduleSection *self,
|
|
GcalEvent *event)
|
|
{
|
|
GCAL_ENTRY;
|
|
|
|
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)))
|
|
{
|
|
remove_recurrence_properties (event);
|
|
|
|
if (self->values->curr.recur)
|
|
{
|
|
gcal_event_set_recurrence (event, self->values->curr.recur);
|
|
}
|
|
}
|
|
|
|
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_section_values_free (self->values);
|
|
|
|
GcalTimeFormat time_format = gcal_context_get_time_format (self->context);
|
|
self->values = gcal_schedule_section_values_from_event (self->event, time_format);
|
|
|
|
GCAL_EXIT;
|
|
}
|
|
|
|
static gboolean
|
|
recurrence_changed (const GcalScheduleSectionValues *values)
|
|
{
|
|
return !gcal_recurrence_is_equal (values->orig.recur, values->curr.recur);
|
|
}
|
|
|
|
static gboolean
|
|
day_changed (const GcalScheduleSectionValues *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);
|
|
}
|
|
|
|
static gboolean
|
|
values_changed (const GcalScheduleSectionValues *values)
|
|
{
|
|
const GcalScheduleValues *orig = &values->orig;
|
|
const GcalScheduleValues *curr = &values->curr;
|
|
|
|
if (orig->all_day != curr->all_day)
|
|
return TRUE;
|
|
|
|
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);
|
|
|
|
if (!g_date_time_equal (orig->date_start, curr->date_start))
|
|
return TRUE;
|
|
|
|
if (g_strcmp0 (g_time_zone_get_identifier (start_tz), g_time_zone_get_identifier (orig_start_tz)) != 0)
|
|
return TRUE;
|
|
|
|
if (!g_date_time_equal (orig->date_end, curr->date_end))
|
|
return TRUE;
|
|
|
|
if (g_strcmp0 (g_time_zone_get_identifier (end_tz), g_time_zone_get_identifier (orig_end_tz)) != 0)
|
|
return TRUE;
|
|
|
|
return recurrence_changed (values);
|
|
}
|
|
|
|
static gboolean
|
|
gcal_schedule_section_changed (GcalEventEditorSection *section)
|
|
{
|
|
GcalScheduleSection *self = GCAL_SCHEDULE_SECTION (section);
|
|
|
|
GCAL_ENTRY;
|
|
|
|
GCAL_RETURN (values_changed (self->values));
|
|
}
|
|
|
|
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_section_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_return_val_if_fail (GCAL_IS_SCHEDULE_SECTION (self), FALSE);
|
|
|
|
return recurrence_changed (self->values);
|
|
}
|
|
|
|
gboolean
|
|
gcal_schedule_section_day_changed (GcalScheduleSection *self)
|
|
{
|
|
g_return_val_if_fail (GCAL_IS_SCHEDULE_SECTION (self), FALSE);
|
|
|
|
return day_changed (self->values);
|
|
}
|
|
|
|
/**
|
|
* Extracts the values from @event that are needed to populate #GcalScheduleSection.
|
|
*
|
|
* Returns: a #GcalScheduleSectionValues ready for use. Free it with
|
|
* gcal_schedule_section_values_free().
|
|
*/
|
|
GcalScheduleSectionValues *
|
|
gcal_schedule_section_values_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_ref (recur);
|
|
}
|
|
else
|
|
{
|
|
values.recur = NULL;
|
|
}
|
|
}
|
|
|
|
GcalScheduleSectionValues *section_values = g_new0 (GcalScheduleSectionValues, 1);
|
|
|
|
*section_values = (GcalScheduleSectionValues) {
|
|
.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;
|
|
}
|
|
|
|
static GcalScheduleSectionValues *
|
|
section_values_with_date_times (const char *start, const char *end, gboolean all_day)
|
|
{
|
|
GcalScheduleValues values = values_with_date_times (start, end, all_day);
|
|
|
|
GcalScheduleSectionValues *section_values = g_new0 (GcalScheduleSectionValues, 1);
|
|
|
|
*section_values = (GcalScheduleSectionValues) {
|
|
.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 (GcalScheduleSectionValues) values = section_values_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 (GcalScheduleSectionValues) new_values = gcal_schedule_section_values_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 (GcalScheduleSectionValues) values = section_values_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 (GcalScheduleSectionValues) new_values = gcal_schedule_section_values_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 (GcalScheduleSectionValues) values = section_values_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 (GcalScheduleSectionValues) new_values = gcal_schedule_section_values_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 (GcalScheduleSectionValues) values = section_values_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 (GcalScheduleSectionValues) new_values = gcal_schedule_section_values_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_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 (GcalScheduleSectionValues) values = section_values_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);
|
|
|
|
/* 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 (GcalScheduleSectionValues) new_values = gcal_schedule_section_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->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_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 (GcalScheduleSectionValues) values = section_values_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 (GcalScheduleSectionValues) all_day_values = gcal_schedule_section_values_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 (GcalScheduleSectionValues) not_all_day_values = gcal_schedule_section_values_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);
|
|
}
|
|
|
|
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);
|
|
g_test_add_func ("/event_editor/schedule_section/43_switching_to_all_day_preserves_timezones",
|
|
test_43_switching_to_all_day_preserves_timezones);
|
|
g_test_add_func ("/event_editor/schedule_section/recur_changes_to_no_repeat",
|
|
test_recur_changes_to_no_repeat);
|
|
g_test_add_func ("/event_editor/schedule_section/recur_changes_limit_type_to_count",
|
|
test_recur_changes_limit_type_to_count);
|
|
}
|