From 9898c48c67e4c83a62d9c396676a922fae8658a0 Mon Sep 17 00:00:00 2001 From: Arif Hasanic Date: Mon, 2 Feb 2026 23:42:08 +0100 Subject: [PATCH] hover pauses notification --- include/services/notificationController.hpp | 5 +- .../widgets/notification/baseNotification.hpp | 18 ++- resources/notification.css | 12 +- src/services/notificationController.cpp | 104 +++++++++++------- src/widgets/notification/baseNotification.cpp | 92 ++++++++++++++++ .../notification/notificationWindow.cpp | 26 ++--- 6 files changed, 199 insertions(+), 58 deletions(-) diff --git a/include/services/notificationController.hpp b/include/services/notificationController.hpp index b95ce99..32e5fa2 100644 --- a/include/services/notificationController.hpp +++ b/include/services/notificationController.hpp @@ -28,8 +28,9 @@ class NotificationController { private: uint64_t globalNotificationId = 1; std::map>> activeNotifications; + std::map hoverCounts; NotificationController(); std::vector> activeMonitors; - + void updateHoverState(uint64_t notificationId, bool isHovered); void closeNotification(uint64_t notificationId); - }; \ No newline at end of file +}; \ No newline at end of file diff --git a/include/widgets/notification/baseNotification.hpp b/include/widgets/notification/baseNotification.hpp index a84e3db..19a399c 100644 --- a/include/widgets/notification/baseNotification.hpp +++ b/include/widgets/notification/baseNotification.hpp @@ -2,6 +2,9 @@ #include #include +#include + +#include #include "gdkmm/monitor.h" #include "gtkmm/window.h" @@ -13,18 +16,31 @@ class BaseNotification : public Gtk::Window { public: BaseNotification( uint64_t notificationId, std::shared_ptr monitor); + void pauseAutoClose(); + void resumeAutoClose(); + void startAutoClose(int timeoutMs); + sigc::signal signal_close; + sigc::signal signal_hover_changed; virtual ~BaseNotification() = default; uint64_t getNotificationId() const { return this->notificationId; } - private: void ensure_notification_css_loaded(); + void start_auto_close_timeout(int timeoutMs); + void pause_auto_close(); + void resume_auto_close(); + // onClose signal can be added here if needed protected: uint64_t notificationId; + + bool autoClosePaused = false; + int autoCloseRemainingMs = 0; + std::chrono::steady_clock::time_point autoCloseDeadline; + sigc::connection autoCloseConnection; }; \ No newline at end of file diff --git a/resources/notification.css b/resources/notification.css index 360b59f..0944c72 100644 --- a/resources/notification.css +++ b/resources/notification.css @@ -21,6 +21,7 @@ .notification-critical { background: linear-gradient(145deg, #321010, #1e1e1e); border: 1px solid rgba(255, 75, 75, 0.5); + border-left: 3px solid #ff4b4b; box-shadow: 0 4px 30px rgba(0, 0, 0, 0.8), 0 0 15px rgba(255, 75, 75, 0.2), @@ -28,6 +29,7 @@ color: #ffffff; font-weight: 500; + } .notification-normal { @@ -42,10 +44,12 @@ .notification-low { background: linear-gradient(145deg, #2a2a2a, #1e1e1e); border: 1px solid rgba(255, 255, 255, 0.15); + border-left: 3px solid #888888; color: rgba(255, 255, 255, 0.95); /* white box shadow */ box-shadow: 0 4px 20px rgba(255, 255, 255, 0.05); font-size: 13px; + } .notification-button-box { border-top: 1px solid var(--color-border); @@ -53,13 +57,13 @@ } .notification-button { - background-color: #444444; + background-color: #333333; + border: none; + border-radius: 4px; color: #ffffff; padding: 4px 8px; - border-radius: 6px; margin-right: 6px; - font-family: var(--text-font-mono); - font-size: 13px; + font-size: 12px; } .notification-icon-button { diff --git a/src/services/notificationController.cpp b/src/services/notificationController.cpp index e6e1833..52bed82 100644 --- a/src/services/notificationController.cpp +++ b/src/services/notificationController.cpp @@ -7,8 +7,9 @@ #include "widgets/notification/notificationWindow.hpp" #include "widgets/notification/spotifyNotification.hpp" +#include + #include "gdkmm/display.h" -#include "glibmm/main.h" #include "sigc++/adaptors/bind.h" std::shared_ptr NotificationController::instance = nullptr; @@ -36,32 +37,6 @@ NotificationController::NotificationController() { } } -void NotificationController::showSpotifyNotification(MprisPlayer2Message mpris) { - return; - - std::vector> notifications; - - uint64_t id = this->globalNotificationId++; - - for (const auto &monitor : this->activeMonitors) { - auto notification = std::make_shared(id, monitor, mpris); - notification->show(); - notifications.push_back(notification); - notification->signal_close.connect([this, id = notification->getNotificationId()](int) { - closeNotification(id); - }); - - this->activeNotifications[id] = notifications; - - Glib::signal_timeout().connect([notification]() { - notification->close(); - return false; // Don't repeat - }, - DEFAULT_NOTIFICATION_TIMEOUT); - } - - this->activeNotifications[id] = notifications; -} void NotificationController::showNotificationOnAllMonitors(NotifyMessage notify) { uint64_t id = this->globalNotificationId++; @@ -74,27 +49,79 @@ void NotificationController::showNotificationOnAllMonitors(NotifyMessage notify) auto timeout = notify.expire_timeout; notification->show(); // -1 means use default timeout, 0 means never expire - if (timeout <= 0) { + if (timeout < 0) { timeout = DEFAULT_NOTIFICATION_TIMEOUT; } - notification->signal_close.connect( - [this, id = notification->getNotificationId()](int) { - closeNotification(id); - }); + notification->signal_close.connect([this, id = notification->getNotificationId()](int) { + closeNotification(id); + }); + + notification->signal_hover_changed.connect([this, id = notification->getNotificationId()](bool hovered) { + updateHoverState(id, hovered); + }); if (timeout == 0) { continue; } - Glib::signal_timeout().connect([notification]() { - notification->close(); - return false; // Don't repeat - }, - timeout); + notification->startAutoClose(timeout); } this->activeNotifications[id] = notifications; + +} + + +void NotificationController::showSpotifyNotification(MprisPlayer2Message mpris) { + std::vector> notifications; + uint64_t id = this->globalNotificationId++; + + + for (const auto &monitor : this->activeMonitors) { + auto notification = std::make_shared(id, monitor, mpris); + + notification->show(); + notifications.push_back(notification); + + notification->signal_close.connect([this, id = notification->getNotificationId()](int) { + closeNotification(id); + }); + + notification->signal_hover_changed.connect([this, id = notification->getNotificationId()](bool hovered) { + updateHoverState(id, hovered); + }); + + + notification->startAutoClose(10000); + } + + this->activeNotifications[id] = notifications; + +} + +void NotificationController::updateHoverState(uint64_t notificationId, bool isHovered) { + if (this->activeNotifications.find(notificationId) == this->activeNotifications.end()) { + return; + } + + int &count = this->hoverCounts[notificationId]; + if (isHovered) { + count += 1; + } else { + count = std::max(0, count - 1); + } + + auto ¬ifications = this->activeNotifications[notificationId]; + if (count > 0) { + for (const auto ¬ification : notifications) { + notification->pauseAutoClose(); + } + } else { + for (const auto ¬ification : notifications) { + notification->resumeAutoClose(); + } + } } void NotificationController::closeNotification(uint64_t notificationId) { @@ -108,4 +135,5 @@ void NotificationController::closeNotification(uint64_t notificationId) { } this->activeNotifications.erase(notificationId); -} + this->hoverCounts.erase(notificationId); +} \ No newline at end of file diff --git a/src/widgets/notification/baseNotification.cpp b/src/widgets/notification/baseNotification.cpp index 7d55769..7b4a62d 100644 --- a/src/widgets/notification/baseNotification.cpp +++ b/src/widgets/notification/baseNotification.cpp @@ -1,13 +1,19 @@ #include "widgets/notification/baseNotification.hpp" +#include #include #include #include "helpers/system.hpp" #include "gdkmm/monitor.h" +#include "glibmm/main.h" +#include "glibmm/object.h" #include "gtk4-layer-shell.h" +#include "gtkmm/button.h" #include "gtkmm/cssprovider.h" +#include "gtkmm/eventcontrollermotion.h" +#include "gtkmm/gestureclick.h" BaseNotification::BaseNotification(uint64_t notificationId, std::shared_ptr monitor) { ensure_notification_css_loaded(); @@ -25,8 +31,94 @@ BaseNotification::BaseNotification(uint64_t notificationId, std::shared_ptrset_vexpand(false); this->notificationId = notificationId; + + auto window_click = Gtk::GestureClick::create(); + window_click->set_button(GDK_BUTTON_PRIMARY); + window_click->set_exclusive(false); + window_click->signal_released().connect([this](int, double x, double y) { + Gtk::Widget *picked = this->pick(x, y, Gtk::PickFlags::DEFAULT); + for (auto *w = picked; w != nullptr; w = w->get_parent()) { + if (dynamic_cast(w) != nullptr) { + return; + } + } + this->signal_close.emit(this->notificationId); + }); + add_controller(window_click); + + auto window_motion = Gtk::EventControllerMotion::create(); + window_motion->signal_enter().connect([this](double, double) { + signal_hover_changed.emit(true); + pause_auto_close(); + }); + window_motion->signal_leave().connect([this]() { + signal_hover_changed.emit(false); + resume_auto_close(); + }); + add_controller(window_motion); } +void BaseNotification::pauseAutoClose() { + pause_auto_close(); +} + +void BaseNotification::resumeAutoClose() { + resume_auto_close(); +} + +void BaseNotification::startAutoClose(int timeoutMs) { + if (timeoutMs <= 0) { + return; + } + + autoCloseRemainingMs = timeoutMs; + autoClosePaused = false; + start_auto_close_timeout(timeoutMs); +} + +void BaseNotification::start_auto_close_timeout(int timeoutMs) { + if (autoCloseConnection.connected()) { + autoCloseConnection.disconnect(); + } + + autoCloseDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + autoCloseConnection = Glib::signal_timeout().connect([this]() { + this->signal_close.emit(this->notificationId); + return false; // Don't repeat + }, + timeoutMs); +} + +void BaseNotification::pause_auto_close() { + if (autoClosePaused || autoCloseRemainingMs <= 0) { + return; + } + + if (autoCloseConnection.connected()) { + autoCloseConnection.disconnect(); + } + + auto now = std::chrono::steady_clock::now(); + auto remaining = std::chrono::duration_cast(autoCloseDeadline - now).count(); + autoCloseRemainingMs = static_cast(std::max(0, remaining)); + autoClosePaused = true; +} + +void BaseNotification::resume_auto_close() { + if (!autoClosePaused) { + return; + } + + autoClosePaused = false; + if (autoCloseRemainingMs <= 0) { + this->signal_close.emit(this->notificationId); + return; + } + + start_auto_close_timeout(autoCloseRemainingMs); +} + + void BaseNotification::ensure_notification_css_loaded() { static bool loaded = false; if (loaded) { diff --git a/src/widgets/notification/notificationWindow.cpp b/src/widgets/notification/notificationWindow.cpp index 362a4c3..ec1ad2a 100644 --- a/src/widgets/notification/notificationWindow.cpp +++ b/src/widgets/notification/notificationWindow.cpp @@ -12,19 +12,6 @@ NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr monitor, NotifyMessage notify) : BaseNotification(notificationId, monitor) { set_title(notify.summary); - auto window_click = Gtk::GestureClick::create(); - window_click->set_button(GDK_BUTTON_PRIMARY); - window_click->set_exclusive(false); - window_click->signal_released().connect([this](int, double x, double y) { - Gtk::Widget *picked = this->pick(x, y, Gtk::PickFlags::DEFAULT); - for (auto *w = picked; w != nullptr; w = w->get_parent()) { - if (dynamic_cast(w) != nullptr) { - return; - } - } - this->signal_close.emit(this->notificationId); - }); - add_controller(window_click); // Main vertical box auto vbox = Gtk::make_managed(Gtk::Orientation::VERTICAL, 8); @@ -88,6 +75,19 @@ NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr< btn->set_label(action_label); btn->add_css_class("notification-button"); + + switch (notify.urgency) { + case NotificationUrgency::CRITICAL: + btn->add_css_class("notification-critical"); + break; + case NotificationUrgency::NORMAL: + btn->add_css_class("notification-normal"); + break; + case NotificationUrgency::LOW: + btn->add_css_class("notification-low"); + break; + } + btn->signal_clicked().connect([this, action_id, cb = notify.on_action]() { if (cb) { cb(action_id);