[gnome-calendar/wip/gbsneto/sidebar: 1/5] sidebar: introduce a sidebar



commit 298ca41e65d264e4f4706d217266eaada832e122
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date:   Thu Jun 8 07:52:54 2017 -0300

    sidebar: introduce a sidebar
    
    This commit adds GcalSidebar to the code base, which
    is a sidebar that allows quick peak of the events of
    a certain day.

 data/Makefile.am            |    1 +
 data/calendar.gresource.xml |    1 +
 data/theme/gtk-styles.css   |   10 +
 src/Makefile.am             |    2 +
 src/gcal-sidebar.c          |  587 +++++++++++++++++++++++++++++++++++++++++++
 src/gcal-sidebar.h          |   38 +++
 6 files changed, 639 insertions(+), 0 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index 1d8363d..6e2651f 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -78,6 +78,7 @@ EXTRA_DIST=                     \
   ui/multi-choice.ui            \
   ui/quick-add-popover.ui       \
   ui/search-view.ui             \
+  ui/sidebar.ui                 \
   ui/source-dialog.ui           \
   ui/time-selector.ui           \
   ui/week-header.ui             \
diff --git a/data/calendar.gresource.xml b/data/calendar.gresource.xml
index 6b5397e..9d41a35 100644
--- a/data/calendar.gresource.xml
+++ b/data/calendar.gresource.xml
@@ -10,6 +10,7 @@
     <file alias="online-account-row.ui" compressed="true" 
preprocess="xml-stripblanks">ui/online-account-row.ui</file>
     <file alias="quick-add-popover.ui" compressed="true" 
preprocess="xml-stripblanks">ui/quick-add-popover.ui</file>
     <file alias="search-view.ui" compressed="true" preprocess="xml-stripblanks">ui/search-view.ui</file>
+    <file alias="sidebar.ui" compressed="true" preprocess="xml-stripblanks">ui/sidebar.ui</file>
     <file alias="source-dialog.ui" compressed="true" preprocess="xml-stripblanks">ui/source-dialog.ui</file>
     <file alias="time-selector.ui" compressed="true" preprocess="xml-stripblanks">ui/time-selector.ui</file>
     <file alias="week-header.ui" compressed="true" preprocess="xml-stripblanks">ui/week-header.ui</file>
diff --git a/data/theme/gtk-styles.css b/data/theme/gtk-styles.css
index 061373f..44c297d 100644
--- a/data/theme/gtk-styles.css
+++ b/data/theme/gtk-styles.css
@@ -422,3 +422,13 @@ weekgrid:selected,
 .week-header:selected {
     border: solid 1px alpha(@theme_selected_bg_color, 0.8);
 }
+
+/* Sidebar */
+label.tiny-label {
+    font-size: smaller;
+}
+
+/* HACK: fix black separator */
+separator.header-separator {
+    background-color: @borders;
+}
diff --git a/src/Makefile.am b/src/Makefile.am
index d63f545..e11febc 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -71,6 +71,8 @@ gnome_calendar_SOURCES =                                  \
     gcal-search-view.c                                    \
     gcal-shell-search-provider.h                          \
     gcal-shell-search-provider.c                          \
+    gcal-sidebar.c                                        \
+    gcal-sidebar.h                                        \
     gcal-source-dialog.c                                  \
     gcal-source-dialog.h                                  \
     gcal-subscriber-view.c                                \
