diff options
40 files changed, 2582 insertions, 320 deletions
@@ -1,3 +1,26 @@ +### mate-notification-daemon 1.29.0 + + * update translations + * daemon: Fix use-after-free crash in idle reposition timeout + * daemon: Fix crash when monitor is disconnected + * history: Add enable/disable toggle + * capplet: Add notification count badge + * capplet: Add notification history context module + * capplet: Add D-Bus context for notification daemon communication + * daemon: Add notification history tracking API + * daemon: Bump libwnck to 43.0 + * capplet: Add timeout and persistence controls + * daemon: implement Desktop Notifications Specification 1.3 + * daemon: report dbus method invocation as handled on error + * Fix background of nodoka theme in RTL + * Crate a second notification with a button + * Use transparent background for the coutdown + * Allow to set countdown color from CSS + * Add countdown CSS class + * Add actions-box CSS class + * Add also theme name + * Add notification-box CSS class + ### mate-notification-daemon 1.28.3 * ci:fix faraday version issue blocking Travis deployment diff --git a/configure.ac b/configure.ac index a2a2ee7..d2f66b5 100644 --- a/configure.ac +++ b/configure.ac @@ -3,7 +3,7 @@ dnl Process this file with autoconf to create configure. dnl ################################################################ dnl # Initialize autoconf dnl ################################################################ -AC_INIT([mate-notification-daemon], [1.28.3], [https://mate-desktop.org]) +AC_INIT([mate-notification-daemon], [1.29.0], [https://mate-desktop.org]) AC_PREREQ(2.63) AC_CONFIG_HEADERS([config.h]) @@ -179,7 +179,7 @@ fi dnl Check if we have the X development libraries have_x11=no if test "x$enable_x11" != "xno"; then - x11_pkg_modules="libwnck-3.0, x11" + x11_pkg_modules="libwnck-3.0 >= 43.0, x11" PKG_CHECK_MODULES(NOTIFICATION_DAEMON_X11, $x11_pkg_modules, [ have_x11=yes AC_DEFINE(HAVE_X11, 1, [Have the X11 development library]) diff --git a/data/org.mate.NotificationDaemon.gschema.xml.in b/data/org.mate.NotificationDaemon.gschema.xml.in index 17ea761..80beac4 100644 --- a/data/org.mate.NotificationDaemon.gschema.xml.in +++ b/data/org.mate.NotificationDaemon.gschema.xml.in @@ -30,5 +30,26 @@ <summary>Do not disturb</summary> <description>When enabled, notifications are not shown.</description> </key> + <key name="default-timeout" type="i"> + <range min="0" max="300000"/> + <default>7000</default> + <summary>Default timeout</summary> + <description>Default timeout for notifications in milliseconds. Use 0 for no timeout (persistent).</description> + </key> + <key name="enable-persistence" type="b"> + <default>true</default> + <summary>Enable persistence</summary> + <description>Allow notifications to be persistent when requested by applications.</description> + </key> + <key name="show-countdown" type="b"> + <default>false</default> + <summary>Show countdown</summary> + <description>Show countdown timer for all non-persistent notifications. If false, only show it for those waiting for user actions.</description> + </key> + <key name="history-enabled" type="b"> + <default>true</default> + <summary>Enable notification history</summary> + <description>When enabled, notifications are stored in history for later viewing. When disabled, no notification history is kept for privacy. Note: This only controls MATE's notification daemon storage; other applications may still have access to notifications through system logs or other means.</description> + </key> </schema> </schemalist> diff --git a/data/org.mate.applets.MateNotificationApplet.desktop.in.in b/data/org.mate.applets.MateNotificationApplet.desktop.in.in index 5b7c538..a335cf7 100644 --- a/data/org.mate.applets.MateNotificationApplet.desktop.in.in +++ b/data/org.mate.applets.MateNotificationApplet.desktop.in.in @@ -6,8 +6,8 @@ Name=Mate Notification Applet Factory Description=Mate Notification Applet Factory [MateNotificationApplet] -Name=Do Not Disturb -Description=Activate the do not disturb mode. +Name=Notification Status +Description=Monitor and control notification status. # Translators: Do NOT translate or transliterate this text (this is an icon file name)! Icon=mate-notification-properties MateComponentId=OAFIID:MATE_Panel_Notification_Applet @@ -5,7 +5,7 @@ # # Translators: # Pandi3a <[email protected]>, 2018 -# Georgi Georgiev (Жоро) <[email protected]>, 2018 +# Georgi Georgiev (RacerBG) <[email protected]>, 2018 # breaker loc <[email protected]>, 2018 # Любомир Василев, 2021 # Замфир Йончев <[email protected]>, 2021 @@ -9,7 +9,7 @@ # Stefano Karapetsas <[email protected]>, 2021 # Tobias Bannert <[email protected]>, 2021 # Marcel Artz, 2021 -# Wolfgang Ulbrich <[email protected]>, 2023 +# Wolfgang Ulbrich <[email protected]>, 2023 # msgid "" msgstr "" @@ -17,7 +17,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Wolfgang Ulbrich <[email protected]>, 2023\n" +"Last-Translator: Wolfgang Ulbrich <[email protected]>, 2023\n" "Language-Team: German (https://app.transifex.com/mate/teams/13566/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -9,7 +9,7 @@ # Νίκος Κοντ. <[email protected]>, 2018 # 437c9d6e19936ed69f57bed9e0fe4716, 2018 # gapan <[email protected]>, 2020 -# Efstathios Iosifidis <[email protected]>, 2021 +# Efstathios Iosifidis <[email protected]>, 2021 # Stefano Karapetsas <[email protected]>, 2021 # Alexandros Kapetanios <[email protected]>, 2021 # @@ -7,7 +7,7 @@ # Lluís Tusquellas <[email protected]>, 2018 # Juan Sánchez <[email protected]>, 2018 # Mario Verdin <[email protected]>, 2018 -# Adolfo Jayme-Barrientos, 2020 +# Adolfo Jayme Barrientos, 2020 # Emiliano Fascetti, 2020 # Stefano Karapetsas <[email protected]>, 2021 # Toni Estévez <[email protected]>, 2021 diff --git a/po/es_CL.po b/po/es_CL.po index 1ecf740..f4e7005 100644 --- a/po/es_CL.po +++ b/po/es_CL.po @@ -6,7 +6,7 @@ # Translators: # Alejo_K <[email protected]>, 2018 # Robert Petitpas <[email protected]>, 2018 -# Pablo Lezaeta Reyes <[email protected]>, 2021 +# Pablo Lezaeta (Jota) <[email protected]>, 2021 # msgid "" msgstr "" @@ -14,7 +14,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Pablo Lezaeta Reyes <[email protected]>, 2021\n" +"Last-Translator: Pablo Lezaeta (Jota) <[email protected]>, 2021\n" "Language-Team: Spanish (Chile) (https://app.transifex.com/mate/teams/13566/es_CL/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -7,7 +7,7 @@ # Eslam Ali <[email protected]>, 2020 # nomen omen, 2020 # Kimmo Kujansuu <[email protected]>, 2020 -# Lasse Liehu <[email protected]>, 2021 +# Lasse Liehu-Inui <[email protected]>, 2021 # Mikko Harhanen <[email protected]>, 2021 # msgid "" @@ -7,7 +7,7 @@ # mauron, 2018 # yoplait <[email protected]>, 2018 # brice nice <[email protected]>, 2018 -# roxfr <[email protected]>, 2020 +# roxfr, 2020 # Tubuntu, 2020 # Stefano Karapetsas <[email protected]>, 2021 # jeremy shields <[email protected]>, 2021 @@ -6,23 +6,23 @@ # Translators: # Stefano Karapetsas <[email protected]>, 2018 # Edward Sawyer <[email protected]>, 2018 -# shy tzedaka <[email protected]>, 2020 # Yaron Shahrabani <[email protected]>, 2021 # 63f334ffc0709ba0fc2361b80bf3c0f0_00ffd1e <ab96c93ca0ac55ba7fa06385427e60dd_878890>, 2021 +# Avi Markovitz <[email protected]>, 2024 # msgid "" msgstr "" -"Project-Id-Version: mate-notification-daemon 1.26.0\n" +"Project-Id-Version: mate-notification-daemon 1.27.1\n" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" -"POT-Creation-Date: 2022-10-29 15:33+0200\n" +"POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: 63f334ffc0709ba0fc2361b80bf3c0f0_00ffd1e <ab96c93ca0ac55ba7fa06385427e60dd_878890>, 2021\n" +"Last-Translator: Avi Markovitz <[email protected]>, 2024\n" "Language-Team: Hebrew (https://app.transifex.com/mate/teams/13566/he/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: he\n" -"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n" +"Plural-Forms: nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;\n" #: data/mate-notification-properties.desktop.in:3 msgid "Popup Notifications" @@ -38,13 +38,13 @@ msgstr "הגדרת העדפות ההתרעות הקופצות שלך" msgid "MATE;Notification;Theme;" msgstr "MATE;התראה;ערכת עיצוב;ערכת נושא;" -#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:5 +#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:6 msgid "Mate Notification Applet Factory" msgstr "" -#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:9 -#: src/capplet/mate-notification-applet.c:176 -#: src/capplet/mate-notification-applet.c:193 +#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:10 +#: src/capplet/mate-notification-applet.c:177 +#: src/capplet/mate-notification-applet.c:196 msgid "Do Not Disturb" msgstr "" @@ -131,15 +131,15 @@ msgstr "" "Yaron Shahrabani <[email protected]>\n" "Omer I.S. <[email protected]>" -#: src/capplet/mate-notification-applet.c:194 +#: src/capplet/mate-notification-applet.c:197 msgid "Notifications Enabled" msgstr "" -#: src/capplet/mate-notification-applet.c:215 +#: src/capplet/mate-notification-applet.c:218 msgid "_Do not disturb" msgstr "" -#: src/capplet/mate-notification-applet.c:216 +#: src/capplet/mate-notification-applet.c:219 msgid "Enable/Disable do-not-disturb mode." msgstr "" @@ -191,7 +191,7 @@ msgstr "_תצוגה מקדימה" #: src/capplet/mate-notification-properties.ui:52 msgid "_Close" -msgstr "_סגור" +msgstr "_סגירה" #: src/capplet/mate-notification-properties.ui:101 msgid "_Theme:" @@ -229,11 +229,11 @@ msgstr "הפעלת מצב לא להפריע" msgid "General Options" msgstr "אפשרויות כלליות" -#: src/daemon/daemon.c:1341 +#: src/daemon/daemon.c:1359 msgid "Exceeded maximum number of notifications" msgstr "מספר ההתרעות המרבי הושג" -#: src/daemon/daemon.c:1646 +#: src/daemon/daemon.c:1664 #, c-format msgid "%u is not a valid notification ID" msgstr "%u אינו מזהה התרעה תקני" @@ -8,6 +8,7 @@ # Rezső Páder <[email protected]>, 2020 # Stefano Karapetsas <[email protected]>, 2021 # Balázs Meskó <[email protected]>, 2021 +# László Varga, 2024 # msgid "" msgstr "" @@ -15,7 +16,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Balázs Meskó <[email protected]>, 2021\n" +"Last-Translator: László Varga, 2024\n" "Language-Team: Hungarian (https://app.transifex.com/mate/teams/13566/hu/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -39,13 +40,13 @@ msgstr "MATE;Értesítés;Téma;" #: data/org.mate.applets.MateNotificationApplet.desktop.in.in:6 msgid "Mate Notification Applet Factory" -msgstr "" +msgstr "Mate értesítési alkalmazás" #: data/org.mate.applets.MateNotificationApplet.desktop.in.in:10 #: src/capplet/mate-notification-applet.c:177 #: src/capplet/mate-notification-applet.c:196 msgid "Do Not Disturb" -msgstr "" +msgstr "Ne zavarjanak" #: data/org.mate.NotificationDaemon.gschema.xml.in:5 msgid "Popup location" @@ -115,15 +116,15 @@ msgstr "_Névjegy" #: src/capplet/mate-notification-applet.c:105 msgid "About Do Not Disturb" -msgstr "" +msgstr "A ne zavarjanak-ról" #: src/capplet/mate-notification-applet.c:107 msgid "Copyright © 2021 MATE developers" -msgstr "" +msgstr "Minden jog fenntartva © 2021 MATE fejlesztők" #: src/capplet/mate-notification-applet.c:108 msgid "Activate the do not disturb mode quickly." -msgstr "" +msgstr "A ne zavarjanak funkció gyors akvitálása" #: src/capplet/mate-notification-applet.c:110 msgid "translator-credits" @@ -139,15 +140,15 @@ msgstr "" #: src/capplet/mate-notification-applet.c:197 msgid "Notifications Enabled" -msgstr "" +msgstr "Értesítések engedélyezve" #: src/capplet/mate-notification-applet.c:218 msgid "_Do not disturb" -msgstr "" +msgstr "_Ne zavarjanak" #: src/capplet/mate-notification-applet.c:219 msgid "Enable/Disable do-not-disturb mode." -msgstr "" +msgstr "Ne zavarjanak mód engedélyezése/tiltása" #: src/capplet/mate-notification-properties.c:327 msgid "Coco" @@ -4,7 +4,7 @@ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # # Translators: -# Slimane Selyan AMIRI <[email protected]>, 2021 +# Azwaw <[email protected]>, 2021 # msgid "" msgstr "" @@ -12,7 +12,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Slimane Selyan AMIRI <[email protected]>, 2021\n" +"Last-Translator: Azwaw <[email protected]>, 2021\n" "Language-Team: Kabyle (https://app.transifex.com/mate/teams/13566/kab/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5,7 +5,7 @@ # # Translators: # Imants Liepiņš <[email protected]>, 2018 -# duck <[email protected]>, 2020 +# reki the human <[email protected]>, 2020 # Rihards Priedītis <[email protected]>, 2021 # Stefano Karapetsas <[email protected]>, 2021 # diff --git a/po/nan.po b/po/nan.po new file mode 100644 index 0000000..6e58cef --- /dev/null +++ b/po/nan.po @@ -0,0 +1,249 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR MATE Desktop Environment team +# This file is distributed under the same license as the mate-notification-daemon package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +# Translators: +# Tan, Kian-ting, 2024 +# +msgid "" +msgstr "" +"Project-Id-Version: mate-notification-daemon 1.27.1\n" +"Report-Msgid-Bugs-To: https://mate-desktop.org\n" +"POT-Creation-Date: 2023-09-02 17:27+0200\n" +"PO-Revision-Date: 2018-03-11 19:46+0000\n" +"Last-Translator: Tan, Kian-ting, 2024\n" +"Language-Team: Chinese (Min Nan) (https://app.transifex.com/mate/teams/13566/nan/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nan\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: data/mate-notification-properties.desktop.in:3 +msgid "Popup Notifications" +msgstr "" + +#: data/mate-notification-properties.desktop.in:4 +msgid "Set your popup notification preferences" +msgstr "" + +#. Translators: Search terms to find this application. Do NOT translate or +#. localize the semicolons! The list MUST also end with a semicolon! +#: data/mate-notification-properties.desktop.in:14 +msgid "MATE;Notification;Theme;" +msgstr "" + +#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:6 +msgid "Mate Notification Applet Factory" +msgstr "" + +#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:10 +#: src/capplet/mate-notification-applet.c:177 +#: src/capplet/mate-notification-applet.c:196 +msgid "Do Not Disturb" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:5 +msgid "Popup location" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:6 +msgid "" +"Default popup location on the workspace for stack notifications. Allowed " +"values: \"top_left\",\"top_right\",\"bottom_left\" and \"bottom_right\"" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:10 +#: src/capplet/mate-notification-properties.ui:186 +msgid "Use Active Monitor" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:11 +msgid "Display the notification on the active monitor." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:15 +msgid "Monitor" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:16 +msgid "" +"Monitor to display the notification. Allowed values: -1 (display on active " +"monitor) and 0 to n - 1 where n is the number of monitors." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:20 +msgid "Current theme" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:21 +msgid "The theme used when displaying notifications." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:25 +msgid "Sound Enabled" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:26 +msgid "Turns on and off sound support for notifications." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:30 +msgid "Do not disturb" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:31 +msgid "When enabled, notifications are not shown." +msgstr "" + +#: src/capplet/mate-notification-applet.c:52 +msgid "_Preferences" +msgstr "" + +#: src/capplet/mate-notification-applet.c:54 +msgid "_About" +msgstr "關係 (_A)" + +#: src/capplet/mate-notification-applet.c:105 +msgid "About Do Not Disturb" +msgstr "" + +#: src/capplet/mate-notification-applet.c:107 +msgid "Copyright © 2021 MATE developers" +msgstr "" + +#: src/capplet/mate-notification-applet.c:108 +msgid "Activate the do not disturb mode quickly." +msgstr "" + +#: src/capplet/mate-notification-applet.c:110 +msgid "translator-credits" +msgstr "Tan Kian-ting <[email protected]>, 2023" + +#: src/capplet/mate-notification-applet.c:197 +msgid "Notifications Enabled" +msgstr "" + +#: src/capplet/mate-notification-applet.c:218 +msgid "_Do not disturb" +msgstr "" + +#: src/capplet/mate-notification-applet.c:219 +msgid "Enable/Disable do-not-disturb mode." +msgstr "" + +#: src/capplet/mate-notification-properties.c:327 +msgid "Coco" +msgstr "" + +#: src/capplet/mate-notification-properties.c:331 +msgid "Nodoka" +msgstr "" + +#: src/capplet/mate-notification-properties.c:335 +msgid "Slider" +msgstr "" + +#: src/capplet/mate-notification-properties.c:339 +msgid "Standard theme" +msgstr "" + +#: src/capplet/mate-notification-properties.c:402 +msgid "Error initializing libmatenotify" +msgstr "" + +#: src/capplet/mate-notification-properties.c:415 +msgid "Notification Test" +msgstr "" + +#: src/capplet/mate-notification-properties.c:415 +msgid "Just a test" +msgstr "" + +#: src/capplet/mate-notification-properties.c:419 +#, c-format +msgid "Error while displaying notification: %s" +msgstr "" + +#: src/capplet/mate-notification-properties.c:459 +#, c-format +msgid "Could not load user interface: %s" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:18 +msgid "Notification Settings" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:36 +msgid "_Preview" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:52 +msgid "_Close" +msgstr "關掉(_C)" + +#: src/capplet/mate-notification-properties.ui:101 +msgid "_Theme:" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:115 +msgid "P_osition:" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:129 +msgid "_Monitor:" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:155 +msgid "Top Left" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:156 +msgid "Top Right" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:157 +msgid "Bottom Left" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:158 +msgid "Bottom Right" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:201 +msgid "Enable Do Not Disturb" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:222 +msgid "General Options" +msgstr "" + +#: src/daemon/daemon.c:1359 +msgid "Exceeded maximum number of notifications" +msgstr "" + +#: src/daemon/daemon.c:1664 +#, c-format +msgid "%u is not a valid notification ID" +msgstr "" + +#: src/daemon/sound.c:37 +msgid "Notification" +msgstr "" + +#: src/themes/coco/coco-theme.c:461 src/themes/nodoka/nodoka-theme.c:815 +#: src/themes/slider/theme.c:417 src/themes/standard/theme.c:734 +msgid "Notification summary text." +msgstr "" + +#: src/themes/coco/coco-theme.c:475 src/themes/nodoka/nodoka-theme.c:868 +#: src/themes/slider/theme.c:438 src/themes/standard/theme.c:786 +msgid "Notification body text." +msgstr "" + +#: src/themes/nodoka/nodoka-theme.c:829 src/themes/nodoka/nodoka-theme.c:831 +#: src/themes/slider/theme.c:399 src/themes/slider/theme.c:401 +#: src/themes/standard/theme.c:750 src/themes/standard/theme.c:752 +msgid "Closes the notification." +msgstr "" @@ -8,7 +8,7 @@ # Stefano Karapetsas <[email protected]>, 2018 # Allan Nordhøy <[email protected]>, 2021 # 87d96f43665dd9fb55eba4603e184cae, 2021 -# Kaci Heskjestad, 2021 +# heskjestad, 2021 # msgid "" msgstr "" @@ -16,7 +16,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Kaci Heskjestad, 2021\n" +"Last-Translator: heskjestad, 2021\n" "Language-Team: Norwegian Bokmål (https://app.transifex.com/mate/teams/13566/nb/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5,7 +5,7 @@ # # Translators: # Stefano Karapetsas <[email protected]>, 2021 -# Benedikt Straub <[email protected]>, 2023 +# Benedikt Straub <[email protected]>, 2025 # msgid "" msgstr "" @@ -13,7 +13,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Benedikt Straub <[email protected]>, 2023\n" +"Last-Translator: Benedikt Straub <[email protected]>, 2025\n" "Language-Team: Low German (https://app.transifex.com/mate/teams/13566/nds/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -92,7 +92,7 @@ msgstr "Gedrüüs an" #: data/org.mate.NotificationDaemon.gschema.xml.in:26 msgid "Turns on and off sound support for notifications." -msgstr "Maakt Gedrüüs-Unnerstütten för Narichten at oder ut." +msgstr "Maakt Gedrüüs-Unnerstütten för Narichten at of ut." #: data/org.mate.NotificationDaemon.gschema.xml.in:30 msgid "Do not disturb" @@ -104,7 +104,7 @@ msgstr "Wenn an, worden keene Mitdeelens wiest." #: src/capplet/mate-notification-applet.c:52 msgid "_Preferences" -msgstr "_Eegenschapten" +msgstr "_Eegenskuppen" #: src/capplet/mate-notification-applet.c:54 msgid "_About" @@ -4,7 +4,7 @@ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # # Translators: -# Piotr Strębski <[email protected]>, 2018 +# Piotr Strebski <[email protected]>, 2018 # Michal Herman <[email protected]>, 2018 # Marcin Kralka <[email protected]>, 2018 # Dominik Adrian Grzywak, 2018 @@ -15,6 +15,7 @@ # Kristoffer Grundström <[email protected]>, 2020 # Patrik Nilsson <[email protected]>, 2021 # Luna Jernberg <[email protected]>, 2021 +# Daniel Nylander <[email protected]>, 2025 # msgid "" msgstr "" @@ -22,7 +23,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Luna Jernberg <[email protected]>, 2021\n" +"Last-Translator: Daniel Nylander <[email protected]>, 2025\n" "Language-Team: Swedish (https://app.transifex.com/mate/teams/13566/sv/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -46,13 +47,13 @@ msgstr "MATE;Notification;Theme;" #: data/org.mate.applets.MateNotificationApplet.desktop.in.in:6 msgid "Mate Notification Applet Factory" -msgstr "" +msgstr "Mate notifieringsappletfabrik" #: data/org.mate.applets.MateNotificationApplet.desktop.in.in:10 #: src/capplet/mate-notification-applet.c:177 #: src/capplet/mate-notification-applet.c:196 msgid "Do Not Disturb" -msgstr "" +msgstr "Stör inte" #: data/org.mate.NotificationDaemon.gschema.xml.in:5 msgid "Popup location" @@ -109,7 +110,7 @@ msgstr "Stör ej" #: data/org.mate.NotificationDaemon.gschema.xml.in:31 msgid "When enabled, notifications are not shown." -msgstr "" +msgstr "När aktiverad kommer notifieringar inte att visas." #: src/capplet/mate-notification-applet.c:52 msgid "_Preferences" @@ -121,7 +122,7 @@ msgstr "_Om" #: src/capplet/mate-notification-applet.c:105 msgid "About Do Not Disturb" -msgstr "" +msgstr "Om Stör inte" #: src/capplet/mate-notification-applet.c:107 msgid "Copyright © 2021 MATE developers" @@ -129,7 +130,7 @@ msgstr "Copyright © 2021 MATE utvecklarna" #: src/capplet/mate-notification-applet.c:108 msgid "Activate the do not disturb mode quickly." -msgstr "" +msgstr "Aktivera stör inte-läget snabbt." #: src/capplet/mate-notification-applet.c:110 msgid "translator-credits" @@ -140,15 +141,15 @@ msgstr "" #: src/capplet/mate-notification-applet.c:197 msgid "Notifications Enabled" -msgstr "" +msgstr "Notifieringar aktiverade" #: src/capplet/mate-notification-applet.c:218 msgid "_Do not disturb" -msgstr "" +msgstr "Stör _inte" #: src/capplet/mate-notification-applet.c:219 msgid "Enable/Disable do-not-disturb mode." -msgstr "" +msgstr "Aktivera/inaktivera stör inte-läge." #: src/capplet/mate-notification-properties.c:327 msgid "Coco" @@ -11,7 +11,7 @@ # Stefano Karapetsas <[email protected]>, 2021 # hilalis <[email protected]>, 2021 # Mehmet, 2022 -# Sabri Ünal <[email protected]>, 2022 +# b83946de5835331df42b9ffcc43e6a33_05e65cd <73a30e0a984b2291d4915f37112ad292_814039>, 2022 # msgid "" msgstr "" @@ -19,7 +19,7 @@ msgstr "" "Report-Msgid-Bugs-To: https://mate-desktop.org\n" "POT-Creation-Date: 2023-09-02 17:27+0200\n" "PO-Revision-Date: 2018-03-11 19:46+0000\n" -"Last-Translator: Sabri Ünal <[email protected]>, 2022\n" +"Last-Translator: b83946de5835331df42b9ffcc43e6a33_05e65cd <73a30e0a984b2291d4915f37112ad292_814039>, 2022\n" "Language-Team: Turkish (https://app.transifex.com/mate/teams/13566/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/po/tzm.po b/po/tzm.po new file mode 100644 index 0000000..8734346 --- /dev/null +++ b/po/tzm.po @@ -0,0 +1,249 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR MATE Desktop Environment team +# This file is distributed under the same license as the mate-notification-daemon package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +# Translators: +# Hakim Oubouali <[email protected]>, 2021 +# +msgid "" +msgstr "" +"Project-Id-Version: mate-notification-daemon 1.27.1\n" +"Report-Msgid-Bugs-To: https://mate-desktop.org\n" +"POT-Creation-Date: 2023-09-02 17:27+0200\n" +"PO-Revision-Date: 2018-03-11 19:46+0000\n" +"Last-Translator: Hakim Oubouali <[email protected]>, 2021\n" +"Language-Team: Central Atlas Tamazight (https://app.transifex.com/mate/teams/13566/tzm/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tzm\n" +"Plural-Forms: nplurals=2; plural=(n == 0 || n == 1 || (n > 10 && n < 100) ? 0 : 1;\n" + +#: data/mate-notification-properties.desktop.in:3 +msgid "Popup Notifications" +msgstr "" + +#: data/mate-notification-properties.desktop.in:4 +msgid "Set your popup notification preferences" +msgstr "" + +#. Translators: Search terms to find this application. Do NOT translate or +#. localize the semicolons! The list MUST also end with a semicolon! +#: data/mate-notification-properties.desktop.in:14 +msgid "MATE;Notification;Theme;" +msgstr "" + +#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:6 +msgid "Mate Notification Applet Factory" +msgstr "" + +#: data/org.mate.applets.MateNotificationApplet.desktop.in.in:10 +#: src/capplet/mate-notification-applet.c:177 +#: src/capplet/mate-notification-applet.c:196 +msgid "Do Not Disturb" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:5 +msgid "Popup location" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:6 +msgid "" +"Default popup location on the workspace for stack notifications. Allowed " +"values: \"top_left\",\"top_right\",\"bottom_left\" and \"bottom_right\"" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:10 +#: src/capplet/mate-notification-properties.ui:186 +msgid "Use Active Monitor" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:11 +msgid "Display the notification on the active monitor." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:15 +msgid "Monitor" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:16 +msgid "" +"Monitor to display the notification. Allowed values: -1 (display on active " +"monitor) and 0 to n - 1 where n is the number of monitors." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:20 +msgid "Current theme" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:21 +msgid "The theme used when displaying notifications." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:25 +msgid "Sound Enabled" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:26 +msgid "Turns on and off sound support for notifications." +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:30 +msgid "Do not disturb" +msgstr "" + +#: data/org.mate.NotificationDaemon.gschema.xml.in:31 +msgid "When enabled, notifications are not shown." +msgstr "" + +#: src/capplet/mate-notification-applet.c:52 +msgid "_Preferences" +msgstr "" + +#: src/capplet/mate-notification-applet.c:54 +msgid "_About" +msgstr "_Ɣef" + +#: src/capplet/mate-notification-applet.c:105 +msgid "About Do Not Disturb" +msgstr "" + +#: src/capplet/mate-notification-applet.c:107 +msgid "Copyright © 2021 MATE developers" +msgstr "" + +#: src/capplet/mate-notification-applet.c:108 +msgid "Activate the do not disturb mode quickly." +msgstr "" + +#: src/capplet/mate-notification-applet.c:110 +msgid "translator-credits" +msgstr "" + +#: src/capplet/mate-notification-applet.c:197 +msgid "Notifications Enabled" +msgstr "" + +#: src/capplet/mate-notification-applet.c:218 +msgid "_Do not disturb" +msgstr "" + +#: src/capplet/mate-notification-applet.c:219 +msgid "Enable/Disable do-not-disturb mode." +msgstr "" + +#: src/capplet/mate-notification-properties.c:327 +msgid "Coco" +msgstr "" + +#: src/capplet/mate-notification-properties.c:331 +msgid "Nodoka" +msgstr "" + +#: src/capplet/mate-notification-properties.c:335 +msgid "Slider" +msgstr "" + +#: src/capplet/mate-notification-properties.c:339 +msgid "Standard theme" +msgstr "" + +#: src/capplet/mate-notification-properties.c:402 +msgid "Error initializing libmatenotify" +msgstr "" + +#: src/capplet/mate-notification-properties.c:415 +msgid "Notification Test" +msgstr "" + +#: src/capplet/mate-notification-properties.c:415 +msgid "Just a test" +msgstr "" + +#: src/capplet/mate-notification-properties.c:419 +#, c-format +msgid "Error while displaying notification: %s" +msgstr "" + +#: src/capplet/mate-notification-properties.c:459 +#, c-format +msgid "Could not load user interface: %s" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:18 +msgid "Notification Settings" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:36 +msgid "_Preview" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:52 +msgid "_Close" +msgstr "_Rgel" + +#: src/capplet/mate-notification-properties.ui:101 +msgid "_Theme:" +msgstr "A_sgum:" + +#: src/capplet/mate-notification-properties.ui:115 +msgid "P_osition:" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:129 +msgid "_Monitor:" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:155 +msgid "Top Left" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:156 +msgid "Top Right" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:157 +msgid "Bottom Left" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:158 +msgid "Bottom Right" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:201 +msgid "Enable Do Not Disturb" +msgstr "" + +#: src/capplet/mate-notification-properties.ui:222 +msgid "General Options" +msgstr "" + +#: src/daemon/daemon.c:1359 +msgid "Exceeded maximum number of notifications" +msgstr "" + +#: src/daemon/daemon.c:1664 +#, c-format +msgid "%u is not a valid notification ID" +msgstr "" + +#: src/daemon/sound.c:37 +msgid "Notification" +msgstr "Tineɣmisin" + +#: src/themes/coco/coco-theme.c:461 src/themes/nodoka/nodoka-theme.c:815 +#: src/themes/slider/theme.c:417 src/themes/standard/theme.c:734 +msgid "Notification summary text." +msgstr "" + +#: src/themes/coco/coco-theme.c:475 src/themes/nodoka/nodoka-theme.c:868 +#: src/themes/slider/theme.c:438 src/themes/standard/theme.c:786 +msgid "Notification body text." +msgstr "" + +#: src/themes/nodoka/nodoka-theme.c:829 src/themes/nodoka/nodoka-theme.c:831 +#: src/themes/slider/theme.c:399 src/themes/slider/theme.c:401 +#: src/themes/standard/theme.c:750 src/themes/standard/theme.c:752 +msgid "Closes the notification." +msgstr "" @@ -6,7 +6,7 @@ # Translators: # Rax Garfield <[email protected]>, 2018 # zubr139, 2018 -# Шаповалов Анатолій Романович <[email protected]>, 2018 +# Анатолій Романович Шаповалов <[email protected]>, 2018 # Юрій Яновський <[email protected]>, 2021 # Микола Ткач <[email protected]>, 2021 # 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, + ×tamp, &closed_timestamp, &reason, &urgency, &read)) { + notification_count++; + /* Add each notification as a new row in the list */ + GtkWidget *row = create_notification_row (id, app_name, app_icon, summary, body, timestamp); + gtk_list_box_insert (GTK_LIST_BOX (list_box), row, -1); + } + + g_variant_iter_free (iter); + g_variant_unref (result); + } + + /* Add message if list is empty */ + if (gtk_list_box_get_row_at_index (GTK_LIST_BOX (list_box), 0) == NULL) { + GtkWidget *row = gtk_list_box_row_new (); + GtkWidget *label = gtk_label_new (_("No notifications")); + gtk_widget_set_sensitive (label, FALSE); + gtk_container_set_border_width (GTK_CONTAINER (row), 12); + gtk_container_add (GTK_CONTAINER (row), label); + gtk_list_box_insert (GTK_LIST_BOX (list_box), row, -1); + } + + /* Add this window for the list of notifications */ + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + + /* Calculate to ensure the popup is the right height: + * 80% of screen height, with some room for buttons */ + GdkScreen *screen = gtk_widget_get_screen (context->main_widget); + gint max_content_height = (gdk_screen_get_height (screen) * 0.8) - 100; + + gint content_height = 100; /* 100px minimum */ + if (notification_count > 0) + content_height = MIN(notification_count * content_height, max_content_height); + + /* Now force the scrollable window to be the calculated height */ + gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (scrolled_window), max_content_height); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scrolled_window), content_height); + gtk_container_add (GTK_CONTAINER (scrolled_window), list_box); + + gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 0); + + /* Add a box for action buttons */ + button_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_container_set_border_width (GTK_CONTAINER (button_box), 6); + + /* DND toggle first */ + dnd_toggle = gtk_check_button_new_with_label (_("Do not disturb")); + gtk_widget_set_halign (dnd_toggle, GTK_ALIGN_START); + + if (context->settings) { + gboolean dnd_enabled = g_settings_get_boolean (context->settings, "do-not-disturb"); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (dnd_toggle), dnd_enabled); + } + + g_signal_connect (dnd_toggle, "toggled", G_CALLBACK (dnd_toggle_clicked), context); + gtk_box_pack_start (GTK_BOX (button_box), dnd_toggle, FALSE, FALSE, 0); + + /* Spacer to push buttons to the right */ + GtkWidget *spacer = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start (GTK_BOX (button_box), spacer, TRUE, TRUE, 0); + + /* Then the action buttons */ + clear_button = gtk_button_new_with_label (_("Clear All")); + close_button = gtk_button_new_with_label (_("Close")); + + g_signal_connect (clear_button, "clicked", G_CALLBACK (clear_history), context); + g_signal_connect_swapped (close_button, "clicked", G_CALLBACK (gtk_widget_destroy), popup); + + gtk_box_pack_end (GTK_BOX (button_box), close_button, FALSE, FALSE, 0); + gtk_box_pack_end (GTK_BOX (button_box), clear_button, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (vbox), button_box, FALSE, FALSE, 0); + + /* 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 & 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) { |
