/* -*- 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 . */ #include "evolution-data-server-config.h" #include #ifdef HAVE_CANBERRA #include #endif #include #ifndef G_OS_WIN32 #include #endif #include "libecal/libecal.h" #include "libedataserverui/libedataserverui.h" #include "e-alarm-notify.h" #ifdef DBUS_SERVICES_PREFIX #define APPLICATION_ID DBUS_SERVICES_PREFIX "." "org.gnome.Evolution-alarm-notify" #else #define APPLICATION_ID "org.gnome.Evolution-alarm-notify" #endif 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_paned_position; 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_ADD_PRIVATE (EAlarmNotify) G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, e_alarm_notify_initable_init)) static void ean_debug_print (const gchar *format, ...) G_GNUC_PRINTF (1, 2); static void ean_debug_print (const gchar *format, ...) { static gint enabled = -1; va_list args; if (enabled == -1) enabled = g_strcmp0 (g_getenv ("EAN_DEBUG"), "1") == 0 ? 1 : 0; if (enabled != 1) return; va_start (args, format); e_util_debug_printv ("EAN", format, args); va_end (args); } 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) { GtkTreeSelection *selection; gtk_window_move (window, an->priv->window_x, an->priv->window_y); selection = gtk_tree_view_get_selection (e_reminders_widget_get_tree_view (an->priv->reminders)); if (!gtk_tree_selection_count_selected_rows (selection)) { GtkTreePath *path; path = gtk_tree_path_new_first (); gtk_tree_selection_select_path (selection, path); gtk_tree_path_free (path); } } } static gboolean e_alarm_notify_audio (EAlarmNotify *an, const EReminderData *rd, ECalComponentAlarm *alarm) { ICalAttach *attach = NULL; GSList *attachments; 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); if (!g_settings_get_boolean (an->priv->settings, "notify-enable-audio")) { ean_debug_print ("Audio notify: Skipped, because disabled in the settings\n"); return FALSE; } attachments = e_cal_component_alarm_get_attachments (alarm); if (attachments && !attachments->next) attach = attachments->data; if (attach && i_cal_attach_get_is_url (attach)) { const gchar *url; url = i_cal_attach_get_url (attach); if (url && *url) { gchar *filename; GError *error = NULL; filename = g_filename_from_uri (url, NULL, &error); if (!filename) { ean_debug_print ("Audio notify: Failed to convert URI '%s' to filename: %s\n", url, error ? error->message : "Unknown error"); } else if (g_file_test (filename, G_FILE_TEST_EXISTS)) { #ifdef HAVE_CANBERRA gint err = ca_context_play (ca_gtk_context_get (), 0, CA_PROP_APPLICATION_NAME, "evolution-alarm-notify", CA_PROP_APPLICATION_VERSION, VERSION, CA_PROP_APPLICATION_ID, "org.gnome.Evolution-alarm-notify", CA_PROP_MEDIA_FILENAME, filename, NULL); did_play = !err; if (err) ean_debug_print ("Audio notify: Cannot play file '%s': %s\n", filename, ca_strerror (err)); #else ean_debug_print ("Audio notify: Cannot play file '%s': Not compiled with libcanberra\n", filename); #endif } else { ean_debug_print ("Audio notify: File '%s' does not exist\n", filename); } g_clear_error (&error); g_free (filename); } else { ean_debug_print ("Audio notify: Alarm has stored empty URL, fallback to default sound\n"); } } else if (!attach) { ean_debug_print ("Audio notify: Alarm has no attachment, fallback to default sound\n"); } else { ean_debug_print ("Audio notify: Alarm attachment is not a URL to sound file, fallback to default sound\n"); } #ifdef HAVE_CANBERRA if (!did_play) { gint err = ca_context_play (ca_gtk_context_get (), 0, CA_PROP_APPLICATION_NAME, "evolution-alarm-notify", CA_PROP_APPLICATION_VERSION, VERSION, CA_PROP_APPLICATION_ID, "org.gnome.Evolution-alarm-notify", CA_PROP_EVENT_ID, "alarm-clock-elapsed", NULL); did_play = !err; if (err) ean_debug_print ("Audio notify: Cannot play event sound: %s\n", ca_strerror (err)); } #endif if (!did_play) { GdkDisplay *display; display = an->priv->window ? gtk_widget_get_display (an->priv->window) : NULL; if (!display) display = gdk_display_get_default (); if (display) gdk_display_beep (display); else ean_debug_print ("Audio notify: Cannot beep, no display found\n"); } 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) { const gchar *desktop; desktop = g_getenv ("XDG_CURRENT_DESKTOP"); runs_gnome = 0; if (desktop != NULL) { gint ii; gchar **desktops = g_strsplit (desktop, ":", -1); for (ii = 0; desktops[ii]; ii++) { if (!g_ascii_strcasecmp (desktops[ii], "gnome")) { runs_gnome = 1; break; } } g_strfreev (desktops); } 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; ECalComponentAlarmInstance *instance; g_return_val_if_fail (rd != NULL, NULL); string = g_string_sized_new (32); if (e_reminder_data_get_source_uid (rd)) { g_string_append (string, e_reminder_data_get_source_uid (rd)); g_string_append_c (string, '\n'); } id = e_cal_component_get_id (e_reminder_data_get_component (rd)); if (id) { if (e_cal_component_id_get_uid (id)) { g_string_append (string, e_cal_component_id_get_uid (id)); g_string_append_c (string, '\n'); } if (e_cal_component_id_get_rid (id)) { g_string_append (string, e_cal_component_id_get_rid (id)); g_string_append_c (string, '\n'); } e_cal_component_id_free (id); } instance = e_reminder_data_get_instance (rd); g_string_append_printf (string, "%" G_GINT64_FORMAT, (gint64) (instance ? e_cal_component_alarm_instance_get_time (instance) : -1)); 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_settings_get_boolean (an->priv->settings, "notify-enable-display") && !g_hash_table_contains (an->priv->notification_ids, notif_id)) { GNotification *notification; GIcon *icon; gchar *detailed_action; notification = g_notification_new (_("Reminders")); g_notification_set_body (notification, description); icon = g_themed_icon_new ("appointment-soon"); if (icon) { g_notification_set_icon (notification, icon); g_object_unref (icon); } detailed_action = g_action_print_detailed_name ("app.show-reminders", NULL); g_notification_set_default_action (notification, detailed_action); 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); } else { g_free (notif_id); if (!g_settings_get_boolean (an->priv->settings, "notify-enable-display")) ean_debug_print ("Display notify: Skipped, because disabled in the settings\n"); } 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, e_reminder_data_get_source_uid (rd)); if (client && !e_client_check_capability (E_CLIENT (client), E_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_width_chars (GTK_LABEL (label), 20); 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 = NULL; GSList *attachments; 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); attachments = e_cal_component_alarm_get_attachments (alarm); if (attachments && !attachments->next) attach = attachments->data; description = e_cal_component_alarm_get_description (alarm); /* If the alarm has no attachment, simply display a notification dialog. */ if (!attach) goto fallback; if (!i_cal_attach_get_is_url (attach)) { goto fallback; } url = i_cal_attach_get_url (attach); g_return_val_if_fail (url != NULL, FALSE); /* Ask for confirmation before executing the stuff */ if (description && e_cal_component_text_get_value (description)) cmd = g_strconcat (url, " ", e_cal_component_text_get_value (description), 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); /* 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; ECalComponentAlarmInstance *instance; 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 (e_reminder_data_get_component (rd)) == E_CAL_COMPONENT_TODO) { ICalPropertyStatus status; status = e_cal_component_get_status (e_reminder_data_get_component (rd)); if (status == I_CAL_STATUS_COMPLETED && !g_settings_get_boolean (an->priv->settings, "notify-completed-tasks")) { return FALSE; } } instance = e_reminder_data_get_instance (rd); alarm = instance ? e_cal_component_get_alarm (e_reminder_data_get_component (rd), e_cal_component_alarm_instance_get_uid (instance)) : NULL; if (!alarm) return FALSE; if (!snoozed && !g_settings_get_boolean (an->priv->settings, "notify-past-events")) { ECalComponentAlarmTrigger *trigger; ICalTime *itt; ICalDuration *duration = NULL; time_t event_relative, orig_trigger_day, today; trigger = e_cal_component_alarm_get_trigger (alarm); event_relative = e_cal_component_alarm_instance_get_occur_start (instance); switch (trigger ? e_cal_component_alarm_trigger_get_kind (trigger) : E_CAL_COMPONENT_ALARM_TRIGGER_NONE) { case E_CAL_COMPONENT_ALARM_TRIGGER_NONE: break; case E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE: itt = e_cal_component_alarm_trigger_get_absolute_time (trigger); if (itt) event_relative = i_cal_time_as_timet_with_zone (itt, i_cal_timezone_get_utc_timezone ()); break; case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START: duration = e_cal_component_alarm_trigger_get_duration (trigger); break; case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_END: event_relative = e_cal_component_alarm_instance_get_occur_end (instance); duration = e_cal_component_alarm_trigger_get_duration (trigger); break; default: break; } /* The trigger time is set as "after the event start/end". */ if (duration && !i_cal_duration_is_neg (duration)) { gint offset; offset = i_cal_duration_as_int (duration); event_relative += offset; } today = time (NULL); #define CLAMP_TO_DAY(x) ((x) - ((x) % (60 * 60 * 24))) event_relative = CLAMP_TO_DAY (event_relative); orig_trigger_day = CLAMP_TO_DAY (e_cal_component_alarm_instance_get_time (instance)); 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; } } action = e_cal_component_alarm_get_action (alarm); 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_window_is_active (GTK_WINDOW (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; guint32 binding_flags; } items[] = { { N_("Display Reminders window with _notifications"), "notify-with-tray", G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN }, { N_("Keep reminder notification window always on _top"), "notify-window-on-top", G_SETTINGS_BIND_DEFAULT }, { N_("Enable _desktop notifications"), "notify-enable-display", G_SETTINGS_BIND_DEFAULT }, { N_("Enable _audio notifications"), "notify-enable-audio", G_SETTINGS_BIND_DEFAULT }, { N_("Display reminders for _completed tasks"), "notify-completed-tasks", G_SETTINGS_BIND_DEFAULT }, { N_("Display reminders for _past events"), "notify-past-events", G_SETTINGS_BIND_DEFAULT } }; 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", items[ii].binding_flags); } 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); set_if_changed ("notify-window-paned-position", an->priv->window_paned_position); #undef set_if_changed } return FALSE; } static void e_alarm_notify_paned_position_changed_cb (GObject *paned, GParamSpec *param, gpointer user_data) { EAlarmNotify *an = user_data; g_return_if_fail (E_IS_ALARM_NOTIFY (an)); if (an->priv->reminders && an->priv->settings) { gint paned_position; paned_position = gtk_paned_get_position (e_reminders_widget_get_paned (an->priv->reminders)); if (paned_position != an->priv->window_paned_position) { an->priv->window_paned_position = paned_position; 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); } } } 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 && an->priv->reminders && 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; gint paned_position; gtk_window_get_position (GTK_WINDOW (an->priv->window), &pos_x, &pos_y); gtk_window_get_size (GTK_WINDOW (an->priv->window), &width, &height); paned_position = gtk_paned_get_position (e_reminders_widget_get_paned (an->priv->reminders)); if (pos_x != an->priv->window_x || pos_y != an->priv->window_y || width != an->priv->window_width || height != an->priv->window_height || paned_position != an->priv->window_paned_position) { an->priv->window_x = pos_x; an->priv->window_y = pos_y; an->priv->window_width = width; an->priv->window_height = height; an->priv->window_paned_position = paned_position; 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); gint paned_position; GtkStyleContext *context; if (g_application_get_is_remote (application)) { g_application_quit (application); return; } g_return_if_fail (an->priv->registry != NULL); if (an->priv->watcher) return; 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)); context = gtk_widget_get_style_context (GTK_WIDGET (an->priv->window)); gtk_style_context_add_class (context, "evolution-alarm-notify-window"); 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"); paned_position = g_settings_get_int (an->priv->settings, "notify-window-paned-position"); if (paned_position <= 0 && g_settings_get_int (an->priv->settings, "notify-window-height") > 0) paned_position = g_settings_get_int (an->priv->settings, "notify-window-height") / 2; if (paned_position > 0) gtk_paned_set_position (e_reminders_widget_get_paned (an->priv->reminders), paned_position); 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 (e_reminders_widget_get_paned (an->priv->reminders), "notify::position", G_CALLBACK (e_alarm_notify_paned_position_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; 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 = e_alarm_notify_get_instance_private (an); 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, #if GLIB_CHECK_VERSION(2, 60, 0) "flags", G_APPLICATION_ALLOW_REPLACEMENT, #endif NULL); }