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.
This commit is contained in:
Georges Basile Stavracas Neto
2021-06-27 18:45:12 -03:00
parent 01ee4a2b49
commit 68160348dc
12 changed files with 1337 additions and 7 deletions
+676
View File
@@ -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, "&nbsp;", " ", 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);
}
+36
View File
@@ -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
+169
View File
@@ -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>
+77
View File
@@ -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
+14 -1
View File
@@ -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);
+208
View File
@@ -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);
}
+33
View File
@@ -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
+78
View File
@@ -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>
+23 -6
View File
@@ -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
+2
View File
@@ -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>
+2
View File
@@ -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',
+19
View File
@@ -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;
}