summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/capplet/Makefile.am10
-rw-r--r--src/capplet/mate-notification-applet-dbus.c214
-rw-r--r--src/capplet/mate-notification-applet-dbus.h52
-rw-r--r--src/capplet/mate-notification-applet-history.c421
-rw-r--r--src/capplet/mate-notification-applet-history.h59
-rw-r--r--src/capplet/mate-notification-applet-menu.xml6
-rw-r--r--src/capplet/mate-notification-applet.c378
-rw-r--r--src/capplet/mate-notification-properties.c79
-rw-r--r--src/capplet/mate-notification-properties.ui136
-rw-r--r--src/common/constants.h4
-rw-r--r--src/daemon/daemon.c422
-rw-r--r--src/daemon/daemon.h15
-rw-r--r--src/daemon/notificationdaemon.xml14
-rw-r--r--src/themes/coco/coco-theme.c124
-rw-r--r--src/themes/nodoka/nodoka-theme.c135
-rw-r--r--src/themes/slider/theme.c93
-rw-r--r--src/themes/standard/theme.c78
17 files changed, 1979 insertions, 261 deletions
diff --git a/src/capplet/Makefile.am b/src/capplet/Makefile.am
index af7569a..0017ab6 100644
--- a/src/capplet/Makefile.am
+++ b/src/capplet/Makefile.am
@@ -45,6 +45,10 @@ libmate_notification_applet_la_SOURCES = \
$(mate_notification_applet_resources_files) \
../common/constants.h \
mate-notification-applet.c \
+ mate-notification-applet-dbus.h \
+ mate-notification-applet-dbus.c \
+ mate-notification-applet-history.h \
+ mate-notification-applet-history.c \
$(NULL)
libmate_notification_applet_la_CFLAGS = \
@@ -64,6 +68,10 @@ mate_notification_applet_SOURCES = \
$(mate_notification_applet_resources_files) \
../common/constants.h \
mate-notification-applet.c \
+ mate-notification-applet-dbus.h \
+ mate-notification-applet-dbus.c \
+ mate-notification-applet-history.h \
+ mate-notification-applet-history.c \
$(NULL)
mate_notification_applet_CFLAGS = \
@@ -104,6 +112,8 @@ EXTRA_DIST = \
$(mate_notification_properties_resources_xml) \
mate-notification-applet-menu.xml \
mate-notification-properties.ui \
+ mate-notification-applet-dbus.h \
+ mate-notification-applet-history.h \
$(NULL)
-include $(top_srcdir)/git.mk
diff --git a/src/capplet/mate-notification-applet-dbus.c b/src/capplet/mate-notification-applet-dbus.c
new file mode 100644
index 0000000..68ed6f3
--- /dev/null
+++ b/src/capplet/mate-notification-applet-dbus.c
@@ -0,0 +1,214 @@
+/* mate-notification-applet-dbus.c - MATE Notification Applet - D-Bus Context
+ *
+ * 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 <glib.h>
+#include <gio/gio.h>
+
+#include "mate-notification-applet-dbus.h"
+#include "../common/constants.h"
+
+MateNotificationDBusContext *
+dbus_context_new (void)
+{
+ MateNotificationDBusContext *context = g_new0 (MateNotificationDBusContext, 1);
+ context->daemon_proxy = NULL;
+ context->daemon_available = FALSE;
+ context->watch_id = 0;
+ return context;
+}
+
+void
+dbus_context_free (MateNotificationDBusContext *context)
+{
+ if (context) {
+ dbus_context_disconnect (context);
+ g_free (context);
+ }
+}
+
+gboolean
+dbus_context_connect (MateNotificationDBusContext *context)
+{
+ GError *error = NULL;
+
+ if (!context)
+ return FALSE;
+
+ /* Clean up existing connection */
+ dbus_context_disconnect (context);
+
+ /* Create D-Bus proxy for notification daemon */
+ context->daemon_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL, /* GDBusInterfaceInfo */
+ NOTIFICATION_BUS_NAME,
+ NOTIFICATION_BUS_PATH,
+ "org.freedesktop.Notifications",
+ NULL, /* GCancellable */
+ &error);
+
+ if (error) {
+ g_warning ("Failed to connect to notification daemon: %s", error->message);
+ g_error_free (error);
+ context->daemon_available = FALSE;
+ return FALSE;
+ }
+
+ context->daemon_available = TRUE;
+ return TRUE;
+}
+
+void
+dbus_context_disconnect (MateNotificationDBusContext *context)
+{
+ if (!context)
+ return;
+
+ if (context->daemon_proxy) {
+ g_object_unref (context->daemon_proxy);
+ context->daemon_proxy = NULL;
+ }
+
+ context->daemon_available = FALSE;
+}
+
+gboolean
+dbus_context_is_available (MateNotificationDBusContext *context)
+{
+ return context && context->daemon_available && context->daemon_proxy;
+}
+
+guint
+dbus_context_get_notification_count (MateNotificationDBusContext *context)
+{
+ GVariant *result;
+ GError *error = NULL;
+ guint count = 0;
+
+ if (!dbus_context_is_available (context))
+ return 0;
+
+ result = g_dbus_proxy_call_sync (context->daemon_proxy,
+ "GetNotificationCount",
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, /* timeout */
+ NULL, /* GCancellable */
+ &error);
+
+ if (error) {
+ g_warning ("Failed to get notification count: %s", error->message);
+ g_error_free (error);
+ context->daemon_available = FALSE;
+ } else if (result) {
+ g_variant_get (result, "(u)", &count);
+ g_variant_unref (result);
+ }
+
+ return count;
+}
+
+gboolean
+dbus_context_clear_notification_history (MateNotificationDBusContext *context)
+{
+ GVariant *result;
+ GError *error = NULL;
+
+ if (!dbus_context_is_available (context))
+ return FALSE;
+
+ result = g_dbus_proxy_call_sync (context->daemon_proxy,
+ "ClearNotificationHistory",
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, /* timeout */
+ NULL, /* GCancellable */
+ &error);
+
+ if (error) {
+ g_warning ("Failed to clear notification history: %s", error->message);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ if (result) {
+ g_variant_unref (result);
+ }
+
+ return TRUE;
+}
+
+gboolean
+dbus_context_mark_all_notifications_as_read (MateNotificationDBusContext *context)
+{
+ GVariant *result;
+ GError *error = NULL;
+
+ if (!dbus_context_is_available (context))
+ return FALSE;
+
+ result = g_dbus_proxy_call_sync (context->daemon_proxy,
+ "MarkAllNotificationsAsRead",
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, /* timeout */
+ NULL, /* GCancellable */
+ &error);
+
+ if (error) {
+ g_warning ("Failed to mark all notifications as read: %s", error->message);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ if (result) {
+ g_variant_unref (result);
+ }
+
+ return TRUE;
+}
+
+GVariant *
+dbus_context_get_notification_history (MateNotificationDBusContext *context)
+{
+ GVariant *result;
+ GError *error = NULL;
+
+ if (!dbus_context_is_available (context))
+ return NULL;
+
+ result = g_dbus_proxy_call_sync (context->daemon_proxy,
+ "GetNotificationHistory",
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, /* timeout */
+ NULL, /* GCancellable */
+ &error);
+
+ if (error) {
+ g_warning ("Failed to get notification history: %s", error->message);
+ g_error_free (error);
+ return NULL;
+ }
+
+ return result; /* Caller owns this reference */
+}
diff --git a/src/capplet/mate-notification-applet-dbus.h b/src/capplet/mate-notification-applet-dbus.h
new file mode 100644
index 0000000..7f95bc3
--- /dev/null
+++ b/src/capplet/mate-notification-applet-dbus.h
@@ -0,0 +1,52 @@
+/* mate-notification-applet-dbus.h - MATE Notification Applet - D-Bus Context
+ *
+ * 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
+ */
+
+#ifndef __MATE_NOTIFICATION_APPLET_DBUS_H__
+#define __MATE_NOTIFICATION_APPLET_DBUS_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+/* D-Bus context structure for notification daemon communication */
+typedef struct {
+ GDBusProxy *daemon_proxy;
+ gboolean daemon_available;
+ guint watch_id;
+} MateNotificationDBusContext;
+
+/* Context creation/cleanup */
+MateNotificationDBusContext *dbus_context_new (void);
+void dbus_context_free (MateNotificationDBusContext *context);
+
+/* Connection management */
+gboolean dbus_context_connect (MateNotificationDBusContext *context);
+void dbus_context_disconnect (MateNotificationDBusContext *context);
+gboolean dbus_context_is_available (MateNotificationDBusContext *context);
+
+/* Notification daemon method calls */
+guint dbus_context_get_notification_count (MateNotificationDBusContext *context);
+gboolean dbus_context_clear_notification_history (MateNotificationDBusContext *context);
+gboolean dbus_context_mark_all_notifications_as_read (MateNotificationDBusContext *context);
+GVariant *dbus_context_get_notification_history (MateNotificationDBusContext *context);
+
+G_END_DECLS
+
+#endif /* __MATE_NOTIFICATION_APPLET_DBUS_H__ */
diff --git a/src/capplet/mate-notification-applet-history.c b/src/capplet/mate-notification-applet-history.c
new file mode 100644
index 0000000..4d762da
--- /dev/null
+++ b/src/capplet/mate-notification-applet-history.c
@@ -0,0 +1,421 @@
+/* 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 <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+
+#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 ("<b>%s</b>", 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 ("<small>%s - %s</small>",
+ 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;
+ }
+}
+
+static void
+position_popup_window (GtkWidget *popup,
+ GtkWidget *reference_widget,
+ GdkScreen *screen,
+ gint popup_width,
+ gint popup_height)
+{
+ gint x, y;
+ GdkWindow *window;
+
+ window = gtk_widget_get_window (reference_widget);
+ if (!window) {
+ return;
+ }
+
+ gdk_window_get_origin (window, &x, &y);
+
+ /* Calculate popup dimensions */
+ gint applet_height = gtk_widget_get_allocated_height (reference_widget);
+
+ /* Get screen dimensions */
+ gint screen_width = gdk_screen_get_width (screen);
+ gint screen_height = gdk_screen_get_height (screen);
+
+ /* Calculate initial position below applet */
+ gint popup_x = x;
+ gint popup_y = y + applet_height;
+
+ /* Check if popup extends beyond screen boundaries and adjust */
+ if (popup_x + popup_width > screen_width) {
+ popup_x = screen_width - popup_width;
+ }
+ if (popup_x < 0) {
+ popup_x = 0;
+ }
+
+ /* If popup extends below screen, place it above the applet instead */
+ if (popup_y + popup_height > screen_height) {
+ popup_y = y - popup_height;
+ /* If it still doesn't fit above, center it vertically */
+ if (popup_y < 0) {
+ popup_y = (screen_height - popup_height) / 2;
+ if (popup_y < 0) popup_y = 0;
+ }
+ }
+
+ gtk_window_move (GTK_WINDOW (popup), popup_x, popup_y);
+}
+
+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;
+
+ 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,
+ &timestamp, &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);
+
+ /* Position popup window with boundary checking */
+ position_popup_window (popup, context->main_widget, screen, 450, content_height + 100);
+
+ /* 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);
+}
diff --git a/src/capplet/mate-notification-applet-history.h b/src/capplet/mate-notification-applet-history.h
new file mode 100644
index 0000000..37d0c31
--- /dev/null
+++ b/src/capplet/mate-notification-applet-history.h
@@ -0,0 +1,59 @@
+/* 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
+ */
+
+#ifndef __MATE_NOTIFICATION_APPLET_HISTORY_H__
+#define __MATE_NOTIFICATION_APPLET_HISTORY_H__
+
+#include <gtk/gtk.h>
+#include <gio/gio.h>
+#include <mate-panel-applet.h>
+
+#include "mate-notification-applet-dbus.h"
+
+G_BEGIN_DECLS
+
+/* History context structure */
+typedef struct {
+ MateNotificationDBusContext *dbus_context;
+ GtkWidget *main_widget;
+ GtkWidget *history_popup;
+ GCallback count_update_callback;
+ gpointer count_update_user_data;
+ GSettings *settings;
+} MateNotificationHistoryContext;
+
+/* History popup functions */
+void show_notification_history (MateNotificationHistoryContext *context);
+
+/* Context creation/cleanup */
+MateNotificationHistoryContext *history_context_new (MateNotificationDBusContext *dbus_context,
+ GtkWidget *main_widget,
+ GCallback count_update_callback,
+ gpointer count_update_user_data,
+ GSettings *settings);
+void history_context_free (MateNotificationHistoryContext *context);
+
+/* Context update functions */
+void history_context_update_dbus (MateNotificationHistoryContext *context,
+ MateNotificationDBusContext *dbus_context);
+
+G_END_DECLS
+
+#endif /* __MATE_NOTIFICATION_APPLET_HISTORY_H__ */
diff --git a/src/capplet/mate-notification-applet-menu.xml b/src/capplet/mate-notification-applet-menu.xml
index cab30ac..9631114 100644
--- a/src/capplet/mate-notification-applet-menu.xml
+++ b/src/capplet/mate-notification-applet-menu.xml
@@ -1,3 +1,9 @@
<menuitem name="DoNotDisturb Item" action="DoNotDisturb" />
+<menuitem name="HistoryEnabled Item" action="HistoryEnabled" />
+<separator />
+<menuitem name="ShowHistory Item" action="ShowHistory" />
+<menuitem name="ClearHistory Item" action="ClearHistory" />
+<menuitem name="MarkAllRead Item" action="MarkAllRead" />
+<separator />
<menuitem name="Preferences Item" action="Preferences" />
<menuitem name="About Item" action="About" />
diff --git a/src/capplet/mate-notification-applet.c b/src/capplet/mate-notification-applet.c
index 7402707..dd2f4e1 100644
--- a/src/capplet/mate-notification-applet.c
+++ b/src/capplet/mate-notification-applet.c
@@ -25,20 +25,29 @@
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>
#include <mate-panel-applet.h>
+#include <gio/gio.h>
#define MATE_DESKTOP_USE_UNSTABLE_API
#include <libmate-desktop/mate-desktop-utils.h>
#include "constants.h"
+#include "mate-notification-applet-dbus.h"
+#include "mate-notification-applet-history.h"
typedef struct
{
MatePanelApplet *applet;
- GtkWidget *image_on;
- GtkWidget *image_off;
+ GtkWidget *status_image;
+ GtkWidget *overlay;
+ GtkWidget *count_label;
GtkActionGroup *action_group;
GSettings *settings;
+ guint unread_count;
+ guint update_timer_id;
+
+ MateNotificationDBusContext *dbus_context;
+ MateNotificationHistoryContext *history_context;
} MateNotificationApplet;
static void
@@ -47,8 +56,43 @@ show_about (GtkAction *action,
static void
call_properties (GtkAction *action,
MateNotificationApplet *applet);
+static void
+call_show_history (GtkAction *action,
+ MateNotificationApplet *applet);
+void
+call_clear_history (GtkAction *action,
+ MateNotificationApplet *applet);
+static void
+call_mark_all_read (GtkAction *action,
+ MateNotificationApplet *applet);
+
+/* D-Bus and notification history functions */
+static void
+setup_daemon_connection (MateNotificationApplet *applet);
+static void
+update_unread_count (MateNotificationApplet *applet);
+static void
+update_applet_display (MateNotificationApplet *applet);
+static void
+update_count_badge (MateNotificationApplet *applet);
+static gboolean
+periodic_update_count (MateNotificationApplet *applet);
+
+/* Click handler functions */
+static gboolean
+applet_button_press_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ MateNotificationApplet *applet);
+static void
+toggle_do_not_disturb (MateNotificationApplet *applet);
static const GtkActionEntry applet_menu_actions [] = {
+ { "ShowHistory", "document-open-recent", N_("_Show History"),
+ NULL, NULL, G_CALLBACK (call_show_history) },
+ { "ClearHistory", "edit-clear", N_("_Clear History"),
+ NULL, NULL, G_CALLBACK (call_clear_history) },
+ { "MarkAllRead", "edit-select-all", N_("_Mark All as Read"),
+ NULL, NULL, G_CALLBACK (call_mark_all_read) },
{ "Preferences", "document-properties", N_("_Preferences"),
NULL, NULL, G_CALLBACK (call_properties) },
{ "About", "help-about", N_("_About"),
@@ -65,12 +109,54 @@ applet_destroy (MatePanelApplet *applet_widget,
{
g_assert (applet);
+ if (applet->update_timer_id > 0)
+ g_source_remove (applet->update_timer_id);
+
+ if (applet->dbus_context) {
+ dbus_context_free (applet->dbus_context);
+ }
+
+ if (applet->history_context) {
+ history_context_free (applet->history_context);
+ }
+
g_object_unref (applet->settings);
g_object_unref (applet->action_group);
g_free (applet);
}
static void
+call_show_history (GtkAction *action,
+ MateNotificationApplet *applet)
+{
+ (void) action;
+
+ if (applet->history_context)
+ show_notification_history (applet->history_context);
+}
+
+void
+call_clear_history (GtkAction *action,
+ MateNotificationApplet *applet)
+{
+ (void) action;
+
+ if (dbus_context_clear_notification_history (applet->dbus_context))
+ update_unread_count (applet);
+}
+
+static void
+call_mark_all_read (GtkAction *action,
+ MateNotificationApplet *applet)
+{
+ (void) action;
+
+ /* Update count after marking all as read */
+ if (dbus_context_mark_all_notifications_as_read (applet->dbus_context))
+ update_unread_count (applet);
+}
+
+static void
call_properties (GtkAction *action,
MateNotificationApplet *applet)
{
@@ -95,6 +181,7 @@ show_about (GtkAction *action,
MateNotificationApplet *applet)
{
static const char *authors[] = {
+ "MATE Developers",
"Robert Buj <[email protected]>",
NULL
};
@@ -102,10 +189,10 @@ show_about (GtkAction *action,
(void) applet;
gtk_show_about_dialog (NULL,
- "title", _("About Do Not Disturb"),
+ "title", _("About Notification Status"),
"version", VERSION,
"copyright", _("Copyright \xc2\xa9 2021 MATE developers"),
- "comments", _("Activate the do not disturb mode quickly."),
+ "comments", _("Monitor and control notification status."),
"authors", authors,
"translator-credits", _("translator-credits"),
"logo_icon_name", "mate-notification-properties",
@@ -114,13 +201,36 @@ show_about (GtkAction *action,
static void
set_status_image (MateNotificationApplet *applet,
- gboolean active)
+ gboolean dnd_active,
+ gboolean history_enabled)
{
+ const char *icon_name;
+ gint size, scale;
+ cairo_surface_t *surface;
+
+ if (dnd_active && history_enabled) {
+ icon_name = "user-busy";
+ } else if (dnd_active && !history_enabled) {
+ icon_name = "user-offline";
+ } else if (!dnd_active && !history_enabled) {
+ icon_name = "user-invisible";
+ } else {
+ icon_name = "user-available";
+ }
+
+ size = (gint) mate_panel_applet_get_size (applet->applet);
+ scale = gtk_widget_get_scale_factor (GTK_WIDGET (applet->applet));
+
+ surface = gtk_icon_theme_load_surface (gtk_icon_theme_get_default (),
+ icon_name,
+ size, scale,
+ NULL, 0, NULL);
+ if (surface) {
+ gtk_image_set_from_surface (GTK_IMAGE (applet->status_image), surface);
+ cairo_surface_destroy (surface);
+ }
+
gtk_widget_show_all (GTK_WIDGET (applet->applet));
- if (active)
- gtk_widget_hide (applet->image_on);
- else
- gtk_widget_hide (applet->image_off);
}
static void
@@ -128,47 +238,146 @@ settings_changed (GSettings *settings,
gchar *key,
MateNotificationApplet *applet)
{
- if (g_strcmp0 (GSETTINGS_KEY_DO_NOT_DISTURB, key) == 0)
- set_status_image (applet,
- g_settings_get_boolean (settings, key));
+ if (g_strcmp0 (GSETTINGS_KEY_DO_NOT_DISTURB, key) == 0 ||
+ g_strcmp0 (GSETTINGS_KEY_HISTORY_ENABLED, key) == 0)
+ update_applet_display (applet);
}
static void
-applet_draw_icon (MatePanelApplet *applet_widget,
- int arg1,
- MateNotificationApplet *applet)
+applet_size_changed (MatePanelApplet *applet_widget,
+ int arg1,
+ MateNotificationApplet *applet)
{
- gint size, scale;
+ update_applet_display (applet);
+}
- g_assert (applet);
+static void
+setup_daemon_connection (MateNotificationApplet *applet)
+{
+ if (dbus_context_connect (applet->dbus_context)) {
+ update_unread_count (applet);
+ } else {
+ applet->unread_count = 0;
+ }
- size = (gint) mate_panel_applet_get_size (applet_widget);
- scale = gtk_widget_get_scale_factor (GTK_WIDGET (applet_widget));
+ /* Update history context with new daemon connection */
+ if (applet->history_context) {
+ history_context_update_dbus (applet->history_context, applet->dbus_context);
+ }
+}
+
+static void
+update_unread_count (MateNotificationApplet *applet)
+{
+ applet->unread_count = dbus_context_get_notification_count (applet->dbus_context);
+ update_applet_display (applet);
+}
+
+static void
+update_applet_display (MateNotificationApplet *applet)
+{
+ gchar *tooltip_text;
+ gboolean dnd_active;
+ gboolean history_enabled;
+
+ dnd_active = g_settings_get_boolean (applet->settings, GSETTINGS_KEY_DO_NOT_DISTURB);
+ history_enabled = g_settings_get_boolean (applet->settings, GSETTINGS_KEY_HISTORY_ENABLED);
+
+ /* Update tooltip based on number of unread notifications and user state */
+ if (!history_enabled)
+ tooltip_text = g_strdup (_("(privacy mode)"));
+ else if (applet->unread_count > 99)
+ tooltip_text = g_strdup (_("(99+ unread)"));
+ else if (applet->unread_count > 0)
+ tooltip_text = g_strdup_printf ("(%d unread)", applet->unread_count);
+ else
+ tooltip_text = g_strdup ("");
+
+ if (dnd_active)
+ tooltip_text = g_strdup_printf ("Do Not Disturb %s", tooltip_text);
+ else if (!dbus_context_is_available (applet->dbus_context))
+ tooltip_text = g_strdup (_("Notifications (daemon unavailable)"));
+ else
+ tooltip_text = g_strdup_printf ("Notifications %s", tooltip_text);
+
+ gtk_widget_set_tooltip_text (GTK_WIDGET (applet->applet), tooltip_text);
+ g_free (tooltip_text);
+
+ set_status_image (applet, dnd_active, history_enabled);
+ update_count_badge (applet);
+}
+
+static gboolean
+applet_button_press_cb (GtkWidget *widget,
+ GdkEventButton *event,
+ MateNotificationApplet *applet)
+{
+ switch (event->button) {
+ case 1: /* Left click */
+ if (applet->history_context) {
+ show_notification_history (applet->history_context);
+ }
+ return TRUE;
+
+ case 2: /* Middle click */
+ toggle_do_not_disturb (applet);
+ return TRUE;
+
+ case 3: /* Right click handled by context menu */
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+static void
+toggle_do_not_disturb (MateNotificationApplet *applet)
+{
+ gboolean current_state;
- cairo_surface_t *image_on = gtk_icon_theme_load_surface (gtk_icon_theme_get_default (),
- "user-available",
- size, scale,
- NULL, 0, NULL);
- cairo_surface_t *image_off = gtk_icon_theme_load_surface (gtk_icon_theme_get_default (),
- "user-invisible",
- size, scale,
- NULL, 0, NULL);
+ current_state = g_settings_get_boolean (applet->settings, GSETTINGS_KEY_DO_NOT_DISTURB);
+ g_settings_set_boolean (applet->settings, GSETTINGS_KEY_DO_NOT_DISTURB, !current_state);
+}
- gtk_image_set_from_surface (GTK_IMAGE (applet->image_on), image_on);
- gtk_image_set_from_surface (GTK_IMAGE (applet->image_off), image_off);
+static void
+update_count_badge (MateNotificationApplet *applet)
+{
+ gchar *count_text;
+ gboolean history_enabled = g_settings_get_boolean (applet->settings, GSETTINGS_KEY_HISTORY_ENABLED);
+
+ /* Only show count badges when history is enabled and there are notifications */
+ if (history_enabled && applet->unread_count > 0) {
+ if (applet->unread_count > 99) {
+ count_text = g_strdup ("99+");
+ } else {
+ count_text = g_strdup_printf ("%u", applet->unread_count);
+ }
+ gtk_label_set_text (GTK_LABEL (applet->count_label), count_text);
+ gtk_widget_show (applet->count_label);
+ g_free (count_text);
+ } else {
+ /* Hide badge when no unread notifications */
+ gtk_widget_hide (applet->count_label);
+ }
+}
- cairo_surface_destroy (image_on);
- cairo_surface_destroy (image_off);
+static gboolean
+periodic_update_count (MateNotificationApplet *applet)
+{
+ if (applet && dbus_context_is_available (applet->dbus_context)) {
+ update_unread_count (applet);
+ }
+ return TRUE; /* Continue periodic updates */
}
static MateNotificationApplet*
applet_main (MatePanelApplet *applet_widget)
{
MateNotificationApplet *applet;
- GtkWidget *box;
#ifndef ENABLE_IN_PROCESS
- g_set_application_name (_("Do Not Disturb"));
+ g_set_application_name (_("Notification Status"));
#endif
gtk_window_set_default_icon_name ("mate-notification-properties");
@@ -176,32 +385,67 @@ applet_main (MatePanelApplet *applet_widget)
applet->applet = applet_widget;
applet->settings = g_settings_new (GSETTINGS_SCHEMA);
+ /* Initialize D-Bus context and notification state */
+ applet->dbus_context = dbus_context_new ();
+ applet->unread_count = 0;
+ applet->update_timer_id = 0;
+
#ifndef ENABLE_IN_PROCESS
/* needed to clamp ourselves to the panel size */
- mate_panel_applet_set_flags (MATE_PANEL_APPLET (applet), MATE_PANEL_APPLET_EXPAND_MINOR);
+ mate_panel_applet_set_flags (MATE_PANEL_APPLET (applet->applet), MATE_PANEL_APPLET_EXPAND_MINOR);
#endif
- box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
-
- applet->image_on = gtk_image_new ();
- applet->image_off = gtk_image_new ();
- applet_draw_icon (applet_widget, 0, applet);
-
- gtk_widget_set_tooltip_text (applet->image_off, _("Do Not Disturb"));
- gtk_widget_set_tooltip_text (applet->image_on, _("Notifications Enabled"));
-
- gtk_box_pack_start (GTK_BOX (box), applet->image_on,
- TRUE, TRUE, 0);
- gtk_box_pack_start (GTK_BOX (box), applet->image_off,
- TRUE, TRUE, 0);
- gtk_container_add (GTK_CONTAINER (applet_widget), box);
+ /* Create overlay for icons and count badge */
+ applet->overlay = gtk_overlay_new ();
+
+ /* Create status icon */
+ applet->status_image = gtk_image_new ();
+
+ /* Add status icon as main overlay child */
+ gtk_container_add (GTK_CONTAINER (applet->overlay), applet->status_image);
+
+ /* Create count badge label */
+ applet->count_label = gtk_label_new ("");
+ gtk_widget_set_name (applet->count_label, "notification-count-badge");
+ gtk_widget_set_halign (applet->count_label, GTK_ALIGN_END);
+ gtk_widget_set_valign (applet->count_label, GTK_ALIGN_END);
+ GtkCssProvider *css_provider = gtk_css_provider_new ();
+ const gchar *css_data =
+ "#notification-count-badge {"
+ " background-color: rgba(255,255,255,0.9);"
+ " color: #000000;"
+ " border-radius: 8px;"
+ " min-width: 12px;"
+ " min-height: 4px;"
+ " padding: 1px 1px;"
+ " font-size: 10px;"
+ " font-weight: bold;"
+ " text-shadow: none;"
+ " border: 1px solid rgba(0,0,0,0.1);"
+ "}";
+ gtk_css_provider_load_from_data (css_provider, css_data, -1, NULL);
+ gtk_style_context_add_provider (gtk_widget_get_style_context (applet->count_label),
+ GTK_STYLE_PROVIDER (css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ g_object_unref (css_provider);
+
+ /* Add count label as overlay */
+ gtk_overlay_add_overlay (GTK_OVERLAY (applet->overlay), applet->count_label);
+
+ /* Add overlay to applet */
+ gtk_container_add (GTK_CONTAINER (applet_widget), applet->overlay);
set_status_image (applet,
- g_settings_get_boolean (applet->settings,
- GSETTINGS_KEY_DO_NOT_DISTURB));
+ g_settings_get_boolean (applet->settings, GSETTINGS_KEY_DO_NOT_DISTURB),
+ g_settings_get_boolean (applet->settings, GSETTINGS_KEY_HISTORY_ENABLED));
+
+ /* click handling */
+ gtk_widget_add_events (GTK_WIDGET (applet_widget), GDK_BUTTON_PRESS_MASK);
+ g_signal_connect (G_OBJECT (applet_widget), "button-press-event",
+ G_CALLBACK (applet_button_press_cb), applet);
/* set up context menu */
- applet->action_group = gtk_action_group_new ("Do Not Disturb Actions");
+ applet->action_group = gtk_action_group_new ("Notification Status Actions");
#ifdef ENABLE_NLS
gtk_action_group_set_translation_domain (applet->action_group, GETTEXT_PACKAGE);
#endif /* ENABLE_NLS */
@@ -215,6 +459,13 @@ applet_main (MatePanelApplet *applet_widget)
gtk_action_group_add_action (applet->action_group,
GTK_ACTION (do_not_disturb_toggle_action));
+ GtkToggleAction *history_toggle_action =
+ gtk_toggle_action_new ("HistoryEnabled", _("_Enable History"),
+ _("Enable/Disable notification history."), NULL);
+
+ gtk_action_group_add_action (applet->action_group,
+ GTK_ACTION (history_toggle_action));
+
mate_panel_applet_setup_menu_from_resource (applet->applet,
RESOURCE_PATH "menu.xml",
applet->action_group);
@@ -223,12 +474,31 @@ applet_main (MatePanelApplet *applet_widget)
do_not_disturb_toggle_action, "active",
G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (applet->settings, "history-enabled",
+ history_toggle_action, "active",
+ G_SETTINGS_BIND_DEFAULT);
+
g_signal_connect (G_OBJECT (applet->applet), "destroy",
G_CALLBACK (applet_destroy), applet);
- /* GSettings callback */
+ /* GSettings callbacks */
g_signal_connect (G_OBJECT (applet->settings), "changed::" GSETTINGS_KEY_DO_NOT_DISTURB,
G_CALLBACK (settings_changed), applet);
+ g_signal_connect (G_OBJECT (applet->settings), "changed::" GSETTINGS_KEY_HISTORY_ENABLED,
+ G_CALLBACK (settings_changed), applet);
+
+ /* Create new history context */
+ applet->history_context = history_context_new (applet->dbus_context,
+ GTK_WIDGET (applet->applet),
+ G_CALLBACK (update_unread_count),
+ applet,
+ applet->settings);
+
+ setup_daemon_connection (applet);
+
+ /* Set up periodic updates every few seconds */
+#define NOTIFICATION_UPDATE_COUNT 5
+ applet->update_timer_id = g_timeout_add_seconds (NOTIFICATION_UPDATE_COUNT, (GSourceFunc) periodic_update_count, applet);
return applet;
}
@@ -245,7 +515,7 @@ applet_factory (MatePanelApplet *applet_widget,
applet = applet_main (applet_widget);
g_signal_connect (G_OBJECT (applet_widget), "change_size",
- G_CALLBACK (applet_draw_icon),
+ G_CALLBACK (applet_size_changed),
(gpointer) applet);
return TRUE;
@@ -256,6 +526,6 @@ applet_factory (MatePanelApplet *applet_widget,
PANEL_APPLET_FACTORY ("MateNotificationAppletFactory",
PANEL_TYPE_APPLET,
- "Do Not Disturb Applet",
+ "Notification Status Applet",
applet_factory,
NULL)
diff --git a/src/capplet/mate-notification-properties.c b/src/capplet/mate-notification-properties.c
index d8ce983..f81dd2c 100644
--- a/src/capplet/mate-notification-properties.c
+++ b/src/capplet/mate-notification-properties.c
@@ -44,9 +44,14 @@ typedef struct {
GtkWidget* preview_button;
GtkWidget* active_checkbox;
GtkWidget* dnd_checkbox;
+ GtkWidget* history_checkbox;
GtkWidget* monitor_label;
+ GtkWidget* timeout_spin;
+ GtkWidget* persistence_checkbox;
+ GtkWidget* countdown_checkbox;
- NotifyNotification* preview;
+ NotifyNotification* preview1;
+ NotifyNotification* preview2;
} NotificationAppletDialog;
enum {
@@ -387,14 +392,19 @@ static void show_message(NotificationAppletDialog* dialog, const gchar* message)
static void notification_properties_dialog_preview_closed(NotifyNotification* preview, NotificationAppletDialog* dialog)
{
- if (preview == dialog->preview)
- {
- dialog->preview = NULL;
- }
+ if (preview == dialog->preview1)
+ dialog->preview1 = NULL;
+ else if (preview == dialog->preview2)
+ dialog->preview2 = NULL;
g_object_unref(preview);
}
+static gboolean notification_properties_dialog_preview_action(void *data) {
+ // @todo call notification_properties_dialog_preview_closed
+ return FALSE;
+}
+
static void notification_properties_dialog_preview(NotificationAppletDialog* dialog)
{
if (!notify_is_initted() && !notify_init("n-d"))
@@ -405,16 +415,35 @@ static void notification_properties_dialog_preview(NotificationAppletDialog* dia
GError* error = NULL;
- if (dialog->preview)
+ if (dialog->preview1)
+ {
+ notify_notification_close(dialog->preview1, NULL);
+ g_object_unref(dialog->preview1);
+ dialog->preview1 = NULL;
+ }
+ if (dialog->preview2)
+ {
+ notify_notification_close(dialog->preview2, NULL);
+ g_object_unref(dialog->preview2);
+ dialog->preview2 = NULL;
+ }
+
+ dialog->preview1 = notify_notification_new(_("Notification Test"), _("Just a test"), "dialog-information");
+ notify_notification_set_timeout (dialog->preview1, 50000);
+
+ if (!notify_notification_show(dialog->preview1, &error))
{
- notify_notification_close(dialog->preview, NULL);
- g_object_unref(dialog->preview);
- dialog->preview = NULL;
+ char* message = g_strdup_printf(_("Error while displaying notification: %s"), error->message);
+ show_message(dialog, message);
+ g_error_free(error);
+ g_free(message);
}
- dialog->preview = notify_notification_new(_("Notification Test"), _("Just a test"), "dialog-information");
+ dialog->preview2 = notify_notification_new(_("Notification Test"), _("Just a test"), "dialog-information");
+ notify_notification_add_action (dialog->preview2, "nothing", _("Close"), NOTIFY_ACTION_CALLBACK (notification_properties_dialog_preview_action), NULL, NULL);
+ notify_notification_set_timeout (dialog->preview2, 50000);
- if (!notify_notification_show(dialog->preview, &error))
+ if (!notify_notification_show(dialog->preview2, &error))
{
char* message = g_strdup_printf(_("Error while displaying notification: %s"), error->message);
show_message(dialog, message);
@@ -422,7 +451,8 @@ static void notification_properties_dialog_preview(NotificationAppletDialog* dia
g_free(message);
}
- g_signal_connect(dialog->preview, "closed", G_CALLBACK(notification_properties_dialog_preview_closed), dialog);
+ g_signal_connect(dialog->preview1, "closed", G_CALLBACK(notification_properties_dialog_preview_closed), dialog);
+ g_signal_connect(dialog->preview2, "closed", G_CALLBACK(notification_properties_dialog_preview_closed), dialog);
}
static void notification_properties_dialog_response(GtkWidget* widget, int response, NotificationAppletDialog* dialog)
@@ -467,7 +497,11 @@ static gboolean notification_properties_dialog_init(NotificationAppletDialog* di
dialog->theme_combo = GTK_WIDGET(gtk_builder_get_object(builder, "theme_combo"));
dialog->active_checkbox = GTK_WIDGET(gtk_builder_get_object(builder, "use_active_check"));
dialog->dnd_checkbox = GTK_WIDGET(gtk_builder_get_object(builder, "do_not_disturb_check"));
+ dialog->history_checkbox = GTK_WIDGET(gtk_builder_get_object(builder, "history_enabled_check"));
dialog->monitor_label = GTK_WIDGET(gtk_builder_get_object(builder, "monitor_label"));
+ dialog->timeout_spin = GTK_WIDGET(gtk_builder_get_object(builder, "timeout_spin"));
+ dialog->persistence_checkbox = GTK_WIDGET(gtk_builder_get_object(builder, "enable_persistence_check"));
+ dialog->countdown_checkbox = GTK_WIDGET(gtk_builder_get_object(builder, "show_countdown_check"));
g_object_unref (builder);
@@ -478,6 +512,12 @@ static gboolean notification_properties_dialog_init(NotificationAppletDialog* di
g_settings_bind (dialog->gsettings, GSETTINGS_KEY_USE_ACTIVE_MONITOR, dialog->active_checkbox, "active", G_SETTINGS_BIND_DEFAULT);
g_settings_bind (dialog->gsettings, GSETTINGS_KEY_DO_NOT_DISTURB, dialog->dnd_checkbox, "active", G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (dialog->gsettings, GSETTINGS_KEY_HISTORY_ENABLED, dialog->history_checkbox, "active", G_SETTINGS_BIND_DEFAULT);
+
+ GtkAdjustment *timeout_adjustment = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(dialog->timeout_spin));
+ g_settings_bind (dialog->gsettings, GSETTINGS_KEY_DEFAULT_TIMEOUT, timeout_adjustment, "value", G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (dialog->gsettings, GSETTINGS_KEY_ENABLE_PERSISTENCE, dialog->persistence_checkbox, "active", G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (dialog->gsettings, GSETTINGS_KEY_SHOW_COUNTDOWN, dialog->countdown_checkbox, "active", G_SETTINGS_BIND_DEFAULT);
notification_properties_dialog_setup_themes (dialog);
notification_properties_dialog_setup_positions (dialog);
@@ -491,7 +531,8 @@ static gboolean notification_properties_dialog_init(NotificationAppletDialog* di
gtk_widget_show_all(dialog->dialog);
- dialog->preview = NULL;
+ dialog->preview1 = NULL;
+ dialog->preview2 = NULL;
return TRUE;
}
@@ -504,11 +545,17 @@ static void notification_properties_dialog_finalize(NotificationAppletDialog* di
dialog->dialog = NULL;
}
- if (dialog->preview)
+ if (dialog->preview1)
+ {
+ notify_notification_close(dialog->preview1, NULL);
+ dialog->preview1 = NULL;
+ }
+ if (dialog->preview2)
{
- notify_notification_close(dialog->preview, NULL);
- dialog->preview = NULL;
+ notify_notification_close(dialog->preview2, NULL);
+ dialog->preview2 = NULL;
}
+
g_free (dialog);
}
diff --git a/src/capplet/mate-notification-properties.ui b/src/capplet/mate-notification-properties.ui
index d6dbfac..9b461bf 100644
--- a/src/capplet/mate-notification-properties.ui
+++ b/src/capplet/mate-notification-properties.ui
@@ -12,6 +12,13 @@
<property name="can_focus">False</property>
<property name="icon_name">window-close</property>
</object>
+ <object class="GtkAdjustment" id="timeout_adjustment">
+ <property name="lower">0</property>
+ <property name="upper">300000</property>
+ <property name="value">7000</property>
+ <property name="step_increment">1000</property>
+ <property name="page_increment">5000</property>
+ </object>
<object class="GtkDialog" id="dialog">
<property name="can_focus">False</property>
<property name="border_width">12</property>
@@ -211,6 +218,22 @@
<property name="position">2</property>
</packing>
</child>
+ <child>
+ <object class="GtkCheckButton" id="history_enabled_check">
+ <property name="label" translatable="yes">Enable Notification History</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <property name="tooltip_text" translatable="yes">When enabled, notifications are stored for later viewing. When disabled, no notification history is kept for privacy. Note: This only controls MATE's notification storage; system logs may still contain notification data.</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
</object>
</child>
</object>
@@ -232,6 +255,119 @@
<property name="position">1</property>
</packing>
</child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label_xalign">0</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkAlignment">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">_Timeout (ms):</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">timeout_spin</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="timeout_spin">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">timeout_adjustment</property>
+ <property name="climb_rate">1000</property>
+ <property name="digits">0</property>
+ <property name="numeric">True</property>
+ <property name="value">7000</property>
+ <property name="tooltip_text" translatable="yes">Default timeout for notifications in milliseconds. Use 0 for no timeout (persistent).</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="enable_persistence_check">
+ <property name="label" translatable="yes">Allow Persistent Notifications</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <property name="tooltip_text" translatable="yes">Allow applications to create notifications that don't disappear automatically</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="show_countdown_check">
+ <property name="label" translatable="yes">Show Countdown Timer</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <property name="tooltip_text" translatable="yes">Show countdown timer on all timed notifications (not just those with actions)</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Timeout &amp; Behavior</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
</object>
</child>
<action-widgets>
diff --git a/src/common/constants.h b/src/common/constants.h
index d2894da..1a81977 100644
--- a/src/common/constants.h
+++ b/src/common/constants.h
@@ -28,6 +28,10 @@
#define GSETTINGS_KEY_POPUP_LOCATION "popup-location"
#define GSETTINGS_KEY_SOUND_ENABLED "sound-enabled"
#define GSETTINGS_KEY_USE_ACTIVE_MONITOR "use-active-monitor"
+#define GSETTINGS_KEY_DEFAULT_TIMEOUT "default-timeout"
+#define GSETTINGS_KEY_ENABLE_PERSISTENCE "enable-persistence"
+#define GSETTINGS_KEY_SHOW_COUNTDOWN "show-countdown"
+#define GSETTINGS_KEY_HISTORY_ENABLED "history-enabled"
#define NOTIFICATION_BUS_NAME "org.freedesktop.Notifications"
#define NOTIFICATION_BUS_PATH "/org/freedesktop/Notifications"
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,
diff --git a/src/daemon/daemon.h b/src/daemon/daemon.h
index 4c32e12..52cdbab 100644
--- a/src/daemon/daemon.h
+++ b/src/daemon/daemon.h
@@ -34,8 +34,6 @@
#define NOTIFY_TYPE_DAEMON (notify_daemon_get_type())
G_DECLARE_FINAL_TYPE (NotifyDaemon, notify_daemon, NOTIFY, DAEMON, GObject)
-#define NOTIFY_DAEMON_DEFAULT_TIMEOUT 7000
-
enum {
URGENCY_LOW,
URGENCY_NORMAL,
@@ -49,6 +47,19 @@ typedef enum {
NOTIFYD_CLOSED_RESERVED = 4
} NotifydClosedReason;
+typedef struct {
+ guint id; /* Original notification ID */
+ gchar *app_name; /* Application name */
+ gchar *app_icon; /* Icon name/path */
+ gchar *summary; /* Notification title */
+ gchar *body; /* Notification body */
+ gint64 timestamp; /* When notification appeared */
+ gint64 closed_timestamp; /* When notification was closed */
+ NotifydClosedReason reason; /* How it was closed */
+ guint urgency; /* Low/Normal/Critical */
+ gboolean read; /* Has user seen this in history */
+} NotificationHistoryItem;
+
G_BEGIN_DECLS
NotifyDaemon* notify_daemon_new (gboolean replace,
diff --git a/src/daemon/notificationdaemon.xml b/src/daemon/notificationdaemon.xml
index 9a54bb0..3bcf491 100644
--- a/src/daemon/notificationdaemon.xml
+++ b/src/daemon/notificationdaemon.xml
@@ -30,6 +30,20 @@
<arg type="s" name="return_spec_version" direction="out"/>
</method>
+ <method name="GetNotificationHistory">
+ <arg type="a(ussssxxuub)" name="history_items" direction="out"/>
+ </method>
+
+ <method name="ClearNotificationHistory">
+ </method>
+
+ <method name="GetNotificationCount">
+ <arg type="u" name="count" direction="out"/>
+ </method>
+
+ <method name="MarkAllNotificationsAsRead">
+ </method>
+
<signal name="ActionInvoked">
<arg type="u" name="id" />
<arg type="s" name="action_key" />
diff --git a/src/themes/coco/coco-theme.c b/src/themes/coco/coco-theme.c
index 90f95cd..ff76d52 100644
--- a/src/themes/coco/coco-theme.c
+++ b/src/themes/coco/coco-theme.c
@@ -1,11 +1,8 @@
/*
- * coco-theme.c
- * This file is part of notification-daemon-engine-coco
- *
* Copyright (C) 2012 - Stefano Karapetsas <[email protected]>
* Copyright (C) 2010 - Eduardo Grajeda
* Copyright (C) 2008 - Martin Sourada
- * Copyright (C) 2012-2021 MATE Developers
+ * Copyright (C) 2012-2025 MATE Developers
*
* notification-daemon-engine-coco is free software; you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
@@ -91,6 +88,7 @@ void move_notification(GtkWidget *nw, int x, int y);
void set_notification_timeout(GtkWindow *nw, glong timeout);
void set_notification_hints(GtkWindow *nw, GVariant *hints);
void notification_tick(GtkWindow *nw, glong remaining);
+static void create_pie_countdown(WindowData* windata);
#define STRIPE_WIDTH 32
#define WIDTH 300
@@ -110,6 +108,24 @@ void notification_tick(GtkWindow *nw, glong remaining);
/* Support Nodoka Functions */
+static void
+get_background_color (GtkStyleContext *context,
+ GtkStateFlags state,
+ GdkRGBA *color)
+{
+ GdkRGBA *c;
+
+ g_return_if_fail (color != NULL);
+ g_return_if_fail (GTK_IS_STYLE_CONTEXT (context));
+
+ gtk_style_context_get (context, state,
+ "background-color", &c,
+ NULL);
+
+ *color = *c;
+ gdk_rgba_free (c);
+}
+
/* Handle clicking on link */
static gboolean
activate_link (GtkLabel *label, const char *url, WindowData *windata)
@@ -172,7 +188,28 @@ draw_pie(GtkWidget *pie, WindowData *windata, cairo_t *cr)
return;
gdouble arc_angle = 1.0 - (gdouble)windata->remaining / (gdouble)windata->timeout;
- cairo_set_source_rgba (cr, 1.0, 0.4, 0.0, 0.3);
+ GtkStyleContext *context;
+ GdkRGBA orig, bg;
+
+ // :selected { background-color:#aabbcc; } ignored -> 1.0, 1.0, 1.0, 0.3
+ context = gtk_widget_get_style_context (windata->win);
+ gtk_style_context_save (context);
+ gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
+ get_background_color (context, GTK_STATE_FLAG_SELECTED, &orig);
+ gtk_style_context_restore (context);
+
+ // .notification-box .countdown:selected { background-color:#aabbcc; }
+ context = gtk_widget_get_style_context (pie);
+ gtk_style_context_save (context);
+ gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
+ get_background_color (context, GTK_STATE_FLAG_SELECTED, &bg);
+ gtk_style_context_restore (context);
+
+ if (gdk_rgba_equal (&orig, &bg))
+ cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.3);
+ else
+ cairo_set_source_rgba (cr, bg.red, bg.green, bg.blue, bg.alpha);
+
cairo_move_to(cr, PIE_RADIUS, PIE_RADIUS);
cairo_arc_negative(cr, PIE_RADIUS, PIE_RADIUS, PIE_RADIUS,
-G_PI/2, (-0.25 + arc_angle)*2*G_PI);
@@ -255,7 +292,6 @@ paint_window (GtkWidget *widget,
cairo_restore (cr);
update_shape_region (surface, windata);
-
cairo_surface_destroy (surface);
}
@@ -282,31 +318,21 @@ configure_event_cb(GtkWidget *nw,
}
static gboolean
-countdown_expose_cb(GtkWidget *pie,
- cairo_t *cr,
- WindowData *windata)
+countdown_expose_cb(GtkWidget *pie, cairo_t *cr, WindowData *windata)
{
cairo_t *cr2;
cairo_surface_t *surface;
GtkAllocation alloc;
- cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
-
+ cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
gtk_widget_get_allocation (pie, &alloc);
- surface = cairo_surface_create_similar (cairo_get_target (cr),
- CAIRO_CONTENT_COLOR_ALPHA,
- alloc.width,
- alloc.height);
-
+ surface = cairo_surface_create_similar (cairo_get_target (cr), CAIRO_CONTENT_COLOR_ALPHA, alloc.width, alloc.height);
cr2 = cairo_create (surface);
-
- cairo_translate (cr2, -alloc.x, -alloc.y);
- fill_background (pie, windata, cr2);
- cairo_translate (cr2, alloc.x, alloc.y);
- draw_pie (pie, windata, cr2);
+ cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0); // transparent background color
+ cairo_paint (cr2);
+ draw_pie (pie, windata, cr2); // countdown
cairo_fill (cr2);
-
cairo_destroy (cr2);
cairo_save (cr);
@@ -442,7 +468,7 @@ create_notification(UrlClickedCb url_clicked)
gtk_box_pack_start (GTK_BOX(main_vbox), windata->main_hbox, FALSE, FALSE, 0);
gtk_container_set_border_width(GTK_CONTAINER(windata->main_hbox), 13);
- /* The icon goes at the left */
+ /* The icon goes at the left */
windata->iconbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_show(windata->iconbox);
gtk_box_pack_start(GTK_BOX(windata->main_hbox), windata->iconbox,
@@ -452,7 +478,7 @@ create_notification(UrlClickedCb url_clicked)
gtk_box_pack_start(GTK_BOX(windata->iconbox), windata->icon,
FALSE, FALSE, 0);
- /* The title and the text at the right */
+ /* The title and the text at the right */
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_halign (vbox, GTK_ALIGN_START);
gtk_widget_set_margin_start (vbox, 8);
@@ -633,19 +659,7 @@ add_notification_action(GtkWindow *nw, const char *text, const char *key,
if (gtk_widget_get_visible(windata->actions_box))
{
gtk_widget_show(windata->actions_box);
-
- /* Don't try to re-add a pie_countdown */
- if (!windata->pie_countdown) {
- windata->pie_countdown = gtk_drawing_area_new();
- gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
- gtk_widget_show(windata->pie_countdown);
-
- gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
- gtk_widget_set_size_request(windata->pie_countdown,
- PIE_WIDTH, PIE_HEIGHT);
- g_signal_connect(G_OBJECT(windata->pie_countdown), "draw",
- G_CALLBACK(countdown_expose_cb), windata);
- }
+ create_pie_countdown(windata);
}
if (windata->action_icons) {
@@ -730,6 +744,27 @@ move_notification(GtkWidget *nw, int x, int y)
/* Hide notification */
/* Set notification timeout */
+static void create_pie_countdown(WindowData* windata)
+{
+ if (windata->pie_countdown != NULL)
+ return; /* Already created */
+
+ windata->pie_countdown = gtk_drawing_area_new();
+ gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
+ gtk_widget_set_valign (windata->pie_countdown, GTK_ALIGN_CENTER);
+ gtk_widget_show(windata->pie_countdown);
+
+ #if GTK_CHECK_VERSION (4,0,0)
+ gtk_widget_add_css_class (windata->pie_countdown, "countdown");
+ #else
+ gtk_style_context_add_class (gtk_widget_get_style_context (windata->pie_countdown), "countdown");
+ #endif
+
+ gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
+ gtk_widget_set_size_request(windata->pie_countdown, PIE_WIDTH, PIE_HEIGHT);
+ g_signal_connect(G_OBJECT(windata->pie_countdown), "draw", G_CALLBACK(countdown_expose_cb), windata);
+}
+
void
set_notification_timeout(GtkWindow *nw, glong timeout)
{
@@ -737,6 +772,21 @@ set_notification_timeout(GtkWindow *nw, glong timeout)
g_assert(windata != NULL);
windata->timeout = timeout;
+
+ /* Check if we should show countdown for all timed notifications */
+ if (timeout > 0)
+ {
+ GSettings *gsettings = g_settings_new ("org.mate.NotificationDaemon");
+ gboolean show_countdown = g_settings_get_boolean (gsettings, "show-countdown");
+ g_object_unref (gsettings);
+
+ if (show_countdown)
+ {
+ /* Ensure actions_box is visible for countdown */
+ gtk_widget_show(windata->actions_box);
+ create_pie_countdown(windata);
+ }
+ }
}
/* Set notification hints */
diff --git a/src/themes/nodoka/nodoka-theme.c b/src/themes/nodoka/nodoka-theme.c
index 1859f28..d478862 100644
--- a/src/themes/nodoka/nodoka-theme.c
+++ b/src/themes/nodoka/nodoka-theme.c
@@ -1,10 +1,7 @@
/*
- * nodoka-theme.c
- * This file is part of notification-daemon-engine-nodoka
- *
* Copyright (C) 2012 - Stefano Karapetsas <[email protected]>
* Copyright (C) 2008 - Martin Sourada
- * Copyright (C) 2012-2021 MATE Developers
+ * Copyright (C) 2012-2025 MATE Developers
*
* notification-daemon-engine-nodoka is free software; you can redistribute it
* and/or modify it under the terms of the GNU General Public License as
@@ -31,7 +28,6 @@
#include <libxml/xmlmemory.h>
#include <libxml/xpath.h>
-/* Define basic nodoka types */
typedef void (*ActionInvokedCb)(GtkWindow *nw, const char *key);
typedef void (*UrlClickedCb)(GtkWindow *nw, const char *url);
@@ -105,6 +101,7 @@ void move_notification(GtkWidget *nw, int x, int y);
void set_notification_timeout(GtkWindow *nw, glong timeout);
void set_notification_hints(GtkWindow *nw, GVariant *hints);
void notification_tick(GtkWindow *nw, glong remaining);
+static void create_pie_countdown(WindowData* windata);
#define STRIPE_WIDTH 32
#define WIDTH 400
@@ -124,6 +121,24 @@ void notification_tick(GtkWindow *nw, glong remaining);
/* Support Nodoka Functions */
+static void
+get_background_color (GtkStyleContext *context,
+ GtkStateFlags state,
+ GdkRGBA *color)
+{
+ GdkRGBA *c;
+
+ g_return_if_fail (color != NULL);
+ g_return_if_fail (GTK_IS_STYLE_CONTEXT (context));
+
+ gtk_style_context_get (context, state,
+ "background-color", &c,
+ NULL);
+
+ *color = *c;
+ gdk_rgba_free (c);
+}
+
/* Handle clicking on link */
static gboolean
activate_link (GtkLabel *label, const char *url, WindowData *windata)
@@ -405,8 +420,12 @@ fill_background(GtkWidget *widget, WindowData *windata, cairo_t *cr)
static void
draw_stripe(GtkWidget *widget, WindowData *windata, cairo_t *cr)
{
+ int stripe_x = 0;
+ if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ stripe_x = windata->width - STRIPE_WIDTH - stripe_x;
+
cairo_save (cr);
- cairo_rectangle (cr, 0, 0, STRIPE_WIDTH, windata->height);
+ cairo_rectangle (cr, stripe_x, 0, STRIPE_WIDTH, windata->height);
cairo_clip (cr);
gdouble color_mult = 1.0;
@@ -514,7 +533,28 @@ draw_pie(GtkWidget *pie, WindowData *windata, cairo_t *cr)
return;
gdouble arc_angle = 1.0 - (gdouble)windata->remaining / (gdouble)windata->timeout;
- cairo_set_source_rgba (cr, 1.0, 0.4, 0.0, 0.3);
+ GtkStyleContext *context;
+ GdkRGBA orig, bg;
+
+ // :selected { background-color:#aabbcc; } ignored -> 1.0, 0.4, 0.0, 0.3
+ context = gtk_widget_get_style_context (windata->win);
+ gtk_style_context_save (context);
+ gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
+ get_background_color (context, GTK_STATE_FLAG_SELECTED, &orig);
+ gtk_style_context_restore (context);
+
+ // .notification-box .countdown:selected { background-color:#aabbcc; }
+ context = gtk_widget_get_style_context (pie);
+ gtk_style_context_save (context);
+ gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
+ get_background_color (context, GTK_STATE_FLAG_SELECTED, &bg);
+ gtk_style_context_restore (context);
+
+ if (gdk_rgba_equal (&orig, &bg))
+ cairo_set_source_rgba (cr, 1.0, 0.4, 0.0, 0.3);
+ else
+ cairo_set_source_rgba (cr, bg.red, bg.green, bg.blue, bg.alpha);
+
cairo_move_to(cr, PIE_RADIUS, PIE_RADIUS);
cairo_arc_negative(cr, PIE_RADIUS, PIE_RADIUS, PIE_RADIUS,
-G_PI/2, (-0.25 + arc_angle)*2*G_PI);
@@ -580,11 +620,9 @@ paint_window (GtkWidget *widget,
windata->height);
cr2 = cairo_create (surface);
-
- /* transparent background */
- cairo_rectangle (cr2, 0, 0, windata->width, windata->height);
- cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0);
- cairo_fill (cr2);
+ cairo_rectangle (cr2, 0, 0, windata->width, windata->height);
+ cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0); // transparent background color
+ cairo_fill (cr2);
if (windata->arrow.has_arrow) {
nodoka_rounded_rectangle_with_arrow (cr2, 0, 0,
@@ -614,7 +652,6 @@ paint_window (GtkWidget *widget,
cairo_restore (cr);
update_shape_region (surface, windata);
-
cairo_surface_destroy (surface);
}
@@ -649,30 +686,21 @@ static void on_composited_changed (GtkWidget* window, WindowData* windata)
}
static gboolean
-countdown_expose_cb(GtkWidget *pie,
- cairo_t *cr,
- WindowData *windata)
+countdown_expose_cb(GtkWidget *pie, cairo_t *cr, WindowData *windata)
{
cairo_t *cr2;
cairo_surface_t *surface;
GtkAllocation alloc;
- cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
-
+ cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
gtk_widget_get_allocation (pie, &alloc);
- surface = cairo_surface_create_similar (cairo_get_target (cr),
- CAIRO_CONTENT_COLOR_ALPHA,
- alloc.width,
- alloc.height);
+ surface = cairo_surface_create_similar (cairo_get_target (cr), CAIRO_CONTENT_COLOR_ALPHA, alloc.width, alloc.height);
cr2 = cairo_create (surface);
-
- cairo_translate (cr2, -alloc.x, -alloc.y);
- fill_background (pie, windata, cr2);
- cairo_translate (cr2, alloc.x, alloc.y);
- draw_pie (pie, windata, cr2);
+ cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0); // transparent background color
+ cairo_paint (cr2);
+ draw_pie (pie, windata, cr2); // countdown
cairo_fill (cr2);
-
cairo_destroy (cr2);
cairo_save (cr);
@@ -1037,19 +1065,7 @@ add_notification_action(GtkWindow *nw, const char *text, const char *key,
{
gtk_widget_show(windata->actions_box);
update_content_hbox_visibility(windata);
-
- /* Don't try to re-add a pie_countdown */
- if (!windata->pie_countdown) {
- windata->pie_countdown = gtk_drawing_area_new();
- gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
- gtk_widget_show(windata->pie_countdown);
-
- gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
- gtk_widget_set_size_request(windata->pie_countdown,
- PIE_WIDTH, PIE_HEIGHT);
- g_signal_connect(G_OBJECT(windata->pie_countdown), "draw",
- G_CALLBACK(countdown_expose_cb), windata);
- }
+ create_pie_countdown(windata);
}
if (windata->action_icons) {
@@ -1141,6 +1157,27 @@ move_notification(GtkWidget *nw, int x, int y)
/* Hide notification */
/* Set notification timeout */
+static void create_pie_countdown(WindowData* windata)
+{
+ if (windata->pie_countdown != NULL)
+ return; /* Already created */
+
+ windata->pie_countdown = gtk_drawing_area_new();
+ gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
+ gtk_widget_set_valign (windata->pie_countdown, GTK_ALIGN_CENTER);
+ gtk_widget_show(windata->pie_countdown);
+
+ #if GTK_CHECK_VERSION (4,0,0)
+ gtk_widget_add_css_class (windata->pie_countdown, "countdown");
+ #else
+ gtk_style_context_add_class (gtk_widget_get_style_context (windata->pie_countdown), "countdown");
+ #endif
+
+ gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
+ gtk_widget_set_size_request(windata->pie_countdown, PIE_WIDTH, PIE_HEIGHT);
+ g_signal_connect(G_OBJECT(windata->pie_countdown), "draw", G_CALLBACK(countdown_expose_cb), windata);
+}
+
void
set_notification_timeout(GtkWindow *nw, glong timeout)
{
@@ -1148,6 +1185,22 @@ set_notification_timeout(GtkWindow *nw, glong timeout)
g_assert(windata != NULL);
windata->timeout = timeout;
+
+ /* Check if we should show countdown for all timed notifications */
+ if (timeout > 0)
+ {
+ GSettings *gsettings = g_settings_new ("org.mate.NotificationDaemon");
+ gboolean show_countdown = g_settings_get_boolean (gsettings, "show-countdown");
+ g_object_unref (gsettings);
+
+ if (show_countdown)
+ {
+ /* Ensure actions_box is visible for countdown */
+ gtk_widget_show(windata->actions_box);
+ update_content_hbox_visibility(windata);
+ create_pie_countdown(windata);
+ }
+ }
}
/* Set notification hints */
diff --git a/src/themes/slider/theme.c b/src/themes/slider/theme.c
index 09cc98a..4552af0 100644
--- a/src/themes/slider/theme.c
+++ b/src/themes/slider/theme.c
@@ -1,9 +1,8 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
- *
+/*
* Copyright (C) 2006-2007 Christian Hammond <[email protected]>
* Copyright (C) 2009 Red Hat, Inc.
* Copyright (C) 2011 Perberos <[email protected]>
- * Copyright (C) 2012-2021 MATE Developers
+ * Copyright (C) 2012-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
@@ -84,6 +83,8 @@ void set_notification_timeout(GtkWindow *nw, glong timeout);
void set_notification_hints(GtkWindow *nw, GVariant *hints);
void notification_tick(GtkWindow *nw, glong remaining);
gboolean get_always_stack(GtkWidget* nw);
+static gboolean on_countdown_draw (GtkWidget *widget, cairo_t *cr, WindowData *windata);
+static void create_pie_countdown(WindowData* windata);
#define WIDTH 400
#define DEFAULT_X0 0
@@ -234,14 +235,10 @@ static void paint_window (GtkWidget *widget,
windata->height);
cr2 = cairo_create (surface);
-
- /* transparent background */
cairo_rectangle (cr2, 0, 0, windata->width, windata->height);
- cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0);
+ cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0); // transparent background color
cairo_fill (cr2);
-
fill_background (widget, windata, cr2);
-
cairo_destroy(cr2);
cairo_save (cr);
@@ -495,6 +492,22 @@ void set_notification_timeout(GtkWindow *nw, glong timeout)
g_assert(windata != NULL);
windata->timeout = timeout;
+
+ /* Check if we should show countdown for all timed notifications */
+ if (timeout > 0)
+ {
+ GSettings *gsettings = g_settings_new ("org.mate.NotificationDaemon");
+ gboolean show_countdown = g_settings_get_boolean (gsettings, "show-countdown");
+ g_object_unref (gsettings);
+
+ if (show_countdown)
+ {
+ /* Ensure actions_box is visible for countdown */
+ gtk_widget_show(windata->actions_box);
+ update_content_hbox_visibility(windata);
+ create_pie_countdown(windata);
+ }
+ }
}
void notification_tick(GtkWindow* nw, glong remaining)
@@ -694,45 +707,37 @@ paint_countdown (GtkWidget *pie,
cairo_t* cr2;
cairo_surface_t* surface;
- context = gtk_widget_get_style_context(windata->win);
-
+ // :selected { background-color:#aabbcc; } or
+ // .notification-box .countdown:selected { background-color:#aabbcc; }
+ context = gtk_widget_get_style_context (pie);
gtk_style_context_save (context);
gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
-
get_background_color (context, GTK_STATE_FLAG_SELECTED, &bg);
-
gtk_style_context_restore (context);
gtk_widget_get_allocation(pie, &allocation);
- cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
- surface = cairo_surface_create_similar(cairo_get_target(cr),
- CAIRO_CONTENT_COLOR_ALPHA,
- allocation.width,
- allocation.height);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ surface = cairo_surface_create_similar (cairo_get_target(cr),
+ CAIRO_CONTENT_COLOR_ALPHA,
+ allocation.width,
+ allocation.height);
cr2 = cairo_create (surface);
-
- fill_background (pie, windata, cr2);
-
if (windata->timeout > 0)
{
gdouble pct = (gdouble) windata->remaining / (gdouble) windata->timeout;
-
gdk_cairo_set_source_rgba (cr2, &bg);
-
cairo_move_to (cr2, PIE_RADIUS, PIE_RADIUS);
cairo_arc_negative (cr2, PIE_RADIUS, PIE_RADIUS, PIE_RADIUS, -G_PI_2, -(pct * G_PI * 2) - G_PI_2);
cairo_line_to (cr2, PIE_RADIUS, PIE_RADIUS);
cairo_fill (cr2);
}
-
- cairo_destroy(cr2);
+ cairo_destroy (cr2);
cairo_save (cr);
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
cairo_restore (cr);
-
cairo_surface_destroy(surface);
}
@@ -744,6 +749,27 @@ on_countdown_draw (GtkWidget *widget, cairo_t *cr, WindowData *windata)
return FALSE;
}
+static void create_pie_countdown(WindowData* windata)
+{
+ if (windata->pie_countdown != NULL)
+ return; /* Already created */
+
+ windata->pie_countdown = gtk_drawing_area_new();
+ gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
+ gtk_widget_set_valign (windata->pie_countdown, GTK_ALIGN_CENTER);
+ gtk_widget_show(windata->pie_countdown);
+
+ #if GTK_CHECK_VERSION (4,0,0)
+ gtk_widget_add_css_class (windata->pie_countdown, "countdown");
+ #else
+ gtk_style_context_add_class (gtk_widget_get_style_context (windata->pie_countdown), "countdown");
+ #endif
+
+ gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
+ gtk_widget_set_size_request(windata->pie_countdown, PIE_WIDTH, PIE_HEIGHT);
+ g_signal_connect(G_OBJECT(windata->pie_countdown), "draw", G_CALLBACK(on_countdown_draw), windata);
+}
+
static void on_action_clicked(GtkWidget* w, GdkEventButton *event, ActionInvokedCb action_cb)
{
GtkWindow* nw = g_object_get_data(G_OBJECT(w), "_nw");
@@ -765,24 +791,11 @@ void add_notification_action(GtkWindow* nw, const char* text, const char* key, A
g_assert(windata != NULL);
- if (!gtk_widget_get_visible(windata->actions_box))
+ if (gtk_widget_get_visible(windata->actions_box))
{
gtk_widget_show(windata->actions_box);
update_content_hbox_visibility(windata);
-
- /* Don't try to re-add a pie_countdown */
- if (!windata->pie_countdown) {
- windata->pie_countdown = gtk_drawing_area_new();
- gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
- gtk_widget_set_valign (windata->pie_countdown, GTK_ALIGN_CENTER);
- gtk_widget_show(windata->pie_countdown);
-
- gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
- gtk_widget_set_size_request(windata->pie_countdown,
- PIE_WIDTH, PIE_HEIGHT);
- g_signal_connect(G_OBJECT(windata->pie_countdown), "draw",
- G_CALLBACK(on_countdown_draw), windata);
- }
+ create_pie_countdown(windata);
}
if (windata->action_icons) {
diff --git a/src/themes/standard/theme.c b/src/themes/standard/theme.c
index c21a61c..90171c6 100644
--- a/src/themes/standard/theme.c
+++ b/src/themes/standard/theme.c
@@ -1,9 +1,8 @@
-/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
- *
+/*
* Copyright (C) 2006-2007 Christian Hammond <[email protected]>
* Copyright (C) 2009 Red Hat, Inc.
* Copyright (C) 2011 Perberos <[email protected]>
- * Copyright (C) 2012-2021 MATE Developers
+ * Copyright (C) 2012-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
@@ -20,6 +19,7 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
+
#include "config.h"
#include <glib/gi18n.h>
@@ -100,6 +100,8 @@ void move_notification(GtkWidget *nw, int x, int y);
void set_notification_timeout(GtkWindow *nw, glong timeout);
void set_notification_hints(GtkWindow *nw, GVariant *hints);
void notification_tick(GtkWindow *nw, glong remaining);
+static void create_pie_countdown(WindowData* windata);
+static gboolean on_countdown_draw (GtkWidget *widget, cairo_t *cr, WindowData *windata);
//#define ENABLE_GRADIENT_LOOK
@@ -153,7 +155,6 @@ static void fill_background(GtkWidget* widget, WindowData* windata, cairo_t* cr)
#ifdef ENABLE_GRADIENT_LOOK
cairo_pattern_t *gradient;
int gradient_y;
-
gradient_y = allocation.height - BOTTOM_GRADIENT_HEIGHT;
#endif
@@ -836,6 +837,27 @@ void set_notification_hints(GtkWindow *nw, GVariant *hints)
}
}
+static void create_pie_countdown(WindowData* windata)
+{
+ if (windata->pie_countdown != NULL)
+ return; /* Already created */
+
+ windata->pie_countdown = gtk_drawing_area_new();
+ gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
+ gtk_widget_set_valign (windata->pie_countdown, GTK_ALIGN_CENTER);
+ gtk_widget_show(windata->pie_countdown);
+
+ #if GTK_CHECK_VERSION (4,0,0)
+ gtk_widget_add_css_class (windata->pie_countdown, "countdown");
+ #else
+ gtk_style_context_add_class (gtk_widget_get_style_context (windata->pie_countdown), "countdown");
+ #endif
+
+ gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
+ gtk_widget_set_size_request(windata->pie_countdown, PIE_WIDTH, PIE_HEIGHT);
+ g_signal_connect(G_OBJECT(windata->pie_countdown), "draw", G_CALLBACK(on_countdown_draw), windata);
+}
+
void set_notification_timeout(GtkWindow* nw, glong timeout)
{
WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
@@ -843,6 +865,22 @@ void set_notification_timeout(GtkWindow* nw, glong timeout)
g_assert(windata != NULL);
windata->timeout = timeout;
+
+ /* Check if we should show countdown for all timed notifications */
+ if (timeout > 0)
+ {
+ GSettings *gsettings = g_settings_new ("org.mate.NotificationDaemon");
+ gboolean show_countdown = g_settings_get_boolean (gsettings, "show-countdown");
+ g_object_unref (gsettings);
+
+ if (show_countdown)
+ {
+ /* Ensure actions_box is visible for countdown */
+ gtk_widget_show(windata->actions_box);
+ update_content_hbox_visibility(windata);
+ create_pie_countdown(windata);
+ }
+ }
}
void notification_tick(GtkWindow* nw, glong remaining)
@@ -999,45 +1037,37 @@ paint_countdown (GtkWidget *pie,
cairo_t* cr2;
cairo_surface_t* surface;
- context = gtk_widget_get_style_context (windata->win);
-
+ // :selected { background-color:#aabbcc; } or
+ // .notification-box .countdown:selected { background-color:#aabbcc; }
+ context = gtk_widget_get_style_context (pie);
gtk_style_context_save (context);
gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
-
get_background_color (context, GTK_STATE_FLAG_SELECTED, &bg);
-
gtk_style_context_restore (context);
gtk_widget_get_allocation(pie, &alloc);
- cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
surface = cairo_surface_create_similar (cairo_get_target(cr),
CAIRO_CONTENT_COLOR_ALPHA,
alloc.width,
alloc.height);
cr2 = cairo_create (surface);
-
- fill_background (pie, windata, cr2);
-
if (windata->timeout > 0)
{
gdouble pct = (gdouble) windata->remaining / (gdouble) windata->timeout;
-
gdk_cairo_set_source_rgba (cr2, &bg);
-
cairo_move_to (cr2, PIE_RADIUS, PIE_RADIUS);
cairo_arc_negative (cr2, PIE_RADIUS, PIE_RADIUS, PIE_RADIUS, -G_PI_2, -(pct * G_PI * 2) - G_PI_2);
cairo_line_to (cr2, PIE_RADIUS, PIE_RADIUS);
cairo_fill (cr2);
}
-
- cairo_destroy(cr2);
+ cairo_destroy (cr2);
cairo_save (cr);
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
cairo_restore (cr);
-
cairo_surface_destroy(surface);
}
@@ -1075,19 +1105,7 @@ void add_notification_action(GtkWindow* nw, const char* text, const char* key, A
{
gtk_widget_show(windata->actions_box);
update_content_hbox_visibility(windata);
-
- /* Don't try to re-add a pie_countdown */
- if (!windata->pie_countdown) {
- windata->pie_countdown = gtk_drawing_area_new();
- gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
- gtk_widget_show(windata->pie_countdown);
-
- gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
- gtk_widget_set_size_request(windata->pie_countdown,
- PIE_WIDTH, PIE_HEIGHT);
- g_signal_connect(G_OBJECT(windata->pie_countdown), "draw",
- G_CALLBACK(on_countdown_draw), windata);
- }
+ create_pie_countdown(windata);
}
if (windata->action_icons) {