diff options
author | Milan Crha <mcrha@redhat.com> | 2018-05-11 12:50:54 +0200 |
---|---|---|
committer | Milan Crha <mcrha@redhat.com> | 2018-05-11 12:55:00 +0200 |
commit | 8ba0b06b72e96604db51be8c032afd33fcabe400 (patch) | |
tree | 2639235d3044811bc4d9177fc161bbc2ec42b8a3 | |
parent | 2be90d75c054de32697381c5a55fcf971da6c6f6 (diff) | |
download | evolution-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.
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 (®istry); + } 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, ×trptr, 254, NULL); + + if (!*timestr) + e_reminder_watcher_format_time_impl (watcher, rd, &itt, ×trptr, 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, ¤t, + -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 Binary files differnew file mode 100644 index 000000000..658545225 --- /dev/null +++ b/src/services/evolution-alarm-notify/evolution-alarm-notify.ico |