[gnome-calendar] Add GcalAgendaView



commit 06aab9952aaec850b4920b803dcd8693b2d135bc
Author: Adrien Plazas <kekun plazas laposte net>
Date:   Wed Apr 20 15:19:23 2022 +0200

    Add GcalAgendaView

 po/POTFILES.in                    |   1 +
 src/gui/views/gcal-agenda-view.c  | 743 ++++++++++++++++++++++++++++++++++++++
 src/gui/views/gcal-agenda-view.h  |  29 ++
 src/gui/views/gcal-agenda-view.ui |  51 +++
 src/gui/views/meson.build         |   1 +
 src/gui/views/views.gresource.xml |   1 +
 src/theme/Adwaita.css             |  14 +
 7 files changed, 840 insertions(+)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index cf9e2e88..2672a876 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -39,6 +39,7 @@ src/gui/importer/gcal-import-dialog.c
 src/gui/importer/gcal-import-dialog.ui
 src/gui/importer/gcal-importer.c
 src/gui/importer/gcal-import-file-row.c
+src/gui/views/gcal-agenda-view.c
 src/gui/views/gcal-month-popover.ui
 src/gui/views/gcal-week-grid.c
 src/gui/views/gcal-week-header.c
diff --git a/src/gui/views/gcal-agenda-view.c b/src/gui/views/gcal-agenda-view.c
new file mode 100644
index 00000000..e817155f
--- /dev/null
+++ b/src/gui/views/gcal-agenda-view.c
@@ -0,0 +1,743 @@
+/* gcal-agenda-view.c
+ *
+ * Copyright (C) 2022 Purism SPC
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "GcalAgendaView"
+
+#include "gcal-agenda-view.h"
+#include "gcal-debug.h"
+#include "gcal-enums.h"
+#include "gcal-event-widget.h"
+#include "gcal-range-tree.h"
+#include "gcal-timeline-subscriber.h"
+#include "gcal-utils.h"
+#include "gcal-view.h"
+
+#include <adwaita.h>
+#include <glib/gi18n.h>
+
+#include <math.h>
+
+typedef struct
+{
+  GtkWidget          *widget;
+  GcalEvent          *event;
+  GcalAgendaView     *self;
+} ChildData;
+
+struct _GcalAgendaView
+{
+  GtkBox              parent;
+
+  GtkWidget          *list_box;
+  GtkWidget          *no_events_row;
+  GtkWidget          *scrolled_window;
+
+  /* property */
+  GDateTime          *date;
+  GcalContext        *context;
+
+  GcalRangeTree      *events;
+  guint               scroll_grid_timeout_id;
+  gulong              stack_page_changed_id;
+
+  gint                events_on_date;
+  gint                clicked_cell;
+};
+
+static void          schedule_position_scroll                    (GcalAgendaView       *self);
+
+static void          gcal_view_interface_init                    (GcalViewInterface  *iface);
+
+static void          gcal_timeline_subscriber_interface_init     (GcalTimelineSubscriberInterface *iface);
+
+enum
+{
+  PROP_0,
+  PROP_DATE,
+  PROP_CONTEXT,
+  N_PROPS,
+};
+
+
+G_DEFINE_TYPE_WITH_CODE (GcalAgendaView, gcal_agenda_view, GTK_TYPE_BOX,
+                         G_IMPLEMENT_INTERFACE (GCAL_TYPE_VIEW, gcal_view_interface_init)
+                         G_IMPLEMENT_INTERFACE (GCAL_TYPE_TIMELINE_SUBSCRIBER,
+                                                gcal_timeline_subscriber_interface_init));
+
+/*
+ * 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 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
+    return g_date_time_format (date, "%A %B %e");
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_event_activated (GcalAgendaView *self,
+                    GtkWidget      *widget)
+{
+  g_signal_emit_by_name (self, "event-activated", widget);
+}
+
+static void
+stack_visible_child_changed_cb (AdwViewStack   *stack,
+                                GParamSpec     *pspec,
+                                GcalAgendaView *self)
+{
+  if (adw_view_stack_get_visible_child (stack) != (GtkWidget*) self)
+    return;
+
+  schedule_position_scroll (self);
+
+  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)
+{
+  g_autoptr(GDateTime) week_start = NULL;
+  g_autoptr(GDateTime) week_end = NULL;
+  g_autoptr(GDateTime) now = NULL;
+  GtkAdjustment *vadjustment;
+  gdouble minutes, real_value;
+  gdouble max, page, page_increment, value;
+  gboolean dummy;
+
+  /* While the scrolled window is not mapped, we keep waiting */
+  if (!gtk_widget_get_realized (self->scrolled_window) ||
+      !gtk_widget_get_mapped (self->scrolled_window))
+    {
+      if (self->stack_page_changed_id == 0)
+        {
+          GtkWidget *stack = gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_VIEW_STACK);
+
+          self->stack_page_changed_id = g_signal_connect_object (stack,
+                                                                 "notify::visible-child",
+                                                                 G_CALLBACK (stack_visible_child_changed_cb),
+                                                                 self,
+                                                                 0);
+        }
+
+      self->scroll_grid_timeout_id = 0;
+
+      GCAL_RETURN (G_SOURCE_REMOVE);
+    }
+
+  now = g_date_time_new_now_local ();
+  week_start = gcal_date_time_get_start_of_week (self->date);
+  week_end = gcal_date_time_get_end_of_week (self->date);
+
+  /* Don't animate when not today */
+  if (gcal_date_time_compare_date (now, week_start) < 0 || gcal_date_time_compare_date (now, week_end) >= 0)
+    GCAL_GOTO (out);
+
+  vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window));
+  minutes = g_date_time_get_hour (now) * 60 + g_date_time_get_minute (now);
+  page = gtk_adjustment_get_page_size (vadjustment);
+  max = gtk_adjustment_get_upper (vadjustment);
+
+  real_value = max / MINUTES_PER_DAY * minutes - (page / 2.0);
+  page_increment = gtk_adjustment_get_page_increment (vadjustment);
+  value = gtk_adjustment_get_value (vadjustment);
+
+  gtk_adjustment_set_page_increment (vadjustment, real_value - value);
+
+  g_signal_emit_by_name (self->scrolled_window,
+                         "scroll-child",
+                         GTK_SCROLL_PAGE_FORWARD,
+                         FALSE,
+                         &dummy);
+
+  gtk_adjustment_set_page_increment (vadjustment, page_increment);
+
+out:
+  self->scroll_grid_timeout_id = 0;
+  GCAL_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+schedule_position_scroll (GcalAgendaView *self)
+{
+  /* Nothing is scheduled, we should do it now */
+  if (self->scroll_grid_timeout_id > 0)
+    g_source_remove (self->scroll_grid_timeout_id);
+
+  self->scroll_grid_timeout_id = g_timeout_add (200,
+                                                (GSourceFunc) update_grid_scroll_position,
+                                                self);
+}
+
+static int
+sort_func (GtkListBoxRow *row1,
+           GtkListBoxRow *row2,
+           gpointer       user_data)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (user_data);
+  gboolean multiday1, multiday2;
+  int result;
+
+  result = gcal_date_time_compare_date (get_start_date_for_row (self, row1),
+                                        get_start_date_for_row (self, row2));
+
+  if (result != 0)
+    return result;
+
+  multiday1 = get_row_is_multiday (row1);
+  multiday2 = get_row_is_multiday (row2);
+
+  /* Sort multi day events before multi single day events of the same day. */
+  if (multiday1 != multiday2)
+    return multiday1 ? -1 : 1;
+
+  return gcal_date_time_compare_date (get_end_date_for_row (self, row1),
+                                      get_end_date_for_row (self, row2));
+}
+
+static void
+update_header (GtkListBoxRow *row,
+               GtkListBoxRow *before,
+               gpointer       user_data)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (user_data);
+  GDateTime *start = get_start_date_for_row (self, row);
+  GDateTime *end = get_end_date_for_row (self, row);
+  g_autofree gchar *label = NULL;
+  GtkWidget *header;
+  g_autoptr (GDateTime) today = NULL;
+
+  today = g_date_time_new_now_local ();
+
+  if (before)
+    {
+      GDateTime *before_start = get_start_date_for_row (self, before);
+      GDateTime *before_end = get_end_date_for_row (self, before);
+
+      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;
+        }
+
+      if (gcal_date_time_compare_date (start, before_start) == 0 &&
+          gcal_date_time_compare_date (end, before_end) == 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,
+                         NULL);
+  gtk_widget_add_css_class (header, "caption-heading");
+
+  gtk_list_box_row_set_header (row, header);
+}
+
+
+/*
+ * GcalView interface
+ */
+
+static GDateTime*
+gcal_agenda_view_get_date (GcalView *view)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (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;
+
+  if (gcal_date_time_compare_date (self->date, gcal_event_get_date_start (child_data->event)) == 0)
+    self->events_on_date++;
+
+  return FALSE;
+}
+
+static void
+gcal_agenda_view_set_date (GcalView  *view,
+                           GDateTime *date)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);
+
+  GCAL_ENTRY;
+
+  gcal_set_date_time (&self->date, 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));
+
+  GCAL_EXIT;
+}
+
+static GList*
+gcal_agenda_view_get_children_by_uuid (GcalView              *view,
+                                       GcalRecurrenceModType  mod,
+                                       const gchar           *uuid)
+{
+  GCAL_ENTRY;
+
+  /* FIXME Not sure what to do here. */
+
+  GCAL_RETURN (NULL);
+}
+
+static void
+gcal_agenda_view_clear_marks (GcalView *view)
+{
+  /* FIXME Not sure what to do here. */
+}
+
+static GDateTime*
+gcal_agenda_view_get_next_date (GcalView *view)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);
+
+  g_assert (self->date != NULL);
+  return g_date_time_add_weeks (self->date, 1);
+}
+
+
+static GDateTime*
+gcal_agenda_view_get_previous_date (GcalView *view)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);
+
+  g_assert (self->date != NULL);
+  return g_date_time_add_weeks (self->date, -1);
+}
+
+static void
+gcal_view_interface_init (GcalViewInterface *iface)
+{
+  iface->get_date = gcal_agenda_view_get_date;
+  iface->set_date = gcal_agenda_view_set_date;
+  iface->get_children_by_uuid = gcal_agenda_view_get_children_by_uuid;
+  iface->clear_marks = gcal_agenda_view_clear_marks;
+  iface->get_next_date = gcal_agenda_view_get_next_date;
+  iface->get_previous_date = gcal_agenda_view_get_previous_date;
+}
+
+
+/*
+ * GcalTimelineSubscriber iface
+ */
+
+static GcalRange*
+gcal_agenda_view_get_range (GcalTimelineSubscriber *subscriber)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber);
+
+  return gcal_range_new_take (g_date_time_ref (self->date),
+                              gcal_date_time_get_end_of_week (self->date),
+                              GCAL_RANGE_DEFAULT);
+}
+
+static void
+gcal_agenda_view_add_event (GcalTimelineSubscriber *subscriber,
+                            GcalEvent              *event)
+{
+  GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber);
+  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);
+
+  if (gcal_date_time_compare_date (self->date, gcal_event_get_date_start (event)) == 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,
+                         "context", self->context,
+                         "event", event,
+                         "orientation", GTK_ORIENTATION_VERTICAL,
+                         "timestamp-policy", timestamp_policy,
+                         NULL);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "activatable", FALSE,
+                      "child", widget,
+                      NULL);
+
+  gcal_range_tree_add_range (self->events,
+                             gcal_event_get_range (event),
+                             child_data_new (row, event, self));
+
+  /* FIXME */
+  /* 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);
+  GPtrArray *widgets;
+  guint i;
+
+  GCAL_ENTRY;
+
+  widgets = gcal_range_tree_get_all_data (self->events);
+
+  if (gcal_date_time_compare_date (self->date, gcal_event_get_date_start (event)) == 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
+gcal_agenda_view_update_event (GcalTimelineSubscriber *subscriber,
+                               GcalEvent              *event)
+{
+  GCAL_ENTRY;
+
+  gcal_agenda_view_remove_event (subscriber, event);
+  gcal_agenda_view_add_event (subscriber, event);
+
+  GCAL_EXIT;
+}
+
+static void
+gcal_timeline_subscriber_interface_init (GcalTimelineSubscriberInterface *iface)
+{
+  iface->get_range = gcal_agenda_view_get_range;
+  iface->add_event = gcal_agenda_view_add_event;
+  iface->update_event = gcal_agenda_view_update_event;
+  iface->remove_event = gcal_agenda_view_remove_event;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_agenda_view_dispose (GObject *object)
+{
+  GcalAgendaView *self;
+
+  self = GCAL_AGENDA_VIEW (object);
+
+  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);
+
+  g_clear_pointer (&self->date, g_date_time_unref);
+
+  g_clear_object (&self->context);
+
+  /* Chain up to parent's finalize() method. */
+  G_OBJECT_CLASS (gcal_agenda_view_parent_class)->finalize (object);
+}
+
+static void
+gcal_agenda_view_set_property (GObject      *object,
+                               guint         property_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GcalAgendaView *self = (GcalAgendaView *) object;
+
+  switch (property_id)
+    {
+    case PROP_DATE:
+      gcal_view_set_date (GCAL_VIEW (object), g_value_get_boxed (value));
+      break;
+
+    case PROP_CONTEXT:
+      g_assert (self->context == NULL);
+      self->context = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gcal_agenda_view_get_property (GObject    *object,
+                               guint       property_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GcalAgendaView *self;
+
+  g_return_if_fail (GCAL_IS_AGENDA_VIEW (object));
+  self = GCAL_AGENDA_VIEW (object);
+
+  switch (property_id)
+    {
+    case PROP_DATE:
+      g_value_set_boxed (value, self->date);
+      break;
+
+    case PROP_CONTEXT:
+      g_value_set_object (value, self->context);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gcal_agenda_view_class_init (GcalAgendaViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = gcal_agenda_view_dispose;
+  object_class->finalize = gcal_agenda_view_finalize;
+  object_class->set_property = gcal_agenda_view_set_property;
+  object_class->get_property = gcal_agenda_view_get_property;
+
+  g_object_class_override_property (object_class, PROP_DATE, "active-date");
+  g_object_class_override_property (object_class, PROP_CONTEXT, "context");
+
+  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, scrolled_window);
+
+  gtk_widget_class_bind_template_callback (widget_class, on_event_activated);
+
+  gtk_widget_class_set_css_name (widget_class, "agenda-view");
+}
+
+static void
+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);
+
+  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);
+}
diff --git a/src/gui/views/gcal-agenda-view.h b/src/gui/views/gcal-agenda-view.h
new file mode 100644
index 00000000..59b215ef
--- /dev/null
+++ b/src/gui/views/gcal-agenda-view.h
@@ -0,0 +1,29 @@
+/* gcal-agenda-view.h
+ *
+ * Copyright (C) 2022 Purism SPC
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "gcal-manager.h"
+#include "gcal-view.h"
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_AGENDA_VIEW (gcal_agenda_view_get_type ())
+G_DECLARE_FINAL_TYPE (GcalAgendaView, gcal_agenda_view, GCAL, AGENDA_VIEW, GtkBox)
+
+G_END_DECLS
diff --git a/src/gui/views/gcal-agenda-view.ui b/src/gui/views/gcal-agenda-view.ui
new file mode 100644
index 00000000..c15a4f12
--- /dev/null
+++ b/src/gui/views/gcal-agenda-view.ui
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<interface>
+  <template class="GcalAgendaView" parent="GtkBox">
+    <property name="orientation">vertical</property>
+    <style>
+      <class name="week-view" />
+    </style>
+    <child>
+      <object class="GcalWeekHeader" id="header">
+        <property name="visible">False</property>
+        <signal name="event-activated" handler="on_event_activated" object="GcalAgendaView" swapped="yes" />
+      </object>
+    </child>
+    <child>
+      <object class="GcalWeekGrid" id="week_grid">
+        <property name="visible">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <signal name="event-activated" handler="on_event_activated" object="GcalAgendaView" swapped="yes" />
+      </object>
+    </child>
+    <child>
+      <object class="GtkScrolledWindow" id="scrolled_window">
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GtkListBox" id="list_box">
+            <property name="hexpand">True</property>
+            <property name="selection-mode">none</property>
+            <property name="vexpand">True</property>
+            <child>
+              <object class="GtkListBoxRow" id="no_events_row">
+                <property name="activatable">False</property>
+                <property name="child">
+                  <object class="GtkLabel">
+                    <property name="label">No events</property>
+                    <property name="xalign">0</property>
+                    <style>
+                      <class name="no-events" />
+                      <class name="dim-label" />
+                    </style>
+                  </object>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gui/views/meson.build b/src/gui/views/meson.build
index 717adf4a..f68b3dc9 100644
--- a/src/gui/views/meson.build
+++ b/src/gui/views/meson.build
@@ -7,6 +7,7 @@ built_sources += gnome.compile_resources(
 )
 
 sources += files(
+  'gcal-agenda-view.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 dea33d62..e1c21c87 100644
--- a/src/gui/views/views.gresource.xml
+++ b/src/gui/views/views.gresource.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/org/gnome/calendar/ui/views">
+    <file compressed="true">gcal-agenda-view.ui</file>
     <file compressed="true">gcal-month-cell.ui</file>
     <file compressed="true">gcal-month-popover.ui</file>
     <file compressed="true">gcal-month-view.ui</file>
diff --git a/src/theme/Adwaita.css b/src/theme/Adwaita.css
index d09f525c..5538a31f 100644
--- a/src/theme/Adwaita.css
+++ b/src/theme/Adwaita.css
@@ -373,6 +373,20 @@ monthpopover > box {
   background: none;
 }
 
+/* Agenda View CSS */
+
+agenda-view list > row {
+  padding: 2px 12px;
+}
+
+agenda-view list > label {
+  padding: 6px 12px;
+}
+
+label.no-events {
+  font-style: italic;
+}
+
 /*
  * Search
  */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]