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; }