/* mate-notification-applet.c - MATE Notification Applet - History * * Copyright (C) 2025 MATE Developers * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "config.h" #include #include #include #include "mate-notification-applet-history.h" #include "../common/constants.h" #define IMAGE_SIZE 48 static GtkWidget * create_notification_icon (const gchar *icon_name) { GdkPixbuf *pixbuf = NULL; GtkWidget *image; if (icon_name && *icon_name) { if (g_path_is_absolute (icon_name)) { pixbuf = gdk_pixbuf_new_from_file_at_scale (icon_name, IMAGE_SIZE, IMAGE_SIZE, TRUE, NULL); } else { GtkIconTheme *theme = gtk_icon_theme_get_default (); pixbuf = gtk_icon_theme_load_icon (theme, icon_name, IMAGE_SIZE, GTK_ICON_LOOKUP_USE_BUILTIN, NULL); } } if (pixbuf) { image = gtk_image_new_from_pixbuf (pixbuf); g_object_unref (pixbuf); } else { image = gtk_image_new_from_icon_name ("mate-notification-properties", GTK_ICON_SIZE_DIALOG); } gtk_widget_set_valign (image, GTK_ALIGN_CENTER); return image; } static GtkWidget * create_popup_window (void) { GtkWidget *popup = gtk_window_new (GTK_WINDOW_POPUP); g_object_set (popup, "type-hint", GDK_WINDOW_TYPE_HINT_POPUP_MENU, "skip-taskbar-hint", TRUE, "skip-pager-hint", TRUE, "decorated", FALSE, "resizable", FALSE, NULL); gtk_container_set_border_width (GTK_CONTAINER (popup), 1); return popup; } static void popup_destroyed_cb (GtkWidget *popup, MateNotificationHistoryContext *context) { (void) popup; context->history_popup = NULL; } static GtkWidget * create_notification_row (guint id, const gchar *app_name, const gchar *app_icon, const gchar *summary, const gchar *body, gint64 timestamp) { GtkWidget *row, *hbox, *icon_image, *content_box; GtkWidget *title_label, *body_label, *time_label; GDateTime *dt; gchar *time_str, *markup; /* Format timestamp */ dt = g_date_time_new_from_unix_local (timestamp / G_TIME_SPAN_SECOND); time_str = g_date_time_format (dt, "%H:%M"); g_date_time_unref (dt); /* Create row container for the entire notification */ row = gtk_list_box_row_new (); hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); gtk_container_set_border_width (GTK_CONTAINER (hbox), 6); icon_image = create_notification_icon (app_icon); content_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 3); /* Title */ markup = g_markup_printf_escaped ("%s", summary ? summary : _("(No summary)")); title_label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (title_label), markup); gtk_widget_set_halign (title_label, GTK_ALIGN_START); gtk_label_set_ellipsize (GTK_LABEL (title_label), PANGO_ELLIPSIZE_END); gtk_label_set_max_width_chars (GTK_LABEL (title_label), 40); g_free (markup); /* Body */ body_label = NULL; if (body && *body) { body_label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (body_label), body); gtk_widget_set_halign (body_label, GTK_ALIGN_START); gtk_label_set_ellipsize (GTK_LABEL (body_label), PANGO_ELLIPSIZE_END); gtk_label_set_max_width_chars (GTK_LABEL (body_label), 40); /* Set tooltip in case the body overflows */ gchar *tooltip_text = g_strdup_printf ("%s\n%s", summary, body); gtk_widget_set_tooltip_text (row, tooltip_text); g_free (tooltip_text); } /* Time and app label */ markup = g_markup_printf_escaped ("%s - %s", app_name ? app_name : _("Unknown"), time_str); time_label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (time_label), markup); gtk_widget_set_halign (time_label, GTK_ALIGN_START); g_free (markup); g_free (time_str); /* Pack content box */ gtk_box_pack_start (GTK_BOX (content_box), title_label, FALSE, FALSE, 0); if (body_label) gtk_box_pack_start (GTK_BOX (content_box), body_label, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (content_box), time_label, FALSE, FALSE, 0); /* Pack horizontal box */ gtk_box_pack_start (GTK_BOX (hbox), icon_image, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (hbox), content_box, TRUE, TRUE, 0); gtk_container_add (GTK_CONTAINER (row), hbox); return row; } static void clear_history (GtkWidget *button, MateNotificationHistoryContext *context) { (void) button; /* Clear the notification history */ if (dbus_context_clear_notification_history (context->dbus_context)) { /* Trigger count update after clearing */ if (context->count_update_callback) { ((void (*)(gpointer)) context->count_update_callback) (context->count_update_user_data); } } /* Close the popup */ if (context->history_popup) { gtk_widget_destroy (context->history_popup); } } static void dnd_toggle_clicked (GtkToggleButton *toggle, MateNotificationHistoryContext *context) { gboolean active = gtk_toggle_button_get_active (toggle); if (context->settings) { g_settings_set_boolean (context->settings, "do-not-disturb", active); } } MateNotificationHistoryContext * history_context_new (MateNotificationDBusContext *dbus_context, GtkWidget *main_widget, GCallback count_update_callback, gpointer count_update_user_data, GSettings *settings) { MateNotificationHistoryContext *context = g_new0 (MateNotificationHistoryContext, 1); context->dbus_context = dbus_context; context->main_widget = main_widget; context->history_popup = NULL; context->count_update_callback = count_update_callback; context->count_update_user_data = count_update_user_data; context->settings = settings; return context; } void history_context_free (MateNotificationHistoryContext *context) { if (context) { if (context->history_popup) { gtk_widget_destroy (context->history_popup); } g_free (context); } } void history_context_update_dbus (MateNotificationHistoryContext *context, MateNotificationDBusContext *dbus_context) { if (context) { context->dbus_context = dbus_context; } } void show_notification_history (MateNotificationHistoryContext *context) { GtkWidget *popup; GtkWidget *vbox; GtkWidget *scrolled_window; GtkWidget *list_box; GtkWidget *button_box; GtkWidget *dnd_toggle; GtkWidget *clear_button; GtkWidget *close_button; GVariant *result; GVariantIter *iter; guint id, urgency; gchar *app_name, *app_icon, *summary, *body; gint64 timestamp, closed_timestamp; guint reason; gboolean read; gint x, y; GdkWindow *window; if (!dbus_context_is_available (context->dbus_context)) { g_warning ("Cannot show history: daemon not available"); return; } /* Check if history is enabled */ if (context->settings && !g_settings_get_boolean (context->settings, GSETTINGS_KEY_HISTORY_ENABLED)) { g_warning ("Cannot show history: history is disabled for privacy"); return; } /* If popup already exists, destroy it (basically toggle off) */ if (context->history_popup) { gtk_widget_destroy (context->history_popup); context->history_popup = NULL; return; } /* Get notification history from daemon */ result = dbus_context_get_notification_history (context->dbus_context); if (!result) { g_warning ("Failed to get notification history"); return; } /* Trigger count update since accessing history marks all as read */ if (context->count_update_callback) { ((void (*)(gpointer)) context->count_update_callback) (context->count_update_user_data); } popup = create_popup_window (); context->history_popup = popup; g_signal_connect (popup, "destroy", G_CALLBACK (popup_destroyed_cb), context); /* Create main container */ vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_container_add (GTK_CONTAINER (popup), vbox); /* Create list box for all notifications */ list_box = gtk_list_box_new (); gtk_list_box_set_selection_mode (GTK_LIST_BOX (list_box), GTK_SELECTION_NONE); /* Get history data and add to list */ gint notification_count = 0; if (result) { g_variant_get (result, "(a(ussssxxuub))", &iter); while (g_variant_iter_loop (iter, "(ussssxxuub)", &id, &app_name, &app_icon, &summary, &body, ×tamp, &closed_timestamp, &reason, &urgency, &read)) { notification_count++; /* Add each notification as a new row in the list */ GtkWidget *row = create_notification_row (id, app_name, app_icon, summary, body, timestamp); gtk_list_box_insert (GTK_LIST_BOX (list_box), row, -1); } g_variant_iter_free (iter); g_variant_unref (result); } /* Add message if list is empty */ if (gtk_list_box_get_row_at_index (GTK_LIST_BOX (list_box), 0) == NULL) { GtkWidget *row = gtk_list_box_row_new (); GtkWidget *label = gtk_label_new (_("No notifications")); gtk_widget_set_sensitive (label, FALSE); gtk_container_set_border_width (GTK_CONTAINER (row), 12); gtk_container_add (GTK_CONTAINER (row), label); gtk_list_box_insert (GTK_LIST_BOX (list_box), row, -1); } /* Add this window for the list of notifications */ scrolled_window = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); /* Calculate to ensure the popup is the right height: * 80% of screen height, with some room for buttons */ GdkScreen *screen = gtk_widget_get_screen (context->main_widget); gint max_content_height = (gdk_screen_get_height (screen) * 0.8) - 100; gint content_height = 100; /* 100px minimum */ if (notification_count > 0) content_height = MIN(notification_count * content_height, max_content_height); /* Now force the scrollable window to be the calculated height */ gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (scrolled_window), max_content_height); gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scrolled_window), content_height); gtk_container_add (GTK_CONTAINER (scrolled_window), list_box); gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 0); /* Add a box for action buttons */ button_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); gtk_container_set_border_width (GTK_CONTAINER (button_box), 6); /* DND toggle first */ dnd_toggle = gtk_check_button_new_with_label (_("Do not disturb")); gtk_widget_set_halign (dnd_toggle, GTK_ALIGN_START); if (context->settings) { gboolean dnd_enabled = g_settings_get_boolean (context->settings, "do-not-disturb"); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (dnd_toggle), dnd_enabled); } g_signal_connect (dnd_toggle, "toggled", G_CALLBACK (dnd_toggle_clicked), context); gtk_box_pack_start (GTK_BOX (button_box), dnd_toggle, FALSE, FALSE, 0); /* Spacer to push buttons to the right */ GtkWidget *spacer = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start (GTK_BOX (button_box), spacer, TRUE, TRUE, 0); /* Then the action buttons */ clear_button = gtk_button_new_with_label (_("Clear All")); close_button = gtk_button_new_with_label (_("Close")); g_signal_connect (clear_button, "clicked", G_CALLBACK (clear_history), context); g_signal_connect_swapped (close_button, "clicked", G_CALLBACK (gtk_widget_destroy), popup); gtk_box_pack_end (GTK_BOX (button_box), close_button, FALSE, FALSE, 0); gtk_box_pack_end (GTK_BOX (button_box), clear_button, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (vbox), button_box, FALSE, FALSE, 0); /* Place popup window below the applet */ window = gtk_widget_get_window (context->main_widget); if (window) { gdk_window_get_origin (window, &x, &y); gtk_window_move (GTK_WINDOW (popup), x, y + gtk_widget_get_allocated_height (context->main_widget)); } /* Set popup size based on content (with space for buttons) */ gtk_window_set_default_size (GTK_WINDOW (popup), 450, content_height + 100); gtk_widget_show_all (popup); }