From 68160348dcce3bfda2facc56a3734048946cdd96 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Wed, 11 Nov 2020 17:04:30 -0300 Subject: [PATCH] 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(-) create mode 100644 src/gui/gcal-event-popover.c create mode 100644 src/gui/gcal-event-popover.h create mode 100644 src/gui/gcal-event-popover.ui create mode 100644 src/gui/gcal-meeting-row.c create mode 100644 src/gui/gcal-meeting-row.h create mode 100644 src/gui/gcal-meeting-row.ui 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 + * + * 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 "GcalEventPopover" + +#include "gcal-debug.h" +#include "gcal-event-popover.h" +#include "gcal-meeting-row.h" +#include "gcal-utils.h" + +#include +#include + +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, ""); + g_string_append (string, location); + g_string_append (string, ""); + + 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, "
", "\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 + * + * 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-context.h" +#include "gcal-event.h" + +#include + +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 @@ + + + + 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 + * + * 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 "GcalMeetingRow" + +#include "gcal-meeting-row.h" +#include "gcal-utils.h" + +#include + +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 ("%s", 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 + * + * 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 + +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 @@ + + + + + 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 @@ gcal-calendar-popover.ui + gcal-event-popover.ui gcal-event-widget.ui + gcal-meeting-row.ui gcal-quick-add-popover.ui gcal-weather-settings.ui gcal-window.ui 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; }