diff --git a/src/gcal-sidebar.c b/src/gcal-sidebar.c
new file mode 100644
index 0000000..4bc9a86
--- /dev/null
+++ b/src/gcal-sidebar.c
@@ -0,0 +1,587 @@
+/* gcal-sidebar.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#define G_LOG_DOMAIN "GcalSidebar"
+
+#include "e-cal-data-model-subscriber.h"
+#include "gcal-date-chooser.h"
+#include "gcal-event.h"
+#include "gcal-manager.h"
+#include "gcal-sidebar.h"
+#include "gcal-utils.h"
+#include "gcal-view.h"
+
+#include <glib/gi18n.h>
+
+struct _GcalSidebar
+{
+  GtkRevealer         parent;
+
+  GcalDateChooser    *date_chooser;
+  GtkWidget          *empty_placeholder_widget;
+  GtkWidget          *listbox;
+  GtkWidget          *search_placeholder_widget;
+  GtkWidget          *sidebar_header;
+
+  icaltimetype       *active_date;
+  GcalManager        *manager;
+};
+
+static void           gcal_view_interface_init                   (GcalViewInterface *iface);
+
+static void   e_cal_data_model_subscriber_interface_init         (ECalDataModelSubscriberInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GcalSidebar, gcal_sidebar, GTK_TYPE_REVEALER,
+                         G_IMPLEMENT_INTERFACE (GCAL_TYPE_VIEW, gcal_view_interface_init)
+                         G_IMPLEMENT_INTERFACE (E_TYPE_CAL_DATA_MODEL_SUBSCRIBER,
+                                                e_cal_data_model_subscriber_interface_init))
+
+enum
+{
+  PROP_0,
+  PROP_ACTIVE_DATE,
+  PROP_MANAGER,
+  N_PROPS
+};
+
+/* Auxiliary methods */
+
+static GtkWidget*
+create_row_for_event (GcalEvent *event)
+{
+  cairo_surface_t *surface;
+  GtkWidget *row, *grid, *icon, *label;
+  GdkRGBA *color;
+
+  /* Box */
+  grid = g_object_new (GTK_TYPE_GRID,
+                       "column-spacing", 6,
+                       "margin", 12,
+                       "margin-bottom", 6,
+                       "margin-top", 6,
+                       NULL);
+  /* Circular icon */
+  color = gcal_event_get_color (event);
+  surface = get_circle_surface_from_color (color, 12);
+
+  icon = gtk_image_new_from_surface (surface);
+
+  gtk_grid_attach (GTK_GRID (grid), icon, 0, 0, 1, 1);
+
+  /* Summary label */
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", gcal_event_get_summary (event),
+                        "hexpand", TRUE,
+                        "xalign", 0.0,
+                        "ellipsize", PANGO_ELLIPSIZE_END,
+                        "single-line-mode", TRUE,
+                        NULL);
+
+  gtk_grid_attach (GTK_GRID (grid), label, 1, 0, 1, 1);
+
+  /* Description label */
+  if (g_utf8_strlen (gcal_event_get_description (event), -1) > 0)
+    {
+      label = g_object_new (GTK_TYPE_LABEL,
+                        "label", gcal_event_get_description (event),
+                        "hexpand", TRUE,
+                        "xalign", 0.0,
+                        "ellipsize", PANGO_ELLIPSIZE_END,
+                        "single-line-mode", TRUE,
+                        NULL);
+
+      gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+      gtk_style_context_add_class (gtk_widget_get_style_context (label), "tiny-label");
+
+      gtk_grid_attach (GTK_GRID (grid), label, 1, 1, 1, 1);
+    }
+
+  /* Row */
+  row = gtk_list_box_row_new ();
+  gtk_container_add (GTK_CONTAINER (row), grid);
+
+  g_object_set_data_full (G_OBJECT (row), "event", g_object_ref (event), g_object_unref);
+
+  gtk_widget_show_all (row);
+
+  g_clear_pointer (&surface, cairo_surface_destroy);
+  g_clear_pointer (&color, gdk_rgba_free);
+
+  return row;
+}
+
+static void
+reload_events (GcalSidebar *self)
+{
+  g_autoptr (GDateTime) end_date, now;
+  GDateTime *selected_date;
+  guint64 start, end;
+
+  selected_date = gcal_date_chooser_get_date (GCAL_DATE_CHOOSER (self->date_chooser));
+  now = g_date_time_new_now_local ();
+
+  /*
+   * If the selected date is today, show the next 2 days. Otherwise
+   * only show events of the selected date.
+   */
+  if (datetime_compare_date (now, selected_date) == 0)
+    end_date = g_date_time_add_days (selected_date, 2);
+  else
+    end_date = g_date_time_add_days (selected_date, 1);
+
+  start = g_date_time_to_unix (selected_date);
+  end = g_date_time_to_unix (end_date);
+
+  gcal_manager_set_subscriber (self->manager,
+                               E_CAL_DATA_MODEL_SUBSCRIBER (self),
+                               start,
+                               end);
+}
+
+/* Stealed from GNOME To Do */
+static void
+get_date_offset (GDateTime *dt,
+                 gint      *days_diff,
+                 gint      *next_year_diff)
+{
+  g_autoptr (GDateTime) now, next_year;
+
+  now = g_date_time_new_now_local ();
+  next_year = g_date_time_new_utc (g_date_time_get_year (now) + 1,
+                                   G_DATE_JANUARY,
+                                   1,
+                                   0, 0, 0);
+
+  if (g_date_time_get_year (dt) == g_date_time_get_year (now))
+    {
+      if (days_diff)
+        *days_diff = g_date_time_get_day_of_year (dt) - g_date_time_get_day_of_year (now);
+    }
+  else
+    {
+      if (next_year_diff)
+        *next_year_diff = g_date_time_difference (dt, now) / G_TIME_SPAN_DAY;
+    }
+
+  if (next_year_diff)
+    *next_year_diff = g_date_time_difference (next_year, now) / G_TIME_SPAN_DAY;
+}
+
+static gchar*
+get_string_for_date (GDateTime *dt)
+{
+  gchar *str;
+  gint days_diff;
+  gint next_year_diff;
+
+  /* This case should never happen */
+  if (!dt)
+    return g_strdup (_("No date set"));
+
+  days_diff = next_year_diff = 0;
+
+  get_date_offset (dt, &days_diff, &next_year_diff);
+
+  if (days_diff < 0)
+    {
+      g_autofree gchar *date_format;
+
+      date_format = g_date_time_format (dt, "%x");
+      str = g_strdup_printf (g_dngettext (NULL, "Yesterday", date_format, -days_diff), -days_diff);
+    }
+  else if (days_diff == 0)
+    {
+      str = g_strdup (_("Today"));
+    }
+  else if (days_diff == 1)
+    {
+      str = g_strdup (_("Tomorrow"));
+    }
+  else if (days_diff > 1 && days_diff < 7)
+    {
+      str = g_date_time_format (dt, "%A"); // Weekday name
+    }
+  else
+    {
+      str = g_date_time_format (dt, "%x");
+    }
+
+  return str;
+}
+
+/* Listbox functions */
+
+static void
+header_func (GtkListBoxRow *row,
+             GtkListBoxRow *before,
+             gpointer       user_data)
+{
+  GcalSidebar *self;
+  GDateTime *row_date, *selected_date;
+  GcalEvent *row_event;
+  GtkWidget *label;
+  g_autofree gchar *text;
+
+  text = NULL;
+  self = GCAL_SIDEBAR (user_data);
+  selected_date = gcal_date_chooser_get_date (self->date_chooser);
+  row_event = g_object_get_data (G_OBJECT (row), "event");
+  row_date = gcal_event_get_date_start (row_event);
+
+  gtk_list_box_row_set_header (row, NULL);
+
+  /* Normalize row date */
+  if (datetime_compare_date (row_date, selected_date) < 0)
+    row_date = selected_date;
+
+  if (before)
+    {
+      GcalEvent *before_event;
+      GDateTime *before_date;
+
+      before_event = g_object_get_data (G_OBJECT (before), "event");
+      before_date = gcal_event_get_date_start (before_event);
+
+      /* Normalize previous row date */
+      if (datetime_compare_date (before_date, selected_date) < 0)
+        before_date = selected_date;
+
+      /* Don't add headers if the start dates are equal */
+      if (datetime_compare_date (before_date, row_date) == 0)
+        return;
+    }
+
+  /* Add the label as the row header */
+  text = get_string_for_date (row_date);
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", text,
+                        "xalign", 0.0,
+                        "hexpand", TRUE,
+                        "margin-start", 12,
+                        "margin-top", before ? 12 : 6,
+                        "margin-bottom", 6,
+                        NULL);
+
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), "sidebar-header");
+  gtk_list_box_row_set_header (row, label);
+}
+
+static gint
+sort_func (GtkListBoxRow *a,
+           GtkListBoxRow *b,
+           gpointer       user_data)
+{
+  GcalEvent *event_a, *event_b;
+
+  event_a = g_object_get_data (G_OBJECT (a), "event");
+  event_b = g_object_get_data (G_OBJECT (b), "event");
+
+  return gcal_event_compare (event_a, event_b);
+}
+
+/* Callbacks */
+
+static void
+update_header_visibility (GcalSidebar *self)
+{
+  if (!gtk_revealer_get_child_revealed (GTK_REVEALER (self)))
+    gtk_widget_hide (self->sidebar_header);
+
+  if (gtk_revealer_get_reveal_child (GTK_REVEALER (self)))
+    gtk_widget_show (self->sidebar_header);
+
+  /*
+   * We can't bind 'reveal-child' with GcalSidebar because it'll make the
+   * 'reveal-child' property change ~before~ it reaches here and makes the
+   * sidebar header visible. When that happens, the header skips the reveal
+   * animation, and looses the synchronization with the sidebar animation.
+   */
+  gtk_revealer_set_reveal_child (GTK_REVEALER (self->sidebar_header),
+                                 gtk_revealer_get_reveal_child (GTK_REVEALER (self)));
+}
+
+/* ECalDataModelSubscriber */
+
+static void
+gcal_sidebar_component_added (ECalDataModelSubscriber *subscriber,
+                              ECalClient              *client,
+                              ECalComponent           *comp)
+{
+  GcalSidebar *self;
+  GcalEvent *event;
+  ESource *source;
+  GError *error;
+
+  error = NULL;
+  self = GCAL_SIDEBAR (subscriber);
+  source = e_client_get_source (E_CLIENT (client));
+  event = gcal_event_new (source, comp, &error);
+
+  if (error)
+    {
+      g_warning ("Error adding events: %s", error->message);
+      return;
+    }
+
+  gtk_container_add (GTK_CONTAINER (self->listbox), create_row_for_event (event));
+}
+
+static void
+gcal_sidebar_component_changed (ECalDataModelSubscriber *subscriber,
+                                ECalClient              *client,
+                                ECalComponent           *comp)
+{
+  ECalComponentId *id;
+
+  id = e_cal_component_get_id (comp);
+
+  e_cal_data_model_subscriber_component_removed (subscriber, client, id->uid, id->rid);
+  e_cal_data_model_subscriber_component_added (subscriber, client, comp);
+
+  g_clear_pointer (&id, e_cal_component_free_id);
+}
+
+static void
+gcal_sidebar_component_removed (ECalDataModelSubscriber *subscriber,
+                                ECalClient              *client,
+                                const gchar             *uid,
+                                const gchar             *rid)
+{
+  GcalSidebar *self;
+  ESource *source;
+  GList *children, *l;
+  g_autofree gchar *uuid;
+
+  self = GCAL_SIDEBAR (subscriber);
+  source = e_client_get_source (E_CLIENT (client));
+
+  if (rid)
+    uuid = g_strdup_printf ("%s:%s:%s", e_source_get_uid (source), uid, rid);
+  else
+    uuid = g_strdup_printf ("%s:%s", e_source_get_uid (source), uid);
+
+  /* Remove the rows with the given uuid */
+  children = gtk_container_get_children (GTK_CONTAINER (self->listbox));
+
+  for (l = children; l != NULL; l = l->next)
+    {
+      GcalEvent *event;
+
+      event = g_object_get_data (l->data, "event");
+
+      if (g_str_equal (uuid, gcal_event_get_uid (event)))
+        gtk_widget_destroy (l->data);
+    }
+
+  g_list_free (children);
+}
+
+static void
+gcal_sidebar_freeze (ECalDataModelSubscriber *subscriber)
+{
+}
+
+static void
+gcal_sidebar_thaw (ECalDataModelSubscriber *subscriber)
+{
+}
+
+/* GcalView */
+
+static icaltimetype*
+gcal_sidebar_get_date (GcalView *view)
+{
+  GcalSidebar *self = GCAL_SIDEBAR (view);
+
+  return self->active_date;
+}
+
+static void
+gcal_sidebar_set_date (GcalView     *view,
+                       icaltimetype *active_date)
+{
+  GcalSidebar *self;
+  GDateTime *date;
+
+  self = GCAL_SIDEBAR (view);
+  date = icaltime_to_datetime (active_date);
+
+  g_clear_pointer (&self->active_date, g_free);
+  self->active_date = gcal_dup_icaltime (active_date);
+
+  gcal_date_chooser_set_date (self->date_chooser, date);
+
+  gcal_clear_datetime (&date);
+}
+
+static GList*
+gcal_sidebar_get_children_by_uuid (GcalView    *self,
+                                   const gchar *uuid)
+{
+  return NULL;
+}
+
+static void
+gcal_sidebar_clear_marks (GcalView *view)
+{
+}
+
+static void
+gcal_view_interface_init (GcalViewInterface *iface)
+{
+  iface->get_date = gcal_sidebar_get_date;
+  iface->set_date = gcal_sidebar_set_date;
+  iface->get_children_by_uuid = gcal_sidebar_get_children_by_uuid;
+  iface->clear_marks = gcal_sidebar_clear_marks;
+}
+
+static void
+e_cal_data_model_subscriber_interface_init (ECalDataModelSubscriberInterface *iface)
+{
+  iface->component_added = gcal_sidebar_component_added;
+  iface->component_modified = gcal_sidebar_component_changed;
+  iface->component_removed = gcal_sidebar_component_removed;
+  iface->freeze = gcal_sidebar_freeze;
+  iface->thaw = gcal_sidebar_thaw;
+}
+
+static void
+gcal_sidebar_finalize (GObject *object)
+{
+  GcalSidebar *self = (GcalSidebar *)object;
+
+  G_OBJECT_CLASS (gcal_sidebar_parent_class)->finalize (object);
+}
+
+static void
+gcal_sidebar_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  GcalSidebar *self = GCAL_SIDEBAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE_DATE:
+      g_value_set_boxed (value, self->active_date);
+      break;
+
+    case PROP_MANAGER:
+      g_value_set_object (value, self->manager);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_sidebar_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  GcalSidebar *self = GCAL_SIDEBAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE_DATE:
+      gcal_sidebar_set_date (GCAL_VIEW (self), g_value_get_boxed (value));
+      break;
+
+    case PROP_MANAGER:
+      self->manager = g_value_dup_object (value);
+      reload_events (self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gcal_sidebar_class_init (GcalSidebarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gcal_sidebar_finalize;
+  object_class->get_property = gcal_sidebar_get_property;
+  object_class->set_property = gcal_sidebar_set_property;
+
+
+  /**
+   * GcalSidebar::manager:
+   *
+   * The manager of the sidebar.
+   */
+  g_object_class_install_property (object_class,
+                                   PROP_MANAGER,
+                                   g_param_spec_object ("manager",
+                                                        "Manager",
+                                                        "Manager of the application",
+                                                        GCAL_TYPE_MANAGER,
+                                                        G_PARAM_READWRITE));
+
+  g_object_class_override_property (object_class, PROP_ACTIVE_DATE, "active-date");
+
+  g_type_ensure (GCAL_TYPE_DATE_CHOOSER);
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/sidebar.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, GcalSidebar, date_chooser);
+  gtk_widget_class_bind_template_child (widget_class, GcalSidebar, empty_placeholder_widget);
+  gtk_widget_class_bind_template_child (widget_class, GcalSidebar, listbox);
+  gtk_widget_class_bind_template_child (widget_class, GcalSidebar, search_placeholder_widget);
+  gtk_widget_class_bind_template_child (widget_class, GcalSidebar, sidebar_header);
+
+  gtk_widget_class_bind_template_callback (widget_class, reload_events);
+  gtk_widget_class_bind_template_callback (widget_class, update_header_visibility);
+}
+
+static void
+gcal_sidebar_init (GcalSidebar *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_list_box_set_placeholder (GTK_LIST_BOX (self->listbox), self->empty_placeholder_widget);
+
+  gtk_list_box_set_header_func (GTK_LIST_BOX (self->listbox),
+                                header_func,
+                                self,
+                                NULL);
+
+  gtk_list_box_set_sort_func (GTK_LIST_BOX (self->listbox),
+                              sort_func,
+                              NULL,
+                              NULL);
+}
+
+GtkWidget*
+gcal_sidebar_new (void)
+{
+  return g_object_new (GCAL_TYPE_SIDEBAR, NULL);
+}
+
+GtkWidget*
+gcal_sidebar_get_header_widget (GcalSidebar *self)
+{
+  g_return_val_if_fail (GCAL_IS_SIDEBAR (self), NULL);
+
+  return self->sidebar_header;
+}
diff --git a/src/gcal-sidebar.h b/src/gcal-sidebar.h
new file mode 100644
index 0000000..38c8fc3
--- /dev/null
+++ b/src/gcal-sidebar.h
@@ -0,0 +1,38 @@
+/* gcal-sidebar.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#ifndef GCAL_SIDEBAR_H
+#define GCAL_SIDEBAR_H
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_SIDEBAR (gcal_sidebar_get_type())
+
+G_DECLARE_FINAL_TYPE (GcalSidebar, gcal_sidebar, GCAL, SIDEBAR, GtkRevealer)
+
+GtkWidget*           gcal_sidebar_new                            (void);
+
+GtkWidget*           gcal_sidebar_get_header_widget              (GcalSidebar        *self);
+
+G_END_DECLS
+
+#endif /* GCAL_SIDEBAR_H */
+


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