diff options
Diffstat (limited to 'src/daemon/daemon.c')
| -rw-r--r-- | src/daemon/daemon.c | 422 |
1 files changed, 381 insertions, 41 deletions
diff --git a/src/daemon/daemon.c b/src/daemon/daemon.c index ef83301..062e9a5 100644 --- a/src/daemon/daemon.c +++ b/src/daemon/daemon.c @@ -93,9 +93,19 @@ typedef struct { GtkWindow *nw; guint has_timeout : 1; guint paused : 1; + guint resident : 1; #ifdef HAVE_X11 Window src_window_xid; #endif /* HAVE_X11 */ + + /* Data for notification history */ + gchar *app_name; + gchar *app_icon; + gchar *summary; + gchar *body; + guint urgency; + gint64 timestamp; + NotifydClosedReason close_reason; } NotifyTimeout; typedef struct { @@ -124,6 +134,11 @@ struct _NotifyDaemon { NotifyStackLocation stack_location; NotifyScreen* screen; + + /* Notification history */ + GQueue *notification_history; + guint history_max_items; + gboolean history_enabled; }; typedef struct { @@ -138,6 +153,15 @@ static void _emit_closed_signal(GtkWindow* nw, NotifydClosedReason reason); static void _action_invoked_cb(GtkWindow* nw, const char* key); static NotifyStackLocation get_stack_location_from_string(const gchar *slocation); +static void _init_notification_history(NotifyDaemon* daemon); +static void _cleanup_notification_history(NotifyDaemon* daemon); +static void _add_notification_to_history(NotifyDaemon* daemon, guint id, + const gchar *app_name, const gchar *app_icon, + const gchar *summary, const gchar *body, guint urgency, + NotifydClosedReason reason); +static void _history_item_free(NotificationHistoryItem* item); +static void _trim_notification_history(NotifyDaemon* daemon); + #ifdef HAVE_X11 static GdkFilterReturn _notify_x11_filter(GdkXEvent* xevent, GdkEvent* event, NotifyDaemon* daemon); static void sync_notification_position(NotifyDaemon* daemon, GtkWindow* nw, Window source); @@ -148,6 +172,10 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, static gboolean notify_daemon_close_notification_handler(NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, guint arg_id, gpointer user_data); static gboolean notify_daemon_get_capabilities( NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation); static gboolean notify_daemon_get_server_information (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data); +static gboolean notify_daemon_get_notification_history (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data); +static gboolean notify_daemon_clear_notification_history (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data); +static gboolean notify_daemon_get_notification_count (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data); +static gboolean notify_daemon_mark_all_notifications_as_read (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data); static GParamSpec *properties[LAST_PROP] = { NULL }; @@ -168,6 +196,10 @@ static void bus_acquired_handler_cb (GDBusConnection *connection, g_signal_connect (daemon->skeleton, "handle-close-notification", G_CALLBACK (notify_daemon_close_notification_handler), daemon); g_signal_connect (daemon->skeleton, "handle-get-capabilities", G_CALLBACK (notify_daemon_get_capabilities), daemon); g_signal_connect (daemon->skeleton, "handle-get-server-information", G_CALLBACK (notify_daemon_get_server_information), daemon); + g_signal_connect (daemon->skeleton, "handle-get-notification-history", G_CALLBACK (notify_daemon_get_notification_history), daemon); + g_signal_connect (daemon->skeleton, "handle-clear-notification-history", G_CALLBACK (notify_daemon_clear_notification_history), daemon); + g_signal_connect (daemon->skeleton, "handle-get-notification-count", G_CALLBACK (notify_daemon_get_notification_count), daemon); + g_signal_connect (daemon->skeleton, "handle-mark-all-notifications-as-read", G_CALLBACK (notify_daemon_mark_all_notifications_as_read), daemon); exported = g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (daemon->skeleton), connection, NOTIFICATION_BUS_PATH, &error); @@ -261,6 +293,12 @@ static void _notify_timeout_destroy(NotifyTimeout* nt) */ g_signal_handlers_disconnect_by_func(nt->nw, _notification_destroyed_cb, nt->daemon); gtk_widget_destroy(GTK_WIDGET(nt->nw)); + + g_free(nt->app_name); + g_free(nt->app_icon); + g_free(nt->summary); + g_free(nt->body); + g_free(nt); } @@ -490,6 +528,20 @@ static void on_popup_location_changed(GSettings *settings, gchar *key, NotifyDae } } +static void on_history_enabled_changed(GSettings *settings, gchar *key, NotifyDaemon* daemon) +{ + gboolean history_enabled; + + history_enabled = g_settings_get_boolean(daemon->gsettings, key); + daemon->history_enabled = history_enabled; + + if (!history_enabled && daemon->notification_history) { + /* Wipe the history when disabled to preserve privacy */ + g_queue_free_full(daemon->notification_history, (GDestroyNotify)_history_item_free); + daemon->notification_history = g_queue_new(); + } +} + static void notify_daemon_init(NotifyDaemon* daemon) { gchar *location; @@ -503,6 +555,7 @@ static void notify_daemon_init(NotifyDaemon* daemon) daemon->gsettings = g_settings_new (GSETTINGS_SCHEMA); g_signal_connect (daemon->gsettings, "changed::" GSETTINGS_KEY_POPUP_LOCATION, G_CALLBACK (on_popup_location_changed), daemon); + g_signal_connect (daemon->gsettings, "changed::" GSETTINGS_KEY_HISTORY_ENABLED, G_CALLBACK (on_history_enabled_changed), daemon); location = g_settings_get_string (daemon->gsettings, GSETTINGS_KEY_POPUP_LOCATION); daemon->stack_location = get_stack_location_from_string(location); @@ -510,6 +563,8 @@ static void notify_daemon_init(NotifyDaemon* daemon) daemon->screen = NULL; + _init_notification_history(daemon); + create_screen(daemon); daemon->idle_reposition_notify_ids = g_hash_table_new(NULL, NULL); @@ -582,6 +637,8 @@ static void notify_daemon_finalize(GObject* object) destroy_screen(daemon); + _cleanup_notification_history(daemon); + if (daemon->bus_name_id > 0) { g_bus_unown_name (daemon->bus_name_id); @@ -647,6 +704,12 @@ static void _close_notification(NotifyDaemon* daemon, guint id, gboolean hide_no if (nt != NULL) { + /* Add notification to history right before closing. This way we can access it later (e.g. from the applet) */ + _add_notification_to_history(daemon, nt->id, + nt->app_name, nt->app_icon, + nt->summary, nt->body, nt->urgency, + reason); + _emit_closed_signal(nt->nw, reason); if (hide_notification) @@ -687,6 +750,13 @@ typedef struct { gint id; } IdleRepositionData; +static void idle_reposition_data_destroy(gpointer user_data) +{ + IdleRepositionData* data = (IdleRepositionData*) user_data; + g_object_unref(data->daemon); + g_free(data); +} + static gboolean idle_reposition_notification(IdleRepositionData* data) { NotifyDaemon* daemon; @@ -705,8 +775,6 @@ static gboolean idle_reposition_notification(IdleRepositionData* data) } g_hash_table_remove(daemon->idle_reposition_notify_ids, GINT_TO_POINTER(notify_id)); - g_object_unref(daemon); - g_free(data); return FALSE; } @@ -729,7 +797,7 @@ static void _queue_idle_reposition_notification(NotifyDaemon* daemon, gint notif data->id = notify_id; /* We do this as a short timeout to avoid repositioning spam */ - idle_id = g_timeout_add_full(G_PRIORITY_LOW, 50, (GSourceFunc) idle_reposition_notification, data, NULL); + idle_id = g_timeout_add_full(G_PRIORITY_LOW, 50, (GSourceFunc) idle_reposition_notification, data, idle_reposition_data_destroy); g_hash_table_insert(daemon->idle_reposition_notify_ids, GINT_TO_POINTER(notify_id), GUINT_TO_POINTER(idle_id)); } @@ -833,6 +901,13 @@ static gboolean _is_expired(gpointer key, NotifyTimeout* nt, gboolean* phas_more if (time_span <= 0) { theme_notification_tick(nt->nw, 0); + + /* Add to history before removing (same as in _close_notification, since that function won't be called from here) */ + _add_notification_to_history(nt->daemon, nt->id, + nt->app_name, nt->app_icon, + nt->summary, nt->body, nt->urgency, + NOTIFYD_CLOSED_EXPIRED); + _emit_closed_signal(nt->nw, NOTIFYD_CLOSED_EXPIRED); return TRUE; } @@ -869,9 +944,22 @@ static gboolean _check_expiration(NotifyDaemon* daemon) return has_more_timeouts; } -static void _calculate_timeout(NotifyDaemon* daemon, NotifyTimeout* nt, int timeout) +static void _calculate_timeout(NotifyDaemon* daemon, NotifyTimeout* nt, int timeout, gboolean resident) { - if (timeout == 0) + GSettings *gsettings = g_settings_new ("org.mate.NotificationDaemon"); + gboolean persistence_enabled = g_settings_get_boolean (gsettings, "enable-persistence"); + g_object_unref (gsettings); + + nt->resident = resident && persistence_enabled; + + if (timeout == -1) + { + GSettings *gsettings = g_settings_new ("org.mate.NotificationDaemon"); + timeout = g_settings_get_int (gsettings, "default-timeout"); + g_object_unref (gsettings); + } + + if (timeout == 0 || nt->resident) { nt->has_timeout = FALSE; } @@ -881,11 +969,6 @@ static void _calculate_timeout(NotifyDaemon* daemon, NotifyTimeout* nt, int time nt->has_timeout = TRUE; - if (timeout == -1) - { - timeout = NOTIFY_DAEMON_DEFAULT_TIMEOUT; - } - theme_set_notification_timeout(nt->nw, timeout); usec = (gint64) timeout * G_TIME_SPAN_MILLISECOND; /* convert from msec to usec */ @@ -925,7 +1008,10 @@ static guint _generate_id(NotifyDaemon* daemon) return id; } -static NotifyTimeout* _store_notification(NotifyDaemon* daemon, GtkWindow* nw, int timeout) +static NotifyTimeout* _store_notification(NotifyDaemon* daemon, GtkWindow* nw, + int timeout, gboolean resident, + const gchar *app_name, const gchar *app_icon, + const gchar *summary, const gchar *body, guint urgency) { NotifyTimeout* nt; guint id = _generate_id(daemon); @@ -935,7 +1021,16 @@ static NotifyTimeout* _store_notification(NotifyDaemon* daemon, GtkWindow* nw, i nt->nw = nw; nt->daemon = daemon; - _calculate_timeout(daemon, nt, timeout); + /* Store notification data for history */ + nt->app_name = g_strdup(app_name ? app_name : ""); + nt->app_icon = g_strdup(app_icon ? app_icon : ""); + nt->summary = g_strdup(summary ? summary : ""); + nt->body = g_strdup(body ? body : ""); + nt->urgency = urgency; + nt->timestamp = g_get_real_time(); + nt->close_reason = NOTIFYD_CLOSED_EXPIRED; /* Default to expired, since we don't care about active notifications */ + + _calculate_timeout(daemon, nt, timeout, resident); #if GLIB_CHECK_VERSION (2, 68, 0) g_hash_table_insert(daemon->notification_hash, g_memdup2(&id, sizeof(guint)), nt); @@ -1183,22 +1278,29 @@ static gboolean screensaver_active(GtkWidget* nw) #ifdef HAVE_X11 static gboolean fullscreen_window_exists(GtkWidget* nw) { + WnckHandle* wnck_handle; WnckScreen* wnck_screen; WnckWorkspace* wnck_workspace; GList* l; + gboolean retval = FALSE; g_return_val_if_fail (GDK_IS_X11_DISPLAY (gdk_display_get_default ()), FALSE); - wnck_screen = wnck_screen_get (GDK_SCREEN_XNUMBER (gdk_window_get_screen (gtk_widget_get_window (nw)))); + wnck_handle = wnck_handle_new (WNCK_CLIENT_TYPE_APPLICATION); + + g_return_val_if_fail (WNCK_IS_HANDLE (wnck_handle), FALSE); + + wnck_screen = wnck_handle_get_screen (wnck_handle, GDK_SCREEN_XNUMBER (gdk_window_get_screen (gtk_widget_get_window (nw)))); + + if (!wnck_screen) + goto cleanup; wnck_screen_force_update (wnck_screen); wnck_workspace = wnck_screen_get_active_workspace (wnck_screen); if (!wnck_workspace) - { - return FALSE; - } + goto cleanup; for (l = wnck_screen_get_windows_stacked (wnck_screen); l != NULL; l = l->next) { @@ -1219,12 +1321,15 @@ static gboolean fullscreen_window_exists(GtkWidget* nw) if (sw == w && sh == h) { - return TRUE; + retval = TRUE; + goto cleanup; } } } - return FALSE; +cleanup: + g_clear_object (&wnck_handle); + return retval; } static Window get_window_parent(GdkDisplay* display, Window window, Window* root) @@ -1363,6 +1468,12 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, GdkPixbuf* pixbuf; GSettings* gsettings; gboolean fullscreen_window; + gboolean resident = FALSE; + gboolean transient = FALSE; + const gchar *desktop_entry = NULL; + const gchar *category = NULL; + gchar *resolved_icon = NULL; /* Store resolved icon name in history */ + guint urgency = URGENCY_NORMAL; /* Default to normal urgency */ #ifdef HAVE_X11 Window window_xid = None; @@ -1371,7 +1482,7 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, if (g_hash_table_size (daemon->notification_hash) > MAX_NOTIFICATIONS) { g_dbus_method_invocation_return_error (invocation, notify_daemon_error_quark(), 1, _("Exceeded maximum number of notifications")); - return FALSE; + return TRUE; } /* Grab the settings */ @@ -1380,14 +1491,6 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, do_not_disturb = g_settings_get_boolean (gsettings, GSETTINGS_KEY_DO_NOT_DISTURB); g_object_unref (gsettings); - /* If we are in do-not-disturb mode, just grab a new id and close the notification */ - if (do_not_disturb) - { - return_id = _generate_id (daemon); - notify_daemon_notifications_complete_notify (object, invocation, return_id); - return TRUE; - } - if (id > 0) { nt = (NotifyTimeout *) g_hash_table_lookup (daemon->notification_hash, &id); @@ -1472,6 +1575,27 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, } } + /* process resident and transient hints for persistence */ + g_variant_lookup(hints, "resident", "b", &resident); + g_variant_lookup(hints, "transient", "b", &transient); + + g_variant_lookup(hints, "desktop-entry", "s", &desktop_entry); + // TODO: Use 'category' for future category-based notification settings + g_variant_lookup(hints, "category", "s", &category); + + /* Get urgency from hints */ + if (g_variant_lookup(hints, "urgency", "@*", &data)) + { + if (g_variant_is_of_type (data, G_VARIANT_TYPE_BYTE)) + { + urgency = g_variant_get_byte(data); + /* Make sure it's in a valid range */ + if (urgency > URGENCY_CRITICAL) + urgency = URGENCY_CRITICAL; + } + g_variant_unref(data); + } + /* set up action buttons */ for (i = 0; actions[i] != NULL; i += 2) { @@ -1490,31 +1614,76 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, pixbuf = NULL; - if (g_variant_lookup(hints, "image_data", "@(iiibiiay)", &data)) + /* According to the Desktop Notifications Specification 1.3 spec, icons have the following precedence: + * 1. image-data (or image_data) + * 2. image-path (or image_path) + * 3. icon (which falls back to the desktop entry) + * 4. icon_data (for backward compatibility) + */ + if (g_variant_lookup(hints, "image-data", "@(iiibiiay)", &data)) { pixbuf = _notify_daemon_pixbuf_from_data_hint (data); g_variant_unref(data); } - else if (g_variant_lookup(hints, "image-data", "@(iiibiiay)", &data)) + else if (g_variant_lookup(hints, "image_data", "@(iiibiiay)", &data)) { pixbuf = _notify_daemon_pixbuf_from_data_hint (data); g_variant_unref(data); } - else if (g_variant_lookup(hints, "image_path", "@s", &data)) + else if (g_variant_lookup(hints, "image-path", "@s", &data)) { const char *path = g_variant_get_string (data, NULL); pixbuf = _notify_daemon_pixbuf_from_path (path); + resolved_icon = g_strdup(path); g_variant_unref(data); } - else if (g_variant_lookup(hints, "image-path", "@s", &data)) + else if (g_variant_lookup(hints, "image_path", "@s", &data)) { const char *path = g_variant_get_string (data, NULL); pixbuf = _notify_daemon_pixbuf_from_path (path); + resolved_icon = g_strdup(path); g_variant_unref(data); } else if (*icon != '\0') { pixbuf = _notify_daemon_pixbuf_from_path (icon); + resolved_icon = g_strdup(icon); + } + else if (desktop_entry != NULL && *desktop_entry != '\0') + { + /* Use desktop-entry to resolve application icon as fallback */ + gchar *desktop_file = g_strdup_printf("%s.desktop", desktop_entry); + gchar *icon_name = NULL; + + /* Try to get icon from desktop file */ + GKeyFile *key_file = g_key_file_new(); + const gchar * const *search_dirs = g_get_system_data_dirs(); + + for (const gchar * const *dir = search_dirs; *dir != NULL; dir++) + { + gchar *desktop_path = g_build_filename(*dir, "applications", desktop_file, NULL); + if (g_key_file_load_from_file(key_file, desktop_path, G_KEY_FILE_NONE, NULL)) + { + icon_name = g_key_file_get_string(key_file, "Desktop Entry", "Icon", NULL); + g_free(desktop_path); + break; + } + g_free(desktop_path); + } + + if (icon_name != NULL) + { + pixbuf = _notify_daemon_pixbuf_from_path (icon_name); + + if (!resolved_icon && (*icon == '\0' || icon == NULL)) { + resolved_icon = g_strdup(icon_name); + } + + g_free(icon_name); + } + + g_key_file_free(key_file); + g_free(desktop_file); } else if (g_variant_lookup(hints, "icon_data", "@(iiibiiay)", &data)) { @@ -1592,19 +1761,32 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, g_settings_get_int(daemon->gsettings, GSETTINGS_KEY_MONITOR_NUMBER)); } - if (_gtk_get_monitor_num (monitor_id) >= daemon->screen->n_stacks) + /* If the monitor was disconnected or invalid, fall back to the last available monitor */ + if (monitor_id == NULL && daemon->screen->n_stacks > 0) + { + monitor_id = gdk_display_get_monitor (gdk_display_get_default(), (int) daemon->screen->n_stacks - 1); + } + + if (monitor_id != NULL && _gtk_get_monitor_num (monitor_id) >= daemon->screen->n_stacks) { /* screw it - dump it on the last one we'll get a monitors-changed signal soon enough*/ monitor_id = gdk_display_get_monitor (gdk_display_get_default(), (int) daemon->screen->n_stacks - 1); } - notify_stack_add_window (daemon->screen->stacks[_gtk_get_monitor_num (monitor_id)], nw, new_notification); + /* If we still don't have a valid monitor, something is seriously wrong */ + if (monitor_id != NULL && daemon->screen->n_stacks > 0) + { + notify_stack_add_window (daemon->screen->stacks[_gtk_get_monitor_num (monitor_id)], nw, new_notification); + } } if (id == 0) { - nt = _store_notification (daemon, nw, timeout); + nt = _store_notification (daemon, nw, + timeout, resident && !transient, + app_name, resolved_icon ? resolved_icon : icon, + summary, body, urgency); return_id = nt->id; } else @@ -1639,11 +1821,14 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, */ if (!nt->has_timeout || (!screensaver_active (GTK_WIDGET (nw)) && !fullscreen_window)) { - theme_show_notification (nw); - - if (sound_file != NULL) + if (!do_not_disturb) { - sound_play_file (GTK_WIDGET (nw), sound_file); + theme_show_notification (nw); + + if (sound_file != NULL) + { + sound_play_file (GTK_WIDGET (nw), sound_file); + } } } else @@ -1664,7 +1849,11 @@ static gboolean notify_daemon_notify_handler(NotifyDaemonNotifications *object, if (nt) { - _calculate_timeout (daemon, nt, timeout); + _calculate_timeout (daemon, nt, timeout, resident && !transient); + } + + if (resolved_icon) { + g_free(resolved_icon); } notify_daemon_notifications_complete_notify ( object, invocation, return_id); @@ -1676,7 +1865,7 @@ static gboolean notify_daemon_close_notification_handler(NotifyDaemonNotificatio if (id == 0) { g_dbus_method_invocation_return_error (invocation, notify_daemon_error_quark(), 100, _("%u is not a valid notification ID"), id); - return FALSE; + return TRUE; } else { @@ -1701,6 +1890,7 @@ static gboolean notify_daemon_get_capabilities( NotifyDaemonNotifications *objec g_variant_builder_add (builder, "s", "body-markup"); g_variant_builder_add (builder, "s", "icon-static"); g_variant_builder_add (builder, "s", "sound"); + g_variant_builder_add (builder, "s", "persistence"); value = g_variant_new ("as", builder); g_variant_builder_unref (builder); notify_daemon_notifications_complete_get_capabilities ( @@ -1718,10 +1908,160 @@ static gboolean notify_daemon_get_server_information (NotifyDaemonNotifications "Notification Daemon", "MATE", PACKAGE_VERSION, - "1.1"); + "1.3"); return TRUE; } +static gboolean notify_daemon_get_notification_history (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data) +{ + NotifyDaemon *daemon = NOTIFY_DAEMON (user_data); + GVariantBuilder *builder; + GList *items; + + if (!daemon->notification_history || !daemon->history_enabled) { + /* Return empty array if history is disabled or not initialized */ + builder = g_variant_builder_new (G_VARIANT_TYPE ("a(ussssxxuub)")); + notify_daemon_notifications_complete_get_notification_history (object, invocation, g_variant_new ("a(ussssxxuub)", builder)); + g_variant_builder_unref (builder); + return TRUE; + } + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a(ussssxxuub)")); + + for (items = g_queue_peek_head_link(daemon->notification_history); items != NULL; items = items->next) { + NotificationHistoryItem *item = (NotificationHistoryItem *) items->data; + + /* Mark notification as read when reading through history */ + item->read = TRUE; + + g_variant_builder_add (builder, "(ussssxxuub)", + item->id, + item->app_name, + item->app_icon, + item->summary, + item->body, + item->timestamp, + item->closed_timestamp, + (guint) item->reason, + item->urgency, + item->read); + } + + notify_daemon_notifications_complete_get_notification_history (object, invocation, g_variant_new ("a(ussssxxuub)", builder)); + g_variant_builder_unref (builder); + return TRUE; +} + +static gboolean notify_daemon_clear_notification_history (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data) +{ + NotifyDaemon *daemon = NOTIFY_DAEMON (user_data); + + if (daemon->notification_history) { + g_queue_free_full(daemon->notification_history, (GDestroyNotify)_history_item_free); + daemon->notification_history = g_queue_new(); + } + + notify_daemon_notifications_complete_clear_notification_history (object, invocation); + return TRUE; +} + +static gboolean notify_daemon_mark_all_notifications_as_read (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data) +{ + NotifyDaemon *daemon = NOTIFY_DAEMON (user_data); + GList *items; + + if (daemon->notification_history && daemon->history_enabled) { + for (items = g_queue_peek_head_link(daemon->notification_history); items != NULL; items = items->next) { + NotificationHistoryItem *item = (NotificationHistoryItem *) items->data; + item->read = TRUE; + } + } + + notify_daemon_notifications_complete_mark_all_notifications_as_read (object, invocation); + return TRUE; +} + +static gboolean notify_daemon_get_notification_count (NotifyDaemonNotifications *object, GDBusMethodInvocation *invocation, gpointer user_data) +{ + NotifyDaemon *daemon = NOTIFY_DAEMON (user_data); + guint count = 0; + GList *items; + + if (daemon->notification_history && daemon->history_enabled) { + /* Count unread notifications */ + for (items = g_queue_peek_head_link(daemon->notification_history); items != NULL; items = items->next) { + NotificationHistoryItem *item = (NotificationHistoryItem *) items->data; + if (!item->read) { + count++; + } + } + } + + notify_daemon_notifications_complete_get_notification_count (object, invocation, count); + return TRUE; +} + +static void _history_item_free(NotificationHistoryItem* item) +{ + if (item) { + g_free(item->app_name); + g_free(item->app_icon); + g_free(item->summary); + g_free(item->body); + g_free(item); + } +} + +static void _init_notification_history(NotifyDaemon* daemon) +{ + daemon->notification_history = g_queue_new(); + daemon->history_max_items = 100; /* Default max items */ + daemon->history_enabled = g_settings_get_boolean(daemon->gsettings, GSETTINGS_KEY_HISTORY_ENABLED); +} + +static void _cleanup_notification_history(NotifyDaemon* daemon) +{ + if (daemon->notification_history) { + g_queue_free_full(daemon->notification_history, (GDestroyNotify)_history_item_free); + daemon->notification_history = NULL; + } +} + +static void _trim_notification_history(NotifyDaemon* daemon) +{ + if (!daemon->notification_history) + return; + + while (g_queue_get_length(daemon->notification_history) > daemon->history_max_items) { + NotificationHistoryItem* item = g_queue_pop_tail(daemon->notification_history); + _history_item_free(item); + } +} + +static void _add_notification_to_history(NotifyDaemon* daemon, guint id, + const gchar *app_name, const gchar *app_icon, + const gchar *summary, const gchar *body, guint urgency, + NotifydClosedReason reason) +{ + if (!daemon->history_enabled || !daemon->notification_history) + return; + + NotificationHistoryItem* item = g_new0(NotificationHistoryItem, 1); + item->id = id; + item->app_name = g_strdup(app_name ? app_name : ""); + item->app_icon = g_strdup(app_icon ? app_icon : ""); + item->summary = g_strdup(summary ? summary : ""); + item->body = g_strdup(body ? body : ""); + item->timestamp = g_get_real_time(); + item->closed_timestamp = g_get_real_time(); + item->reason = reason; + item->urgency = urgency; + item->read = FALSE; /* Mark as unread initially */ + + g_queue_push_head(daemon->notification_history, item); + _trim_notification_history(daemon); +} + NotifyDaemon* notify_daemon_new (gboolean replace, gboolean idle_exit) { return g_object_new (NOTIFY_TYPE_DAEMON, |
