[gnome-calendar/gbsneto/event-preview-popover: 2/2] Introduce GcalEventPopover
- From: Georges Basile Stavracas Neto <gbsneto src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-calendar/gbsneto/event-preview-popover: 2/2] Introduce GcalEventPopover
- Date: Sun, 27 Jun 2021 21:45:30 +0000 (UTC)
commit 68160348dcce3bfda2facc56a3734048946cdd96
Author: Georges Basile Stavracas Neto <georges stavracas gmail com>
Date: Wed Nov 11 17:04:30 2020 -0300
Introduce GcalEventPopover
GcalEventPopover gives a preview of the event, and handy access
to the event description and information. It also allows easily
joining meetings if any one if available.
The list of supported meeting URLs is small now, but I hope
more people will contribute as we go.
It is also possible to edit the event through an edit button.
src/gui/gcal-event-popover.c | 676 ++++++++++++++++++++++++++++++++++++++++++
src/gui/gcal-event-popover.h | 36 +++
src/gui/gcal-event-popover.ui | 169 +++++++++++
src/gui/gcal-event-widget.c | 77 +++++
src/gui/gcal-event-widget.h | 15 +-
src/gui/gcal-meeting-row.c | 208 +++++++++++++
src/gui/gcal-meeting-row.h | 33 +++
src/gui/gcal-meeting-row.ui | 78 +++++
src/gui/gcal-window.c | 29 +-
src/gui/gui.gresource.xml | 2 +
src/gui/meson.build | 2 +
src/theme/Adwaita.css | 19 ++
12 files changed, 1337 insertions(+), 7 deletions(-)
---
diff --git a/src/gui/gcal-event-popover.c b/src/gui/gcal-event-popover.c
new file mode 100644
index 00000000..94cfabcf
--- /dev/null
+++ b/src/gui/gcal-event-popover.c
@@ -0,0 +1,676 @@
+/* gcal-event-popover.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 "GcalEventPopover"
+
+#include "gcal-debug.h"
+#include "gcal-event-popover.h"
+#include "gcal-meeting-row.h"
+#include "gcal-utils.h"
+
+#include <glib/gi18n.h>
+#include <handy.h>
+
+struct _GcalEventPopover
+{
+ GtkPopover parent;
+
+ GtkLabel *date_time_label;
+ GtkLabel *description_label;
+ GtkWidget *edit_button;
+ GtkWidget *location_box;
+ GtkLabel *location_label;
+ GtkListBox *meetings_listbox;
+ GtkLabel *placeholder_label;
+ GtkLabel *summary_label;
+
+ GcalContext *context;
+ GcalEvent *event;
+};
+
+static void on_join_meeting_cb (GcalMeetingRow *meeting_row,
+ const gchar *url,
+ GcalEventPopover *self);
+
+G_DEFINE_TYPE (GcalEventPopover, gcal_event_popover, GTK_TYPE_POPOVER)
+
+enum
+{
+ PROP_0,
+ PROP_CONTEXT,
+ PROP_EVENT,
+ N_PROPS
+};
+
+enum
+{
+ EDIT,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+static gint
+get_number_of_days_from_today (GDateTime *now,
+ GDateTime *day)
+{
+ GDate event_day;
+ GDate today;
+
+ g_date_set_dmy (&today,
+ g_date_time_get_day_of_month (now),
+ g_date_time_get_month (now),
+ g_date_time_get_year (now));
+
+ g_date_set_dmy (&event_day,
+ g_date_time_get_day_of_month (day),
+ g_date_time_get_month (day),
+ g_date_time_get_year (day));
+
+ return g_date_days_between (&today, &event_day);
+}
+
+static gchar*
+format_time (GcalEventPopover *self,
+ GDateTime *date)
+{
+ GcalTimeFormat time_format;
+
+ time_format = gcal_context_get_time_format (self->context);
+
+ switch (time_format)
+ {
+ case GCAL_TIME_FORMAT_12H:
+ return g_date_time_format (date, "%I:%M %P");
+
+ case GCAL_TIME_FORMAT_24H:
+ return g_date_time_format (date, "%R");
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ return NULL;
+}
+
+static const gchar*
+get_month_name (gint month)
+{
+ const gchar *month_names[] = {
+ N_("January"),
+ N_("February"),
+ N_("March"),
+ N_("April"),
+ N_("May"),
+ N_("June"),
+ N_("July"),
+ N_("August"),
+ N_("September"),
+ N_("October"),
+ N_("November"),
+ N_("December"),
+ NULL
+ };
+
+ return gettext (month_names[month]);
+}
+
+static gchar*
+format_multiday_date (GcalEventPopover *self,
+ GDateTime *dt,
+ gboolean force_show_year,
+ gboolean show_time)
+{
+ g_autoptr (GDateTime) now = NULL;
+ gint n_days_from_dt;
+
+ now = g_date_time_new_now_local ();
+ n_days_from_dt = get_number_of_days_from_today (now, dt);
+
+ if (show_time)
+ {
+ g_autofree gchar *hours = format_time (self, dt);
+
+ if (n_days_from_dt == 0)
+ {
+ return g_strdup_printf (_("Today %s"), hours);
+ }
+ else if (n_days_from_dt == 1)
+ {
+ return g_strdup_printf (_("Tomorrow %s"), hours);
+ }
+ else if (n_days_from_dt == -1)
+ {
+ return g_strdup_printf (_("Yesterday %s"), hours);
+ }
+ else if (!force_show_year && g_date_time_get_year (now) == g_date_time_get_year (dt))
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), %2$d is the day
+ * of month, and %3$ is the hour. This format string results in dates
+ * like "November 21, 22:00".
+ */
+ return g_strdup_printf (_("%1$s %2$d, %3$s"),
+ get_month_name (g_date_time_get_month (dt) - 1),
+ g_date_time_get_day_of_month (dt),
+ hours);
+ }
+ else
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), %2$d is the day
+ * of month, %3$d is the year, and %4$s is the hour. This format string
+ * results in dates like "November 21, 2020, 22:00".
+ */
+ return g_strdup_printf (_("%1$s %2$d, %3$d, %4$s"),
+ get_month_name (g_date_time_get_month (dt) - 1),
+ g_date_time_get_day_of_month (dt),
+ g_date_time_get_year (dt),
+ hours);
+ }
+
+ }
+ else
+ {
+ if (n_days_from_dt == 0)
+ {
+ return g_strdup (_("Today"));
+ }
+ else if (n_days_from_dt == 1)
+ {
+ return g_strdup (_("Tomorrow"));
+ }
+ else if (n_days_from_dt == -1)
+ {
+ return g_strdup (_("Yesterday"));
+ }
+ else if (!force_show_year && g_date_time_get_year (now) == g_date_time_get_year (dt))
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), and %2$d is
+ * the day of month. This format string results in dates like
+ * "November 21".
+ */
+ return g_strdup_printf (_("%1$s %2$d"),
+ get_month_name (g_date_time_get_month (dt) - 1),
+ g_date_time_get_day_of_month (dt));
+ }
+ else
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), %2$d is the day
+ * of month, and %3$d is the year. This format string results in dates
+ * like "November 21, 2020".
+ */
+ return g_strdup_printf (_("%1$s %2$d, %3$d"),
+ get_month_name (g_date_time_get_month (dt) - 1),
+ g_date_time_get_day_of_month (dt),
+ g_date_time_get_year (dt));
+ }
+
+ }
+
+ g_assert_not_reached ();
+}
+
+static gchar*
+format_single_day (GcalEventPopover *self,
+ GDateTime *start_dt,
+ GDateTime *end_dt,
+ gboolean show_time)
+{
+ g_autoptr (GDateTime) now = NULL;
+ gint n_days_from_dt;
+
+ now = g_date_time_new_now_local ();
+ n_days_from_dt = get_number_of_days_from_today (now, start_dt);
+
+ if (show_time)
+ {
+ g_autofree gchar *start_hours = format_time (self, start_dt);
+ g_autofree gchar *end_hours = format_time (self, end_dt);
+
+ if (n_days_from_dt == 0)
+ {
+ /*
+ * Translators: %1$s is the start hour, and %2$s is the end hour, for
+ * example: "Today, 19:00 — 22:00"
+ */
+ return g_strdup_printf (_("Today, %1$s — %2$s"), start_hours, end_hours);
+ }
+ else if (n_days_from_dt == 1)
+ {
+ /*
+ * Translators: %1$s is the start hour, and %2$s is the end hour, for
+ * example: "Tomorrow, 19:00 — 22:00"
+ */
+ return g_strdup_printf (_("Tomorrow, %1$s – %2$s"), start_hours, end_hours);
+ }
+ else if (n_days_from_dt == -1)
+ {
+ /*
+ * Translators: %1$s is the start hour, and %2$s is the end hour, for
+ * example: "Tomorrow, 19:00 — 22:00"
+ */
+ return g_strdup_printf (_("Yesterday, %1$s – %2$s"), start_hours, end_hours);
+ }
+ else if (g_date_time_get_year (now) == g_date_time_get_year (start_dt))
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), %2$d is the day
+ * of month, %3$s is the start hour, and %4$s is the end hour. This
+ * format string results in dates like "November 21, 19:00 — 22:00".
+ */
+ return g_strdup_printf (_("%1$s %2$d, %3$s – %4$s"),
+ get_month_name (g_date_time_get_month (start_dt) - 1),
+ g_date_time_get_day_of_month (start_dt),
+ start_hours,
+ end_hours);
+ }
+ else
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), %2$d is the day
+ * of month, %3$d is the year, %4$s is the start hour, and %5$s is the
+ * end hour. This format string results in dates like:
+ *
+ * "November 21, 2021, 19:00 — 22:00".
+ */
+ return g_strdup_printf (_("%1$s %2$d, %3$d, %4$s – %5$s"),
+ get_month_name (g_date_time_get_month (start_dt) - 1),
+ g_date_time_get_day_of_month (start_dt),
+ g_date_time_get_year (start_dt),
+ start_hours,
+ end_hours);
+ }
+ }
+ else
+ {
+ if (n_days_from_dt == 0)
+ {
+ return g_strdup (_("Today"));
+ }
+ else if (n_days_from_dt == 1)
+ {
+ return g_strdup (_("Tomorrow"));
+ }
+ else if (n_days_from_dt == -1)
+ {
+ return g_strdup (_("Yesterday"));
+ }
+ else if (g_date_time_get_year (now) == g_date_time_get_year (start_dt))
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), and %2$d is
+ * the day of month. This format string results in dates like
+ * "November 21".
+ */
+ return g_strdup_printf (_("%1$s %2$d"),
+ get_month_name (g_date_time_get_month (start_dt) - 1),
+ g_date_time_get_day_of_month (start_dt));
+ }
+ else
+ {
+ /*
+ * Translators: %1$s is a month name (e.g. November), %2$d is the day
+ * of month, and %3$d is the year. This format string results in dates
+ * like "November 21, 2020".
+ */
+ return g_strdup_printf (_("%1$s %2$d, %3$d"),
+ get_month_name (g_date_time_get_month (start_dt) - 1),
+ g_date_time_get_day_of_month (start_dt),
+ g_date_time_get_year (start_dt));
+ }
+ }
+
+ g_assert_not_reached ();
+}
+
+static void
+update_date_time_label (GcalEventPopover *self)
+{
+ g_autoptr (GString) string = NULL;
+ GDateTime *end_dt;
+ GDateTime *start_dt;
+ gboolean show_hours;
+ gboolean multiday;
+ gboolean all_day;
+
+ string = g_string_new ("");
+ all_day = gcal_event_get_all_day (self->event);
+ multiday = gcal_event_is_multiday (self->event);
+ show_hours = !all_day;
+
+ end_dt = gcal_event_get_date_end (self->event);
+ start_dt = gcal_event_get_date_start (self->event);
+
+ if (multiday)
+ {
+ g_autofree gchar *start_str = NULL;
+ g_autofree gchar *end_str = NULL;
+ gboolean show_year;
+
+ show_year = g_date_time_get_year (start_dt) != g_date_time_get_year (end_dt);
+ start_str = format_multiday_date (self, start_dt, show_year, show_hours);
+ end_str = format_multiday_date (self, end_dt, show_year, show_hours);
+
+ /* Translators: %1$s is the start date, and %2$s. For example: June 21 - November 29, 2022 */
+ g_string_printf (string, _("%1$s — %2$s"), start_str, end_str);
+
+ }
+ else
+ {
+ g_autofree gchar *str = format_single_day (self, start_dt, end_dt, show_hours);
+ g_string_append (string, str);
+ }
+
+ gtk_label_set_label (self->date_time_label, string->str);
+}
+
+static void
+update_placeholder_label (GcalEventPopover *self)
+{
+ gboolean placeholder_visible = FALSE;
+
+ placeholder_visible |= !gtk_widget_get_visible (GTK_WIDGET (self->location_box)) &&
+ !gtk_widget_get_visible (GTK_WIDGET (self->description_label));
+ gtk_widget_set_visible (GTK_WIDGET (self->placeholder_label), placeholder_visible);
+}
+
+static void
+add_meeting (GcalEventPopover *self,
+ const gchar *url)
+{
+ GtkWidget *row;
+
+ row = gcal_meeting_row_new (url);
+ g_signal_connect (row, "join-meeting", G_CALLBACK (on_join_meeting_cb), self);
+ gtk_container_add (GTK_CONTAINER (self->meetings_listbox), row);
+
+ gtk_widget_show (GTK_WIDGET (self->meetings_listbox));
+}
+
+static void
+setup_location_label (GcalEventPopover *self)
+{
+ g_autoptr (SoupURI) soup_uri = NULL;
+ g_autofree gchar *location = NULL;
+
+ location = g_strdup (gcal_event_get_location (self->event));
+ g_strstrip (location);
+
+ soup_uri = soup_uri_new (location);
+ if (soup_uri)
+ {
+ GString *string;
+
+ string = g_string_new (NULL);
+ g_string_append (string, "<a href=\"");
+ g_string_append (string, location);
+ g_string_append (string, "\">");
+ g_string_append (string, location);
+ g_string_append (string, "</a>");
+
+ add_meeting (self, location);
+
+ g_clear_pointer (&location, g_free);
+ location = g_string_free (string, FALSE);
+ }
+
+ gtk_widget_set_visible (self->location_box,
+ location && g_utf8_strlen (location, -1) > 0);
+ gtk_label_set_markup (self->location_label, location);
+}
+
+static void
+setup_description_label (GcalEventPopover *self)
+{
+ g_autofree gchar *description = NULL;
+ g_autofree gchar *meeting_url = NULL;
+ g_autoptr (GString) string = NULL;
+
+ gcal_utils_extract_google_section (gcal_event_get_description (self->event),
+ &description,
+ &meeting_url);
+ g_strstrip (description);
+
+ if (meeting_url)
+ add_meeting (self, meeting_url);
+
+ string = g_string_new (description);
+ g_string_replace (string, "<br>", "\n", 0);
+ g_string_replace (string, " ", " ", 0);
+
+ gtk_label_set_markup (self->description_label, string->str);
+ gtk_widget_set_visible (GTK_WIDGET (self->description_label), string->str && *string->str);
+}
+
+static void
+set_event_internal (GcalEventPopover *self,
+ GcalEvent *event)
+{
+ g_set_object (&self->event, event);
+
+ gtk_label_set_label (self->summary_label, gcal_event_get_summary (event));
+
+ setup_description_label (self);
+ setup_location_label (self);
+ update_placeholder_label (self);
+ update_date_time_label (self);
+
+ gtk_widget_grab_focus (self->edit_button);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_edit_button_clicked_cb (GtkButton *edit_button,
+ GcalEventPopover *self)
+{
+ g_signal_emit (self, signals[EDIT], 0);
+ gtk_popover_popdown (GTK_POPOVER (self));
+}
+
+static void
+on_join_meeting_cb (GcalMeetingRow *meeting_row,
+ const gchar *url,
+ GcalEventPopover *self)
+{
+ g_autoptr (GError) error = NULL;
+ GtkWindow *window;
+
+ window = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+ g_assert (window != NULL);
+
+ gtk_show_uri_on_window (window, url, GDK_CURRENT_TIME, &error);
+ if (error)
+ {
+ g_warning ("Error opening URL: %s", error->message);
+ return;
+ }
+
+ /* For some reason, gtk_popover_popdown() crashes when called here */
+ gtk_widget_hide (GTK_WIDGET (self));
+}
+
+static void
+on_time_format_changed_cb (GcalEventPopover *self)
+{
+ GCAL_ENTRY;
+
+ update_date_time_label (self);
+
+ GCAL_EXIT;
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_event_popover_finalize (GObject *object)
+{
+ GcalEventPopover *self = (GcalEventPopover *)object;
+
+ g_clear_object (&self->context);
+ g_clear_object (&self->event);
+
+ G_OBJECT_CLASS (gcal_event_popover_parent_class)->finalize (object);
+}
+
+static void
+gcal_event_popover_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalEventPopover *self = GCAL_EVENT_POPOVER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ g_value_set_object (value, self->context);
+ break;
+
+ case PROP_EVENT:
+ g_value_set_object (value, self->event);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_event_popover_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalEventPopover *self = GCAL_EVENT_POPOVER (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;
+
+ case PROP_EVENT:
+ g_assert (self->event == NULL);
+ set_event_internal (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_event_popover_class_init (GcalEventPopoverClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gcal_event_popover_finalize;
+ object_class->get_property = gcal_event_popover_get_property;
+ object_class->set_property = gcal_event_popover_set_property;
+
+ /**
+ * GcalEventPopover::context:
+ *
+ * The context of the event popover.
+ */
+ properties[PROP_CONTEXT] = g_param_spec_object ("context",
+ "Context",
+ "Context",
+ GCAL_TYPE_CONTEXT,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+ /**
+ * GcalEventPopover::event:
+ *
+ * The event this popover represents.
+ */
+ properties[PROP_EVENT] = g_param_spec_object ("event",
+ "Event",
+ "The event this popover represents",
+ GCAL_TYPE_EVENT,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ signals[EDIT] = g_signal_new ("edit",
+ GCAL_TYPE_EVENT_POPOVER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/calendar/ui/gui/gcal-event-popover.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, date_time_label);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, description_label);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, edit_button);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, location_box);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, location_label);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, meetings_listbox);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, placeholder_label);
+ gtk_widget_class_bind_template_child (widget_class, GcalEventPopover, summary_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_edit_button_clicked_cb);
+}
+
+static void
+gcal_event_popover_init (GcalEventPopover *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_LEFT);
+ else
+ gtk_popover_set_position (GTK_POPOVER (self), GTK_POS_RIGHT);
+}
+
+GtkWidget*
+gcal_event_popover_new (GcalContext *context,
+ GcalEvent *event)
+{
+ return g_object_new (GCAL_TYPE_EVENT_POPOVER,
+ "context", context,
+ "event", event,
+ NULL);
+}
diff --git a/src/gui/gcal-event-popover.h b/src/gui/gcal-event-popover.h
new file mode 100644
index 00000000..03f3bc7e
--- /dev/null
+++ b/src/gui/gcal-event-popover.h
@@ -0,0 +1,36 @@
+/* gcal-event-popover.h
+ *
+ * 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
+ */
+
+#pragma once
+
+#include "gcal-context.h"
+#include "gcal-event.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_EVENT_POPOVER (gcal_event_popover_get_type())
+G_DECLARE_FINAL_TYPE (GcalEventPopover, gcal_event_popover, GCAL, EVENT_POPOVER, GtkPopover)
+
+GtkWidget* gcal_event_popover_new (GcalContext *context,
+ GcalEvent *event);
+
+G_END_DECLS
diff --git a/src/gui/gcal-event-popover.ui b/src/gui/gcal-event-popover.ui
new file mode 100644
index 00000000..3f366e28
--- /dev/null
+++ b/src/gui/gcal-event-popover.ui
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GcalEventPopover" parent="GtkPopover">
+
+ <style>
+ <class name="event-popover" />
+ </style>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="spacing">0</property>
+ <property name="orientation">vertical</property>
+
+ <!-- Title -->
+ <child>
+ <object class="GtkLabel" id="summary_label">
+ <property name="visible">True</property>
+ <property name="margin">12</property>
+ <property name="ellipsize">end</property>
+ <property name="width-chars">30</property>
+ <property name="max-width-chars">40</property>
+ <attributes>
+ <attribute name="weight" value="bold" />
+ </attributes>
+ </object>
+ </child>
+
+ <!-- Date & Time -->
+ <child>
+ <object class="GtkLabel" id="date_time_label">
+ <property name="visible">True</property>
+ <property name="ellipsize">end</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-width">True</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="max-content-height">400</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="margin">12</property>
+ <property name="spacing">12</property>
+ <property name="orientation">vertical</property>
+
+
+ <!-- Placeholder -->
+ <child>
+ <object class="GtkLabel" id="placeholder_label">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">No event information</property>
+ <attributes>
+ <attribute name="style" value="italic" />
+ </attributes>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Description -->
+ <child>
+ <object class="GtkLabel" id="description_label">
+ <property name="use-markup">True</property>
+ <property name="selectable">True</property>
+ <property name="max-width-chars">50</property>
+ <property name="xalign">0.0</property>
+ <property name="wrap">True</property>
+ <property name="wrap-mode">word-char</property>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+
+ <!-- Location -->
+ <child>
+ <object class="GtkBox" id="location_box">
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="xalign">0.0</property>
+ <property name="label" translatable="yes">Location</property>
+ <attributes>
+ <attribute name="weight" value="bold" />
+ </attributes>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkLabel" id="location_label">
+ <property name="visible">True</property>
+ <property name="selectable">True</property>
+ <property name="xalign">0.0</property>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+
+ <!-- Meetings -->
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible" bind-source="meetings_listbox" bind-property="visible"
bind-flags="default|sync-create" />
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkListBox" id="meetings_listbox">
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="background" />
+ </style>
+ </object>
+ </child>
+
+
+ <!-- Edit button -->
+ <child>
+ <object class="GtkButton" id="edit_button">
+ <property name="visible">True</property>
+ <property name="can-default">True</property>
+ <property name="label" translatable="yes">Edit…</property>
+ <signal name="clicked" handler="on_edit_button_clicked_cb" object="GcalEventPopover"
swapped="no" />
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/src/gui/gcal-event-widget.c b/src/gui/gcal-event-widget.c
index 99c9286f..dd34f1c4 100644
--- a/src/gui/gcal-event-widget.c
+++ b/src/gui/gcal-event-widget.c
@@ -25,6 +25,8 @@
#include "gcal-application.h"
#include "gcal-context.h"
#include "gcal-clock.h"
+#include "gcal-debug.h"
+#include "gcal-event-popover.h"
#include "gcal-event-widget.h"
#include "gcal-utils.h"
@@ -32,6 +34,13 @@
#define INTENSITY(c) ((c->red) * 0.30 + (c->green) * 0.59 + (c->blue) * 0.11)
#define ICON_SIZE 16
+typedef struct
+{
+ GcalEventWidget *event_widget;
+ GcalEventPreviewCallback callback;
+ gpointer user_data;
+} PreviewData;
+
struct _GcalEventWidget
{
GtkBin parent;
@@ -527,6 +536,40 @@ gcal_event_widget_set_event_internal (GcalEventWidget *self,
G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
}
+static void
+reply_preview_callback (GtkWidget *event_popover,
+ PreviewData *data,
+ GcalEventPreviewAction action)
+{
+ if (data->callback)
+ data->callback (data->event_widget, action, data->user_data);
+
+ g_signal_handlers_disconnect_by_data (event_popover, data);
+
+ gtk_widget_destroy (event_popover);
+
+ g_clear_pointer (&data, g_free);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_event_popover_closed_cb (GtkWidget *event_popover,
+ PreviewData *data)
+{
+ reply_preview_callback (event_popover, data, GCAL_EVENT_PREVIEW_ACTION_NONE);
+}
+
+static void
+on_event_popover_edit_cb (GtkWidget *event_popover,
+ PreviewData *data)
+{
+ reply_preview_callback (event_popover, data, GCAL_EVENT_PREVIEW_ACTION_EDIT);
+}
+
/*
* GtkWidget overrides
@@ -1134,6 +1177,40 @@ gcal_event_widget_set_date_start (GcalEventWidget *self,
}
}
+/**
+ * gcal_event_widget_show_preview:
+ * @self: a #GcalEventWidget
+ * @callback: (nullable): callback for the event preview
+ * @user_data: (nullable): user data for @callback
+ *
+ * Shows an event preview popover for this event widget, and
+ * calls @callback when the popover is either dismissed, or
+ * the edit button is clicked.
+ */
+void
+gcal_event_widget_show_preview (GcalEventWidget *self,
+ GcalEventPreviewCallback callback,
+ gpointer user_data)
+{
+ PreviewData *data;
+ GtkWidget *event_popover;
+
+ GCAL_ENTRY;
+
+ data = g_new0 (PreviewData, 1);
+ data->event_widget = self;
+ data->callback = callback;
+ data->user_data = user_data;
+
+ event_popover = gcal_event_popover_new (self->context, self->event);
+ gtk_popover_set_relative_to (GTK_POPOVER (event_popover), GTK_WIDGET (self));
+ g_signal_connect (event_popover, "closed", G_CALLBACK (on_event_popover_closed_cb), data);
+ g_signal_connect (event_popover, "edit", G_CALLBACK (on_event_popover_edit_cb), data);
+ gtk_popover_popup (GTK_POPOVER (event_popover));
+
+ GCAL_EXIT;
+}
+
/**
* gcal_event_widget_get_event:
* @self: a #GcalEventWidget
diff --git a/src/gui/gcal-event-widget.h b/src/gui/gcal-event-widget.h
index 05ad16bc..c1bb81a1 100644
--- a/src/gui/gcal-event-widget.h
+++ b/src/gui/gcal-event-widget.h
@@ -26,10 +26,19 @@
G_BEGIN_DECLS
-#define GCAL_TYPE_EVENT_WIDGET (gcal_event_widget_get_type ())
+typedef enum
+{
+ GCAL_EVENT_PREVIEW_ACTION_NONE,
+ GCAL_EVENT_PREVIEW_ACTION_EDIT,
+} GcalEventPreviewAction;
+#define GCAL_TYPE_EVENT_WIDGET (gcal_event_widget_get_type ())
G_DECLARE_FINAL_TYPE (GcalEventWidget, gcal_event_widget, GCAL, EVENT_WIDGET, GtkBin)
+typedef void (*GcalEventPreviewCallback) (GcalEventWidget *event_widget,
+ GcalEventPreviewAction action,
+ gpointer user_data);
+
GtkWidget* gcal_event_widget_new (GcalContext *context,
GcalEvent *event);
@@ -48,6 +57,10 @@ void gcal_event_widget_set_date_end (GcalEventWidge
void gcal_event_widget_set_read_only (GcalEventWidget *event,
gboolean read_only);
+void gcal_event_widget_show_preview (GcalEventWidget *self,
+ GcalEventPreviewCallback callback,
+ gpointer user_data);
+
/* Utilities */
GtkWidget* gcal_event_widget_clone (GcalEventWidget *widget);
diff --git a/src/gui/gcal-meeting-row.c b/src/gui/gcal-meeting-row.c
new file mode 100644
index 00000000..ac2a0c54
--- /dev/null
+++ b/src/gui/gcal-meeting-row.c
@@ -0,0 +1,208 @@
+/* gcal-meeting-row.c
+ *
+ * Copyright 2021 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 "GcalMeetingRow"
+
+#include "gcal-meeting-row.h"
+#include "gcal-utils.h"
+
+#include <glib/gi18n.h>
+
+struct _GcalMeetingRow
+{
+ GtkListBoxRow parent_instance;
+
+ GtkLabel *service_label;
+ GtkLabel *url_label;
+
+ gchar *url;
+};
+
+G_DEFINE_TYPE (GcalMeetingRow, gcal_meeting_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum
+{
+ PROP_0,
+ PROP_URL,
+ N_PROPS
+};
+
+enum
+{
+ JOIN_MEETING,
+ N_SIGNALS,
+};
+
+static guint signals[N_SIGNALS] = { 0, };
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+
+/*
+ * Auxiliary methods
+ */
+
+const gchar*
+get_service_name_from_url (const gchar *url)
+{
+ struct {
+ const gchar *needle;
+ const gchar *service_name;
+ } service_name_vtable[] = {
+ { "meet.google.com", N_("Google Meet") },
+ { "meet.jit.si", N_("Jitsi") },
+ { "whereby.com", N_("Whereby") },
+ { "zoom.us", N_("Zoom") },
+ };
+ gsize i;
+
+ for (i = 0; i < G_N_ELEMENTS (service_name_vtable); i++)
+ {
+ if (strstr (url, service_name_vtable[i].needle))
+ return gettext (service_name_vtable[i].service_name);
+ }
+
+ return _("Unknown Service");
+}
+
+static void
+setup_meeting (GcalMeetingRow *self)
+{
+ g_autofree gchar *markup_url = NULL;
+
+ gtk_label_set_label (self->service_label, get_service_name_from_url (self->url));
+
+ markup_url = g_strdup_printf ("<a href=\"%s\">%s</a>", self->url, self->url);
+ gtk_label_set_markup (self->url_label, markup_url);
+}
+
+
+/*
+ * Callbacks
+ */
+
+static void
+on_join_button_clicked_cb (GtkButton *button,
+ GcalMeetingRow *self)
+{
+ g_signal_emit (self, signals[JOIN_MEETING], 0, self->url);
+}
+
+
+/*
+ * GObject overrides
+ */
+
+static void
+gcal_meeting_row_finalize (GObject *object)
+{
+ GcalMeetingRow *self = (GcalMeetingRow *)object;
+
+ g_clear_pointer (&self->url, g_free);
+
+ G_OBJECT_CLASS (gcal_meeting_row_parent_class)->finalize (object);
+}
+
+static void
+gcal_meeting_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalMeetingRow *self = GCAL_MEETING_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_URL:
+ g_value_set_string (value, self->url);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_meeting_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalMeetingRow *self = GCAL_MEETING_ROW (object);
+
+ switch (prop_id)
+ {
+ case PROP_URL:
+ g_assert (self->url == NULL);
+ self->url = g_value_dup_string (value);
+ setup_meeting (self);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_meeting_row_class_init (GcalMeetingRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = gcal_meeting_row_finalize;
+ object_class->get_property = gcal_meeting_row_get_property;
+ object_class->set_property = gcal_meeting_row_set_property;
+
+ signals[JOIN_MEETING] = g_signal_new ("join-meeting",
+ GCAL_TYPE_MEETING_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ properties[PROP_URL] = g_param_spec_string ("url",
+ "URL of the meeting",
+ "URL of the meeting",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/calendar/ui/gui/gcal-meeting-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GcalMeetingRow, service_label);
+ gtk_widget_class_bind_template_child (widget_class, GcalMeetingRow, url_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_join_button_clicked_cb);
+}
+
+static void
+gcal_meeting_row_init (GcalMeetingRow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget*
+gcal_meeting_row_new (const gchar *url)
+{
+ return g_object_new (GCAL_TYPE_MEETING_ROW,
+ "url", url,
+ NULL);
+}
diff --git a/src/gui/gcal-meeting-row.h b/src/gui/gcal-meeting-row.h
new file mode 100644
index 00000000..51367021
--- /dev/null
+++ b/src/gui/gcal-meeting-row.h
@@ -0,0 +1,33 @@
+/* gcal-meeting-row.h
+ *
+ * Copyright 2021 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
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_MEETING_ROW (gcal_meeting_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GcalMeetingRow, gcal_meeting_row, GCAL, MEETING_ROW, GtkListBoxRow)
+
+GtkWidget* gcal_meeting_row_new (const gchar *url);
+
+G_END_DECLS
diff --git a/src/gui/gcal-meeting-row.ui b/src/gui/gcal-meeting-row.ui
new file mode 100644
index 00000000..71d4b96f
--- /dev/null
+++ b/src/gui/gcal-meeting-row.ui
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.14"/>
+ <template class="GcalMeetingRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <property name="activatable">False</property>
+ <focus-chain>
+ <widget name="url_label"/>
+ <widget name="join_button"/>
+ </focus-chain>
+
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">6</property>
+ <property name="column-spacing">18</property>
+
+ <child>
+ <object class="GtkLabel" id="service_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0.0</property>
+ <style>
+ <class name="title" />
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">0</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkLabel" id="url_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0.0</property>
+ <style>
+ <class name="subtitle" />
+ <class name="dim-label" />
+ </style>
+ </object>
+ <packing>
+ <property name="top-attach">1</property>
+ <property name="left-attach">0</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkButton" id="join_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <!-- Translators: "Join" as in "Join meeting" -->
+ <property name="label" translatable="yes">Join</property>
+ <style>
+ <class name="suggested-action" />
+ </style>
+ <signal name="clicked" handler="on_join_button_clicked_cb" object="GcalMeetingRow" swapped="no"
/>
+ </object>
+ <packing>
+ <property name="top-attach">0</property>
+ <property name="left-attach">1</property>
+ <property name="height">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/src/gui/gcal-window.c b/src/gui/gcal-window.c
index dd1ecadc..65183985 100644
--- a/src/gui/gcal-window.c
+++ b/src/gui/gcal-window.c
@@ -636,17 +636,34 @@ create_event_detailed_cb (GcalView *view,
g_clear_object (&comp);
}
+static void
+event_preview_cb (GcalEventWidget *event_widget,
+ GcalEventPreviewAction action,
+ gpointer user_data)
+{
+ GcalWindow *self = GCAL_WINDOW (user_data);
+ GcalEvent *event;
+
+ switch (action)
+ {
+ case GCAL_EVENT_PREVIEW_ACTION_EDIT:
+ event = gcal_event_widget_get_event (event_widget);
+ gcal_event_editor_dialog_set_event (self->event_editor, event, FALSE);
+ gtk_window_present (GTK_WINDOW (self->event_editor));
+ break;
+
+ case GCAL_EVENT_PREVIEW_ACTION_NONE:
+ default:
+ break;
+ }
+}
+
static void
event_activated (GcalView *view,
GcalEventWidget *event_widget,
gpointer user_data)
{
- GcalWindow *window = GCAL_WINDOW (user_data);
- GcalEvent *event;
-
- event = gcal_event_widget_get_event (event_widget);
- gcal_event_editor_dialog_set_event (window->event_editor, event, FALSE);
- gtk_widget_show (GTK_WIDGET (window->event_editor));
+ gcal_event_widget_show_preview (event_widget, event_preview_cb, user_data);
}
static void
diff --git a/src/gui/gui.gresource.xml b/src/gui/gui.gresource.xml
index 5c481b5b..1ecfe3ca 100644
--- a/src/gui/gui.gresource.xml
+++ b/src/gui/gui.gresource.xml
@@ -2,7 +2,9 @@
<gresources>
<gresource prefix="/org/gnome/calendar/ui/gui">
<file compressed="true">gcal-calendar-popover.ui</file>
+ <file compressed="true">gcal-event-popover.ui</file>
<file compressed="true">gcal-event-widget.ui</file>
+ <file compressed="true">gcal-meeting-row.ui</file>
<file compressed="true">gcal-quick-add-popover.ui</file>
<file compressed="true">gcal-weather-settings.ui</file>
<file compressed="true">gcal-window.ui</file>
diff --git a/src/gui/meson.build b/src/gui/meson.build
index d4f55962..6a076076 100644
--- a/src/gui/meson.build
+++ b/src/gui/meson.build
@@ -15,8 +15,10 @@ built_sources += gnome.compile_resources(
sources += files(
'gcal-application.c',
'gcal-calendar-popover.c',
+ 'gcal-event-popover.c',
'gcal-event-widget.c',
'gcal-expandable-entry.c',
+ 'gcal-meeting-row.c',
'gcal-quick-add-popover.c',
'gcal-search-button.c',
'gcal-weather-settings.c',
diff --git a/src/theme/Adwaita.css b/src/theme/Adwaita.css
index c674a3fa..7658eb4d 100644
--- a/src/theme/Adwaita.css
+++ b/src/theme/Adwaita.css
@@ -225,6 +225,25 @@ event.color-light:backdrop {
color: rgba(0, 0, 0, 0.3);
}
+/* Event Popover */
+popover.event-popover,
+popover.event-popover > box {
+ padding: 0;
+}
+
+popover.event-popover > box > button {
+ border-left-width: 0;
+ border-right-width: 0;
+ border-bottom-width: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+popover.event-popover textview.view,
+popover.event-popover textview.view text {
+ background-color: transparent;
+}
+
.search-viewport {
background-color: @theme_base_color;
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]