summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMilan Crha <mcrha@redhat.com>2018-05-11 12:50:54 +0200
committerMilan Crha <mcrha@redhat.com>2018-05-11 12:55:00 +0200
commit8ba0b06b72e96604db51be8c032afd33fcabe400 (patch)
tree2639235d3044811bc4d9177fc161bbc2ec42b8a3
parent2be90d75c054de32697381c5a55fcf971da6c6f6 (diff)
downloadevolution-data-server-8ba0b06b72e96604db51be8c032afd33fcabe400.tar.gz
Move evolution-alarm-notify to evolution-data-server
Apart of the move itself, it also contains a UI change of the notification dialog, the same as the changed way of dealing with the reminders: a) reminders persist between sessions, until they are dismissed b) snoozed reminders also persist between sessions.
-rw-r--r--CMakeLists.txt13
-rw-r--r--config.h.in3
-rw-r--r--data/CMakeLists.txt18
-rw-r--r--data/org.gnome.Evolution-alarm-notify.desktop.in.in15
-rw-r--r--data/org.gnome.evolution-data-server.calendar.gschema.xml.in48
-rw-r--r--evolution-data-server.pc.in1
-rw-r--r--po/POTFILES.in4
-rw-r--r--src/calendar/libecal/e-cal-util.c72
-rw-r--r--src/calendar/libecal/e-cal-util.h2
-rw-r--r--src/calendar/libecal/e-reminder-watcher.c563
-rw-r--r--src/calendar/libecal/e-reminder-watcher.h36
-rw-r--r--src/libedataserver/CMakeLists.txt1
-rw-r--r--src/libedataserver/e-data-server-util.c3
-rw-r--r--src/libedataserver/libedataserver-private.h4
-rw-r--r--src/libedataserverui/CMakeLists.txt10
-rw-r--r--src/libedataserverui/e-reminders-widget.c1813
-rw-r--r--src/libedataserverui/e-reminders-widget.h109
-rw-r--r--src/libedataserverui/libedataserverui-private.c49
-rw-r--r--src/libedataserverui/libedataserverui-private.h29
-rw-r--r--src/libedataserverui/libedataserverui.h1
-rw-r--r--src/libedataserverui/libedataserverui.pc.in3
-rw-r--r--src/services/CMakeLists.txt1
-rw-r--r--src/services/evolution-alarm-notify/CMakeLists.txt67
-rw-r--r--src/services/evolution-alarm-notify/e-alarm-notify.c1105
-rw-r--r--src/services/evolution-alarm-notify/e-alarm-notify.h66
-rw-r--r--src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc1
-rw-r--r--src/services/evolution-alarm-notify/evolution-alarm-notify.c106
-rw-r--r--src/services/evolution-alarm-notify/evolution-alarm-notify.icobin0 -> 17542 bytes
28 files changed, 4083 insertions, 60 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2e43fb86d..f419db8f2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -120,6 +120,7 @@ set(libaccounts_glib_minimum_version 1.4)
set(libsignon_glib_minimum_version 1.8)
set(json_glib_minimum_version 1.0.4)
set(webkit2gtk_minimum_version 2.11.91)
+set(libcanberra_gtk_minimum_version 0.25)
# Load modules from the source tree
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules)
@@ -195,6 +196,7 @@ endif(WIN32)
set(imagesdir "${SHARE_INSTALL_PREFIX}/pixmaps/${PROJECT_NAME}")
set(moduledir "${privlibdir}/registry-modules")
set(credentialmoduledir "${privlibdir}/credential-modules")
+set(uimoduledir "${privlibdir}/ui-modules")
set(ebook_backenddir "${privlibdir}/addressbook-backends")
set(ecal_backenddir "${privlibdir}/calendar-backends")
set(ro_sourcesdir "${privdatadir}/ro-sources")
@@ -755,6 +757,17 @@ if(ENABLE_WEATHER)
unset(CMAKE_REQUIRED_LIBRARIES)
endif(ENABLE_WEATHER)
+# ************************************************
+# evolution-alarm-notify : Canberra-GTK for Sound
+# ************************************************
+
+add_printable_option(ENABLE_CANBERRA "Enable Canberra-GTK for sound in evolution-alarm-notify" ON)
+
+if(ENABLE_CANBERRA)
+ pkg_check_modules_for_option(ENABLE_CANBERRA "Canberra-GTK for sound in evolution-alarm-notify" CANBERRA libcanberra-gtk3>=${libcanberra_gtk_minimum_version})
+ set(HAVE_CANBERRA ON)
+endif(ENABLE_CANBERRA)
+
# ******************************
# File locking
# ******************************
diff --git a/config.h.in b/config.h.in
index d39555b06..99481e5f3 100644
--- a/config.h.in
+++ b/config.h.in
@@ -199,3 +199,6 @@
/* gweather_info_new() has only one argument */
#cmakedefine HAVE_ONE_ARG_GWEATHER_INFO_NEW 1
+
+/* evolution-alarm-notify - Define if using Canberra-GTK for sound */
+#cmakedefine HAVE_CANBERRA 1
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
index e1714ea18..e1be69120 100644
--- a/data/CMakeLists.txt
+++ b/data/CMakeLists.txt
@@ -1,4 +1,21 @@
# ********************************
+# evolution-alarm-notify
+# ********************************
+
+set(autostartdir ${SYSCONF_INSTALL_DIR}/xdg/autostart)
+
+configure_file(org.gnome.Evolution-alarm-notify.desktop.in.in
+ org.gnome.Evolution-alarm-notify.desktop.in
+ @ONLY
+)
+
+intltool_merge(${CMAKE_CURRENT_BINARY_DIR}/org.gnome.Evolution-alarm-notify.desktop.in org.gnome.Evolution-alarm-notify.desktop --desktop-style --utf8)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.gnome.Evolution-alarm-notify.desktop
+ DESTINATION ${autostartdir}
+)
+
+# ********************************
# GSettings schemas
# ********************************
@@ -23,6 +40,7 @@ add_custom_command(OUTPUT gschemas.compiled
add_custom_target(data-files ALL
DEPENDS gschemas.compiled
+ org.gnome.Evolution-alarm-notify.desktop
)
add_gsettings_schemas(data-files ${BUILT_SCHEMAS})
diff --git a/data/org.gnome.Evolution-alarm-notify.desktop.in.in b/data/org.gnome.Evolution-alarm-notify.desktop.in.in
new file mode 100644
index 000000000..b0d76b0b0
--- /dev/null
+++ b/data/org.gnome.Evolution-alarm-notify.desktop.in.in
@@ -0,0 +1,15 @@
+[Desktop Entry]
+Type=Application
+_Name=Evolution Alarm Notify
+_Comment=Calendar event notifications
+Icon=appointment-soon
+Exec=@privlibexecdir@/evolution-alarm-notify
+Terminal=false
+Categories=
+OnlyShowIn=GNOME;Unity;XFCE;Dawati;MATE;
+NoDisplay=true
+X-Meego-Priority=Low
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=evolution-data-server
+X-GNOME-Bugzilla-Component=calendar
+X-GNOME-Bugzilla-Version=@BASE_VERSION@.x
diff --git a/data/org.gnome.evolution-data-server.calendar.gschema.xml.in b/data/org.gnome.evolution-data-server.calendar.gschema.xml.in
index 2e82c9783..4878eb5b4 100644
--- a/data/org.gnome.evolution-data-server.calendar.gschema.xml.in
+++ b/data/org.gnome.evolution-data-server.calendar.gschema.xml.in
@@ -32,5 +32,53 @@
<default>['']</default>
<_summary>Snoozed reminders for EReminderWatcher</_summary>
</key>
+
+ <key name="notify-programs" type="as">
+ <default>[]</default>
+ <_summary>Reminder programs</_summary>
+ <_description>Programs that are allowed to be run by reminders</_description>
+ </key>
+ <key name="notify-with-tray" type="b">
+ <default>true</default>
+ <_summary>Show reminders in notification tray only</_summary>
+ <_description>When set to true, the reminders are shown only in the notification tray, otherwise the reminders dialog is shown immediately</_description>
+ </key>
+ <key name="notify-window-on-top" type="b">
+ <default>true</default>
+ <_summary>Show reminder notification dialog always on top</_summary>
+ <_description>Whether or not to show reminder notification dialog always on top. Note this works only as a hint for the window manager, which may or may not obey it.</_description>
+ </key>
+ <key name="notify-window-x" type="i">
+ <default>-1</default>
+ <_summary>X position of the reminder notification dialog</_summary>
+ </key>
+ <key name="notify-window-y" type="i">
+ <default>-1</default>
+ <_summary>Y position of the reminder notification dialog</_summary>
+ </key>
+ <key name="notify-window-width" type="i">
+ <default>-1</default>
+ <_summary>Width of the reminder notification dialog</_summary>
+ </key>
+ <key name="notify-window-height" type="i">
+ <default>-1</default>
+ <_summary>Height of the reminder notification dialog</_summary>
+ </key>
+ <key name="notify-completed-tasks" type="b">
+ <default>true</default>
+ <_summary>Show reminder notification for completed tasks</_summary>
+ </key>
+ <key name="notify-past-events" type="b">
+ <default>true</default>
+ <_summary>Show reminder notification for past events</_summary>
+ </key>
+ <key name="notify-last-snooze-minutes" type="i">
+ <default>5</default>
+ <_summary>The last used snooze time, in minutes</_summary>
+ </key>
+ <key name="notify-custom-snooze-minutes" type="ai">
+ <default>[]</default>
+ <_summary>User-defined snooze times, in minutes</_summary>
+ </key>
</schema>
</schemalist>
diff --git a/evolution-data-server.pc.in b/evolution-data-server.pc.in
index 319735bb9..d3d582374 100644
--- a/evolution-data-server.pc.in
+++ b/evolution-data-server.pc.in
@@ -5,6 +5,7 @@ privlibdir=@privlibdir@
datarootdir=@SHARE_INSTALL_PREFIX@
datadir=@SHARE_INSTALL_PREFIX@
privdatadir=@privdatadir@
+privlibexecdir=@privlibexecdir@
addressbookdbusservicename=@ADDRESS_BOOK_DBUS_SERVICE_NAME@
calendardbusservicename=@CALENDAR_DBUS_SERVICE_NAME@
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 3c1450d7e..de740a4b2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -37,6 +37,7 @@ src/calendar/libecal/e-cal-client-view.c
src/calendar/libecal/e-cal-component.c
src/calendar/libecal/e-cal-recur.c
src/calendar/libecal/e-cal-util.c
+src/calendar/libecal/e-reminder-watcher.c
src/calendar/libedata-cal/e-cal-backend.c
src/calendar/libedata-cal/e-cal-backend-sexp.c
src/calendar/libedata-cal/e-cal-backend-sync.c
@@ -183,6 +184,7 @@ data/org.gnome.evolution-data-server.addressbook.gschema.xml.in
data/org.gnome.evolution-data-server.calendar.gschema.xml.in
data/org.gnome.evolution-data-server.gschema.xml.in
data/org.gnome.evolution.shell.network-config.gschema.xml.in
+data/org.gnome.Evolution-alarm-notify.desktop.in.in
src/libebackend/e-backend.c
src/libebackend/e-cache.c
src/libebackend/e-collection-backend.c
@@ -210,6 +212,7 @@ src/libedataserver/e-webdav-session.c
src/libedataserverui/e-credentials-prompter.c
src/libedataserverui/e-credentials-prompter-impl-oauth2.c
src/libedataserverui/e-credentials-prompter-impl-password.c
+src/libedataserverui/e-reminders-widget.c
src/libedataserverui/e-trust-prompt.c
src/libedataserverui/e-webdav-discover-widget.c
src/modules/gnome-online-accounts/e-goa-password-based.c
@@ -233,6 +236,7 @@ src/modules/ubuntu-online-accounts/uoa-utils.c
[type: gettext/xml]src/modules/ubuntu-online-accounts/yahoo-mail.service.in.in
src/modules/yahoo-backend/module-yahoo-backend.c
src/services/evolution-addressbook-factory/evolution-addressbook-factory.c
+src/services/evolution-alarm-notify/e-alarm-notify.c
src/services/evolution-calendar-factory/evolution-calendar-factory.c
[type: gettext/ini]src/services/evolution-source-registry/builtin/birthdays.source.in
[type: gettext/ini]src/services/evolution-source-registry/builtin/caldav-stub.source.in
diff --git a/src/calendar/libecal/e-cal-util.c b/src/calendar/libecal/e-cal-util.c
index 8c45dab49..c59082892 100644
--- a/src/calendar/libecal/e-cal-util.c
+++ b/src/calendar/libecal/e-cal-util.c
@@ -744,6 +744,78 @@ e_cal_util_priority_from_string (const gchar *string)
return priority;
}
+/**
+ * e_cal_util_seconds_to_string:
+ * @seconds: actual time, in seconds
+ *
+ * Converts time, in seconds, into a string representation readable by humans
+ * and localized into the current locale. This can be used to convert event
+ * duration to string or similar use cases.
+ *
+ * Free the returned string with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): a newly allocated string with localized description
+ * of the given time in seconds.
+ *
+ * Since: 3.30
+ **/
+gchar *
+e_cal_util_seconds_to_string (gint64 seconds)
+{
+ gchar *times[6], *text;
+ gint ii;
+
+ ii = 0;
+ if (seconds >= 7 * 24 * 3600) {
+ gint weeks;
+
+ weeks = seconds / (7 * 24 * 3600);
+ seconds %= (7 * 24 * 3600);
+
+ times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d week", "%d weeks", weeks), weeks);
+ }
+
+ if (seconds >= 24 * 3600) {
+ gint days;
+
+ days = seconds / (24 * 3600);
+ seconds %= (24 * 3600);
+
+ times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d day", "%d days", days), days);
+ }
+
+ if (seconds >= 3600) {
+ gint hours;
+
+ hours = seconds / 3600;
+ seconds %= 3600;
+
+ times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", hours), hours);
+ }
+
+ if (seconds >= 60) {
+ gint minutes;
+
+ minutes = seconds / 60;
+ seconds %= 60;
+
+ times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", minutes), minutes);
+ }
+
+ if (seconds != 0) {
+ /* Translators: here, "second" is the time division (like "minute"), not the ordinal number (like "third") */
+ times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d second", "%d seconds", seconds), (gint) seconds);
+ }
+
+ times[ii] = NULL;
+ text = g_strjoinv (" ", times);
+ while (ii > 0) {
+ g_free (times[--ii]);
+ }
+
+ return text;
+}
+
/* callback for icalcomponent_foreach_tzid */
typedef struct {
icalcomponent *vcal_comp;
diff --git a/src/calendar/libecal/e-cal-util.h b/src/calendar/libecal/e-cal-util.h
index d43f1bf71..e96661433 100644
--- a/src/calendar/libecal/e-cal-util.h
+++ b/src/calendar/libecal/e-cal-util.h
@@ -81,6 +81,8 @@ gint e_cal_util_generate_alarms_for_list
const gchar * e_cal_util_priority_to_string (gint priority);
gint e_cal_util_priority_from_string (const gchar *string);
+gchar * e_cal_util_seconds_to_string (gint64 seconds);
+
void e_cal_util_add_timezones_from_component
(icalcomponent *vcal_comp,
icalcomponent *icalcomp);
diff --git a/src/calendar/libecal/e-reminder-watcher.c b/src/calendar/libecal/e-reminder-watcher.c
index 56c70c516..c8eeed7e3 100644
--- a/src/calendar/libecal/e-reminder-watcher.c
+++ b/src/calendar/libecal/e-reminder-watcher.c
@@ -32,11 +32,14 @@
#include "evolution-data-server-config.h"
#include <string.h>
+#include <glib/gi18n-lib.h>
#include "libedataserver/libedataserver.h"
#include "e-cal-client.h"
+#include "e-cal-system-timezone.h"
#include "e-cal-time-util.h"
+#include "e-cal-util.h"
#include "e-reminder-watcher.h"
@@ -53,6 +56,7 @@ struct _EReminderWatcherPrivate {
ESourceRegistryWatcher *registry_watcher;
GCancellable *cancellable;
GSettings *settings;
+ gboolean timers_enabled;
gulong past_changed_handler_id;
gulong snoozed_changed_handler_id;
guint expected_past_changes;
@@ -77,12 +81,13 @@ struct _EReminderWatcherPrivate {
enum {
PROP_0,
PROP_REGISTRY,
- PROP_DEFAULT_ZONE
+ PROP_DEFAULT_ZONE,
+ PROP_TIMERS_ENABLED
};
enum {
+ FORMAT_TIME,
TRIGGERED,
- REMOVED,
CHANGED,
LAST_SIGNAL
};
@@ -1028,7 +1033,7 @@ e_reminder_watcher_calc_next_midnight (EReminderWatcher *watcher)
e_reminder_watcher_debug_print ("Required correction of the day end, now at %s\n", e_reminder_watcher_timet_as_string ((gint64) midnight));
}
- if (watcher->priv->next_midnight != midnight) {
+ if (watcher->priv->next_midnight != midnight && watcher->priv->timers_enabled) {
GSList *link;
e_reminder_watcher_debug_print ("Next midnight at %s\n", e_reminder_watcher_timet_as_string ((gint64) midnight));
@@ -1084,6 +1089,26 @@ e_reminder_watcher_schedule_timer_impl (EReminderWatcher *watcher,
g_rec_mutex_unlock (&watcher->priv->lock);
}
+static void
+e_reminder_watcher_format_time_impl (EReminderWatcher *watcher,
+ const EReminderData *rd,
+ struct icaltimetype *itt,
+ gchar **inout_buffer,
+ gint buffer_size)
+{
+ struct tm tm;
+
+ g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
+ g_return_if_fail (rd != NULL);
+ g_return_if_fail (itt != NULL);
+ g_return_if_fail (inout_buffer != NULL);
+ g_return_if_fail (*inout_buffer != NULL);
+ g_return_if_fail (buffer_size > 0);
+
+ tm = icaltimetype_to_tm (itt);
+ e_time_format_date_and_time (&tm, FALSE, FALSE, FALSE, *inout_buffer, buffer_size);
+}
+
static GSList * /* EReminderData * */
e_reminder_watcher_reminders_from_key (EReminderWatcher *watcher,
const gchar *key)
@@ -1191,6 +1216,7 @@ typedef struct _EmitSignalData {
EReminderWatcher *watcher;
guint signal_id;
GSList *reminders; /* EReminderData * */
+ gboolean is_snoozed; /* only for the triggered signal */
} EmitSignalData;
static void
@@ -1213,7 +1239,10 @@ e_reminder_watcher_emit_signal_idle_cb (gpointer user_data)
g_return_val_if_fail (esd != NULL, FALSE);
g_return_val_if_fail (E_IS_REMINDER_WATCHER (esd->watcher), FALSE);
- g_signal_emit (esd->watcher, esd->signal_id, 0, esd->reminders, NULL);
+ if (esd->signal_id == signals[TRIGGERED])
+ g_signal_emit (esd->watcher, esd->signal_id, 0, esd->reminders, esd->is_snoozed, NULL);
+ else
+ g_signal_emit (esd->watcher, esd->signal_id, 0, esd->reminders, NULL);
return FALSE;
}
@@ -1221,7 +1250,8 @@ e_reminder_watcher_emit_signal_idle_cb (gpointer user_data)
static void
e_reminder_watcher_emit_signal_idle_multiple (EReminderWatcher *watcher,
guint signal_id,
- const GSList *reminders) /* EReminderData * */
+ const GSList *reminders, /* EReminderData * */
+ gboolean is_snoozed)
{
EmitSignalData *esd;
@@ -1229,6 +1259,7 @@ e_reminder_watcher_emit_signal_idle_multiple (EReminderWatcher *watcher,
esd->watcher = g_object_ref (watcher);
esd->signal_id = signal_id;
esd->reminders = g_slist_copy_deep ((GSList *) reminders, (GCopyFunc) e_reminder_data_copy, NULL);
+ esd->is_snoozed = is_snoozed;
g_idle_add_full (G_PRIORITY_HIGH_IDLE, e_reminder_watcher_emit_signal_idle_cb, esd, emit_signal_data_free);
}
@@ -1243,7 +1274,7 @@ e_reminder_watcher_emit_signal_idle (EReminderWatcher *watcher,
if (rd)
reminders = g_slist_prepend (NULL, e_reminder_data_copy (rd));
- e_reminder_watcher_emit_signal_idle_multiple (watcher, signal_id, reminders);
+ e_reminder_watcher_emit_signal_idle_multiple (watcher, signal_id, reminders, FALSE);
g_slist_free_full (reminders, e_reminder_data_free);
}
@@ -1299,8 +1330,6 @@ e_reminder_watcher_remove_from_past (EReminderWatcher *watcher,
e_reminder_watcher_save_past (watcher, reminders);
- e_reminder_watcher_emit_signal_idle (watcher, signals[REMOVED], found);
-
e_reminder_watcher_debug_print ("Removed reminder from past for '%s' from %s at %s\n",
icalcomponent_get_summary (e_cal_component_get_icalcomponent (found->component)),
found->source_uid,
@@ -1351,7 +1380,8 @@ e_reminder_watcher_remove_from_snoozed (EReminderWatcher *watcher,
static ECalClient *
e_reminder_watcher_ref_client (EReminderWatcher *watcher,
- const gchar *source_uid)
+ const gchar *source_uid,
+ GCancellable *cancellable)
{
ECalClient *client = NULL;
GSList *link;
@@ -1372,7 +1402,47 @@ e_reminder_watcher_ref_client (EReminderWatcher *watcher,
}
}
- g_rec_mutex_unlock (&watcher->priv->lock);
+ if (!client && cancellable) {
+ ESourceRegistry *registry;
+ ESource *source;
+
+ registry = g_object_ref (watcher->priv->registry);
+
+ g_rec_mutex_unlock (&watcher->priv->lock);
+
+ source = e_source_registry_ref_source (registry, source_uid);
+ if (source) {
+ ECalClientSourceType source_type = E_CAL_CLIENT_SOURCE_TYPE_LAST;
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR))
+ source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS;
+ else if (e_source_has_extension (source, E_SOURCE_EXTENSION_MEMO_LIST))
+ source_type = E_CAL_CLIENT_SOURCE_TYPE_MEMOS;
+ else if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST))
+ source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS;
+
+ if (source_type != E_CAL_CLIENT_SOURCE_TYPE_LAST) {
+ EClient *tmp_client;
+ GError *local_error = NULL;
+
+ tmp_client = e_cal_client_connect_sync (source, source_type, 30, cancellable, &local_error);
+ if (tmp_client)
+ client = E_CAL_CLIENT (tmp_client);
+
+ if (!client) {
+ e_reminder_watcher_debug_print ("Failed to connect client '%s': %s\n", source_uid, local_error ? local_error->message : "Unknown error");
+ g_clear_error (&local_error);
+ } else if (tmp_client) {
+ client = E_CAL_CLIENT (tmp_client);
+ }
+ }
+ }
+
+ g_clear_object (&source);
+ g_clear_object (&registry);
+ } else {
+ g_rec_mutex_unlock (&watcher->priv->lock);
+ }
return client;
}
@@ -1437,6 +1507,11 @@ e_reminder_watcher_maybe_schedule_next_trigger (EReminderWatcher *watcher,
{
g_rec_mutex_lock (&watcher->priv->lock);
+ if (!watcher->priv->timers_enabled) {
+ g_rec_mutex_unlock (&watcher->priv->lock);
+ return;
+ }
+
e_reminder_watcher_calc_next_midnight (watcher);
if (watcher->priv->snoozed && watcher->priv->snoozed->data) {
@@ -1629,7 +1704,9 @@ e_reminder_watcher_client_connect_cb (GObject *source_object,
e_reminder_watcher_debug_print ("Connected client: %s (%s)\n", e_source_get_uid (source), e_source_get_display_name (source));
watcher->priv->clients = g_slist_prepend (watcher->priv->clients, cd);
- client_data_start_view (cd, watcher->priv->next_midnight, watcher->priv->cancellable);
+
+ if (watcher->priv->timers_enabled)
+ client_data_start_view (cd, watcher->priv->next_midnight, watcher->priv->cancellable);
}
g_rec_mutex_unlock (&watcher->priv->lock);
@@ -1657,7 +1734,8 @@ e_reminder_watcher_source_appeared_cb (EReminderWatcher *watcher,
return;
}
- e_cal_client_connect (source, source_type, 30, watcher->priv->cancellable, e_reminder_watcher_client_connect_cb, watcher);
+ if (watcher->priv->timers_enabled)
+ e_cal_client_connect (source, source_type, 30, watcher->priv->cancellable, e_reminder_watcher_client_connect_cb, watcher);
g_rec_mutex_unlock (&watcher->priv->lock);
}
@@ -1766,6 +1844,12 @@ e_reminder_watcher_set_property (GObject *object,
E_REMINDER_WATCHER (object),
g_value_get_boxed (value));
return;
+
+ case PROP_TIMERS_ENABLED:
+ e_reminder_watcher_set_timers_enabled (
+ E_REMINDER_WATCHER (object),
+ g_value_get_boolean (value));
+ return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -1791,6 +1875,13 @@ e_reminder_watcher_get_property (GObject *object,
e_reminder_watcher_dup_default_zone (
E_REMINDER_WATCHER (object)));
return;
+
+ case PROP_TIMERS_ENABLED:
+ g_value_set_boolean (
+ value,
+ e_reminder_watcher_get_timers_enabled (
+ E_REMINDER_WATCHER (object)));
+ return;
}
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -1894,6 +1985,7 @@ e_reminder_watcher_class_init (EReminderWatcherClass *klass)
object_class->finalize = e_reminder_watcher_finalize;
klass->schedule_timer = e_reminder_watcher_schedule_timer_impl;
+ klass->format_time = e_reminder_watcher_format_time_impl;
/**
* EReminderWatcher:registry:
@@ -1934,47 +2026,76 @@ e_reminder_watcher_class_init (EReminderWatcherClass *klass)
G_PARAM_STATIC_STRINGS));
/**
- * EReminderWatcher::triggered:
+ * EReminderWatcher:timers-enabled:
+ *
+ * Whether timers are enabled for the #EReminderWatcher. See
+ * e_reminder_watcher_set_timers_enabled() for more information
+ * what it means.
+ *
+ * Default: %TRUE
+ *
+ * Since: 3.30
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_TIMERS_ENABLED,
+ g_param_spec_boolean (
+ "timers-enabled",
+ "Timers Enabled",
+ "Whether can schedule timers",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * EReminderWatcher::format-time:
* @watcher: an #EReminderWatcher
- * @reminders: (element-type EReminderData): a #GSList of #EReminderData
+ * @rd: an #EReminderData
+ * @itt: a pointer to struct icaltimetype
+ * @inout_buffer: (caller allocates) (inout): a pointer to a buffer to fill with formatted @itt
+ * @buffer_size: size of inout_buffer
*
- * Signal is emitted when any reminder is either overdue or triggered.
+ * Formats time @itt to a string and writes it to @inout_buffer, which can hold
+ * up to @buffer_size bytes. The first character of @inout_buffer is the nul-byte
+ * when nothing wrote to it yet.
*
* Since: 3.30
**/
- signals[TRIGGERED] = g_signal_new (
- "triggered",
+ signals[FORMAT_TIME] = g_signal_new (
+ "format-time",
G_OBJECT_CLASS_TYPE (klass),
- G_SIGNAL_RUN_LAST,
- G_STRUCT_OFFSET (EReminderWatcherClass, triggered),
+ G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (EReminderWatcherClass, format_time),
NULL,
NULL,
g_cclosure_marshal_generic,
- G_TYPE_NONE, 1,
- G_TYPE_POINTER);
+ G_TYPE_NONE, 4,
+ G_TYPE_POINTER,
+ G_TYPE_POINTER,
+ G_TYPE_POINTER,
+ G_TYPE_INT);
/**
- * EReminderWatcher::removed:
+ * EReminderWatcher::triggered:
* @watcher: an #EReminderWatcher
* @reminders: (element-type EReminderData): a #GSList of #EReminderData
+ * @snoozed: %TRUE, when the @reminders had been snoozed, %FALSE otherwise
*
- * Signal is emitted when any reminder is removed from the past reminders.
- * It's also followed by an EReminderWatcher::changed signal. This is used
- * when it's known which reminders had been removed from the list of past
- * reminders. It's not used when there's a notification from GSettings.
+ * Signal is emitted when any reminder is either overdue or triggered.
*
* Since: 3.30
**/
- signals[REMOVED] = g_signal_new (
- "removed",
+ signals[TRIGGERED] = g_signal_new (
+ "triggered",
G_OBJECT_CLASS_TYPE (klass),
G_SIGNAL_RUN_LAST,
- G_STRUCT_OFFSET (EReminderWatcherClass, removed),
+ G_STRUCT_OFFSET (EReminderWatcherClass, triggered),
NULL,
NULL,
g_cclosure_marshal_generic,
- G_TYPE_NONE, 1,
- G_TYPE_POINTER);
+ G_TYPE_NONE, 2,
+ G_TYPE_POINTER,
+ G_TYPE_BOOLEAN);
/**
* EReminderWatcher::changed:
@@ -2001,11 +2122,24 @@ e_reminder_watcher_class_init (EReminderWatcherClass *klass)
static void
e_reminder_watcher_init (EReminderWatcher *watcher)
{
+ icaltimezone *zone = NULL;
+ gchar *location;
+
+ location = e_cal_system_timezone_get_location ();
+ if (location) {
+ zone = icaltimezone_get_builtin_timezone (location);
+ g_free (location);
+ }
+
+ if (!zone)
+ zone = icaltimezone_get_utc_timezone ();
+
watcher->priv = G_TYPE_INSTANCE_GET_PRIVATE (watcher, E_TYPE_REMINDER_WATCHER, EReminderWatcherPrivate);
watcher->priv->cancellable = g_cancellable_new ();
watcher->priv->settings = g_settings_new ("org.gnome.evolution-data-server.calendar");
watcher->priv->scheduled = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_reminder_watcher_free_rd_slist);
- watcher->priv->default_zone = icaltimezone_get_utc_timezone ();
+ watcher->priv->default_zone = icaltimezone_copy (zone);
+ watcher->priv->timers_enabled = TRUE;
g_rec_mutex_init (&watcher->priv->lock);
}
@@ -2048,6 +2182,26 @@ e_reminder_watcher_get_registry (EReminderWatcher *watcher)
}
/**
+ * e_reminders_widget_ref_opened_client:
+ * @watcher: an #EReminderWatcher
+ * @source_uid: an #ESource UID of the calendar to return
+ *
+ * Returns: (nullable) (transfer full): a referenced #ECalClient for the @source_uid,
+ * if any such is opened; %NULL otherwise.
+ *
+ * Since: 3.30
+ **/
+ECalClient *
+e_reminder_watcher_ref_opened_client (EReminderWatcher *watcher,
+ const gchar *source_uid)
+{
+ g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), NULL);
+ g_return_val_if_fail (source_uid != NULL, NULL);
+
+ return e_reminder_watcher_ref_client (watcher, source_uid, NULL);
+}
+
+/**
* e_reminder_watcher_set_default_zone:
* @watcher: an #EReminderWatcher
* @zone: (nullable): an icaltimezone or #EReminderWatcherZone structure
@@ -2112,6 +2266,279 @@ e_reminder_watcher_dup_default_zone (EReminderWatcher *watcher)
return zone;
}
+/**
+ * e_reminder_watcher_get_timers_enabled:
+ * @watcher: an #EReminderWatcher
+ *
+ * Returns: whether timers are enabled for the @watcher. See
+ * e_reminder_watcher_set_timers_enabled() for more information
+ * what it means.
+ *
+ * Since: 3.30
+ **/
+gboolean
+e_reminder_watcher_get_timers_enabled (EReminderWatcher *watcher)
+{
+ gboolean enabled;
+
+ g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), FALSE);
+
+ g_rec_mutex_lock (&watcher->priv->lock);
+
+ enabled = watcher->priv->timers_enabled;
+
+ g_rec_mutex_unlock (&watcher->priv->lock);
+
+ return enabled;
+}
+
+/**
+ * e_reminder_watcher_set_timers_enabled:
+ * @watcher: an #EReminderWatcher
+ * @enable: a value to set
+ *
+ * The @watcher can be used both for scheduling the timers for the reminders
+ * and respond to them through the "triggered" signal, or only to listen for
+ * changes on the past reminders. The default is to have timers enabled, thus
+ * to response to scheduled reminders. Disabling the timers also means there
+ * will be less resources needed by the @watcher.
+ *
+ * Since: 3.30
+ **/
+void
+e_reminder_watcher_set_timers_enabled (EReminderWatcher *watcher,
+ gboolean enabled)
+{
+ g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
+
+ g_rec_mutex_lock (&watcher->priv->lock);
+
+ if (!enabled == !watcher->priv->timers_enabled) {
+ g_rec_mutex_unlock (&watcher->priv->lock);
+ return;
+ }
+
+ watcher->priv->timers_enabled = enabled;
+
+ if (watcher->priv->timers_enabled &&
+ !watcher->priv->construct_idle_id) {
+ e_source_registry_watcher_reclaim (watcher->priv->registry_watcher);
+ e_reminder_watcher_maybe_schedule_next_trigger (watcher, 0);
+ }
+
+ g_rec_mutex_unlock (&watcher->priv->lock);
+
+ g_object_notify (G_OBJECT (watcher), "timers-enabled");
+}
+
+static gchar *
+e_reminder_watcher_get_alarm_summary (EReminderWatcher *watcher,
+ const EReminderData *rd)
+{
+ ECalComponentText summary_text, alarm_text;
+ ECalComponentAlarm *alarm;
+ gchar *alarm_summary;
+
+ g_return_val_if_fail (watcher != NULL, NULL);
+ g_return_val_if_fail (rd != NULL, NULL);
+
+ summary_text.value = NULL;
+ alarm_text.value = NULL;
+
+ e_cal_component_get_summary (rd->component, &summary_text);
+
+ alarm = e_cal_component_get_alarm (rd->component, rd->instance.auid);
+ if (alarm) {
+ ECalClient *client;
+
+ client = e_reminder_watcher_ref_opened_client (watcher, rd->source_uid);
+
+ if (client && e_client_check_capability (E_CLIENT (client), CAL_STATIC_CAPABILITY_ALARM_DESCRIPTION)) {
+ e_cal_component_alarm_get_description (alarm, &alarm_text);
+ if (!alarm_text.value || !*alarm_text.value)
+ alarm_text.value = NULL;
+ }
+
+ g_clear_object (&client);
+ }
+
+ if (alarm_text.value && summary_text.value &&
+ e_util_utf8_strcasecmp (alarm_text.value, summary_text.value) == 0)
+ alarm_text.value = NULL;
+
+ if (summary_text.value && *summary_text.value &&
+ alarm_text.value && *alarm_text.value)
+ alarm_summary = g_strconcat (summary_text.value, "\n", alarm_text.value, NULL);
+ else if (summary_text.value && *summary_text.value)
+ alarm_summary = g_strdup (summary_text.value);
+ else if (alarm_text.value && *alarm_text.value)
+ alarm_summary = g_strdup (alarm_text.value);
+ else
+ alarm_summary = NULL;
+
+ if (alarm)
+ e_cal_component_alarm_free (alarm);
+
+ return alarm_summary;
+}
+
+/**
+ * e_reminder_watcher_describe_data:
+ * @watcher: an #EReminderWatcher
+ * @rd: an #EReminderData
+ * @flags: bit-or of #EReminderWatcherDescribeFlags
+ *
+ * Returns a new string with a text description of the @rd. The text format
+ * can be influenced with @flags.
+ *
+ * Free the returned string with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): a new string with a text description of the @rd.
+ *
+ * Since: 3.30
+ **/
+gchar *
+e_reminder_watcher_describe_data (EReminderWatcher *watcher,
+ const EReminderData *rd,
+ guint32 flags)
+{
+ icalcomponent *icalcomp;
+ gchar *description = NULL;
+ gboolean use_markup;
+
+ g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), NULL);
+ g_return_val_if_fail (rd != NULL, NULL);
+
+ use_markup = (flags & E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP) != 0;
+
+ icalcomp = e_cal_component_get_icalcomponent (rd->component);
+ if (icalcomp) {
+ gchar *summary;
+ const gchar *location;
+ gchar *timediff = NULL, *tmp;
+ gchar timestr[255];
+ GString *markup;
+
+ timestr[0] = 0;
+ markup = g_string_sized_new (256);
+ summary = e_reminder_watcher_get_alarm_summary (watcher, rd);
+ location = icalcomponent_get_location (icalcomp);
+
+ if (rd->instance.occur_start > 0) {
+ gchar *timestrptr = timestr;
+ icaltimezone *zone;
+ struct icaltimetype itt;
+ gboolean is_date = FALSE;
+
+ if (rd->instance.occur_end > rd->instance.occur_start) {
+ timediff = e_cal_util_seconds_to_string (rd->instance.occur_end - rd->instance.occur_start);
+ }
+
+ zone = e_reminder_watcher_dup_default_zone (watcher);
+ if (zone && (!icaltimezone_get_location (zone) || g_strcmp0 (icaltimezone_get_location (zone), "UTC") == 0)) {
+ icaltimezone_free (zone, 1);
+ zone = NULL;
+ }
+
+ itt = icalcomponent_get_dtstart (icalcomp);
+ if (icaltime_is_valid_time (itt) && !icaltime_is_null_time (itt))
+ is_date = itt.is_date;
+
+ itt = icaltime_from_timet_with_zone (rd->instance.occur_start, is_date, zone);
+
+ g_signal_emit (watcher, signals[FORMAT_TIME], 0, rd, &itt, &timestrptr, 254, NULL);
+
+ if (!*timestr)
+ e_reminder_watcher_format_time_impl (watcher, rd, &itt, &timestrptr, 254);
+
+ if (zone)
+ icaltimezone_free (zone, 1);
+ }
+
+ if (!summary || !*summary) {
+ g_free (summary);
+ summary = g_strdup (_( "No Summary"));
+ }
+
+ if (use_markup) {
+ tmp = g_markup_printf_escaped ("<b>%s</b>", summary);
+ g_string_append (markup, tmp);
+ g_free (tmp);
+ } else {
+ g_string_append (markup, summary);
+ }
+ g_string_append_c (markup, '\n');
+
+ if (*timestr) {
+ /* Translators: The first %s is replaced with the time string,
+ the second %s with a duration, and the third %s with an event location,
+ making it something like: "24.1.2018 10:30 (30 minutes) Meeting room A1" */
+ #define FMT_TIME_TIME_LOCATION C_("overdue", "%s (%s) %s")
+
+ /* Translators: The first %s is replaced with the time string,
+ the second %s with a duration, making is something like:
+ "24.1.2018 10:30 (30 minutes)" */
+ #define FMT_TIME_TIME C_("overdue", "%s (%s)")
+
+ /* Translators: The first %s is replaced with the time string,
+ the second %s with an event location, making it something like:
+ "24.1.2018 10:30 Meeting room A1" */
+ #define FMT_TIME_LOCATION C_("overdue", "%s %s")
+
+ if (timediff && *timediff) {
+ if (location && *location) {
+ if (use_markup)
+ tmp = g_markup_printf_escaped (FMT_TIME_TIME_LOCATION, timestr, timediff, location);
+ else
+ tmp = g_strdup_printf (FMT_TIME_TIME_LOCATION, timestr, timediff, location);
+ } else {
+ if (use_markup)
+ tmp = g_markup_printf_escaped (FMT_TIME_TIME, timestr, timediff);
+ else
+ tmp = g_strdup_printf (FMT_TIME_TIME, timestr, timediff);
+ }
+ } else if (location && *location) {
+ if (use_markup)
+ tmp = g_markup_printf_escaped (FMT_TIME_LOCATION, timestr, location);
+ else
+ tmp = g_strdup_printf (FMT_TIME_LOCATION, timestr, location);
+ } else {
+ if (use_markup)
+ tmp = g_markup_escape_text (timestr, -1);
+ else
+ tmp = g_strdup (timestr);
+ }
+
+ if (use_markup)
+ g_string_append (markup, "<small>");
+ g_string_append (markup, tmp);
+ if (use_markup)
+ g_string_append (markup, "</small>");
+
+ g_free (tmp);
+ } else if (location && *location) {
+ if (use_markup) {
+ tmp = g_markup_printf_escaped ("%s", location);
+
+ g_string_append (markup, "<small>");
+ g_string_append (markup, tmp);
+ g_string_append (markup, "</small>");
+
+ g_free (tmp);
+ } else {
+ g_string_append (markup, location);
+ }
+ }
+
+ description = g_string_free (markup, FALSE);
+
+ g_free (timediff);
+ g_free (summary);
+ }
+
+ return description;
+}
+
typedef struct _ForeachTriggerData {
gint64 current_time;
GSList *triggered; /* EReminderData * */
@@ -2183,7 +2610,7 @@ void
e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
{
ForeachTriggerData ftd;
- GSList *snoozed, *link;
+ GSList *snoozed, *link, *triggered_snoozed = NULL;
gboolean changed = FALSE;
g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
@@ -2230,13 +2657,13 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
changed = e_reminder_watcher_remove_from_snoozed (watcher, rd, FALSE) || changed;
- ftd.triggered = g_slist_prepend (ftd.triggered, rd);
+ triggered_snoozed = g_slist_prepend (triggered_snoozed, rd);
}
}
g_slist_free_full (snoozed, e_reminder_data_free);
- if (ftd.triggered) {
+ if (ftd.triggered || triggered_snoozed) {
GHashTable *last_notifies;
GHashTableIter iter;
GSList *past;
@@ -2266,6 +2693,26 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
}
}
+ for (link = triggered_snoozed; link; link = g_slist_next (link)) {
+ EReminderData *rd = e_reminder_data_copy (link->data);
+
+ if (rd) {
+ if (e_reminder_watcher_add (&past, rd, TRUE, FALSE)) {
+ time_t *ptrigger;
+
+ ptrigger = g_hash_table_lookup (last_notifies, rd->source_uid);
+ if (ptrigger) {
+ if (*ptrigger < rd->instance.trigger)
+ *ptrigger = rd->instance.trigger;
+ } else {
+ ptrigger = g_new0 (time_t, 1);
+ *ptrigger = rd->instance.trigger;
+ g_hash_table_insert (last_notifies, rd->source_uid, ptrigger);
+ }
+ }
+ }
+ }
+
e_reminder_watcher_save_past (watcher, past);
g_hash_table_iter_init (&iter, last_notifies);
@@ -2274,7 +2721,7 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
const time_t *ptrigger = value;
if (source_uid && ptrigger) {
- ECalClient *client = e_reminder_watcher_ref_client (watcher, source_uid);
+ ECalClient *client = e_reminder_watcher_ref_client (watcher, source_uid, NULL);
if (client) {
client_set_last_notification_time (client, *ptrigger);
@@ -2291,10 +2738,16 @@ e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher)
if (changed)
e_reminder_watcher_save_snoozed (watcher);
- if (ftd.triggered) {
- e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[TRIGGERED], ftd.triggered);
+ if (ftd.triggered || triggered_snoozed) {
+ if (triggered_snoozed)
+ e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[TRIGGERED], triggered_snoozed, TRUE);
+
+ if (ftd.triggered)
+ e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[TRIGGERED], ftd.triggered, FALSE);
+
e_reminder_watcher_emit_signal_idle (watcher, signals[CHANGED], NULL);
+ g_slist_free_full (triggered_snoozed, e_reminder_data_free);
g_slist_free_full (ftd.triggered, e_reminder_data_free);
}
@@ -2375,9 +2828,8 @@ e_reminder_watcher_dup_snoozed (EReminderWatcher *watcher)
*
* Snoozes @rd until @until, which is an absolute time when the @rd
* should be retriggered. This moves the @rd from the list of past
- * reminders into the list of snoozed reminders and invokes the "removed"
- * signal when the @rd was in the past reminders. It also invokes
- * the "changed" signal.
+ * reminders into the list of snoozed reminders and invokes the "changed"
+ * signal.
*
* Since: 3.30
**/
@@ -2522,9 +2974,9 @@ e_reminder_watcher_dismiss_one_sync (ECalClient *client,
success = e_cal_client_discard_alarm_sync (client, id->uid, id->rid, rd->instance.auid, cancellable, &local_error);
- e_reminder_watcher_debug_print ("Discard alarm for '%s' from %s %s%s%s%s\n",
+ e_reminder_watcher_debug_print ("Discard alarm for '%s' from %s (uid:%s rid:%s auid:%s) %s%s%s%s\n",
icalcomponent_get_summary (e_cal_component_get_icalcomponent (rd->component)),
- rd->source_uid,
+ rd->source_uid, id->uid, id->rid ? id->rid : "null", rd->instance.auid,
success ? "succeeded" : "failed",
(!success || local_error) ? " (" : "",
local_error ? local_error->message : success ? "" : "Unknown error",
@@ -2583,7 +3035,7 @@ e_reminder_watcher_dismiss_sync (EReminderWatcher *watcher,
changed = e_reminder_watcher_remove_from_snoozed (watcher, rd_copy, TRUE) || changed;
if (changed)
- client = e_reminder_watcher_ref_client (watcher, rd_copy->source_uid);
+ client = e_reminder_watcher_ref_client (watcher, rd_copy->source_uid, cancellable ? cancellable : watcher->priv->cancellable);
e_reminder_watcher_maybe_schedule_next_trigger (watcher, 0);
@@ -2699,36 +3151,36 @@ e_reminder_watcher_dismiss_all_sync (EReminderWatcher *watcher,
GCancellable *cancellable,
GError **error)
{
- GSList *reminders, *link, *dismissed = NULL;
+ GHashTable *clients; /* gchar *source_uid ~> ECalClient * */
+ GSList *reminders, *link;
gboolean success = TRUE, changed = FALSE;
g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), FALSE);
g_rec_mutex_lock (&watcher->priv->lock);
+ clients = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
reminders = e_reminder_watcher_dup_past (watcher);
for (link = reminders; link; link = g_slist_next (link)) {
EReminderData *rd = link->data;
ECalClient *client;
- client = e_reminder_watcher_ref_client (watcher, rd->source_uid);
+ client = g_hash_table_lookup (clients, rd->source_uid);
+ if (!client) {
+ client = e_reminder_watcher_ref_client (watcher, rd->source_uid, cancellable ? cancellable : watcher->priv->cancellable);
+ if (client) {
+ g_hash_table_insert (clients, g_strdup (rd->source_uid), client);
+ }
+ }
+
if (client) {
success = e_reminder_watcher_dismiss_one_sync (client, rd, cancellable, error);
- g_object_unref (client);
/* To keep the failed discard in the saved list. */
if (!success)
break;
}
-
- dismissed = g_slist_prepend (dismissed, rd);
- link->data = NULL;
- }
-
- if (dismissed) {
- e_reminder_watcher_emit_signal_idle_multiple (watcher, signals[REMOVED], dismissed);
- g_slist_free_full (dismissed, e_reminder_data_free);
}
if (link != reminders && reminders) {
@@ -2737,6 +3189,7 @@ e_reminder_watcher_dismiss_all_sync (EReminderWatcher *watcher,
}
g_slist_free_full (reminders, e_reminder_data_free);
+ g_hash_table_destroy (clients);
g_rec_mutex_unlock (&watcher->priv->lock);
diff --git a/src/calendar/libecal/e-reminder-watcher.h b/src/calendar/libecal/e-reminder-watcher.h
index 96a830ee0..3cb6a33e5 100644
--- a/src/calendar/libecal/e-reminder-watcher.h
+++ b/src/calendar/libecal/e-reminder-watcher.h
@@ -23,6 +23,7 @@
#define E_REMINDER_WATCHER_H
#include <libedataserver/libedataserver.h>
+#include <libecal/e-cal-client.h>
/* Standard GObject macros */
#define E_TYPE_REMINDER_WATCHER \
@@ -71,7 +72,7 @@ void e_reminder_data_free (gpointer rd); /* EReminderData * */
/**
* EReminderWatcherZone:
*
- * A libical's icaltimezone encapsulated as a GByxed type.
+ * A libical's icaltimezone encapsulated as a GBoxed type.
* It can be retyped into icaltimezone directly.
*
* Since: 3.30
@@ -88,6 +89,21 @@ typedef struct _EReminderWatcherClass EReminderWatcherClass;
typedef struct _EReminderWatcherPrivate EReminderWatcherPrivate;
/**
+ * EReminderWatcherDescribeFlags:
+ * @E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE: None flags
+ * @E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP: Returned description will contain
+ * also markup. Without it it'll be a plain text.
+ *
+ * Flags modifying behaviour of e_reminder_watcher_describe_data().
+ *
+ * Since: 3.30
+ **/
+typedef enum { /*< flags >*/
+ E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE = 0,
+ E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP = (1 << 1)
+} EReminderWatcherDescribeFlags;
+
+/**
* EReminderWatcher:
*
* Contains only private data that should be read and manipulated using the
@@ -107,10 +123,14 @@ struct _EReminderWatcherClass {
/* Virtual methods and signals */
void (* schedule_timer) (EReminderWatcher *watcher,
gint64 at_time);
+ void (* format_time) (EReminderWatcher *watcher,
+ const EReminderData *rd,
+ struct icaltimetype *itt,
+ gchar **inout_buffer,
+ gint buffer_size);
void (* triggered) (EReminderWatcher *watcher,
- const GSList *reminders); /* EReminderData * */
- void (* removed) (EReminderWatcher *watcher,
- const GSList *reminders); /* EReminderData * */
+ const GSList *reminders, /* EReminderData * */
+ gboolean snoozed);
void (* changed) (EReminderWatcher *watcher);
/* Padding for future expansion */
@@ -122,9 +142,17 @@ EReminderWatcher *
e_reminder_watcher_new (ESourceRegistry *registry);
ESourceRegistry *
e_reminder_watcher_get_registry (EReminderWatcher *watcher);
+ECalClient * e_reminder_watcher_ref_opened_client (EReminderWatcher *watcher,
+ const gchar *source_uid);
void e_reminder_watcher_set_default_zone (EReminderWatcher *watcher,
const icaltimezone *zone);
icaltimezone * e_reminder_watcher_dup_default_zone (EReminderWatcher *watcher);
+gboolean e_reminder_watcher_get_timers_enabled (EReminderWatcher *watcher);
+void e_reminder_watcher_set_timers_enabled (EReminderWatcher *watcher,
+ gboolean enabled);
+gchar * e_reminder_watcher_describe_data (EReminderWatcher *watcher,
+ const EReminderData *rd,
+ guint32 flags); /* bit-or of EReminderWatcherDescribeFlags */
void e_reminder_watcher_timer_elapsed (EReminderWatcher *watcher);
GSList * e_reminder_watcher_dup_past (EReminderWatcher *watcher); /* EReminderData * */
GSList * e_reminder_watcher_dup_snoozed (EReminderWatcher *watcher); /* EReminderData * */
diff --git a/src/libedataserver/CMakeLists.txt b/src/libedataserver/CMakeLists.txt
index f4e7caa35..e82b3d7ed 100644
--- a/src/libedataserver/CMakeLists.txt
+++ b/src/libedataserver/CMakeLists.txt
@@ -244,6 +244,7 @@ target_compile_definitions(edataserver PRIVATE
-DE_DATA_SERVER_LOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
-DE_DATA_SERVER_IMAGESDIR=\"${imagesdir}\"
-DE_DATA_SERVER_CREDENTIALMODULEDIR=\"${credentialmoduledir}\"
+ -DE_DATA_SERVER_UIMODULEDIR=\"${uimoduledir}\"
-DE_DATA_SERVER_PRIVDATADIR=\"${privdatadir}\"
-DLIBEDATASERVER_COMPILATION
)
diff --git a/src/libedataserver/e-data-server-util.c b/src/libedataserver/e-data-server-util.c
index 5f2c76b54..db50c340d 100644
--- a/src/libedataserver/e-data-server-util.c
+++ b/src/libedataserver/e-data-server-util.c
@@ -1863,6 +1863,7 @@ static const gchar *cp_prefix;
static const gchar *localedir;
static const gchar *imagesdir;
static const gchar *credentialmoduledir;
+static const gchar *uimoduledir;
static HMODULE hmodule;
G_LOCK_DEFINE_STATIC (mutex);
@@ -1971,6 +1972,7 @@ setup (void)
localedir = replace_prefix (cp_prefix, E_DATA_SERVER_LOCALEDIR);
imagesdir = replace_prefix (prefix, E_DATA_SERVER_IMAGESDIR);
credentialmoduledir = replace_prefix (prefix, E_DATA_SERVER_CREDENTIALMODULEDIR);
+ uimoduledir = replace_prefix (prefix, E_DATA_SERVER_UIMODULEDIR);
G_UNLOCK (mutex);
}
@@ -1995,6 +1997,7 @@ e_util_get_##varbl (void) \
PRIVATE_GETTER (imagesdir)
PRIVATE_GETTER (credentialmoduledir);
+PRIVATE_GETTER (uimoduledir);
PUBLIC_GETTER (prefix)
PUBLIC_GETTER (cp_prefix)
diff --git a/src/libedataserver/libedataserver-private.h b/src/libedataserver/libedataserver-private.h
index b9733f02f..abba7b26c 100644
--- a/src/libedataserver/libedataserver-private.h
+++ b/src/libedataserver/libedataserver-private.h
@@ -25,6 +25,7 @@
const gchar * _libedataserver_get_imagesdir (void) G_GNUC_CONST;
const gchar * _libedataserver_get_credentialmoduledir (void) G_GNUC_CONST;
+const gchar * _libedataserver_get_uimoduledir (void) G_GNUC_CONST;
#undef E_DATA_SERVER_IMAGESDIR
#define E_DATA_SERVER_IMAGESDIR _libedataserver_get_imagesdir ()
@@ -32,6 +33,9 @@ const gchar * _libedataserver_get_credentialmoduledir (void) G_GNUC_CONST;
#undef E_DATA_SERVER_CREDENTIALMODULEDIR
#define E_DATA_SERVER_CREDENTIALMODULEDIR _libedataserver_get_credentialmoduledir ()
+#undef E_DATA_SERVER_UIMODULEDIR
+#define E_DATA_SERVER_UIMODULEDIR _libedataserver_get_uimoduledir ()
+
#endif /* G_OS_WIN32 */
#endif /* LIBEDATASERVER_PRIVATE_H */
diff --git a/src/libedataserverui/CMakeLists.txt b/src/libedataserverui/CMakeLists.txt
index f039ff075..eebb7ec11 100644
--- a/src/libedataserverui/CMakeLists.txt
+++ b/src/libedataserverui/CMakeLists.txt
@@ -6,8 +6,11 @@ set(SOURCES
e-credentials-prompter-impl.c
e-credentials-prompter-impl-oauth2.c
e-credentials-prompter-impl-password.c
+ e-reminders-widget.c
e-trust-prompt.c
e-webdav-discover-widget.c
+ libedataserverui-private.h
+ libedataserverui-private.c
)
set(HEADERS
@@ -17,6 +20,7 @@ set(HEADERS
e-credentials-prompter-impl.h
e-credentials-prompter-impl-oauth2.h
e-credentials-prompter-impl-password.h
+ e-reminders-widget.h
e-trust-prompt.h
e-webdav-discover-widget.h
)
@@ -24,6 +28,7 @@ set(HEADERS
set(DEPENDENCIES
camel
ebackend
+ ecal
edataserver
)
@@ -45,6 +50,7 @@ set_target_properties(edataserverui PROPERTIES
target_compile_definitions(edataserverui PRIVATE
-DG_LOG_DOMAIN=\"e-data-server-ui\"
-DLIBEDATASERVERUI_COMPILATION
+ -DE_DATA_SERVER_UIMODULEDIR=\"${uimoduledir}\"
)
target_compile_options(edataserverui PUBLIC
@@ -104,14 +110,18 @@ set(gir_identifies_prefixes E)
set(gir_includes GObject-2.0 Gio-2.0 Gtk-3.0 Soup-2.4)
set(gir_cflags
-DLIBEDATASERVERUI_COMPILATION
+ -I${CMAKE_BINARY_DIR}/src/calendar
+ -I${CMAKE_SOURCE_DIR}/src/calendar
)
set(gir_libdirs
${CMAKE_BINARY_DIR}/src/private
+ ${CMAKE_BINARY_DIR}/src/calendar/libecal
${CMAKE_BINARY_DIR}/src/camel
${CMAKE_BINARY_DIR}/src/libedataserver
)
set(gir_libs
camel
+ ecal
edataserver
edataserverui
)
diff --git a/src/libedataserverui/e-reminders-widget.c b/src/libedataserverui/e-reminders-widget.c
new file mode 100644
index 000000000..7b42c2d95
--- /dev/null
+++ b/src/libedataserverui/e-reminders-widget.c
@@ -0,0 +1,1813 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-reminders-widget
+ * @include: libedataserverui/libedataserverui.h
+ * @short_description: An #ERemindersWidget to work with past reminders
+ *
+ * The #ERemindersWidget is a widget which does common tasks on past reminders
+ * provided by #EReminderWatcher. The owner should connect to the "changed" signal
+ * to be notified on any changes, including when the list of past reminders
+ * is either expanded or shrunk, which usually causes the dialog with this
+ * widget to be shown or hidden.
+ *
+ * The widget itself is an #EExtensible.
+ *
+ * The widget does not listen to #EReminderWatcher::triggered signal.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "libedataserver/libedataserver.h"
+#include "libecal/libecal.h"
+
+#include "libedataserverui-private.h"
+
+#include "e-reminders-widget.h"
+
+#define MAX_CUSTOM_SNOOZE_VALUES 7
+
+struct _ERemindersWidgetPrivate {
+ EReminderWatcher *watcher;
+ GSettings *settings;
+ gboolean is_empty;
+
+ GtkTreeView *tree_view;
+ GtkWidget *dismiss_button;
+ GtkWidget *dismiss_all_button;
+ GtkWidget *snooze_combo;
+ GtkWidget *snooze_button;
+
+ GtkWidget *add_snooze_popover;
+ GtkWidget *add_snooze_days_spin;
+ GtkWidget *add_snooze_hours_spin;
+ GtkWidget *add_snooze_minutes_spin;
+ GtkWidget *add_snooze_add_button;
+
+ GtkInfoBar *info_bar;
+
+ GCancellable *cancellable;
+ guint refresh_idle_id;
+
+ gboolean is_mapped;
+ guint overdue_update_id;
+ gint64 last_overdue_update; /* in seconds */
+ gboolean overdue_update_rounded;
+
+ gboolean updating_snooze_combo;
+ gint last_selected_snooze_minutes; /* not the same as the saved value in GSettings */
+};
+
+enum {
+ CHANGED,
+ ACTIVATED,
+ LAST_SIGNAL
+};
+
+enum {
+ PROP_0,
+ PROP_WATCHER,
+ PROP_EMPTY
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE (ERemindersWidget, e_reminders_widget, GTK_TYPE_GRID,
+ G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))
+
+static gboolean
+reminders_widget_snooze_combo_separator_cb (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ gint32 minutes = -1;
+
+ if (!model || !iter)
+ return FALSE;
+
+ gtk_tree_model_get (model, iter, 1, &minutes, -1);
+
+ return !minutes;
+}
+
+static GtkWidget *
+reminders_widget_new_snooze_combo (void)
+{
+ GtkWidget *combo;
+ GtkListStore *list_store;
+ GtkCellRenderer *renderer;
+
+ list_store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT);
+
+ combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (list_store));
+
+ g_object_unref (list_store);
+
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", 0, NULL);
+
+ gtk_combo_box_set_row_separator_func (GTK_COMBO_BOX (combo),
+ reminders_widget_snooze_combo_separator_cb, NULL, NULL);
+
+ return combo;
+}
+
+static void
+reminders_widget_fill_snooze_combo (ERemindersWidget *reminders,
+ gint preselect_minutes)
+{
+ const gint predefined_minutes[] = {
+ 5,
+ 10,
+ 15,
+ 30,
+ 60,
+ 24 * 60,
+ 7 * 24 * 60
+ };
+ gint ii, last_sel = -1;
+ GtkComboBox *combo;
+ GtkListStore *list_store;
+ GtkTreeIter iter, tosel_iter;
+ GVariant *variant;
+ gboolean tosel_set = FALSE;
+ gboolean any_stored_added = FALSE;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ reminders->priv->updating_snooze_combo = TRUE;
+
+ combo = GTK_COMBO_BOX (reminders->priv->snooze_combo);
+ list_store = GTK_LIST_STORE (gtk_combo_box_get_model (combo));
+
+ if (gtk_combo_box_get_active_iter (combo, &iter)) {
+ gtk_tree_model_get (GTK_TREE_MODEL (list_store), &iter, 1, &last_sel, -1);
+ }
+
+ gtk_list_store_clear (list_store);
+
+ #define add_minutes(_minutes) G_STMT_START { \
+ gint32 minutes = (_minutes); \
+ gchar *text; \
+ \
+ text = e_cal_util_seconds_to_string (minutes * 60); \
+ gtk_list_store_append (list_store, &iter); \
+ gtk_list_store_set (list_store, &iter, \
+ 0, text, \
+ 1, minutes, \
+ -1); \
+ g_free (text); \
+ \
+ if (preselect_minutes > 0 && preselect_minutes == minutes) { \
+ tosel_set = TRUE; \
+ tosel_iter = iter; \
+ last_sel = -1; \
+ } else if (last_sel > 0 && minutes == last_sel) { \
+ tosel_set = TRUE; \
+ tosel_iter = iter; \
+ } \
+ } G_STMT_END
+
+ /* Custom user values first */
+ variant = g_settings_get_value (reminders->priv->settings, "notify-custom-snooze-minutes");
+ if (variant) {
+ const gint32 *stored;
+ gsize nstored = 0;
+
+ stored = g_variant_get_fixed_array (variant, &nstored, sizeof (gint32));
+ if (stored && nstored > 0) {
+ for (ii = 0; ii < nstored; ii++) {
+ if (stored[ii] > 0) {
+ add_minutes (stored[ii]);
+ any_stored_added = TRUE;
+ }
+ }
+ }
+
+ g_variant_unref (variant);
+
+ if (any_stored_added) {
+ /* Separator */
+ gtk_list_store_append (list_store, &iter);
+ gtk_list_store_set (list_store, &iter, 1, 0, -1);
+ }
+ }
+
+ for (ii = 0; ii < G_N_ELEMENTS (predefined_minutes); ii++) {
+ add_minutes (predefined_minutes[ii]);
+ }
+
+ #undef add_minutes
+
+ /* Separator */
+ gtk_list_store_append (list_store, &iter);
+ gtk_list_store_set (list_store, &iter, 1, 0, -1);
+
+ gtk_list_store_append (list_store, &iter);
+ gtk_list_store_set (list_store, &iter, 0, _("Add custom time…"), 1, -1, -1);
+
+ if (any_stored_added) {
+ gtk_list_store_append (list_store, &iter);
+ gtk_list_store_set (list_store, &iter, 0, _("Clear custom times"), 1, -2, -1);
+ }
+
+ reminders->priv->updating_snooze_combo = FALSE;
+
+ if (tosel_set)
+ gtk_combo_box_set_active_iter (combo, &tosel_iter);
+ else
+ gtk_combo_box_set_active (combo, 0);
+}
+
+static void
+reminders_widget_custom_snooze_minutes_changed_cb (GSettings *settings,
+ const gchar *key,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ reminders_widget_fill_snooze_combo (reminders, -1);
+}
+
+static void
+reminders_get_reminder_markups (ERemindersWidget *reminders,
+ const EReminderData *rd,
+ gchar **out_overdue_markup,
+ gchar **out_description_markup)
+{
+ g_return_if_fail (rd != NULL);
+
+ if (out_overdue_markup) {
+ gint64 diff;
+ gboolean in_future;
+ gchar *time_str;
+
+ diff = (g_get_real_time () / G_USEC_PER_SEC) - ((gint64) rd->instance.occur_start);
+ in_future = diff < 0;
+ if (in_future)
+ diff = (-1) * diff;
+
+ /* in minutes */
+ if (in_future && (diff % 60) > 0)
+ diff += 60;
+
+ diff = diff / 60;
+
+ if (!diff) {
+ time_str = g_strdup (C_("overdue", "now"));
+ } else if (diff < 60) {
+ time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", diff), (gint) diff);
+ } else if (diff < 24 * 60) {
+ gint hours = diff / 60;
+
+ time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", hours), hours);
+ } else if (diff < 7 * 24 * 60) {
+ gint days = diff / (24 * 60);
+
+ time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d day", "%d days", days), days);
+ } else if (diff < 54 * 7 * 24 * 60) {
+ gint weeks = diff / (7 * 24 * 60);
+
+ time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d week", "%d weeks", weeks), weeks);
+ } else {
+ gint years = diff / (366 * 24 * 60);
+
+ time_str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d year", "%d years", years), years);
+ }
+
+ if (in_future || !diff) {
+ *out_overdue_markup = g_markup_printf_escaped ("<span size=\"x-small\">%s</span>", time_str);
+ } else {
+ *out_overdue_markup = g_markup_printf_escaped ("<span size=\"x-small\">%s\n%s</span>", time_str, C_("overdue", "overdue"));
+ }
+
+ g_free (time_str);
+ }
+
+ if (out_description_markup) {
+ *out_description_markup = e_reminder_watcher_describe_data (reminders->priv->watcher, rd, E_REMINDER_WATCHER_DESCRIBE_FLAG_MARKUP);
+ }
+}
+
+static void
+reminders_widget_overdue_update (ERemindersWidget *reminders)
+{
+ GtkListStore *list_store;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gboolean any_changed = FALSE;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ model = gtk_tree_view_get_model (reminders->priv->tree_view);
+ if (!model)
+ return;
+
+ if (!gtk_tree_model_get_iter_first (model, &iter))
+ return;
+
+ list_store = GTK_LIST_STORE (model);
+
+ do {
+ EReminderData *rd = NULL;
+
+ gtk_tree_model_get (model, &iter,
+ E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, &rd,
+ -1);
+
+ if (rd) {
+ gchar *overdue_markup = NULL;
+
+ reminders_get_reminder_markups (reminders, rd, &overdue_markup, NULL);
+ if (overdue_markup) {
+ gchar *current = NULL;
+
+ gtk_tree_model_get (model, &iter,
+ E_REMINDERS_WIDGET_COLUMN_OVERDUE, &current,
+ -1);
+
+ if (g_strcmp0 (current, overdue_markup) != 0) {
+ gtk_list_store_set (list_store, &iter,
+ E_REMINDERS_WIDGET_COLUMN_OVERDUE, overdue_markup,
+ -1);
+ any_changed = TRUE;
+ }
+
+ g_free (overdue_markup);
+ g_free (current);
+ }
+
+ e_reminder_data_free (rd);
+ }
+ } while (gtk_tree_model_iter_next (model, &iter));
+
+ if (any_changed) {
+ GtkTreeViewColumn *column;
+
+ column = gtk_tree_view_get_column (reminders->priv->tree_view, 0);
+ if (column)
+ gtk_tree_view_column_queue_resize (column);
+ }
+}
+
+static gboolean
+reminders_widget_overdue_update_cb (gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ gint64 now_seconds, last_update;
+
+ if (g_source_is_destroyed (g_main_current_source ()))
+ return FALSE;
+
+ g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), FALSE);
+
+ reminders_widget_overdue_update (reminders);
+
+ now_seconds = g_get_real_time () / G_USEC_PER_SEC;
+ last_update = reminders->priv->last_overdue_update;
+ reminders->priv->last_overdue_update = now_seconds;
+
+ if (!last_update || (
+ (now_seconds - last_update) % 60 > 2 &&
+ (now_seconds - last_update) % 60 < 58)) {
+ gint until_minute = 60 - (now_seconds % 60);
+
+ if (until_minute >= 59) {
+ reminders->priv->overdue_update_rounded = TRUE;
+ until_minute = 60;
+ } else {
+ reminders->priv->overdue_update_rounded = FALSE;
+ }
+
+ reminders->priv->overdue_update_id = g_timeout_add_seconds (until_minute,
+ reminders_widget_overdue_update_cb, reminders);
+
+ return FALSE;
+ } else if (!reminders->priv->overdue_update_rounded) {
+ reminders->priv->overdue_update_rounded = TRUE;
+ reminders->priv->overdue_update_id = g_timeout_add_seconds (60,
+ reminders_widget_overdue_update_cb, reminders);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+reminders_widget_maybe_schedule_overdue_update (ERemindersWidget *reminders)
+{
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (reminders->priv->is_empty || !reminders->priv->is_mapped) {
+ if (reminders->priv->overdue_update_id) {
+ g_source_remove (reminders->priv->overdue_update_id);
+ reminders->priv->overdue_update_id = 0;
+ }
+ } else if (!reminders->priv->overdue_update_id) {
+ gint until_minute = 60 - ((g_get_real_time () / G_USEC_PER_SEC) % 60);
+
+ reminders->priv->last_overdue_update = g_get_real_time () / G_USEC_PER_SEC;
+
+ if (until_minute >= 59) {
+ reminders->priv->overdue_update_rounded = TRUE;
+ until_minute = 60;
+ } else {
+ reminders->priv->overdue_update_rounded = FALSE;
+ }
+
+ reminders->priv->overdue_update_id = g_timeout_add_seconds (until_minute,
+ reminders_widget_overdue_update_cb, reminders);
+ }
+}
+
+static void
+reminders_widget_map (GtkWidget *widget)
+{
+ ERemindersWidget *reminders;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (widget));
+
+ /* Chain up to parent's method. */
+ GTK_WIDGET_CLASS (e_reminders_widget_parent_class)->map (widget);
+
+ reminders = E_REMINDERS_WIDGET (widget);
+ reminders->priv->is_mapped = TRUE;
+
+ reminders_widget_maybe_schedule_overdue_update (reminders);
+}
+
+
+static void
+reminders_widget_unmap (GtkWidget *widget)
+{
+ ERemindersWidget *reminders;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (widget));
+
+ /* Chain up to parent's method. */
+ GTK_WIDGET_CLASS (e_reminders_widget_parent_class)->unmap (widget);
+
+ reminders = E_REMINDERS_WIDGET (widget);
+ reminders->priv->is_mapped = FALSE;
+
+ reminders_widget_maybe_schedule_overdue_update (reminders);
+}
+
+static gint
+reminders_sort_by_occur (gconstpointer ptr1,
+ gconstpointer ptr2)
+{
+ const EReminderData *rd1 = ptr1, *rd2 = ptr2;
+ gint cmp;
+
+ if (!rd1 || !rd2)
+ return rd1 == rd2 ? 0 : rd1 ? 1 : -1;
+
+ if (rd1->instance.occur_start != rd2->instance.occur_start)
+ return rd1->instance.occur_start < rd2->instance.occur_start ? -1 : 1;
+
+ if (rd1->instance.trigger != rd2->instance.trigger)
+ return rd1->instance.trigger < rd2->instance.trigger ? -1 : 1;
+
+ cmp = g_strcmp0 (rd1->source_uid, rd2->source_uid);
+ if (!cmp)
+ cmp = g_strcmp0 (rd1->instance.auid, rd2->instance.auid);
+
+ return cmp;
+}
+
+static void
+reminders_widget_set_is_empty (ERemindersWidget *reminders,
+ gboolean is_empty)
+{
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (!is_empty == !reminders->priv->is_empty)
+ return;
+
+ reminders->priv->is_empty = is_empty;
+
+ g_object_notify (G_OBJECT (reminders), "empty");
+
+ reminders_widget_maybe_schedule_overdue_update (reminders);
+}
+
+static gint
+reminders_widget_invert_tree_path_compare (gconstpointer ptr1,
+ gconstpointer ptr2)
+{
+ return (-1) * gtk_tree_path_compare (ptr1, ptr2);
+}
+
+static void
+reminders_widget_select_one_of (ERemindersWidget *reminders,
+ GList **inout_previous_paths) /* GtkTreePath * */
+{
+ GList *link;
+ guint len;
+ gint to_select = -1;
+ gint n_rows;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (!inout_previous_paths || !*inout_previous_paths)
+ return;
+
+ n_rows = gtk_tree_model_iter_n_children (gtk_tree_view_get_model (reminders->priv->tree_view), NULL);
+ if (n_rows <= 0)
+ return;
+
+ *inout_previous_paths = g_list_sort (*inout_previous_paths, reminders_widget_invert_tree_path_compare);
+
+ len = g_list_length (*inout_previous_paths);
+
+ for (link = *inout_previous_paths; link && to_select == -1; link = g_list_next (link), len--) {
+ GtkTreePath *path = link->data;
+ gint *indices, index;
+
+ if (!path || gtk_tree_path_get_depth (path) != 1)
+ continue;
+
+ indices = gtk_tree_path_get_indices (path);
+ if (!indices)
+ continue;
+
+ index = indices[0] - len + 1;
+
+ if (index >= n_rows)
+ to_select = n_rows - 1;
+ else
+ to_select = index;
+ }
+
+ if (to_select >= 0 && to_select < n_rows) {
+ GtkTreePath *path;
+
+ path = gtk_tree_path_new_from_indices (to_select, -1);
+ if (path) {
+ gtk_tree_selection_select_path (gtk_tree_view_get_selection (reminders->priv->tree_view), path);
+ gtk_tree_path_free (path);
+ }
+ }
+}
+
+static gboolean
+reminders_widget_refresh_content_cb (gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ GList *previous_paths;
+ GSList *past;
+ GtkTreeModel *model;
+ GtkTreeSelection *selection;
+ GtkListStore *list_store;
+
+ if (g_source_is_destroyed (g_main_current_source ()))
+ return FALSE;
+
+ g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), FALSE);
+
+ reminders->priv->refresh_idle_id = 0;
+
+ model = gtk_tree_view_get_model (reminders->priv->tree_view);
+ if (!model)
+ return FALSE;
+
+ selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+ previous_paths = gtk_tree_selection_get_selected_rows (selection, NULL);
+ list_store = GTK_LIST_STORE (model);
+
+ g_object_ref (model);
+ gtk_tree_view_set_model (reminders->priv->tree_view, NULL);
+
+ gtk_list_store_clear (list_store);
+
+ past = e_reminder_watcher_dup_past (reminders->priv->watcher);
+ if (past) {
+ GSList *link;
+ GtkTreeIter iter;
+
+ past = g_slist_sort (past, reminders_sort_by_occur);
+ for (link = past; link; link = g_slist_next (link)) {
+ const EReminderData *rd = link->data;
+ gchar *overdue = NULL, *description = NULL;
+
+ if (!rd || !rd->component)
+ continue;
+
+ reminders_get_reminder_markups (reminders, rd, &overdue, &description);
+
+ gtk_list_store_append (list_store, &iter);
+ gtk_list_store_set (list_store, &iter,
+ E_REMINDERS_WIDGET_COLUMN_OVERDUE, overdue,
+ E_REMINDERS_WIDGET_COLUMN_DESCRIPTION, description,
+ E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, rd,
+ -1);
+
+ g_free (description);
+ g_free (overdue);
+ }
+ }
+
+ gtk_tree_view_set_model (reminders->priv->tree_view, model);
+ g_object_unref (model);
+
+ reminders_widget_set_is_empty (reminders, !past);
+
+ if (past) {
+ GtkTreeViewColumn *column;
+
+ column = gtk_tree_view_get_column (reminders->priv->tree_view, 0);
+ if (column)
+ gtk_tree_view_column_queue_resize (column);
+
+ reminders_widget_select_one_of (reminders, &previous_paths);
+ }
+
+ g_list_free_full (previous_paths, (GDestroyNotify) gtk_tree_path_free);
+ g_slist_free_full (past, e_reminder_data_free);
+
+ g_signal_emit (reminders, signals[CHANGED], 0, NULL);
+
+ return FALSE;
+}
+
+static void
+reminders_widget_schedule_content_refresh (ERemindersWidget *reminders)
+{
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (!reminders->priv->refresh_idle_id) {
+ reminders->priv->refresh_idle_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
+ reminders_widget_refresh_content_cb, reminders, NULL);
+ }
+}
+
+static void
+reminders_widget_watcher_changed_cb (EReminderWatcher *watcher,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ reminders_widget_schedule_content_refresh (reminders);
+}
+
+static void
+reminders_widget_gather_selected_cb (GtkTreeModel *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ GSList **inout_selected = user_data;
+ EReminderData *rd = NULL;
+
+ g_return_if_fail (inout_selected != NULL);
+
+ gtk_tree_model_get (model, iter, E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, &rd, -1);
+
+ if (rd)
+ *inout_selected = g_slist_prepend (*inout_selected, rd);
+}
+
+static void
+reminders_widget_do_dismiss_cb (ERemindersWidget *reminders,
+ const EReminderData *rd,
+ GString *gathered_errors,
+ GCancellable *cancellable,
+ gpointer user_data)
+{
+ GError *local_error = NULL;
+
+ if (g_cancellable_is_cancelled (cancellable))
+ return;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+ g_return_if_fail (rd != NULL);
+
+ if (!e_reminder_watcher_dismiss_sync (reminders->priv->watcher, rd, cancellable, &local_error) && local_error && gathered_errors &&
+ !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ if (gathered_errors->len)
+ g_string_append_c (gathered_errors, '\n');
+ g_string_append (gathered_errors, local_error->message);
+ }
+
+ g_clear_error (&local_error);
+}
+
+typedef void (* ForeachSelectedSyncFunc) (ERemindersWidget *reminders,
+ const EReminderData *rd,
+ GString *gathered_errors,
+ GCancellable *cancellable,
+ gpointer user_data);
+
+typedef struct _ForeachSelectedData {
+ GSList *selected; /* EReminderData * */
+ ForeachSelectedSyncFunc sync_func;
+ gpointer user_data;
+ GDestroyNotify user_data_destroy;
+ gchar *error_prefix;
+} ForeachSelectedData;
+
+static void
+foreach_selected_data_free (gpointer ptr)
+{
+ ForeachSelectedData *fsd = ptr;
+
+ if (fsd) {
+ g_slist_free_full (fsd->selected, e_reminder_data_free);
+ if (fsd->user_data_destroy)
+ fsd->user_data_destroy (fsd->user_data);
+ g_free (fsd->error_prefix);
+ g_free (fsd);
+ }
+}
+
+static void
+reminders_widget_foreach_selected_thread (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ ForeachSelectedData *fsd = task_data;
+ GString *gathered_errors;
+ GSList *link;
+
+ g_return_if_fail (fsd != NULL);
+ g_return_if_fail (fsd->selected != NULL);
+ g_return_if_fail (fsd->sync_func != NULL);
+
+ if (g_cancellable_is_cancelled (cancellable))
+ return;
+
+ gathered_errors = g_string_new ("");
+
+ for (link = fsd->selected; link && !g_cancellable_is_cancelled (cancellable); link = g_slist_next (link)) {
+ const EReminderData *rd = link->data;
+
+ fsd->sync_func (source_object, rd, gathered_errors, cancellable, fsd->user_data);
+ }
+
+ if (gathered_errors->len) {
+ if (fsd->error_prefix) {
+ g_string_prepend_c (gathered_errors, '\n');
+ g_string_prepend (gathered_errors, fsd->error_prefix);
+ }
+
+ g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", gathered_errors->str);
+ } else {
+ g_task_return_boolean (task, TRUE);
+ }
+
+ g_string_free (gathered_errors, TRUE);
+}
+
+static void
+reminders_widget_foreach_selected_done_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (source_object));
+
+ reminders = E_REMINDERS_WIDGET (source_object);
+ g_return_if_fail (g_task_is_valid (result, reminders));
+
+ if (!g_task_propagate_boolean (G_TASK (result), &local_error) && local_error) {
+ e_reminders_widget_report_error (reminders, NULL, local_error);
+ }
+
+ g_clear_error (&local_error);
+}
+
+static void
+reminders_widget_foreach_selected (ERemindersWidget *reminders,
+ ForeachSelectedSyncFunc sync_func,
+ gpointer user_data,
+ GDestroyNotify user_data_destroy,
+ const gchar *error_prefix)
+{
+ GtkTreeSelection *selection;
+ GSList *selected = NULL; /* EReminderData * */
+ GTask *task;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+ g_return_if_fail (sync_func != NULL);
+
+ selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+ gtk_tree_selection_selected_foreach (selection, reminders_widget_gather_selected_cb, &selected);
+
+ if (selected) {
+ ForeachSelectedData *fsd;
+
+ fsd = g_new0 (ForeachSelectedData, 1);
+ fsd->selected = selected; /* Takes ownership */
+ fsd->sync_func = sync_func;
+ fsd->user_data = user_data;
+ fsd->user_data_destroy = user_data_destroy;
+ fsd->error_prefix = g_strdup (error_prefix);
+
+ task = g_task_new (reminders, reminders->priv->cancellable, reminders_widget_foreach_selected_done_cb, NULL);
+ g_task_set_task_data (task, fsd, foreach_selected_data_free);
+ g_task_set_check_cancellable (task, FALSE);
+ g_task_run_in_thread (task, reminders_widget_foreach_selected_thread);
+ g_object_unref (task);
+ }
+}
+
+static void
+reminders_widget_row_activated_cb (GtkTreeView *tree_view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (!path)
+ return;
+
+ model = gtk_tree_view_get_model (reminders->priv->tree_view);
+ if (gtk_tree_model_get_iter (model, &iter, path)) {
+ EReminderData *rd = NULL;
+
+ gtk_tree_model_get (model, &iter,
+ E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA, &rd,
+ -1);
+
+ if (rd) {
+ gboolean result = FALSE;
+
+ g_signal_emit (reminders, signals[ACTIVATED], 0, rd, &result);
+
+ if (!result) {
+ const gchar *scheme = NULL;
+ const gchar *comp_uid = NULL;
+
+ e_cal_component_get_uid (rd->component, &comp_uid);
+
+ switch (e_cal_component_get_vtype (rd->component)) {
+ case E_CAL_COMPONENT_EVENT:
+ scheme = "calendar:";
+ break;
+ case E_CAL_COMPONENT_TODO:
+ scheme = "task:";
+ break;
+ case E_CAL_COMPONENT_JOURNAL:
+ scheme = "memo:";
+ break;
+ default:
+ break;
+ }
+
+ if (scheme && comp_uid && rd->source_uid) {
+ GString *uri;
+ gchar *tmp;
+ GError *error = NULL;
+
+ uri = g_string_sized_new (128);
+ g_string_append (uri, scheme);
+ g_string_append (uri, "///?");
+
+ tmp = g_uri_escape_string (rd->source_uid, NULL, TRUE);
+ g_string_append (uri, "source-uid=");
+ g_string_append (uri, tmp);
+ g_free (tmp);
+
+ g_string_append (uri, "&");
+
+ tmp = g_uri_escape_string (comp_uid, NULL, TRUE);
+ g_string_append (uri, "comp-uid=");
+ g_string_append (uri, tmp);
+ g_free (tmp);
+
+ if (!g_app_info_launch_default_for_uri (uri->str, NULL, &error) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
+ gchar *prefix = g_strdup_printf (_("Failed to launch URI “%s”:"), uri->str);
+ e_reminders_widget_report_error (reminders, prefix, error);
+ g_free (prefix);
+ }
+
+ g_string_free (uri, TRUE);
+ g_clear_error (&error);
+ }
+ }
+
+ e_reminder_data_free (rd);
+ }
+ }
+}
+
+static void
+reminders_widget_selection_changed_cb (GtkTreeSelection *selection,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ gint nselected;
+
+ g_return_if_fail (GTK_IS_TREE_SELECTION (selection));
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ nselected = gtk_tree_selection_count_selected_rows (selection);
+ gtk_widget_set_sensitive (reminders->priv->snooze_combo, nselected > 0);
+ gtk_widget_set_sensitive (reminders->priv->snooze_button, nselected > 0);
+ gtk_widget_set_sensitive (reminders->priv->dismiss_button, nselected > 0);
+}
+
+static void
+reminders_widget_dismiss_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ g_signal_handlers_block_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, reminders);
+
+ reminders_widget_foreach_selected (reminders, reminders_widget_do_dismiss_cb, NULL, NULL, _("Failed to dismiss reminder:"));
+
+ g_signal_handlers_unblock_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, reminders);
+
+ reminders_widget_watcher_changed_cb (reminders->priv->watcher, reminders);
+}
+
+static void
+reminders_widget_dismiss_all_done_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_REMINDER_WATCHER (source_object));
+
+ if (!e_reminder_watcher_dismiss_all_finish (reminders->priv->watcher, result, &local_error) &&
+ !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ e_reminders_widget_report_error (reminders, _("Failed to dismiss all:"), local_error);
+ }
+
+ g_clear_error (&local_error);
+}
+
+static void
+reminders_widget_dismiss_all_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ e_reminder_watcher_dismiss_all (reminders->priv->watcher, reminders->priv->cancellable,
+ reminders_widget_dismiss_all_done_cb, reminders);
+}
+
+static void
+reminders_widget_add_snooze_add_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gboolean found = FALSE;
+ gint new_minutes;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ new_minutes =
+ gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_minutes_spin)) +
+ (60 * gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_hours_spin))) +
+ (24 * 60 * gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_days_spin)));
+ g_return_if_fail (new_minutes > 0);
+
+ gtk_widget_hide (reminders->priv->add_snooze_popover);
+
+ model = gtk_combo_box_get_model (GTK_COMBO_BOX (reminders->priv->snooze_combo));
+ g_return_if_fail (model != NULL);
+
+ if (gtk_tree_model_get_iter_first (model, &iter)) {
+ do {
+ gint minutes = 0;
+
+ gtk_tree_model_get (model, &iter, 1, &minutes, -1);
+
+ if (minutes == new_minutes) {
+ found = TRUE;
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (reminders->priv->snooze_combo), &iter);
+ break;
+ }
+ } while (gtk_tree_model_iter_next (model, &iter));
+ }
+
+ if (!found) {
+ GVariant *variant;
+ gint32 array[MAX_CUSTOM_SNOOZE_VALUES] = { 0 }, narray = 0, ii;
+
+ variant = g_settings_get_value (reminders->priv->settings, "notify-custom-snooze-minutes");
+ if (variant) {
+ const gint32 *stored;
+ gsize nstored = 0;
+
+ stored = g_variant_get_fixed_array (variant, &nstored, sizeof (gint32));
+ if (stored && nstored > 0) {
+ /* Skip the oldest, when too many stored */
+ for (ii = nstored >= MAX_CUSTOM_SNOOZE_VALUES ? 1 : 0; ii < MAX_CUSTOM_SNOOZE_VALUES && ii < nstored; ii++) {
+ array[narray] = stored[ii];
+ narray++;
+ }
+ }
+
+ g_variant_unref (variant);
+ }
+
+ /* Add the new at the end of the array */
+ array[narray] = new_minutes;
+ narray++;
+
+ variant = g_variant_new_fixed_array (G_VARIANT_TYPE_INT32, array, narray, sizeof (gint32));
+ g_settings_set_value (reminders->priv->settings, "notify-custom-snooze-minutes", variant);
+
+ reminders_widget_fill_snooze_combo (reminders, new_minutes);
+ }
+}
+
+static void
+reminders_widget_add_snooze_update_sensitize_cb (GtkSpinButton *spin,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ gtk_widget_set_sensitive (reminders->priv->add_snooze_add_button,
+ gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_minutes_spin)) +
+ gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_hours_spin)) +
+ gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (reminders->priv->add_snooze_days_spin)) > 0);
+}
+
+static void
+reminders_widget_snooze_add_custom (ERemindersWidget *reminders)
+{
+ GtkTreeIter iter;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (!reminders->priv->add_snooze_popover) {
+ GtkWidget *widget;
+ GtkBox *vbox, *box;
+
+ reminders->priv->add_snooze_days_spin = gtk_spin_button_new_with_range (0.0, 366.0, 1.0);
+ reminders->priv->add_snooze_hours_spin = gtk_spin_button_new_with_range (0.0, 23.0, 1.0);
+ reminders->priv->add_snooze_minutes_spin = gtk_spin_button_new_with_range (0.0, 59.0, 1.0);
+
+ g_object_set (G_OBJECT (reminders->priv->add_snooze_days_spin),
+ "digits", 0,
+ "numeric", TRUE,
+ "snap-to-ticks", TRUE,
+ NULL);
+
+ g_object_set (G_OBJECT (reminders->priv->add_snooze_hours_spin),
+ "digits", 0,
+ "numeric", TRUE,
+ "snap-to-ticks", TRUE,
+ NULL);
+
+ g_object_set (G_OBJECT (reminders->priv->add_snooze_minutes_spin),
+ "digits", 0,
+ "numeric", TRUE,
+ "snap-to-ticks", TRUE,
+ NULL);
+
+ vbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_VERTICAL, 2));
+
+ widget = gtk_label_new (_("Set a custom snooze time for"));
+ gtk_box_pack_start (vbox, widget, FALSE, FALSE, 0);
+
+ box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+ g_object_set (G_OBJECT (box),
+ "halign", GTK_ALIGN_START,
+ "hexpand", FALSE,
+ "valign", GTK_ALIGN_CENTER,
+ "vexpand", FALSE,
+ NULL);
+
+ gtk_box_pack_start (box, reminders->priv->add_snooze_days_spin, FALSE, FALSE, 4);
+ /* Translators: this is part of: "Set a custom snooze time for [nnn] days [nnn] hours [nnn] minutes", where the text in "[]" means a separate widget */
+ widget = gtk_label_new_with_mnemonic (C_("reminders-snooze", "da_ys"));
+ gtk_label_set_mnemonic_widget (GTK_LABEL (widget), reminders->priv->add_snooze_days_spin);
+ gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+ gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+ box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+ g_object_set (G_OBJECT (box),
+ "halign", GTK_ALIGN_START,
+ "hexpand", FALSE,
+ "valign", GTK_ALIGN_CENTER,
+ "vexpand", FALSE,
+ NULL);
+
+ gtk_box_pack_start (box, reminders->priv->add_snooze_hours_spin, FALSE, FALSE, 4);
+ /* Translators: this is part of: "Set a custom snooze time for [nnn] days [nnn] hours [nnn] minutes", where the text in "[]" means a separate widget */
+ widget = gtk_label_new_with_mnemonic (C_("reminders-snooze", "_hours"));
+ gtk_label_set_mnemonic_widget (GTK_LABEL (widget), reminders->priv->add_snooze_hours_spin);
+ gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+ gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+ box = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2));
+ g_object_set (G_OBJECT (box),
+ "halign", GTK_ALIGN_START,
+ "hexpand", FALSE,
+ "valign", GTK_ALIGN_CENTER,
+ "vexpand", FALSE,
+ NULL);
+
+ gtk_box_pack_start (box, reminders->priv->add_snooze_minutes_spin, FALSE, FALSE, 4);
+ /* Translators: this is part of: "Set a custom snooze time for [nnn] days [nnn] hours [nnn] minutes", where the text in "[]" means a separate widget */
+ widget = gtk_label_new_with_mnemonic (C_("reminders-snooze", "_minutes"));
+ gtk_label_set_mnemonic_widget (GTK_LABEL (widget), reminders->priv->add_snooze_minutes_spin);
+ gtk_box_pack_start (box, widget, FALSE, FALSE, 4);
+
+ gtk_box_pack_start (vbox, GTK_WIDGET (box), FALSE, FALSE, 0);
+
+ reminders->priv->add_snooze_add_button = gtk_button_new_with_mnemonic (_("_Add Snooze time"));
+ g_object_set (G_OBJECT (reminders->priv->add_snooze_add_button),
+ "halign", GTK_ALIGN_CENTER,
+ NULL);
+
+ gtk_box_pack_start (vbox, reminders->priv->add_snooze_add_button, FALSE, FALSE, 0);
+
+ gtk_widget_show_all (GTK_WIDGET (vbox));
+
+ reminders->priv->add_snooze_popover = gtk_popover_new (GTK_WIDGET (reminders));
+ gtk_popover_set_position (GTK_POPOVER (reminders->priv->add_snooze_popover), GTK_POS_BOTTOM);
+ gtk_container_add (GTK_CONTAINER (reminders->priv->add_snooze_popover), GTK_WIDGET (vbox));
+ gtk_container_set_border_width (GTK_CONTAINER (reminders->priv->add_snooze_popover), 6);
+
+ g_signal_connect (reminders->priv->add_snooze_add_button, "clicked",
+ G_CALLBACK (reminders_widget_add_snooze_add_button_clicked_cb), reminders);
+
+ g_signal_connect (reminders->priv->add_snooze_days_spin, "value-changed",
+ G_CALLBACK (reminders_widget_add_snooze_update_sensitize_cb), reminders);
+
+ g_signal_connect (reminders->priv->add_snooze_hours_spin, "value-changed",
+ G_CALLBACK (reminders_widget_add_snooze_update_sensitize_cb), reminders);
+
+ g_signal_connect (reminders->priv->add_snooze_minutes_spin, "value-changed",
+ G_CALLBACK (reminders_widget_add_snooze_update_sensitize_cb), reminders);
+
+ reminders_widget_add_snooze_update_sensitize_cb (NULL, reminders);
+ }
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (reminders->priv->snooze_combo), &iter)) {
+ gint minutes = -1;
+
+ gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (reminders->priv->snooze_combo)), &iter, 1, &minutes, -1);
+
+ if (minutes > 0) {
+ gtk_spin_button_set_value (GTK_SPIN_BUTTON (reminders->priv->add_snooze_minutes_spin), minutes % 60);
+
+ minutes = minutes / 60;
+ gtk_spin_button_set_value (GTK_SPIN_BUTTON (reminders->priv->add_snooze_hours_spin), minutes % 24);
+
+ minutes = minutes / 24;
+ gtk_spin_button_set_value (GTK_SPIN_BUTTON (reminders->priv->add_snooze_days_spin), minutes);
+ }
+ }
+
+ gtk_widget_hide (reminders->priv->add_snooze_popover);
+ gtk_popover_set_relative_to (GTK_POPOVER (reminders->priv->add_snooze_popover), reminders->priv->snooze_combo);
+ gtk_widget_show (reminders->priv->add_snooze_popover);
+
+ gtk_widget_grab_focus (reminders->priv->add_snooze_days_spin);
+}
+
+static void
+reminders_widget_snooze_combo_changed_cb (GtkComboBox *combo,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ GtkTreeIter iter;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (reminders->priv->updating_snooze_combo)
+ return;
+
+ if (gtk_combo_box_get_active_iter (combo, &iter)) {
+ GtkTreeModel *model;
+ gint minutes = -3;
+
+ model = gtk_combo_box_get_model (combo);
+
+ gtk_tree_model_get (model, &iter, 1, &minutes, -1);
+
+ if (minutes > 0) {
+ reminders->priv->last_selected_snooze_minutes = minutes;
+ } else if (minutes == -1 || minutes == -2) {
+ if (reminders->priv->last_selected_snooze_minutes) {
+ reminders->priv->updating_snooze_combo = TRUE;
+
+ if (gtk_tree_model_get_iter_first (model, &iter)) {
+ do {
+ gint stored = -1;
+
+ gtk_tree_model_get (model, &iter, 1, &stored, -1);
+ if (stored == reminders->priv->last_selected_snooze_minutes) {
+ gtk_combo_box_set_active_iter (combo, &iter);
+ break;
+ }
+ } while (gtk_tree_model_iter_next (model, &iter));
+ }
+
+ reminders->priv->updating_snooze_combo = FALSE;
+ }
+
+ /* The "Add custom" item was selected */
+ if (minutes == -1) {
+ reminders_widget_snooze_add_custom (reminders);
+ /* The "Clear custom times" item was selected */
+ } else if (minutes == -2) {
+ g_settings_reset (reminders->priv->settings, "notify-custom-snooze-minutes");
+ }
+ }
+ }
+}
+
+static void
+reminders_widget_snooze_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+ GtkTreeSelection *selection;
+ GSList *selected = NULL, *link;
+ GtkTreeIter iter;
+ gint minutes = 0;
+ gint64 until;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+ g_return_if_fail (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (reminders->priv->snooze_combo), &iter));
+
+ gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (reminders->priv->snooze_combo)), &iter, 1, &minutes, -1);
+
+ g_return_if_fail (minutes > 0);
+
+ until = (g_get_real_time () / G_USEC_PER_SEC) + (minutes * 60);
+
+ g_settings_set_int (reminders->priv->settings, "notify-last-snooze-minutes", minutes);
+
+ selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+ gtk_tree_selection_selected_foreach (selection, reminders_widget_gather_selected_cb, &selected);
+
+ g_signal_handlers_block_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, reminders);
+
+ for (link = selected; link; link = g_slist_next (link)) {
+ const EReminderData *rd = link->data;
+
+ e_reminder_watcher_snooze (reminders->priv->watcher, rd, until);
+ }
+
+ g_slist_free_full (selected, e_reminder_data_free);
+
+ g_signal_handlers_unblock_by_func (reminders->priv->watcher, reminders_widget_watcher_changed_cb, reminders);
+
+ if (selected)
+ reminders_widget_watcher_changed_cb (reminders->priv->watcher, reminders);
+}
+
+static void
+reminders_widget_set_watcher (ERemindersWidget *reminders,
+ EReminderWatcher *watcher)
+{
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+ g_return_if_fail (E_IS_REMINDER_WATCHER (watcher));
+ g_return_if_fail (reminders->priv->watcher == NULL);
+
+ reminders->priv->watcher = g_object_ref (watcher);
+}
+
+static void
+reminders_widget_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_WATCHER:
+ reminders_widget_set_watcher (
+ E_REMINDERS_WIDGET (object),
+ g_value_get_object (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+reminders_widget_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_WATCHER:
+ g_value_set_object (
+ value, e_reminders_widget_get_watcher (
+ E_REMINDERS_WIDGET (object)));
+ return;
+
+ case PROP_EMPTY:
+ g_value_set_boolean (
+ value, e_reminders_widget_is_empty (
+ E_REMINDERS_WIDGET (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+reminders_widget_constructed (GObject *object)
+{
+ ERemindersWidget *reminders = E_REMINDERS_WIDGET (object);
+ GtkWidget *scrolled_window;
+ GtkListStore *list_store;
+ GtkTreeSelection *selection;
+ GtkTreeViewColumn *column;
+ GtkCellRenderer *renderer;
+ GtkWidget *widget;
+ GtkBox *box;
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_reminders_widget_parent_class)->constructed (object);
+
+ scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+ g_object_set (G_OBJECT (scrolled_window),
+ "halign", GTK_ALIGN_FILL,
+ "hexpand", TRUE,
+ "valign", GTK_ALIGN_FILL,
+ "vexpand", TRUE,
+ "hscrollbar-policy", GTK_POLICY_NEVER,
+ "vscrollbar-policy", GTK_POLICY_AUTOMATIC,
+ "shadow-type", GTK_SHADOW_IN,
+ NULL);
+
+ gtk_grid_attach (GTK_GRID (reminders), scrolled_window, 0, 0, 1, 1);
+
+ list_store = gtk_list_store_new (E_REMINDERS_WIDGET_N_COLUMNS,
+ G_TYPE_STRING, /* E_REMINDERS_WIDGET_COLUMN_OVERDUE */
+ G_TYPE_STRING, /* E_REMINDERS_WIDGET_COLUMN_DESCRIPTION */
+ E_TYPE_REMINDER_DATA); /* E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA */
+
+ reminders->priv->tree_view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (GTK_TREE_MODEL (list_store)));
+
+ g_object_unref (list_store);
+
+ g_object_set (G_OBJECT (reminders->priv->tree_view),
+ "halign", GTK_ALIGN_FILL,
+ "hexpand", TRUE,
+ "valign", GTK_ALIGN_FILL,
+ "vexpand", TRUE,
+ "activate-on-single-click", FALSE,
+ "enable-search", FALSE,
+ "fixed-height-mode", TRUE,
+ "headers-visible", FALSE,
+ "hover-selection", FALSE,
+ NULL);
+
+ gtk_container_add (GTK_CONTAINER (scrolled_window), GTK_WIDGET (reminders->priv->tree_view));
+
+ gtk_tree_view_set_tooltip_column (reminders->priv->tree_view, E_REMINDERS_WIDGET_COLUMN_DESCRIPTION);
+
+ /* Headers not visible, thus column's caption is not localized */
+ gtk_tree_view_insert_column_with_attributes (reminders->priv->tree_view, -1, "Overdue",
+ gtk_cell_renderer_text_new (), "markup", E_REMINDERS_WIDGET_COLUMN_OVERDUE, NULL);
+
+ renderer = gtk_cell_renderer_text_new ();
+ g_object_set (G_OBJECT (renderer),
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ NULL);
+
+ gtk_tree_view_insert_column_with_attributes (reminders->priv->tree_view, -1, "Description",
+ renderer, "markup", E_REMINDERS_WIDGET_COLUMN_DESCRIPTION, NULL);
+
+ column = gtk_tree_view_get_column (reminders->priv->tree_view, 0);
+ gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
+
+ column = gtk_tree_view_get_column (reminders->priv->tree_view, 1);
+ gtk_tree_view_column_set_expand (column, TRUE);
+
+ reminders->priv->dismiss_button = gtk_button_new_with_mnemonic (_("_Dismiss"));
+ reminders->priv->dismiss_all_button = gtk_button_new_with_mnemonic (_("Dismiss _All"));
+ reminders->priv->snooze_combo = reminders_widget_new_snooze_combo ();
+ reminders->priv->snooze_button = gtk_button_new_with_mnemonic (_("_Snooze"));
+
+ reminders_widget_fill_snooze_combo (reminders,
+ g_settings_get_int (reminders->priv->settings, "notify-last-snooze-minutes"));
+
+ box = GTK_BOX (gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL));
+ g_object_set (G_OBJECT (box),
+ "halign", GTK_ALIGN_END,
+ "hexpand", TRUE,
+ "valign", GTK_ALIGN_CENTER,
+ "vexpand", FALSE,
+ "margin-top", 4,
+ NULL);
+
+ widget = gtk_label_new ("");
+
+ gtk_box_pack_start (box, reminders->priv->snooze_combo, FALSE, FALSE, 0);
+ gtk_box_pack_start (box, reminders->priv->snooze_button, FALSE, FALSE, 0);
+ gtk_box_pack_start (box, widget, FALSE, FALSE, 0);
+ gtk_box_pack_start (box, reminders->priv->dismiss_button, FALSE, FALSE, 0);
+ gtk_box_pack_start (box, reminders->priv->dismiss_all_button, FALSE, FALSE, 0);
+
+ gtk_button_box_set_child_non_homogeneous (GTK_BUTTON_BOX (box), reminders->priv->snooze_combo, TRUE);
+ gtk_button_box_set_child_non_homogeneous (GTK_BUTTON_BOX (box), widget, TRUE);
+
+ gtk_grid_attach (GTK_GRID (reminders), GTK_WIDGET (box), 0, 1, 1, 1);
+
+ gtk_widget_show_all (GTK_WIDGET (reminders));
+
+ selection = gtk_tree_view_get_selection (reminders->priv->tree_view);
+ gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
+
+ g_signal_connect (reminders->priv->tree_view, "row-activated",
+ G_CALLBACK (reminders_widget_row_activated_cb), reminders);
+
+ g_signal_connect (selection, "changed",
+ G_CALLBACK (reminders_widget_selection_changed_cb), reminders);
+
+ g_signal_connect (reminders->priv->snooze_button, "clicked",
+ G_CALLBACK (reminders_widget_snooze_button_clicked_cb), reminders);
+
+ g_signal_connect (reminders->priv->dismiss_button, "clicked",
+ G_CALLBACK (reminders_widget_dismiss_button_clicked_cb), reminders);
+
+ g_signal_connect (reminders->priv->dismiss_all_button, "clicked",
+ G_CALLBACK (reminders_widget_dismiss_all_button_clicked_cb), reminders);
+
+ g_signal_connect (reminders->priv->watcher, "changed",
+ G_CALLBACK (reminders_widget_watcher_changed_cb), reminders);
+
+ g_signal_connect (reminders->priv->snooze_combo, "changed",
+ G_CALLBACK (reminders_widget_snooze_combo_changed_cb), reminders);
+
+ g_signal_connect (reminders->priv->settings, "changed::notify-custom-snooze-minutes",
+ G_CALLBACK (reminders_widget_custom_snooze_minutes_changed_cb), reminders);
+
+ e_binding_bind_property (reminders, "empty",
+ reminders->priv->dismiss_all_button, "sensitive",
+ G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+
+ e_binding_bind_property (reminders, "empty",
+ scrolled_window, "sensitive",
+ G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+
+ _libedataserverui_load_modules ();
+
+ e_extensible_load_extensions (E_EXTENSIBLE (object));
+
+ reminders_widget_schedule_content_refresh (reminders);
+}
+
+static void
+reminders_widget_dispose (GObject *object)
+{
+ ERemindersWidget *reminders = E_REMINDERS_WIDGET (object);
+
+ g_cancellable_cancel (reminders->priv->cancellable);
+
+ if (reminders->priv->refresh_idle_id) {
+ g_source_remove (reminders->priv->refresh_idle_id);
+ reminders->priv->refresh_idle_id = 0;
+ }
+
+ if (reminders->priv->overdue_update_id) {
+ g_source_remove (reminders->priv->overdue_update_id);
+ reminders->priv->overdue_update_id = 0;
+ }
+
+ if (reminders->priv->watcher)
+ g_signal_handlers_disconnect_by_data (reminders->priv->watcher, reminders);
+
+ if (reminders->priv->settings)
+ g_signal_handlers_disconnect_by_data (reminders->priv->settings, reminders);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_reminders_widget_parent_class)->dispose (object);
+}
+
+static void
+reminders_widget_finalize (GObject *object)
+{
+ ERemindersWidget *reminders = E_REMINDERS_WIDGET (object);
+
+ g_clear_object (&reminders->priv->watcher);
+ g_clear_object (&reminders->priv->settings);
+ g_clear_object (&reminders->priv->cancellable);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_reminders_widget_parent_class)->finalize (object);
+}
+
+static void
+e_reminders_widget_class_init (ERemindersWidgetClass *klass)
+{
+ GObjectClass *object_class;
+ GtkWidgetClass *widget_class;
+
+ g_type_class_add_private (klass, sizeof (ERemindersWidgetPrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->set_property = reminders_widget_set_property;
+ object_class->get_property = reminders_widget_get_property;
+ object_class->constructed = reminders_widget_constructed;
+ object_class->dispose = reminders_widget_dispose;
+ object_class->finalize = reminders_widget_finalize;
+
+ widget_class = GTK_WIDGET_CLASS (klass);
+ widget_class->map = reminders_widget_map;
+ widget_class->unmap = reminders_widget_unmap;
+
+ /**
+ * ERemindersWidget::watcher:
+ *
+ * An #EReminderWatcher used to work with reminders.
+ *
+ * Since: 3.30
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_WATCHER,
+ g_param_spec_object (
+ "watcher",
+ "Reminder Watcher",
+ "The reminder watcher used to work with reminders",
+ E_TYPE_REMINDER_WATCHER,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ERemindersWidget::empty:
+ *
+ * Set to %TRUE when there's no past reminder in the widget.
+ *
+ * Since: 3.30
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_EMPTY,
+ g_param_spec_boolean (
+ "empty",
+ "Empty",
+ "Whether there are no past reminders",
+ TRUE,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ERemindersWidget:changed:
+ * @reminders: an #ERemindersWidget
+ *
+ * A signal being called to notify about changes in the past reminders list.
+ *
+ * Since: 3.30
+ **/
+ signals[CHANGED] = g_signal_new (
+ "changed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (ERemindersWidgetClass, changed),
+ NULL,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 0,
+ G_TYPE_NONE);
+
+ /**
+ * ERemindersWidget:activated:
+ * @reminders: an #ERemindersWidget
+ * @rd: an #EReminderData
+ *
+ * A signal being called when the user activates one of the past reminders in the tree view.
+ * The @rd corresponds to the activated reminder.
+ *
+ * Returns: %TRUE, when the further processing of this signal should be stopped, %FALSE otherwise.
+ *
+ * Since: 3.30
+ **/
+ signals[ACTIVATED] = g_signal_new (
+ "activated",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (ERemindersWidgetClass, activated),
+ g_signal_accumulator_first_wins,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 1,
+ E_TYPE_REMINDER_DATA);
+}
+
+static void
+e_reminders_widget_init (ERemindersWidget *reminders)
+{
+ reminders->priv = G_TYPE_INSTANCE_GET_PRIVATE (reminders, E_TYPE_REMINDERS_WIDGET, ERemindersWidgetPrivate);
+ reminders->priv->settings = g_settings_new ("org.gnome.evolution-data-server.calendar");
+ reminders->priv->cancellable = g_cancellable_new ();
+ reminders->priv->is_empty = TRUE;
+ reminders->priv->is_mapped = FALSE;
+}
+
+/**
+ * e_reminders_widget_new:
+ * @watcher: an #EReminderWatcher
+ *
+ * Creates a new instance of #ERemindersWidget. It adds its own reference
+ * on the @watcher.
+ *
+ * Returns: (transfer full): a new instance of #ERemindersWidget.
+ *
+ * Since: 3.30
+ **/
+ERemindersWidget *
+e_reminders_widget_new (EReminderWatcher *watcher)
+{
+ g_return_val_if_fail (E_IS_REMINDER_WATCHER (watcher), NULL);
+
+ return g_object_new (E_TYPE_REMINDERS_WIDGET,
+ "watcher", watcher,
+ NULL);
+}
+
+/**
+ * e_reminders_widget_get_watcher:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: (transfer none): an #EReminderWatcher with which the @reminders had
+ * been created. Do on unref it, it's owned by the @reminders.
+ *
+ * Since: 3.30
+ **/
+EReminderWatcher *
+e_reminders_widget_get_watcher (ERemindersWidget *reminders)
+{
+ g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), NULL);
+
+ return reminders->priv->watcher;
+}
+
+/**
+ * e_reminders_widget_get_settings:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: (transfer none): a #GSettings pointing to org.gnome.evolution-data-server.calendar
+ * used by the @reminders widget.
+ *
+ * Since: 3.30
+ **/
+GSettings *
+e_reminders_widget_get_settings (ERemindersWidget *reminders)
+{
+ g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), NULL);
+
+ return reminders->priv->settings;
+}
+
+/**
+ * e_reminders_widget_is_empty:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: %TRUE, when there is no past reminder left, %FALSE otherwise.
+ *
+ * Since: 3.30
+ **/
+gboolean
+e_reminders_widget_is_empty (ERemindersWidget *reminders)
+{
+ g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), FALSE);
+
+ return reminders->priv->is_empty;
+}
+
+/**
+ * e_reminders_widget_get_tree_view:
+ * @reminders: an #ERemindersWidget
+ *
+ * Returns: (transfer none): a #GtkTreeView with past reminders. It's owned
+ * by the @reminders widget.
+ *
+ * Since: 3.30
+ **/
+GtkTreeView *
+e_reminders_widget_get_tree_view (ERemindersWidget *reminders)
+{
+ g_return_val_if_fail (E_IS_REMINDERS_WIDGET (reminders), NULL);
+
+ return reminders->priv->tree_view;
+}
+
+static void
+reminders_widget_error_response_cb (GtkInfoBar *info_bar,
+ gint response_id,
+ gpointer user_data)
+{
+ ERemindersWidget *reminders = user_data;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (reminders->priv->info_bar == info_bar) {
+ gtk_widget_destroy (GTK_WIDGET (reminders->priv->info_bar));
+ reminders->priv->info_bar = NULL;
+ }
+}
+
+/**
+ * e_reminders_widget_report_error:
+ * @reminders: an #ERemindersWidget
+ * @prefix: (nullable): an optional prefix to show before the error message, or %NULL for none
+ * @error: (nullable): a #GError to show the message from in the UI, or %NULL for unknown error
+ *
+ * Shows a warning in the GUI with the @error message, optionally prefixed
+ * with @prefix. When @error is %NULL, an "Unknown error" message is shown
+ * instead.
+ *
+ * Since: 3.30
+ **/
+void
+e_reminders_widget_report_error (ERemindersWidget *reminders,
+ const gchar *prefix,
+ const GError *error)
+{
+ GtkLabel *label;
+ const gchar *message;
+ gchar *tmp = NULL;
+
+ g_return_if_fail (E_IS_REMINDERS_WIDGET (reminders));
+
+ if (error)
+ message = error->message;
+ else
+ message = _("Unknown error");
+
+ if (prefix && *prefix) {
+ if (gtk_widget_get_direction (GTK_WIDGET (reminders)) == GTK_TEXT_DIR_RTL)
+ tmp = g_strconcat (message, " ", prefix, NULL);
+ else
+ tmp = g_strconcat (prefix, " ", message, NULL);
+
+ message = tmp;
+ }
+
+ if (reminders->priv->info_bar) {
+ gtk_widget_destroy (GTK_WIDGET (reminders->priv->info_bar));
+ reminders->priv->info_bar = NULL;
+ }
+
+ reminders->priv->info_bar = GTK_INFO_BAR (gtk_info_bar_new ());
+ gtk_info_bar_set_message_type (reminders->priv->info_bar, GTK_MESSAGE_ERROR);
+ gtk_info_bar_set_show_close_button (reminders->priv->info_bar, TRUE);
+
+ label = GTK_LABEL (gtk_label_new (message));
+ gtk_label_set_max_width_chars (label, 120);
+ gtk_label_set_line_wrap (label, TRUE);
+ gtk_label_set_selectable (label, TRUE);
+ gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (reminders->priv->info_bar)), GTK_WIDGET (label));
+ gtk_widget_show (GTK_WIDGET (label));
+ gtk_widget_show (GTK_WIDGET (reminders->priv->info_bar));
+
+ g_signal_connect (reminders->priv->info_bar, "response", G_CALLBACK (reminders_widget_error_response_cb), reminders);
+
+ gtk_grid_attach (GTK_GRID (reminders), GTK_WIDGET (reminders->priv->info_bar), 0, 2, 1, 1);
+
+ g_free (tmp);
+}
diff --git a/src/libedataserverui/e-reminders-widget.h b/src/libedataserverui/e-reminders-widget.h
new file mode 100644
index 000000000..ea20c137d
--- /dev/null
+++ b/src/libedataserverui/e-reminders-widget.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATASERVERUI_H_INSIDE__) && !defined (LIBEDATASERVERUI_COMPILATION)
+#error "Only <libedataserverui/libedataserverui.h> should be included directly."
+#endif
+
+#ifndef E_REMINDERS_WIDGET_H
+#define E_REMINDERS_WIDGET_H
+
+#include <gtk/gtk.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_REMINDERS_WIDGET \
+ (e_reminders_widget_get_type ())
+#define E_REMINDERS_WIDGET(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_REMINDERS_WIDGET, ERemindersWidget))
+#define E_REMINDERS_WIDGET_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_REMINDERS_WIDGET, ERemindersWidgetClass))
+#define E_IS_REMINDERS_WIDGET(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_REMINDERS_WIDGET))
+#define E_IS_REMINDERS_WIDGET_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_REMINDERS_WIDGET))
+#define E_REMINDERS_WIDGET_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_REMINDERS_WIDGET, ERemindersWidgetClass))
+
+G_BEGIN_DECLS
+
+enum {
+ E_REMINDERS_WIDGET_COLUMN_OVERDUE, /* gchar *, markup with time to start/overdue description */
+ E_REMINDERS_WIDGET_COLUMN_DESCRIPTION, /* gchar *, markup describing the reminder, not component's DESCRIPTION property */
+ E_REMINDERS_WIDGET_COLUMN_REMINDER_DATA,/* EReminderData * */
+ E_REMINDERS_WIDGET_N_COLUMNS
+};
+
+typedef struct _ERemindersWidget ERemindersWidget;
+typedef struct _ERemindersWidgetClass ERemindersWidgetClass;
+typedef struct _ERemindersWidgetPrivate ERemindersWidgetPrivate;
+
+/**
+ * ERemindersWidget:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.30
+ **/
+struct _ERemindersWidget {
+ /*< private >*/
+ GtkGrid parent;
+ ERemindersWidgetPrivate *priv;
+};
+
+/**
+ * ERemindersWidgetClass:
+ *
+ * Class structure for the #ERemindersWidget class.
+ *
+ * Since: 3.30
+ **/
+struct _ERemindersWidgetClass {
+ /*< private >*/
+ GtkGridClass parent_class;
+
+ /* Signals and methods */
+ void (* changed) (ERemindersWidget *reminders);
+ gboolean (* activated) (ERemindersWidget *reminders,
+ const EReminderData *rd);
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_reminders_widget_get_type (void) G_GNUC_CONST;
+
+ERemindersWidget *
+ e_reminders_widget_new (EReminderWatcher *watcher);
+EReminderWatcher *
+ e_reminders_widget_get_watcher (ERemindersWidget *reminders);
+GSettings * e_reminders_widget_get_settings (ERemindersWidget *reminders);
+gboolean e_reminders_widget_is_empty (ERemindersWidget *reminders);
+GtkTreeView * e_reminders_widget_get_tree_view(ERemindersWidget *reminders);
+void e_reminders_widget_report_error (ERemindersWidget *reminders,
+ const gchar *prefix,
+ const GError *error);
+
+G_END_DECLS
+
+#endif /* E_REMINDERS_WIDGET_H */
diff --git a/src/libedataserverui/libedataserverui-private.c b/src/libedataserverui/libedataserverui-private.c
new file mode 100644
index 000000000..83cb41459
--- /dev/null
+++ b/src/libedataserverui/libedataserverui-private.c
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <glib.h>
+
+#include "libedataserver/libedataserver.h"
+#include "libedataserver/libedataserver-private.h"
+
+#include "libedataserverui-private.h"
+
+/*
+ * _libedataserverui_load_modules:
+ *
+ * Usually called in a GObject::constructed() method to ensure
+ * the modules from the UI module directories are loaded.
+ *
+ * Since: 3.30
+ **/
+void
+_libedataserverui_load_modules (void)
+{
+ static gboolean modules_loaded = FALSE;
+
+ /* Load modules only once. */
+ if (!modules_loaded) {
+ GList *module_types;
+
+ modules_loaded = TRUE;
+
+ module_types = e_module_load_all_in_directory (E_DATA_SERVER_UIMODULEDIR);
+ g_list_free_full (module_types, (GDestroyNotify) g_type_module_unuse);
+ }
+}
diff --git a/src/libedataserverui/libedataserverui-private.h b/src/libedataserverui/libedataserverui-private.h
new file mode 100644
index 000000000..61762bb7a
--- /dev/null
+++ b/src/libedataserverui/libedataserverui-private.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBEDATASERVERUI_PRIVATE_H
+#define LIBEDATASERVERUI_PRIVATE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+void _libedataserverui_load_modules (void);
+
+G_END_DECLS
+
+#endif /* LIBEDATASERVERUI_PRIVATE_H */
diff --git a/src/libedataserverui/libedataserverui.h b/src/libedataserverui/libedataserverui.h
index 4e3a87515..30def6370 100644
--- a/src/libedataserverui/libedataserverui.h
+++ b/src/libedataserverui/libedataserverui.h
@@ -25,6 +25,7 @@
#include <libedataserverui/e-credentials-prompter-impl.h>
#include <libedataserverui/e-credentials-prompter-impl-oauth2.h>
#include <libedataserverui/e-credentials-prompter-impl-password.h>
+#include <libedataserverui/e-reminders-widget.h>
#include <libedataserverui/e-trust-prompt.h>
#include <libedataserverui/e-webdav-discover-widget.h>
diff --git a/src/libedataserverui/libedataserverui.pc.in b/src/libedataserverui/libedataserverui.pc.in
index c3fae5f3b..1b9747f54 100644
--- a/src/libedataserverui/libedataserverui.pc.in
+++ b/src/libedataserverui/libedataserverui.pc.in
@@ -9,11 +9,12 @@ privlibdir=@privlibdir@
privincludedir=@privincludedir@
credentialmoduledir=@credentialmoduledir@
+uimoduledir=@uimoduledir@
Name: libedataserverui
Description: UI utility library for Evolution Data Server
Version: @PROJECT_VERSION@
-Requires: gio-2.0 gmodule-2.0 libsecret-1 libxml-2.0 libsoup-2.4 gtk+-3.0 libedataserver-@API_VERSION@
+Requires: gio-2.0 gmodule-2.0 libsecret-1 libxml-2.0 libsoup-2.4 gtk+-3.0 libedataserver-@API_VERSION@ libecal-@API_VERSION@
Requires.private: camel-@API_VERSION@
Libs: -L${libdir} -ledataserver-@API_VERSION@ -ledataserverui-@API_VERSION@
Cflags: -I${privincludedir}
diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt
index a3f1c3f92..908d33f23 100644
--- a/src/services/CMakeLists.txt
+++ b/src/services/CMakeLists.txt
@@ -3,5 +3,6 @@ add_subdirectory(evolution-calendar-factory)
add_subdirectory(evolution-source-registry)
if(HAVE_GTK)
+ add_subdirectory(evolution-alarm-notify)
add_subdirectory(evolution-user-prompter)
endif(HAVE_GTK)
diff --git a/src/services/evolution-alarm-notify/CMakeLists.txt b/src/services/evolution-alarm-notify/CMakeLists.txt
new file mode 100644
index 000000000..a4c0a41e7
--- /dev/null
+++ b/src/services/evolution-alarm-notify/CMakeLists.txt
@@ -0,0 +1,67 @@
+set(DEPENDENCIES
+ ecal
+ edataserverui
+)
+
+set(SOURCES
+ evolution-alarm-notify.c
+ e-alarm-notify.h
+ e-alarm-notify.c
+)
+
+add_executable(evolution-alarm-notify
+ ${SOURCES}
+)
+
+add_dependencies(evolution-alarm-notify
+ ${DEPENDENCIES}
+)
+
+target_compile_definitions(evolution-alarm-notify PRIVATE
+ -DG_LOG_DOMAIN=\"evolution-alarm-notify\"
+ -DLOCALEDIR=\"${LOCALE_INSTALL_DIR}\"
+)
+
+target_compile_options(evolution-alarm-notify PUBLIC
+ ${CANBERRA_CFLAGS}
+ ${DATA_SERVER_CFLAGS}
+ ${GNOME_PLATFORM_CFLAGS}
+ ${GTK_CFLAGS}
+)
+
+target_include_directories(evolution-alarm-notify PUBLIC
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src
+ ${CANBERRA_INCLUDE_DIRS}
+ ${DATA_SERVER_INCLUDE_DIRS}
+ ${GNOME_PLATFORM_INCLUDE_DIRS}
+ ${GTK_INCLUDE_DIRS}
+)
+
+target_link_libraries(evolution-alarm-notify
+ ${DEPENDENCIES}
+ ${CANBERRA_LDFLAGS}
+ ${DATA_SERVER_LDFLAGS}
+ ${GNOME_PLATFORM_LDFLAGS}
+ ${GTK_LDFLAGS}
+)
+
+if(WIN32)
+ find_program(WINDRES windres)
+ if(WINDRES)
+ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/evolution-alarm-notify-icon.o
+ COMMAND ${WINDRES} ${CMAKE_CURRENT_SOURCE_DIR}/evolution-alarm-notify-icon.rc ${CMAKE_CURRENT_BINARY_DIR}/evolution-alarm-notify-icon.o
+ DEPENDS evolution-alarm-notify-icon.rc
+ evolution-alarm-notify.ico
+ )
+
+ target_link_libraries(evolution-alarm-notify
+ ${CMAKE_CURRENT_BINARY_DIR}/evolution-alarm-notify-icon.o
+ )
+ endif(WINDRES)
+endif(WIN32)
+
+install(TARGETS evolution-alarm-notify
+ DESTINATION ${privlibexecdir}
+)
diff --git a/src/services/evolution-alarm-notify/e-alarm-notify.c b/src/services/evolution-alarm-notify/e-alarm-notify.c
new file mode 100644
index 000000000..be970438a
--- /dev/null
+++ b/src/services/evolution-alarm-notify/e-alarm-notify.c
@@ -0,0 +1,1105 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <time.h>
+
+#ifdef HAVE_CANBERRA
+#include <canberra-gtk.h>
+#endif
+
+#include <glib/gi18n-lib.h>
+
+#ifndef G_OS_WIN32
+#include <gio/gdesktopappinfo.h>
+#endif
+
+#include "libecal/libecal.h"
+#include "libedataserverui/libedataserverui.h"
+
+#include "e-alarm-notify.h"
+
+#define APPLICATION_ID "org.gnome.EvolutionAlarmNotify"
+
+struct _EAlarmNotifyPrivate {
+ ESourceRegistry *registry;
+ EReminderWatcher *watcher;
+ GSettings *settings;
+
+ ERemindersWidget *reminders; /* owned by 'window' */
+ GtkWidget *window;
+ gint window_x;
+ gint window_y;
+ gint window_width;
+ gint window_height;
+ gint window_geometry_save_id;
+
+ GMutex dismiss_lock;
+ GSList *dismiss; /* EReminderData * */
+ GThread *dismiss_thread; /* not referenced, only to know whether it's scheduled */
+
+ GHashTable *notification_ids; /* gchar * ~> NULL, known notifications */
+ GtkStatusIcon *status_icon;
+ gchar *status_icon_tooltip;
+ gint status_icon_blink_id;
+ gint status_icon_blink_countdown;
+ gint last_n_reminders;
+};
+
+/* Forward Declarations */
+static void e_alarm_notify_initable_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EAlarmNotify, e_alarm_notify, GTK_TYPE_APPLICATION,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, e_alarm_notify_initable_init))
+
+static void
+e_alarm_notify_show_window (EAlarmNotify *an,
+ gboolean focus_on_map)
+{
+ GtkWindow *window;
+ gboolean was_visible;
+
+ g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+ window = GTK_WINDOW (an->priv->window);
+
+ gtk_window_set_keep_above (window, g_settings_get_boolean (an->priv->settings, "notify-window-on-top"));
+ gtk_window_set_focus_on_map (window, focus_on_map);
+ gtk_window_set_urgency_hint (window, !focus_on_map);
+
+ was_visible = gtk_widget_get_visible (an->priv->window);
+
+ gtk_window_present (window);
+
+ if (!was_visible)
+ gtk_window_move (window, an->priv->window_x, an->priv->window_y);
+}
+
+static gboolean
+e_alarm_notify_audio (EAlarmNotify *an,
+ const EReminderData *rd,
+ ECalComponentAlarm *alarm)
+{
+ icalattach *attach = NULL;
+ gboolean did_play = FALSE;
+
+ g_return_val_if_fail (an != NULL, FALSE);
+ g_return_val_if_fail (rd != NULL, FALSE);
+ g_return_val_if_fail (alarm != NULL, FALSE);
+
+ e_cal_component_alarm_get_attach (alarm, &attach);
+
+ if (attach && icalattach_get_is_url (attach)) {
+ const gchar *url;
+
+ url = icalattach_get_url (attach);
+ if (url && *url) {
+ gchar *filename;
+ GError *error = NULL;
+
+ filename = g_filename_from_uri (url, NULL, &error);
+
+ if (error != NULL) {
+ g_warning ("%s: Failed to convert URI to filename: %s", G_STRFUNC, error->message);
+ g_error_free (error);
+ } else if (filename && g_file_test (filename, G_FILE_TEST_EXISTS)) {
+#ifdef HAVE_CANBERRA
+ did_play = ca_context_play (ca_gtk_context_get (), 0,
+ CA_PROP_MEDIA_FILENAME, filename,
+ NULL) == 0;
+#endif
+ }
+
+ g_free (filename);
+ }
+ }
+
+ if (!did_play)
+ gdk_beep ();
+
+ if (attach)
+ icalattach_unref (attach);
+
+ return FALSE;
+}
+
+/* Copy of e_util_is_running_gnome() from Evolution */
+static gboolean
+e_alarm_notify_is_running_gnome (void)
+{
+#ifdef G_OS_WIN32
+ return FALSE;
+#else
+ static gint runs_gnome = -1;
+
+ if (runs_gnome == -1) {
+ runs_gnome = g_strcmp0 (g_getenv ("XDG_CURRENT_DESKTOP"), "GNOME") == 0 ? 1 : 0;
+ if (runs_gnome) {
+ GDesktopAppInfo *app_info;
+
+ app_info = g_desktop_app_info_new ("gnome-notifications-panel.desktop");
+ if (!app_info) {
+ runs_gnome = 0;
+ }
+
+ g_clear_object (&app_info);
+ }
+ }
+
+ return runs_gnome != 0;
+#endif
+}
+
+static gchar *
+e_alarm_notify_build_notif_id (const EReminderData *rd)
+{
+ GString *string;
+ ECalComponentId *id;
+
+ g_return_val_if_fail (rd != NULL, NULL);
+
+ string = g_string_sized_new (32);
+
+ if (rd->source_uid) {
+ g_string_append (string, rd->source_uid);
+ g_string_append (string, "\n");
+ }
+
+ id = e_cal_component_get_id (rd->component);
+ if (id) {
+ if (id->uid) {
+ g_string_append (string, id->uid);
+ g_string_append (string, "\n");
+ }
+
+ if (id->rid) {
+ g_string_append (string, id->rid);
+ g_string_append (string, "\n");
+ }
+
+ e_cal_component_free_id (id);
+ }
+
+ g_string_append_printf (string, "%" G_GINT64_FORMAT, rd->instance.trigger);
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+e_alarm_notify_display (EAlarmNotify *an,
+ const EReminderData *rd,
+ ECalComponentAlarm *alarm)
+{
+ gchar *description, *notif_id;
+
+ g_return_val_if_fail (an != NULL, FALSE);
+ g_return_val_if_fail (rd != NULL, FALSE);
+ g_return_val_if_fail (alarm != NULL, FALSE);
+
+ description = e_reminder_watcher_describe_data (an->priv->watcher, rd, E_REMINDER_WATCHER_DESCRIBE_FLAG_NONE);
+
+ notif_id = e_alarm_notify_build_notif_id (rd);
+
+ if (!g_hash_table_contains (an->priv->notification_ids, notif_id)) {
+ GNotification *notification;
+ GtkIconInfo *icon_info;
+ gchar *detailed_action;
+
+ notification = g_notification_new (_("Reminders"));
+ g_notification_set_body (notification, description);
+
+ icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (), "appointment-soon", GTK_ICON_SIZE_DIALOG, 0);
+ if (icon_info) {
+ const gchar *filename;
+
+ filename = gtk_icon_info_get_filename (icon_info);
+ if (filename && *filename) {
+ GFile *file;
+ GIcon *icon;
+
+ file = g_file_new_for_path (filename);
+ icon = g_file_icon_new (file);
+
+ if (icon) {
+ g_notification_set_icon (notification, icon);
+ g_object_unref (icon);
+ }
+
+ g_object_unref (file);
+ }
+
+ gtk_icon_info_free (icon_info);
+ }
+
+ detailed_action = g_action_print_detailed_name ("app.show-reminders", NULL);
+ g_notification_add_button (notification, _("Reminders"), detailed_action);
+ g_free (detailed_action);
+
+ g_application_send_notification (G_APPLICATION (an), notif_id, notification);
+
+ g_object_unref (notification);
+
+ g_hash_table_insert (an->priv->notification_ids, notif_id, NULL);
+ }
+
+ g_free (an->priv->status_icon_tooltip);
+ an->priv->status_icon_tooltip = description; /* takes ownership */
+
+ if (!g_settings_get_boolean (an->priv->settings, "notify-with-tray"))
+ e_alarm_notify_show_window (an, FALSE);
+
+ return TRUE;
+}
+
+static gboolean
+e_alarm_notify_email (EAlarmNotify *an,
+ const EReminderData *rd,
+ ECalComponentAlarm *alarm)
+{
+ ECalClient *client;
+
+ g_return_val_if_fail (an != NULL, FALSE);
+ g_return_val_if_fail (rd != NULL, FALSE);
+ g_return_val_if_fail (alarm != NULL, FALSE);
+
+ client = e_reminder_watcher_ref_opened_client (an->priv->watcher, rd->source_uid);
+ if (client && !e_client_check_capability (E_CLIENT (client), CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS)) {
+ g_object_unref (client);
+ return FALSE;
+ }
+
+ g_clear_object (&client);
+
+ /* Do not know how to send an email from here, but an application can write an extension
+ of E_TYPE_REMINDERS_WIDGET, listen for EReminderWatcher::triggered signal and do what
+ is required from that handler. */
+
+ return FALSE;
+}
+
+static gboolean
+e_alarm_notify_is_blessed_program (GSettings *settings,
+ const gchar *url)
+{
+ gchar **list;
+ gint ii;
+ gboolean found = FALSE;
+
+ g_return_val_if_fail (G_IS_SETTINGS (settings), FALSE);
+ g_return_val_if_fail (url != NULL, FALSE);
+
+ list = g_settings_get_strv (settings, "notify-programs");
+
+ for (ii = 0; list && list[ii] && !found; ii++) {
+ found = g_strcmp0 (list[ii], url) == 0;
+ }
+
+ g_strfreev (list);
+
+ return found;
+}
+
+static void
+e_alarm_notify_save_blessed_program (GSettings *settings,
+ const gchar *url)
+{
+ gchar **list;
+ gint ii;
+ GPtrArray *array;
+
+ g_return_if_fail (G_IS_SETTINGS (settings));
+ g_return_if_fail (url != NULL);
+
+ array = g_ptr_array_new ();
+
+ list = g_settings_get_strv (settings, "notify-programs");
+
+ for (ii = 0; list && list[ii]; ii++) {
+ if (g_strcmp0 (url, list[ii]) != 0)
+ g_ptr_array_add (array, list[ii]);
+ }
+
+ g_ptr_array_add (array, (gpointer) url);
+ g_ptr_array_add (array, NULL);
+
+ g_settings_set_strv (settings, "notify-programs", (const gchar * const *) array->pdata);
+
+ g_ptr_array_free (array, TRUE);
+ g_strfreev (list);
+}
+
+static gboolean
+e_alarm_notify_can_procedure (EAlarmNotify *an,
+ const gchar *cmd,
+ const gchar *url)
+{
+ GtkWidget *container;
+ GtkWidget *dialog;
+ GtkWidget *label;
+ GtkWidget *checkbox;
+ gchar *str;
+ gint response;
+
+ if (e_alarm_notify_is_blessed_program (an->priv->settings, url))
+ return TRUE;
+
+ dialog = gtk_dialog_new_with_buttons (
+ _("Warning"), GTK_WINDOW (an->priv->window), 0,
+ _("_No"), GTK_RESPONSE_CANCEL,
+ _("_Yes"), GTK_RESPONSE_OK,
+ NULL);
+
+ str = g_strdup_printf (
+ _("A calendar reminder is about to trigger. "
+ "This reminder is configured to run the following program:\n\n"
+ " %s\n\n"
+ "Are you sure you want to run this program?"),
+ cmd);
+ label = gtk_label_new (str);
+ gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+ gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_LEFT);
+ gtk_widget_show (label);
+
+ container = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+ gtk_box_pack_start (GTK_BOX (container), label, TRUE, TRUE, 4);
+ g_free (str);
+
+ checkbox = gtk_check_button_new_with_label (_("Do not ask me about this program again"));
+ gtk_widget_show (checkbox);
+ gtk_box_pack_start (GTK_BOX (container), checkbox, TRUE, TRUE, 4);
+
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+
+ if (response == GTK_RESPONSE_OK &&
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (checkbox))) {
+ e_alarm_notify_save_blessed_program (an->priv->settings, url);
+ }
+
+ gtk_widget_destroy (dialog);
+
+ return response == GTK_RESPONSE_OK;
+}
+
+static gboolean
+e_alarm_notify_procedure (EAlarmNotify *an,
+ const EReminderData *rd,
+ ECalComponentAlarm *alarm)
+{
+ ECalComponentText description;
+ icalattach *attach;
+ const gchar *url;
+ gchar *cmd;
+ gboolean result = FALSE;
+
+ g_return_val_if_fail (an != NULL, FALSE);
+ g_return_val_if_fail (rd != NULL, FALSE);
+ g_return_val_if_fail (alarm != NULL, FALSE);
+
+ e_cal_component_alarm_get_attach (alarm, &attach);
+ e_cal_component_alarm_get_description (alarm, &description);
+
+ /* If the alarm has no attachment, simply display a notification dialog. */
+ if (!attach)
+ goto fallback;
+
+ if (!icalattach_get_is_url (attach)) {
+ icalattach_unref (attach);
+ goto fallback;
+ }
+
+ url = icalattach_get_url (attach);
+ g_return_val_if_fail (url != NULL, FALSE);
+
+ /* Ask for confirmation before executing the stuff */
+ if (description.value)
+ cmd = g_strconcat (url, " ", description.value, NULL);
+ else
+ cmd = (gchar *) url;
+
+ if (e_alarm_notify_can_procedure (an, cmd, url))
+ result = g_spawn_command_line_async (cmd, NULL);
+
+ if (cmd != (gchar *) url)
+ g_free (cmd);
+
+ icalattach_unref (attach);
+
+ /* Fall back to display notification if we got an error */
+ if (!result)
+ goto fallback;
+
+ return FALSE;
+
+ fallback:
+ return e_alarm_notify_display (an, rd, alarm);
+}
+
+/* Returns %TRUE to keep in ERemindersWidget */
+static gboolean
+e_alarm_notify_process (EAlarmNotify *an,
+ const EReminderData *rd,
+ gboolean snoozed)
+{
+ ECalComponentAlarm *alarm;
+ ECalComponentAlarmAction action;
+ gboolean keep_in_reminders = FALSE;
+
+ g_return_val_if_fail (an != NULL, FALSE);
+ g_return_val_if_fail (rd != NULL, FALSE);
+
+ if (e_cal_component_get_vtype (rd->component) == E_CAL_COMPONENT_TODO) {
+ icalproperty_status status = ICAL_STATUS_NONE;
+
+ e_cal_component_get_status (rd->component, &status);
+
+ if (status == ICAL_STATUS_COMPLETED &&
+ !g_settings_get_boolean (an->priv->settings, "notify-completed-tasks")) {
+ return FALSE;
+ }
+ }
+
+ alarm = e_cal_component_get_alarm (rd->component, rd->instance.auid);
+ if (!alarm)
+ return FALSE;
+
+ if (!snoozed && !g_settings_get_boolean (an->priv->settings, "notify-past-events")) {
+ ECalComponentAlarmTrigger trigger;
+ ECalComponentAlarmRepeat repeat;
+ time_t offset = 0, event_relative, orig_trigger_day, today;
+
+ e_cal_component_alarm_get_trigger (alarm, &trigger);
+ e_cal_component_alarm_get_repeat (alarm, &repeat);
+
+ switch (trigger.type) {
+ case E_CAL_COMPONENT_ALARM_TRIGGER_NONE:
+ case E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE:
+ break;
+
+ case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START:
+ case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_END:
+ offset = icaldurationtype_as_int (trigger.u.rel_duration);
+ break;
+
+ default:
+ break;
+ }
+
+ today = time (NULL);
+ event_relative = rd->instance.occur_start - offset;
+
+ #define CLAMP_TO_DAY(x) ((x) - ((x) % (60 * 60 * 24)))
+
+ event_relative = CLAMP_TO_DAY (event_relative);
+ orig_trigger_day = CLAMP_TO_DAY (rd->instance.trigger);
+ today = CLAMP_TO_DAY (today);
+
+ #undef CLAMP_TO_DAY
+
+ if (event_relative < today && orig_trigger_day < today) {
+ e_cal_component_alarm_free (alarm);
+ return FALSE;
+ }
+ }
+
+ e_cal_component_alarm_get_action (alarm, &action);
+
+ switch (action) {
+ case E_CAL_COMPONENT_ALARM_AUDIO:
+ keep_in_reminders = e_alarm_notify_audio (an, rd, alarm);
+ break;
+
+ case E_CAL_COMPONENT_ALARM_DISPLAY:
+ keep_in_reminders = e_alarm_notify_display (an, rd, alarm);
+ break;
+
+ case E_CAL_COMPONENT_ALARM_EMAIL:
+ keep_in_reminders = e_alarm_notify_email (an, rd, alarm);
+ break;
+
+ case E_CAL_COMPONENT_ALARM_PROCEDURE:
+ keep_in_reminders = e_alarm_notify_procedure (an, rd, alarm);
+ break;
+
+ case E_CAL_COMPONENT_ALARM_NONE:
+ case E_CAL_COMPONENT_ALARM_UNKNOWN:
+ break;
+ }
+
+ e_cal_component_alarm_free (alarm);
+
+ return keep_in_reminders;
+}
+
+static gpointer
+e_alarm_notify_dismiss_thread (gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+ GSList *dismiss, *link;
+
+ g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), NULL);
+
+ g_mutex_lock (&an->priv->dismiss_lock);
+ dismiss = an->priv->dismiss;
+ an->priv->dismiss = NULL;
+ an->priv->dismiss_thread = NULL;
+ g_mutex_unlock (&an->priv->dismiss_lock);
+
+ if (an->priv->watcher) {
+ for (link = dismiss; link; link = g_slist_next (link)) {
+ EReminderData *rd = link->data;
+
+ if (rd) {
+ /* Silently ignore any errors here */
+ e_reminder_watcher_dismiss_sync (an->priv->watcher, rd, NULL, NULL);
+ }
+ }
+ }
+
+ g_slist_free_full (dismiss, e_reminder_data_free);
+ g_clear_object (&an);
+
+ return NULL;
+}
+
+static void
+e_alarm_notify_triggered_cb (EReminderWatcher *watcher,
+ const GSList *reminders, /* EReminderData * */
+ gboolean snoozed,
+ gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+ GSList *link;
+
+ g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+ g_mutex_lock (&an->priv->dismiss_lock);
+
+ for (link = (GSList *) reminders; link; link = g_slist_next (link)) {
+ const EReminderData *rd = link->data;
+
+ if (rd && !e_alarm_notify_process (an, rd, snoozed)) {
+ an->priv->dismiss = g_slist_prepend (an->priv->dismiss, e_reminder_data_copy (rd));
+ }
+ }
+
+ if (an->priv->dismiss && !an->priv->dismiss_thread) {
+ an->priv->dismiss_thread = g_thread_new (NULL, e_alarm_notify_dismiss_thread, g_object_ref (an));
+ g_warn_if_fail (an->priv->dismiss_thread != NULL);
+ if (an->priv->dismiss_thread)
+ g_thread_unref (an->priv->dismiss_thread);
+ }
+
+ g_mutex_unlock (&an->priv->dismiss_lock);
+}
+
+static void
+e_alarm_notify_status_icon_activated_cb (GtkStatusIcon *status_icon,
+ gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+
+ g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+ if (gtk_widget_get_visible (an->priv->window))
+ gtk_widget_set_visible (an->priv->window, FALSE);
+ else
+ e_alarm_notify_show_window (an, TRUE);
+
+ if (an->priv->status_icon_blink_id > 0) {
+ g_source_remove (an->priv->status_icon_blink_id);
+ an->priv->status_icon_blink_id = -1;
+
+ if (an->priv->status_icon)
+ gtk_status_icon_set_from_icon_name (an->priv->status_icon, "appointment-soon");
+ }
+}
+
+static gboolean
+e_alarm_notify_popup_destroy_idle_cb (gpointer user_data)
+{
+ GtkWidget *widget = user_data;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE);
+
+ gtk_widget_destroy (widget);
+
+ return FALSE;
+}
+
+static void
+e_alarm_notify_schedule_popup_destroy (GtkWidget *widget)
+{
+ g_idle_add_full (G_PRIORITY_LOW, e_alarm_notify_popup_destroy_idle_cb, widget, NULL);
+}
+
+static void
+e_alarm_notify_status_icon_popup_menu_cb (GtkStatusIcon *status_icon,
+ guint button,
+ guint activate_time,
+ gpointer user_data)
+{
+ struct _items {
+ const gchar *label;
+ const gchar *opt_name;
+ } items[] = {
+ { N_("Display reminders in notification area _only"), "notify-with-tray" },
+ { N_("Keep reminder notification window always on _top"), "notify-window-on-top" },
+ { N_("Display reminders for _completed tasks"), "notify-completed-tasks" },
+ { N_("Display reminders for _past events"), "notify-past-events" }
+ };
+
+ EAlarmNotify *an = user_data;
+ GtkWidget *popup_menu;
+ GtkMenuShell *menu_shell;
+ GtkWidget *item;
+ gint ii;
+
+ g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+ popup_menu = gtk_menu_new ();
+ menu_shell = GTK_MENU_SHELL (popup_menu);
+
+ item = gtk_menu_item_new_with_label (_("Reminders Options:"));
+ gtk_widget_set_sensitive (item, FALSE);
+ gtk_menu_shell_append (menu_shell, item);
+
+ item = gtk_separator_menu_item_new ();
+ gtk_menu_shell_append (menu_shell, item);
+
+ for (ii = 0; ii < G_N_ELEMENTS (items); ii++) {
+ item = gtk_check_menu_item_new_with_mnemonic (_(items[ii].label));
+ gtk_menu_shell_append (menu_shell, item);
+
+ g_settings_bind (an->priv->settings, items[ii].opt_name,
+ item, "active",
+ G_SETTINGS_BIND_DEFAULT);
+ }
+
+ g_signal_connect (popup_menu, "deactivate", G_CALLBACK (e_alarm_notify_schedule_popup_destroy), NULL);
+
+ gtk_widget_show_all (popup_menu);
+
+ gtk_menu_popup (GTK_MENU (popup_menu), NULL, NULL, NULL, NULL, button, activate_time);
+}
+
+static gboolean
+e_alarm_notify_status_icon_blink_cb (gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+ const gchar *icon_name;
+
+ if (g_source_is_destroyed (g_main_current_source ()))
+ return FALSE;
+
+ g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), FALSE);
+
+ an->priv->status_icon_blink_countdown--;
+
+ if (!(an->priv->status_icon_blink_countdown & 1) && an->priv->status_icon_blink_countdown > 0)
+ icon_name = "appointment-missed";
+ else
+ icon_name = "appointment-soon";
+
+ if (an->priv->status_icon)
+ gtk_status_icon_set_from_icon_name (an->priv->status_icon, icon_name);
+
+ if (an->priv->status_icon_blink_countdown <= 0 || !an->priv->status_icon)
+ an->priv->status_icon_blink_id = -1;
+
+ return an->priv->status_icon_blink_id != -1;
+}
+
+static void
+e_alarm_notify_reminders_changed_cb (ERemindersWidget *reminders,
+ gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+ GtkTreeView *tree_view;
+ gint n_reminders = 0;
+
+ g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+ tree_view = e_reminders_widget_get_tree_view (an->priv->reminders);
+ if (tree_view) {
+ GtkTreeModel *model;
+
+ model = gtk_tree_view_get_model (tree_view);
+ n_reminders = gtk_tree_model_iter_n_children (model, NULL);
+ }
+
+ /* This is to update tray icon only, which is not used in GNOME */
+ if (!e_alarm_notify_is_running_gnome ()) {
+ if (n_reminders <= 0) {
+ if (an->priv->status_icon) {
+ gtk_status_icon_set_visible (an->priv->status_icon, FALSE);
+ g_clear_object (&an->priv->status_icon);
+ }
+ } else {
+ if (!an->priv->status_icon) {
+ an->priv->status_icon = gtk_status_icon_new ();
+ gtk_status_icon_set_title (an->priv->status_icon, _("Reminders"));
+ gtk_status_icon_set_from_icon_name (an->priv->status_icon, "appointment-soon");
+
+ g_signal_connect (an->priv->status_icon, "activate",
+ G_CALLBACK (e_alarm_notify_status_icon_activated_cb), an);
+
+ g_signal_connect (an->priv->status_icon, "popup-menu",
+ G_CALLBACK (e_alarm_notify_status_icon_popup_menu_cb), an);
+ }
+
+ if (n_reminders == 1 && an->priv->status_icon_tooltip) {
+ gtk_status_icon_set_tooltip_text (an->priv->status_icon, an->priv->status_icon_tooltip);
+ } else {
+ gchar *str;
+
+ str = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE,
+ "You have %d reminder", "You have %d reminders",
+ n_reminders), n_reminders);
+ gtk_status_icon_set_tooltip_text (an->priv->status_icon, str);
+ g_free (str);
+ }
+
+ gtk_status_icon_set_visible (an->priv->status_icon, TRUE);
+
+ if (an->priv->status_icon_blink_id <= 0 &&
+ an->priv->last_n_reminders < n_reminders) {
+ an->priv->status_icon_blink_countdown = 30;
+ an->priv->status_icon_blink_id = e_named_timeout_add (500, e_alarm_notify_status_icon_blink_cb, an);
+ }
+ }
+ }
+
+ an->priv->last_n_reminders = n_reminders;
+
+ g_clear_pointer (&an->priv->status_icon_tooltip, g_free);
+
+ if (n_reminders <= 0 && an->priv->window)
+ gtk_widget_set_visible (an->priv->window, FALSE);
+
+ /* If any reminders were snoozed or dismissed remove their notifications as well */
+ if (g_hash_table_size (an->priv->notification_ids)) {
+ GHashTable *notification_ids;
+
+ notification_ids = an->priv->notification_ids;
+ an->priv->notification_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ if (n_reminders > 0) {
+ GSList *past, *link;
+
+ past = e_reminder_watcher_dup_past (an->priv->watcher);
+ for (link = past; link; link = g_slist_next (link)) {
+ EReminderData *rd = link->data;
+ gchar *notif_id;
+
+ if (!rd)
+ continue;
+
+ notif_id = e_alarm_notify_build_notif_id (rd);
+ if (g_hash_table_remove (notification_ids, notif_id))
+ g_hash_table_insert (an->priv->notification_ids, notif_id, NULL);
+ else
+ g_free (notif_id);
+ }
+
+ g_slist_free_full (past, e_reminder_data_free);
+ }
+
+ if (g_hash_table_size (notification_ids)) {
+ GApplication *application = G_APPLICATION (an);
+ GHashTableIter iter;
+ gpointer key;
+
+ g_hash_table_iter_init (&iter, notification_ids);
+ while (g_hash_table_iter_next (&iter, &key, NULL)) {
+ const gchar *notif_id = key;
+
+ if (notif_id)
+ g_application_withdraw_notification (application, notif_id);
+ }
+ }
+
+ g_hash_table_destroy (notification_ids);
+ }
+}
+
+static gboolean
+e_alarm_notify_window_geometry_save_cb (gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+
+ if (g_source_is_destroyed (g_main_current_source ()))
+ return FALSE;
+
+ g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), FALSE);
+
+ an->priv->window_geometry_save_id = 0;
+
+ if (an->priv->settings) {
+ #define set_if_changed(_name, _value) G_STMT_START { \
+ if (g_settings_get_int (an->priv->settings, _name) != _value) \
+ g_settings_set_int (an->priv->settings, _name, _value); \
+ } G_STMT_END
+
+ set_if_changed ("notify-window-x", an->priv->window_x);
+ set_if_changed ("notify-window-y", an->priv->window_y);
+ set_if_changed ("notify-window-width", an->priv->window_width);
+ set_if_changed ("notify-window-height", an->priv->window_height);
+
+ #undef set_if_changed
+ }
+
+ return FALSE;
+}
+
+static gboolean
+e_alarm_notify_window_configure_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+
+ g_return_val_if_fail (E_IS_ALARM_NOTIFY (an), FALSE);
+
+ if (an->priv->window && an->priv->settings && gtk_widget_get_visible (an->priv->window)) {
+ gint pos_x = an->priv->window_x, pos_y = an->priv->window_y;
+ gint width = an->priv->window_width, height = an->priv->window_height;
+
+ gtk_window_get_position (GTK_WINDOW (an->priv->window), &pos_x, &pos_y);
+ gtk_window_get_size (GTK_WINDOW (an->priv->window), &width, &height);
+
+ if (pos_x != an->priv->window_x || pos_y != an->priv->window_y ||
+ width != an->priv->window_width || height != an->priv->window_height) {
+ an->priv->window_x = pos_x;
+ an->priv->window_y = pos_y;
+ an->priv->window_width = width;
+ an->priv->window_height = height;
+
+ if (an->priv->window_geometry_save_id > 0)
+ g_source_remove (an->priv->window_geometry_save_id);
+
+ an->priv->window_geometry_save_id = e_named_timeout_add_seconds (1,
+ e_alarm_notify_window_geometry_save_cb, an);
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+e_alarm_notify_action_activate_cb (GSimpleAction *action,
+ GVariant *parameter,
+ gpointer user_data)
+{
+ EAlarmNotify *an = user_data;
+ const gchar *name;
+
+ g_return_if_fail (G_IS_ACTION (action));
+ g_return_if_fail (E_IS_ALARM_NOTIFY (an));
+
+ name = g_action_get_name (G_ACTION (action));
+ g_return_if_fail (name != NULL);
+
+ if (g_str_equal (name, "show-reminders")) {
+ e_alarm_notify_show_window (an, TRUE);
+ } else {
+ g_warning ("%s: Unknown app. action '%s'", G_STRFUNC, name);
+ }
+}
+
+static void
+e_alarm_notify_startup (GApplication *application)
+{
+ const GActionEntry actions[] = {
+ { "show-reminders", e_alarm_notify_action_activate_cb, NULL, NULL, NULL }
+ };
+
+ /* Chain up to parent's method. */
+ G_APPLICATION_CLASS (e_alarm_notify_parent_class)->startup (application);
+
+ /* Keep the application running. */
+ g_application_hold (application);
+
+ g_action_map_add_action_entries (G_ACTION_MAP (application), actions, G_N_ELEMENTS (actions), application);
+}
+
+static void
+e_alarm_notify_activate (GApplication *application)
+{
+ EAlarmNotify *an = E_ALARM_NOTIFY (application);
+
+ if (g_application_get_is_remote (application)) {
+ g_application_quit (application);
+ return;
+ }
+
+ g_return_if_fail (an->priv->registry != NULL);
+
+ an->priv->watcher = e_reminder_watcher_new (an->priv->registry);
+ an->priv->reminders = e_reminders_widget_new (an->priv->watcher);
+ an->priv->settings = g_object_ref (e_reminders_widget_get_settings (an->priv->reminders));
+
+ g_object_set (G_OBJECT (an->priv->reminders),
+ "halign", GTK_ALIGN_FILL,
+ "hexpand", TRUE,
+ "valign", GTK_ALIGN_FILL,
+ "vexpand", TRUE,
+ NULL);
+
+ an->priv->window = gtk_application_window_new (GTK_APPLICATION (an));
+ gtk_window_set_title (GTK_WINDOW (an->priv->window), _("Reminders"));
+ gtk_window_set_icon_name (GTK_WINDOW (an->priv->window), "appointment-soon");
+ gtk_window_set_default_size (GTK_WINDOW (an->priv->window),
+ g_settings_get_int (an->priv->settings, "notify-window-width"),
+ g_settings_get_int (an->priv->settings, "notify-window-height"));
+ an->priv->window_x = g_settings_get_int (an->priv->settings, "notify-window-x");
+ an->priv->window_y = g_settings_get_int (an->priv->settings, "notify-window-y");
+
+ gtk_container_add (GTK_CONTAINER (an->priv->window), GTK_WIDGET (an->priv->reminders));
+
+ gtk_window_set_keep_above (GTK_WINDOW (an->priv->window), g_settings_get_boolean (an->priv->settings, "notify-window-on-top"));
+
+ g_signal_connect (an->priv->watcher, "triggered",
+ G_CALLBACK (e_alarm_notify_triggered_cb), an);
+
+ g_signal_connect (an->priv->reminders, "changed",
+ G_CALLBACK (e_alarm_notify_reminders_changed_cb), an);
+
+ g_signal_connect (an->priv->window, "configure-event",
+ G_CALLBACK (e_alarm_notify_window_configure_event_cb), an);
+
+ g_signal_connect (an->priv->window, "delete-event",
+ G_CALLBACK (gtk_widget_hide_on_delete), an);
+}
+
+static gboolean
+e_alarm_notify_initable (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EAlarmNotify *an = E_ALARM_NOTIFY (initable);
+
+ an->priv->registry = e_source_registry_new_sync (cancellable, error);
+
+ return an->priv->registry != NULL;
+}
+
+static void
+e_alarm_notify_dispose (GObject *object)
+{
+ EAlarmNotify *an = E_ALARM_NOTIFY (object);
+
+ if (an->priv->watcher)
+ g_signal_handlers_disconnect_by_data (an->priv->watcher, an);
+
+ if (an->priv->reminders)
+ g_signal_handlers_disconnect_by_data (an->priv->reminders, an);
+
+ if (an->priv->status_icon_blink_id > 0) {
+ g_source_remove (an->priv->status_icon_blink_id);
+ an->priv->status_icon_blink_id = -1;
+ }
+
+ if (an->priv->window_geometry_save_id > 0) {
+ g_source_remove (an->priv->window_geometry_save_id);
+ an->priv->window_geometry_save_id = 0;
+ }
+
+ if (an->priv->status_icon) {
+ gtk_status_icon_set_visible (an->priv->status_icon, FALSE);
+ g_clear_object (&an->priv->status_icon);
+ }
+
+ if (an->priv->window) {
+ g_signal_handlers_disconnect_by_data (an->priv->window, an);
+
+ gtk_widget_destroy (an->priv->window);
+ an->priv->window = NULL;
+ an->priv->reminders = NULL;
+ }
+
+ g_clear_object (&an->priv->registry);
+ g_clear_object (&an->priv->watcher);
+ g_clear_object (&an->priv->settings);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_alarm_notify_parent_class)->dispose (object);
+}
+
+static void
+e_alarm_notify_finalize (GObject *object)
+{
+ EAlarmNotify *an = E_ALARM_NOTIFY (object);
+
+ g_free (an->priv->status_icon_tooltip);
+ g_mutex_clear (&an->priv->dismiss_lock);
+ g_slist_free_full (an->priv->dismiss, e_reminder_data_free);
+ g_hash_table_destroy (an->priv->notification_ids);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_alarm_notify_parent_class)->finalize (object);
+}
+
+static void
+e_alarm_notify_class_init (EAlarmNotifyClass *klass)
+{
+ GObjectClass *object_class;
+ GApplicationClass *application_class;
+
+ g_type_class_add_private (klass, sizeof (EAlarmNotifyPrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = e_alarm_notify_dispose;
+ object_class->finalize = e_alarm_notify_finalize;
+
+ application_class = G_APPLICATION_CLASS (klass);
+ application_class->startup = e_alarm_notify_startup;
+ application_class->activate = e_alarm_notify_activate;
+}
+
+static void
+e_alarm_notify_initable_init (GInitableIface *iface)
+{
+ iface->init = e_alarm_notify_initable;
+}
+
+static void
+e_alarm_notify_init (EAlarmNotify *an)
+{
+ an->priv = G_TYPE_INSTANCE_GET_PRIVATE (an, E_TYPE_ALARM_NOTIFY, EAlarmNotifyPrivate);
+ an->priv->notification_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ an->priv->last_n_reminders = G_MAXINT32;
+
+ g_mutex_init (&an->priv->dismiss_lock);
+}
+
+/*
+ * e_alarm_notify_new:
+ *
+ * Creates a new #EAlarmNotify object.
+ *
+ * Returns: (transfer full): a newly-created #EAlarmNotify
+ **/
+EAlarmNotify *
+e_alarm_notify_new (GCancellable *cancellable,
+ GError **error)
+{
+ return g_initable_new (
+ E_TYPE_ALARM_NOTIFY, cancellable, error,
+ "application-id", APPLICATION_ID,
+ NULL);
+}
diff --git a/src/services/evolution-alarm-notify/e-alarm-notify.h b/src/services/evolution-alarm-notify/e-alarm-notify.h
new file mode 100644
index 000000000..8c77bc196
--- /dev/null
+++ b/src/services/evolution-alarm-notify/e-alarm-notify.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef E_ALARM_NOTIFY_H
+#define E_ALARM_NOTIFY_H
+
+#include <gtk/gtk.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_ALARM_NOTIFY \
+ (e_alarm_notify_get_type ())
+#define E_ALARM_NOTIFY(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_ALARM_NOTIFY, EAlarmNotify))
+#define E_ALARM_NOTIFY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_ALARM_NOTIFY, EAlarmNotifyClass))
+#define E_IS_ALARM_NOTIFY(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_ALARM_NOTIFY))
+#define E_IS_ALARM_NOTIFY_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_ALARM_NOTIFY))
+#define E_ALARM_NOTIFY_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_ALARM_NOTIFY, EAlarmNotifyClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EAlarmNotify EAlarmNotify;
+typedef struct _EAlarmNotifyClass EAlarmNotifyClass;
+typedef struct _EAlarmNotifyPrivate EAlarmNotifyPrivate;
+
+struct _EAlarmNotify {
+ /*< private >*/
+ GtkApplication parent;
+ EAlarmNotifyPrivate *priv;
+};
+
+struct _EAlarmNotifyClass {
+ /*< private >*/
+ GtkApplicationClass parent_class;
+};
+
+GType e_alarm_notify_get_type (void);
+EAlarmNotify * e_alarm_notify_new (GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_ALARM_NOTIFY_H */
diff --git a/src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc b/src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc
new file mode 100644
index 000000000..1f9ef6587
--- /dev/null
+++ b/src/services/evolution-alarm-notify/evolution-alarm-notify-icon.rc
@@ -0,0 +1 @@
+1 ICON "evolution-alarm-notify.ico"
diff --git a/src/services/evolution-alarm-notify/evolution-alarm-notify.c b/src/services/evolution-alarm-notify/evolution-alarm-notify.c
new file mode 100644
index 000000000..d3a46ba96
--- /dev/null
+++ b/src/services/evolution-alarm-notify/evolution-alarm-notify.c
@@ -0,0 +1,106 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2018 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library 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 Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <libintl.h>
+#include <glib/gi18n.h>
+
+#include <libedataserver/libedataserver.h>
+#include <libedataserverui/libedataserverui.h>
+
+#include "e-alarm-notify.h"
+
+#ifdef G_OS_UNIX
+#include <glib-unix.h>
+
+static gboolean
+handle_term_signal (gpointer data)
+{
+ g_application_quit (data);
+
+ return FALSE;
+}
+#endif
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ EAlarmNotify *alarm_notify;
+ gint exit_status;
+ GError *error = NULL;
+
+#ifdef G_OS_WIN32
+ e_util_win32_initialize ();
+#endif
+
+ setlocale (LC_ALL, "");
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ /* Workaround https://bugzilla.gnome.org/show_bug.cgi?id=674885 */
+ g_type_ensure (G_TYPE_DBUS_CONNECTION);
+ g_type_ensure (G_TYPE_DBUS_PROXY);
+ g_type_ensure (G_BUS_TYPE_SESSION);
+
+ gtk_init (&argc, &argv);
+
+ if (error != NULL) {
+ g_printerr ("%s\n", error->message);
+ exit (EXIT_FAILURE);
+ }
+
+ e_gdbus_templates_init_main_thread ();
+ e_xml_initialize_in_main ();
+
+ alarm_notify = e_alarm_notify_new (NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s\n", error->message);
+ g_error_free (error);
+ exit (EXIT_FAILURE);
+ }
+
+ g_application_register (G_APPLICATION (alarm_notify), NULL, &error);
+
+ if (error != NULL) {
+ g_printerr ("%s\n", error->message);
+ g_error_free (error);
+ g_object_unref (alarm_notify);
+ exit (EXIT_FAILURE);
+ }
+
+ if (g_application_get_is_remote (G_APPLICATION (alarm_notify))) {
+ g_object_unref (alarm_notify);
+ return 0;
+ }
+
+#ifdef G_OS_UNIX
+ g_unix_signal_add_full (
+ G_PRIORITY_DEFAULT, SIGTERM,
+ handle_term_signal, alarm_notify, NULL);
+#endif
+
+ exit_status = g_application_run (G_APPLICATION (alarm_notify), argc, argv);
+
+ g_object_unref (alarm_notify);
+
+ return exit_status;
+}
diff --git a/src/services/evolution-alarm-notify/evolution-alarm-notify.ico b/src/services/evolution-alarm-notify/evolution-alarm-notify.ico
new file mode 100644
index 000000000..658545225
--- /dev/null
+++ b/src/services/evolution-alarm-notify/evolution-alarm-notify.ico
Binary files differ