diff --git a/po/POTFILES.in b/po/POTFILES.in index c8264472..cdd6711a 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -62,8 +62,8 @@ src/gui/importer/gcal-import-dialog.c src/gui/importer/gcal-import-file-row.c src/gui/importer/gcal-importer.c src/gui/shortcuts-dialog.blp -src/gui/views/gcal-agenda-view.blp -src/gui/views/gcal-agenda-view.c +src/gui/views/gcal-agenda-view-day-row.blp +src/gui/views/gcal-agenda-view-day-row.c src/gui/views/gcal-month-popover.blp src/gui/views/gcal-week-grid.c src/gui/views/gcal-week-header.c diff --git a/src/gui/views/gcal-agenda-view-day-row.blp b/src/gui/views/gcal-agenda-view-day-row.blp new file mode 100644 index 00000000..c57a4ec1 --- /dev/null +++ b/src/gui/views/gcal-agenda-view-day-row.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template $GcalAgendaViewDayRow: Adw.Bin { + Box { + orientation: vertical; + + Label title_label { + xalign: 0.0; + label: bind $datetime_to_string_func(template.day as <$GcalAgendaViewDay>.date); + + styles [ + "caption-heading", + ] + } + + ListBox listbox { + hexpand: true; + vexpand: true; + selection-mode: none; + row-activated => $on_list_box_row_activated_cb(template) not-swapped; + + [placeholder] + Label { + label: _("No events"); + xalign: 0; + + styles [ + "no-events", + "dimmed", + ] + } + } + } +} diff --git a/src/gui/views/gcal-agenda-view-day-row.c b/src/gui/views/gcal-agenda-view-day-row.c new file mode 100644 index 00000000..5242d9c8 --- /dev/null +++ b/src/gui/views/gcal-agenda-view-day-row.c @@ -0,0 +1,297 @@ +/* + * gcal-agenda-view-day-row.c + * + * Copyright 2026 Georges Basile Stavracas Neto + * + * 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 + */ + +#include "gcal-agenda-view-day-row.h" +#include "gcal-date-time-utils.h" +#include "gcal-debug.h" +#include "gcal-enums.h" +#include "gcal-event-widget.h" + +#include + +struct _GcalAgendaViewDayRow +{ + AdwBin parent_instance; + + GtkListBox *listbox; + GtkLabel *title_label; + + GcalAgendaViewDay *day; +}; + +G_DEFINE_FINAL_TYPE (GcalAgendaViewDayRow, gcal_agenda_view_day_row, ADW_TYPE_BIN) + +enum { + PROP_0, + PROP_DAY, + N_PROPS, +}; + +enum +{ + EVENT_ACTIVATED, + N_SIGNALS, +}; + +static GParamSpec *properties [N_PROPS]; +static guint signals[N_SIGNALS] = { 0, }; + + +/* + * Auxiliary methods + */ + +static gchar * +new_date_header_string (GDateTime *date) +{ + g_autoptr (GDateTime) today = NULL; + g_autoptr (GDateTime) tomorrow = NULL; + g_autoptr (GDateTime) yesterday = NULL; + + if (date == NULL) + return NULL; + + today = g_date_time_new_now_local (); + tomorrow = g_date_time_add_days (today, 1); + yesterday = g_date_time_add_days (today, -1); + + if (gcal_date_time_compare_date (date, today) == 0) + return g_strdup (_("Today")); + else if (gcal_date_time_compare_date (date, tomorrow) == 0) + return g_strdup (_("Tomorrow")); + else if (gcal_date_time_compare_date (date, yesterday) == 0) + return g_strdup (_("Yesterday")); + else + /* + * Translators: %A is the full day name, %B is the month name + * and %d is the day of the month as a number between 0 and 31. + * More formats can be found on the doc: + * https://docs.gtk.org/glib/method.DateTime.format.html + */ + return g_date_time_format (date, _("%A %B %d")); +} + + +/* + * Callbacks + */ + +static void +on_event_widget_activated_cb (GcalEventWidget *event_widget, + GcalAgendaViewDayRow *self) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY_ROW (self)); + g_assert (GCAL_IS_EVENT_WIDGET (event_widget)); + + g_signal_emit (self, signals[EVENT_ACTIVATED], 0, event_widget); +} + +static void +on_list_box_row_activated_cb (GtkListBox *listbox, + GtkListBoxRow *row, + GcalAgendaViewDayRow *self) +{ + GtkWidget *child = gtk_list_box_row_get_child (row); + + g_assert (GCAL_IS_AGENDA_VIEW_DAY_ROW (self)); + g_assert (GCAL_IS_EVENT_WIDGET (child)); + + g_signal_emit (self, signals[EVENT_ACTIVATED], 0, child); +} + +static GtkWidget * +create_day_row_func (gpointer item, + gpointer user_data) +{ + GcalAgendaViewDayRow *self; + GcalTimestampPolicy timestamp_policy; + GcalEvent *event; + GtkWidget *widget; + GtkWidget *row; + + self = (GcalAgendaViewDayRow *) user_data; + + g_assert (GCAL_IS_EVENT (item)); + g_assert (GCAL_IS_AGENDA_VIEW_DAY_ROW (self)); + + event = item; + + /* Create and add the new event widget */ + if (gcal_event_get_all_day (event) || gcal_event_is_multiday (event)) + timestamp_policy = GCAL_TIMESTAMP_POLICY_END; + else + timestamp_policy = GCAL_TIMESTAMP_POLICY_START; + + widget = g_object_new (GCAL_TYPE_EVENT_WIDGET, + "event", event, + "orientation", GTK_ORIENTATION_VERTICAL, + "timestamp-policy", timestamp_policy, + "focusable", FALSE, + NULL); + + row = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "child", widget, + "activatable", TRUE, + NULL); + + gtk_accessible_update_property (GTK_ACCESSIBLE (row), + GTK_ACCESSIBLE_PROPERTY_HAS_POPUP, TRUE, + -1); + + gtk_accessible_update_relation (GTK_ACCESSIBLE (row), + GTK_ACCESSIBLE_RELATION_LABELLED_BY, widget, NULL, + -1); + + g_signal_connect (widget, "activate", G_CALLBACK (on_event_widget_activated_cb), self); + + return row; +} + +static char * +datetime_to_string_func (gpointer item, + GDateTime *datetime, + gpointer user_data) +{ + return datetime ? new_date_header_string (datetime) : g_strdup (""); +} + + +/* + * GObject overrides + */ + +static void +gcal_agenda_view_day_row_dispose (GObject *object) +{ + GcalAgendaViewDayRow *self = (GcalAgendaViewDayRow *)object; + + g_clear_object (&self->day); + + gtk_widget_dispose_template (GTK_WIDGET (self), GCAL_TYPE_AGENDA_VIEW_DAY_ROW); + + G_OBJECT_CLASS (gcal_agenda_view_day_row_parent_class)->dispose (object); +} + +static void +gcal_agenda_view_day_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalAgendaViewDayRow *self = GCAL_AGENDA_VIEW_DAY_ROW (object); + + switch (prop_id) + { + case PROP_DAY: + g_value_set_object (value, self->day); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_agenda_view_day_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalAgendaViewDayRow *self = GCAL_AGENDA_VIEW_DAY_ROW (object); + + switch (prop_id) + { + case PROP_DAY: + gcal_agenda_view_day_row_set_day (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_agenda_view_day_row_class_init (GcalAgendaViewDayRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gcal_agenda_view_day_row_dispose; + object_class->get_property = gcal_agenda_view_day_row_get_property; + object_class->set_property = gcal_agenda_view_day_row_set_property; + + properties[PROP_DAY] = g_param_spec_object ("day", NULL, NULL, + GCAL_TYPE_AGENDA_VIEW_DAY, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + signals[EVENT_ACTIVATED] = g_signal_new ("event-activated", + GCAL_TYPE_AGENDA_VIEW_DAY_ROW, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + GCAL_TYPE_EVENT_WIDGET); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/views/gcal-agenda-view-day-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalAgendaViewDayRow, listbox); + gtk_widget_class_bind_template_child (widget_class, GcalAgendaViewDayRow, title_label); + + gtk_widget_class_bind_template_callback (widget_class, datetime_to_string_func); + gtk_widget_class_bind_template_callback (widget_class, on_list_box_row_activated_cb); + + gtk_widget_class_set_css_name (widget_class, "agendaviewdayrow"); +} + +static void +gcal_agenda_view_day_row_init (GcalAgendaViewDayRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GcalAgendaViewDay * +gcal_agenda_view_day_row_get_day (GcalAgendaViewDayRow *self) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY_ROW (self)); + + return self->day; +} + +void +gcal_agenda_view_day_row_set_day (GcalAgendaViewDayRow *self, + GcalAgendaViewDay *day) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY_ROW (self)); + g_assert (day == NULL || GCAL_IS_AGENDA_VIEW_DAY (day)); + + if (g_set_object (&self->day, day)) + { + gtk_list_box_bind_model (GTK_LIST_BOX (self->listbox), + day ? G_LIST_MODEL (day) : NULL, + day ? create_day_row_func : NULL, + day ? self : NULL, + NULL); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DAY]); + } +} diff --git a/src/gui/views/gcal-agenda-view-day-row.h b/src/gui/views/gcal-agenda-view-day-row.h new file mode 100644 index 00000000..9f9007da --- /dev/null +++ b/src/gui/views/gcal-agenda-view-day-row.h @@ -0,0 +1,37 @@ +/* + * gcal-agenda-view-day-row.h + * + * Copyright 2026 Georges Basile Stavracas Neto + * + * 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 + +#include "gcal-agenda-view-day.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_AGENDA_VIEW_DAY_ROW (gcal_agenda_view_day_row_get_type()) +G_DECLARE_FINAL_TYPE (GcalAgendaViewDayRow, gcal_agenda_view_day_row, GCAL, AGENDA_VIEW_DAY_ROW, AdwBin) + +GcalAgendaViewDay *gcal_agenda_view_day_row_get_day (GcalAgendaViewDayRow *self); +void gcal_agenda_view_day_row_set_day (GcalAgendaViewDayRow *self, + GcalAgendaViewDay *day); + +G_END_DECLS diff --git a/src/gui/views/gcal-agenda-view-day.c b/src/gui/views/gcal-agenda-view-day.c new file mode 100644 index 00000000..33d34c49 --- /dev/null +++ b/src/gui/views/gcal-agenda-view-day.c @@ -0,0 +1,294 @@ +/* + * gcal-agenda-view-day.c + * + * Copyright 2026 Georges Basile Stavracas Neto + * + * 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 "GcalAgendaViewDay" + +#include "gcal-agenda-view-day.h" + +#include "gcal-date-time-utils.h" +#include "gcal-range.h" + +struct _GcalAgendaViewDay +{ + GObject parent_instance; + + GDateTime *date; + GcalRange *range; + + GtkFilterListModel *filtered_events; +}; + +static void g_list_model_interface_init (GListModelInterface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (GcalAgendaViewDay, gcal_agenda_view_day, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, g_list_model_interface_init)) + +enum { + PROP_0, + PROP_DATE, + PROP_MODEL, + PROP_N_ITEMS, + N_PROPS, +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * Callbacks + */ +static void +on_filtered_events_items_changed_cb (GListModel *model, + unsigned int position, + unsigned int removed, + unsigned int added, + GcalAgendaViewDay *self) +{ + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + if (removed != added) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_N_ITEMS]); +} + + +/* + * GListModel interface + */ + +static GType +gcal_agenda_view_day_get_item_type (GListModel *model G_GNUC_UNUSED) +{ + return GCAL_TYPE_EVENT; +} + +static unsigned int +gcal_agenda_view_day_get_n_items (GListModel *model) +{ + GcalAgendaViewDay *self = (GcalAgendaViewDay *) model; + + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + return g_list_model_get_n_items (G_LIST_MODEL (self->filtered_events)); +} + +static gpointer +gcal_agenda_view_day_get_item (GListModel *model, + guint position) +{ + GcalAgendaViewDay *self = (GcalAgendaViewDay *) model; + + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + return g_list_model_get_item (G_LIST_MODEL (self->filtered_events), position); +} + +static void +g_list_model_interface_init (GListModelInterface *iface) +{ + iface->get_item_type = gcal_agenda_view_day_get_item_type; + iface->get_n_items = gcal_agenda_view_day_get_n_items; + iface->get_item = gcal_agenda_view_day_get_item; +} + + +/* + * GObject overrides + */ + +static void +gcal_agenda_view_day_dispose (GObject *object) +{ + GcalAgendaViewDay *self = (GcalAgendaViewDay *)object; + + g_clear_pointer (&self->range, gcal_range_unref); + gcal_clear_date_time (&self->date); + + G_OBJECT_CLASS (gcal_agenda_view_day_parent_class)->dispose (object); +} + +static void +gcal_agenda_view_day_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalAgendaViewDay *self = GCAL_AGENDA_VIEW_DAY (object); + + switch (prop_id) + { + case PROP_DATE: + g_value_set_boxed (value, self->date); + break; + + case PROP_MODEL: + g_value_set_object (value, gcal_agenda_view_day_get_model (self)); + break; + + case PROP_N_ITEMS: + g_value_set_uint (value, g_list_model_get_n_items (G_LIST_MODEL (self->filtered_events))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_agenda_view_day_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalAgendaViewDay *self = GCAL_AGENDA_VIEW_DAY (object); + + switch (prop_id) + { + case PROP_DATE: + gcal_agenda_view_day_set_date (self, g_value_get_boxed (value)); + break; + + case PROP_MODEL: + gcal_agenda_view_day_set_model (self, g_value_get_object (value)); + break; + + case PROP_N_ITEMS: + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_agenda_view_day_class_init (GcalAgendaViewDayClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gcal_agenda_view_day_dispose; + object_class->get_property = gcal_agenda_view_day_get_property; + object_class->set_property = gcal_agenda_view_day_set_property; + + properties[PROP_DATE] = g_param_spec_boxed ("date", NULL, NULL, + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + properties[PROP_MODEL] = g_param_spec_object ("model", NULL, NULL, + G_TYPE_LIST_MODEL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_N_ITEMS] = g_param_spec_uint ("n-items", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static gboolean +filter_events_func (gpointer item, + gpointer user_data) +{ + GcalAgendaViewDay *self = (GcalAgendaViewDay *) user_data; + + g_assert (GCAL_IS_EVENT (item)); + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + if (!self->range) + return FALSE; + + return gcal_event_overlaps (item, self->range); +} + +static void +gcal_agenda_view_day_init (GcalAgendaViewDay *self) +{ + self->filtered_events = gtk_filter_list_model_new (NULL, + GTK_FILTER (gtk_custom_filter_new (filter_events_func, self, NULL))); + g_signal_connect (self->filtered_events, "items-changed", G_CALLBACK (on_filtered_events_items_changed_cb), self); +} + +GcalAgendaViewDay * +gcal_agenda_view_day_new (void) +{ + return g_object_new (GCAL_TYPE_AGENDA_VIEW_DAY, NULL); +} + +GDateTime * +gcal_agenda_view_day_get_date (GcalAgendaViewDay *self) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + return self->date; +} + +void +gcal_agenda_view_day_set_date (GcalAgendaViewDay *self, + GDateTime *date) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + if (gcal_set_date_time (&self->date, date)) + { + GtkFilter *filter = NULL; + + g_clear_pointer (&self->range, gcal_range_unref); + + if (date) + { + g_autoptr (GDateTime) start = NULL; + g_autoptr (GDateTime) end = NULL; + + start = g_date_time_new (g_date_time_get_timezone (date), + g_date_time_get_year (date), + g_date_time_get_month (date), + g_date_time_get_day_of_month (date), + 0, 0, 0); + end = g_date_time_add_days (start, 1); + + self->range = gcal_range_new_take (g_steal_pointer (&start), + g_steal_pointer (&end), + GCAL_RANGE_DEFAULT); + } + + filter = gtk_filter_list_model_get_filter (self->filtered_events); + // FIXME: better filter change + gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_DATE]); + } +} + +GListModel * +gcal_agenda_view_day_get_model (GcalAgendaViewDay *self) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + return gtk_filter_list_model_get_model (self->filtered_events); +} + +void +gcal_agenda_view_day_set_model (GcalAgendaViewDay *self, + GListModel *model) +{ + g_assert (GCAL_IS_AGENDA_VIEW_DAY (self)); + + if (gtk_filter_list_model_get_model (self->filtered_events) != model) + { + gtk_filter_list_model_set_model (self->filtered_events, model); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]); + } +} diff --git a/src/gui/views/gcal-agenda-view-day.h b/src/gui/views/gcal-agenda-view-day.h new file mode 100644 index 00000000..4fe78e32 --- /dev/null +++ b/src/gui/views/gcal-agenda-view-day.h @@ -0,0 +1,39 @@ +/* + * gcal-agenda-view-day.h + * + * Copyright 2026 Georges Basile Stavracas Neto + * + * 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-event.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_AGENDA_VIEW_DAY (gcal_agenda_view_day_get_type()) +G_DECLARE_FINAL_TYPE (GcalAgendaViewDay, gcal_agenda_view_day, GCAL, AGENDA_VIEW_DAY, GObject) + +GcalAgendaViewDay *gcal_agenda_view_day_new (void); +GDateTime *gcal_agenda_view_day_get_date (GcalAgendaViewDay *self); +void gcal_agenda_view_day_set_date (GcalAgendaViewDay *self, + GDateTime *date); +GListModel *gcal_agenda_view_day_get_model (GcalAgendaViewDay *self); +void gcal_agenda_view_day_set_model (GcalAgendaViewDay *self, + GListModel *model); + +G_END_DECLS diff --git a/src/gui/views/gcal-agenda-view.blp b/src/gui/views/gcal-agenda-view.blp index 037eb25f..3bfbc106 100644 --- a/src/gui/views/gcal-agenda-view.blp +++ b/src/gui/views/gcal-agenda-view.blp @@ -13,26 +13,37 @@ template $GcalAgendaView: Box { ScrolledWindow scrolled_window { hexpand: true; vexpand: true; + hscrollbar-policy: never; - ListBox list_box { - hexpand: true; - selection-mode: none; - vexpand: true; - row-activated => $on_list_box_row_activated_cb(template) not-swapped; + ListView list_view { + hscroll-policy: natural; + vscroll-policy: natural; + single-click-activate: true; - ListBoxRow no_events_row { - activatable: false; + model: Gtk.NoSelection { + model: FilterListModel filtered_days { + watch-items: true; - child: Label { - label: _("No events"); - xalign: 0; - - styles [ - "no-events", - "dimmed", - ] + filter: BoolFilter { + expression: expr $n_items_and_date_to_boolean( + item as <$GcalAgendaViewDay>.n-items, + item as <$GcalAgendaViewDay>.date, + ) as ; + }; }; - } + }; + + factory: BuilderListItemFactory { + template ListItem { + activatable: false; + selectable: false; + + child: $GcalAgendaViewDayRow { + day: bind template.item; + event-activated => $on_day_row_event_activated_cb(template) not-swapped; + }; + } + }; } } } diff --git a/src/gui/views/gcal-agenda-view.c b/src/gui/views/gcal-agenda-view.c index e21a910c..f1e02bc4 100644 --- a/src/gui/views/gcal-agenda-view.c +++ b/src/gui/views/gcal-agenda-view.c @@ -19,6 +19,8 @@ #define G_LOG_DOMAIN "GcalAgendaView" #include "gcal-agenda-view.h" +#include "gcal-agenda-view-day.h" +#include "gcal-agenda-view-day-row.h" #include "gcal-debug.h" #include "gcal-enums.h" #include "gcal-event-widget.h" @@ -28,7 +30,6 @@ #include "gcal-view-private.h" #include -#include #include @@ -43,14 +44,13 @@ struct _GcalAgendaView { GtkBox parent; - GtkWidget *list_box; - GtkWidget *no_events_row; GtkWidget *scrolled_window; /* property */ GDateTime *date; + GListStore *days_model; + GtkFilterListModel *filtered_days; - GcalRangeTree *events; guint scroll_grid_timeout_id; gulong stack_page_changed_id; @@ -82,132 +82,6 @@ G_DEFINE_TYPE_WITH_CODE (GcalAgendaView, gcal_agenda_view, GTK_TYPE_BOX, * Auxiliary methods */ -static ChildData* -child_data_new (GtkWidget *widget, - GcalEvent *event, - GcalAgendaView *self) -{ - ChildData *data; - - data = g_new (ChildData, 1); - data->widget = widget; - data->event = g_object_ref (event); - data->self = self; - - return data; -} - -static void -child_data_free (gpointer data) -{ - ChildData *child_data = data; - - if (!child_data) - return; - - if (child_data->widget && child_data->self) - { - gtk_list_box_remove (GTK_LIST_BOX (child_data->self->list_box), child_data->widget); - child_data->widget = NULL; - child_data->self = NULL; - } - g_clear_object (&child_data->event); - g_free (child_data); -} - -static GDateTime * -get_start_date_for_row (GcalAgendaView *self, - GtkListBoxRow *row) -{ - GtkWidget *widget = gtk_list_box_row_get_child (row); - - if (GCAL_IS_EVENT_WIDGET (widget)) - { - GDateTime *date = gcal_event_widget_get_date_start (GCAL_EVENT_WIDGET (widget)); - - /* Pretend events start at least on the selected date */ - if (gcal_date_time_compare_date (date, self->date) > 0) - return date; - else - return self->date; - } - - - return self->date; -} - -static GDateTime * -get_end_date_for_row (GcalAgendaView *self, - GtkListBoxRow *row) -{ - GtkWidget *widget = gtk_list_box_row_get_child (row); - - if (GCAL_IS_EVENT_WIDGET (widget)) - return gcal_event_widget_get_date_end (GCAL_EVENT_WIDGET (widget)); - - return self->date; -} - -static gboolean -get_row_is_multiday (GtkListBoxRow *row) -{ - GtkWidget *widget = gtk_list_box_row_get_child (row); - - if (GCAL_IS_EVENT_WIDGET (widget)) - { - GcalEvent *event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - - return gcal_event_is_multiday (event); - } - - return FALSE; -} - -static gboolean -get_row_is_all_day (GtkListBoxRow *row) -{ - GtkWidget *widget = gtk_list_box_row_get_child (row); - - if (GCAL_IS_EVENT_WIDGET (widget)) - { - GcalEvent *event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - - return gcal_event_get_all_day (event); - } - - return FALSE; -} - -static gchar * -new_date_header_string (GDateTime *date) -{ - g_autoptr (GDateTime) today = NULL; - g_autoptr (GDateTime) tomorrow = NULL; - g_autoptr (GDateTime) yesterday = NULL; - - if (date == NULL) - return NULL; - - today = g_date_time_new_now_local (); - tomorrow = g_date_time_add_days (today, 1); - yesterday = g_date_time_add_days (today, -1); - - if (gcal_date_time_compare_date (date, today) == 0) - return g_strdup (_("Today")); - else if (gcal_date_time_compare_date (date, tomorrow) == 0) - return g_strdup (_("Tomorrow")); - else if (gcal_date_time_compare_date (date, yesterday) == 0) - return g_strdup (_("Yesterday")); - else - /* - * Translators: %A is the full day name, %B is the month name - * and %d is the day of the month as a number between 0 and 31. - * More formats can be found on the doc: - * https://docs.gtk.org/glib/method.DateTime.format.html - */ - return g_date_time_format (date, _("%A %B %d")); -} - /* * Callbacks @@ -226,20 +100,6 @@ stack_visible_child_changed_cb (AdwViewStack *stack, g_clear_signal_handler (&self->stack_page_changed_id, stack); } -/* Auxiliary methods */ -static void -update_no_events_row (GcalAgendaView *self) -{ - gboolean visible = !self->events_on_date; - gboolean was_visible = gtk_widget_get_visible (self->no_events_row); - - if (visible != was_visible) - { - gtk_widget_set_visible (self->no_events_row, visible); - gtk_list_box_invalidate_headers (GTK_LIST_BOX (self->list_box)); - } -} - static gboolean update_grid_scroll_position (GcalAgendaView *self) { @@ -315,146 +175,30 @@ schedule_position_scroll (GcalAgendaView *self) self); } -static GDateTime* -all_day_to_local (GDateTime *dt) +static gboolean +n_items_and_date_to_boolean (GcalAgendaViewDay *day, + unsigned int n_items, + GDateTime *date, + gpointer user_data) { - return g_date_time_new_local (g_date_time_get_year (dt), - g_date_time_get_month (dt), - g_date_time_get_day_of_month (dt), - 0, 0, 0); -} + GcalAgendaView *self = (GcalAgendaView *) user_data; + g_autoptr (GcalRange) view_range = NULL; -static int -sort_func (GtkListBoxRow *row1, - GtkListBoxRow *row2, - gpointer user_data) -{ - GcalAgendaView *self = GCAL_AGENDA_VIEW (user_data); - g_autoptr (GDateTime) start_date1 = get_start_date_for_row (self, row1); - g_autoptr (GDateTime) start_date2 = get_start_date_for_row (self, row2); - g_autoptr (GDateTime) end_date1 = NULL; - g_autoptr (GDateTime) end_date2 = NULL; - gboolean multiday1, multiday2; - gboolean all_day1, all_day2; - int result; + g_assert (GCAL_IS_AGENDA_VIEW (self)); + g_assert (GCAL_IS_AGENDA_VIEW_DAY (day)); + g_assert (self->date != NULL); - all_day1 = get_row_is_all_day (row1); - all_day2 = get_row_is_all_day (row2); + view_range = gcal_timeline_subscriber_get_range (GCAL_TIMELINE_SUBSCRIBER (self)); + if (!gcal_range_contains_datetime (view_range, date)) + return FALSE; - start_date1 = all_day1 ? all_day_to_local (start_date1) : g_date_time_to_local (start_date1); - start_date2 = all_day2 ? all_day_to_local (start_date2) : g_date_time_to_local (start_date2); + if (n_items > 0) + return TRUE; - result = g_date_time_compare (start_date1, start_date2); + if (gcal_date_time_compare_date (date, self->date) == 0) + return TRUE; - /* Sort multi day events before multi single day events of the same day. */ - if (result != 0) - return result; - - multiday1 = get_row_is_multiday (row1); - multiday2 = get_row_is_multiday (row2); - - if (multiday1 != multiday2) - return multiday1 ? -1 : 1; - - if (all_day1 != all_day2) - return all_day1 ? -1 : 1; - - end_date1 = g_date_time_to_local (get_end_date_for_row (self, row1)); - end_date2 = g_date_time_to_local (get_end_date_for_row (self, row2)); - - return g_date_time_compare (end_date1, end_date2); -} - -static void -update_header (GtkListBoxRow *row, - GtkListBoxRow *before, - gpointer user_data) -{ - GcalAgendaView *self = GCAL_AGENDA_VIEW (user_data); - GtkWidget *widget = gtk_list_box_row_get_child (row); - gboolean event_all_day = get_row_is_all_day (row); - g_autoptr (GDateTime) start = get_start_date_for_row (self, row); - g_autoptr (GDateTime) end = get_end_date_for_row (self, row); - g_autoptr (GDateTime) today = NULL; - g_autoptr (GDateTime) end_midnight = NULL; - GcalEvent *event = NULL; - g_autofree gchar *label = NULL; - GtkWidget *header; - - start = event_all_day ? g_date_time_ref (start) : g_date_time_to_local (start); - end = event_all_day ? g_date_time_ref (end) : g_date_time_to_local (end); - - if (GCAL_IS_EVENT_WIDGET (widget)) - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - - today = g_date_time_new_now_local (); - - if (before) - { - g_autoptr (GDateTime) before_start = get_start_date_for_row (self, before); - g_autoptr (GDateTime) before_end = get_end_date_for_row (self, before); - gboolean before_all_day = get_row_is_all_day (before); - - before_start = before_all_day ? g_date_time_ref (before_start) : g_date_time_to_local (before_start); - before_end = before_all_day ? g_date_time_ref (before_end) : g_date_time_to_local (before_end); - - if (get_row_is_multiday (row) && - get_row_is_multiday (before) && - gcal_date_time_compare_date (start, before_start) == 0 && - gcal_date_time_compare_date (start, today) == 0) - { - gtk_list_box_row_set_header (row, NULL); - return; - } - - end_midnight = event_all_day ? g_date_time_ref (end) : g_date_time_add_seconds (end, -1); - - if (gcal_date_time_compare_date (start, before_start) == 0 && - gcal_date_time_compare_date (end_midnight, before_end) == 0) - { - gtk_list_box_row_set_header (row, NULL); - return; - } - - /* All day events don't need to check for end date */ - if (event && - (before_all_day || event_all_day) && - !get_row_is_multiday (before) && - gcal_date_time_compare_date (start, before_start) == 0) - { - gtk_list_box_row_set_header (row, NULL); - return; - } - } - - if (get_row_is_multiday (row)) - { - g_autofree gchar *start_label = NULL; - g_autofree gchar *end_label = NULL; - if (gcal_date_time_compare_date (start, today) == 0) - { - label = g_strdup (_("On-going")); - } - else - { - start_label = new_date_header_string (start); - end_label = new_date_header_string (end); - - label = g_strdup_printf ("%s – %s", start_label, end_label); - } - } - else - { - label = new_date_header_string (start); - } - - header = g_object_new (GTK_TYPE_LABEL, - "label", label, - "xalign", 0.0f, - NULL); - gtk_widget_add_css_class (header, "caption-heading"); - - gtk_list_box_row_set_header (row, header); + return FALSE; } @@ -470,23 +214,6 @@ gcal_agenda_view_get_date (GcalView *view) return self->date; } -static inline gboolean -count_events_on_data (GcalRange *range, - gpointer data, - gpointer user_data) -{ - ChildData *child_data = data; - GcalAgendaView *self = user_data; - g_autoptr (GDateTime) event_date = gcal_event_get_date_start (child_data->event); - - event_date = gcal_event_get_all_day (child_data->event) ? g_date_time_ref (event_date) : g_date_time_to_local (event_date); - - if (gcal_date_time_compare_date (self->date, event_date) == 0) - self->events_on_date++; - - return FALSE; -} - static void gcal_agenda_view_set_date (GcalView *view, GDateTime *date) @@ -497,17 +224,21 @@ gcal_agenda_view_set_date (GcalView *view, gcal_set_date_time (&self->date, date); + for (unsigned int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->days_model)); i++) + { + g_autoptr (GcalAgendaViewDay) day = NULL; + g_autoptr (GDateTime) next_date = NULL; + + day = g_list_model_get_item (G_LIST_MODEL (self->days_model), i); + next_date = g_date_time_add_days (date, i); + + gcal_agenda_view_day_set_date (day, next_date); + } + schedule_position_scroll (self); - self->events_on_date = 0; - gcal_range_tree_traverse (self->events, G_IN_ORDER, count_events_on_data, self); - update_no_events_row (self); - gcal_timeline_subscriber_range_changed (GCAL_TIMELINE_SUBSCRIBER (view)); - if (self->events_on_date == 0) - gtk_list_box_invalidate_sort (GTK_LIST_BOX (self->list_box)); - GCAL_EXIT; } @@ -565,24 +296,18 @@ gcal_view_interface_init (GcalViewInterface *iface) */ static void -on_event_widget_activated_cb (GcalEventWidget *event_widget, - GcalAgendaView *self) +on_day_row_event_activated_cb (GcalAgendaViewDayRow *day_row, + GcalEventWidget *event_widget, + gpointer user_data) { + GcalAgendaView *self; + + self = GCAL_AGENDA_VIEW (gtk_widget_get_ancestor (GTK_WIDGET (day_row), GCAL_TYPE_AGENDA_VIEW)); + g_assert (GCAL_IS_AGENDA_VIEW (self)); + gcal_view_event_activated (GCAL_VIEW (self), event_widget); } -static void -on_list_box_row_activated_cb (GtkListBox *list_box, - GtkListBoxRow *row, - GcalAgendaView *self) -{ - GtkWidget *child = gtk_list_box_row_get_child (row); - - g_assert (GCAL_IS_EVENT_WIDGET (child)); - - gcal_view_event_activated (GCAL_VIEW (self), GCAL_EVENT_WIDGET (child)); -} - static GcalRange* gcal_agenda_view_get_range (GcalTimelineSubscriber *subscriber) { @@ -597,107 +322,12 @@ static void gcal_agenda_view_add_event (GcalTimelineSubscriber *subscriber, GcalEvent *event) { - GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber); - g_autoptr (GDateTime) event_date = gcal_event_get_date_start (event); - GtkWidget *widget, *row; - GcalTimestampPolicy timestamp_policy; - - GCAL_ENTRY; - - /* FIXME Check gcal_event_is_multiday (event) and gcal_event_get_all_day (event) */ - - g_object_ref (event); - - event_date = gcal_event_get_all_day (event) ? g_date_time_ref (event_date) : g_date_time_to_local (event_date); - - if (gcal_date_time_compare_date (self->date, event_date) == 0) - { - self->events_on_date++; - update_no_events_row (self); - } - - /* Create and add the new event widget */ - if (gcal_event_get_all_day (event) || gcal_event_is_multiday (event)) - timestamp_policy = GCAL_TIMESTAMP_POLICY_END; - else - timestamp_policy = GCAL_TIMESTAMP_POLICY_START; - - widget = g_object_new (GCAL_TYPE_EVENT_WIDGET, - "event", event, - "orientation", GTK_ORIENTATION_VERTICAL, - "timestamp-policy", timestamp_policy, - "focusable", FALSE, - NULL); - - row = g_object_new (GTK_TYPE_LIST_BOX_ROW, - "child", widget, - NULL); - - gcal_range_tree_add_range (self->events, - gcal_event_get_range (event), - child_data_new (row, event, self)); - - gtk_accessible_update_property (GTK_ACCESSIBLE (row), - GTK_ACCESSIBLE_PROPERTY_HAS_POPUP, TRUE, - -1); - - gtk_accessible_update_relation (GTK_ACCESSIBLE (row), - GTK_ACCESSIBLE_RELATION_LABELLED_BY, widget, NULL, - -1); - - g_signal_connect (widget, "activate", G_CALLBACK (on_event_widget_activated_cb), self); - - gtk_list_box_append (GTK_LIST_BOX (self->list_box), row); - - gtk_list_box_invalidate_headers (GTK_LIST_BOX (self->list_box)); - - GCAL_EXIT; } static void gcal_agenda_view_remove_event (GcalTimelineSubscriber *subscriber, GcalEvent *event) { - GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber); - const gchar *uid = gcal_event_get_uid (event); - g_autoptr (GDateTime) event_date = gcal_event_get_date_start (event); - GPtrArray *widgets; - guint i; - - GCAL_ENTRY; - - widgets = gcal_range_tree_get_all_data (self->events); - - event_date = gcal_event_get_all_day (event) ? g_date_time_ref (event_date) : g_date_time_to_local (event_date); - - if (gcal_date_time_compare_date (self->date, event_date) == 0) - { - self->events_on_date--; - update_no_events_row (self); - } - - for (i = 0; widgets && i < widgets->len; i++) - { - ChildData *data; - GtkWidget *widget; - GcalEvent *event; - - data = g_ptr_array_index (widgets, i); - widget = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (data->widget)); - event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget)); - - if (g_strcmp0 (gcal_event_get_uid (event), uid) != 0) - continue; - - gcal_range_tree_remove_range (self->events, gcal_event_get_range (event), data); - gtk_widget_queue_allocate (GTK_WIDGET (self)); - } - - g_clear_pointer (&widgets, g_ptr_array_unref); - - gtk_list_box_invalidate_headers (GTK_LIST_BOX (self->list_box)); - - GCAL_EXIT; } static void @@ -705,10 +335,24 @@ gcal_agenda_view_update_event (GcalTimelineSubscriber *subscriber, GcalEvent *old_event, GcalEvent *event) { +} + +static void +gcal_agenda_view_set_model (GcalTimelineSubscriber *subscriber, + GListModel *model) +{ + GcalAgendaView *self; + GCAL_ENTRY; - gcal_agenda_view_remove_event (subscriber, old_event); - gcal_agenda_view_add_event (subscriber, event); + self = GCAL_AGENDA_VIEW (subscriber); + + for (unsigned int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->days_model)); i++) + { + g_autoptr (GcalAgendaViewDay) day = g_list_model_get_item (G_LIST_MODEL (self->days_model), i); + + gcal_agenda_view_day_set_model (day, model); + } GCAL_EXIT; } @@ -720,6 +364,7 @@ gcal_timeline_subscriber_interface_init (GcalTimelineSubscriberInterface *iface) iface->add_event = gcal_agenda_view_add_event; iface->update_event = gcal_agenda_view_update_event; iface->remove_event = gcal_agenda_view_remove_event; + iface->set_model = gcal_agenda_view_set_model; } @@ -730,22 +375,17 @@ gcal_timeline_subscriber_interface_init (GcalTimelineSubscriberInterface *iface) static void gcal_agenda_view_dispose (GObject *object) { - GcalAgendaView *self; + GcalAgendaView *self = GCAL_AGENDA_VIEW (object); - self = GCAL_AGENDA_VIEW (object); + gtk_widget_dispose_template (GTK_WIDGET (self), GCAL_TYPE_AGENDA_VIEW); - g_clear_pointer (&self->events, gcal_range_tree_unref); - - /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (gcal_agenda_view_parent_class)->dispose (object); } static void gcal_agenda_view_finalize (GObject *object) { - GcalAgendaView *self; - - self = GCAL_AGENDA_VIEW (object); + GcalAgendaView *self = GCAL_AGENDA_VIEW (object); g_clear_pointer (&self->date, g_date_time_unref); @@ -804,6 +444,8 @@ gcal_agenda_view_class_init (GcalAgendaViewClass *klass) GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + g_type_ensure (GCAL_TYPE_AGENDA_VIEW_DAY_ROW); + object_class->dispose = gcal_agenda_view_dispose; object_class->finalize = gcal_agenda_view_finalize; object_class->set_property = gcal_agenda_view_set_property; @@ -814,11 +456,11 @@ gcal_agenda_view_class_init (GcalAgendaViewClass *klass) gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/views/gcal-agenda-view.ui"); - gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, list_box); - gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, no_events_row); + gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, filtered_days); gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, scrolled_window); - gtk_widget_class_bind_template_callback (widget_class, on_list_box_row_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, n_items_and_date_to_boolean); + gtk_widget_class_bind_template_callback (widget_class, on_day_row_event_activated_cb); gtk_widget_class_set_css_name (widget_class, "agenda-view"); } @@ -828,8 +470,21 @@ gcal_agenda_view_init (GcalAgendaView *self) { gtk_widget_init_template (GTK_WIDGET (self)); - self->events = gcal_range_tree_new_with_free_func (child_data_free); + self->date = g_date_time_new_now_local (); + self->days_model = g_list_store_new (GCAL_TYPE_AGENDA_VIEW_DAY); - gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box), sort_func, self, NULL); - gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box), update_header, self, NULL); + for (size_t i = 0; i < N_WEEKDAYS; i++) + { + g_autoptr (GcalAgendaViewDay) day = NULL; + g_autoptr (GDateTime) date = NULL; + + date = g_date_time_add_days (self->date, i); + + day = gcal_agenda_view_day_new (); + gcal_agenda_view_day_set_date (day, date); + + g_list_store_append (self->days_model, day); + } + + gtk_filter_list_model_set_model (self->filtered_days, G_LIST_MODEL (self->days_model)); } diff --git a/src/gui/views/meson.build b/src/gui/views/meson.build index 4cbae4f8..cbabc8a6 100644 --- a/src/gui/views/meson.build +++ b/src/gui/views/meson.build @@ -4,6 +4,7 @@ blueprints = custom_target( 'blueprints', input: files( 'gcal-agenda-view.blp', + 'gcal-agenda-view-day-row.blp', 'gcal-month-cell.blp', 'gcal-month-popover.blp', 'gcal-month-view.blp', @@ -29,6 +30,8 @@ built_sources += gnome.compile_resources( sources += files( 'gcal-agenda-view.c', + 'gcal-agenda-view-day.c', + 'gcal-agenda-view-day-row.c', 'gcal-month-cell.c', 'gcal-month-popover.c', 'gcal-month-view.c', diff --git a/src/gui/views/views.gresource.xml b/src/gui/views/views.gresource.xml index e1c21c87..b70d8fb0 100644 --- a/src/gui/views/views.gresource.xml +++ b/src/gui/views/views.gresource.xml @@ -2,6 +2,7 @@ gcal-agenda-view.ui + gcal-agenda-view-day-row.ui gcal-month-cell.ui gcal-month-popover.ui gcal-month-view.ui diff --git a/src/theme/style.css b/src/theme/style.css index a119e377..d98f5269 100644 --- a/src/theme/style.css +++ b/src/theme/style.css @@ -424,16 +424,24 @@ monthpopover > box { /* Agenda View CSS */ -agenda-view list > row { - padding: 3px; - margin-left: 9px; - margin-right: 9px; - border-radius: 6px; +agenda-view listview > row { background: none; } -agenda-view list > label { - padding: 6px 12px; +agenda-view agendaviewdayrow { + margin-left: 9px; + margin-right: 9px; + margin-bottom: 3px; +} + +agenda-view agendaviewdayrow > box > label { + padding: 6px 0; +} + +agenda-view agendaviewdayrow list > row, +agenda-view agendaviewdayrow list > label { + border-radius: 6px; + background: none; } .sidebar-pane agenda-view .background {