Compare commits

..

2 Commits

Author SHA1 Message Date
9c70065bf6 multi click not fucked 2026-02-03 00:11:59 +01:00
9898c48c67 hover pauses notification 2026-02-02 23:42:08 +01:00
8 changed files with 208 additions and 63 deletions

View File

@@ -2,8 +2,9 @@
#include <cstdint>
#include <functional>
#include <string>
#include <map>
#include <memory>
#include <string>
#include <sys/types.h>
#include <vector>
#include "gdkmm/pixbuf.h"
@@ -39,6 +40,8 @@ struct NotifyMessage {
int32_t expire_timeout;
// Callback to invoke when an action is triggered
std::function<void(const std::string& action_id)> on_action;
// Guard to prevent multiple action invocations across mirrors
std::shared_ptr<bool> actionInvoked;
// image data (if any) from dbus
std::optional<Glib::RefPtr<Gdk::Pixbuf>> imageData;
};

View File

@@ -28,8 +28,9 @@ class NotificationController {
private:
uint64_t globalNotificationId = 1;
std::map<uint64_t, std::vector<std::shared_ptr<BaseNotification>>> activeNotifications;
std::map<uint64_t, int> hoverCounts;
NotificationController();
std::vector<std::shared_ptr<Gdk::Monitor>> activeMonitors;
void updateHoverState(uint64_t notificationId, bool isHovered);
void closeNotification(uint64_t notificationId);
};
};

View File

@@ -2,29 +2,44 @@
#include <csignal>
#include <cstdint>
#include <chrono>
#include <sigc++/connection.h>
#include "gdkmm/monitor.h"
#include "gtkmm/window.h"
#define DEFAULT_NOTIFICATION_TIMEOUT 7000
#define DEFAULT_NOTIFICATION_TIMEOUT 5000
class BaseNotification : public Gtk::Window {
public:
BaseNotification( uint64_t notificationId, std::shared_ptr<Gdk::Monitor> monitor);
void pauseAutoClose();
void resumeAutoClose();
void startAutoClose(int timeoutMs);
sigc::signal<void(int)> signal_close;
sigc::signal<void(bool)> signal_hover_changed;
virtual ~BaseNotification() = default;
uint64_t getNotificationId() const {
return this->notificationId;
}
private:
void ensure_notification_css_loaded();
// onClose signal can be added here if needed
void start_auto_close_timeout(int timeoutMs);
void pause_auto_close();
void resume_auto_close();
protected:
uint64_t notificationId;
bool autoClosePaused = false;
int autoCloseRemainingMs = 0;
std::chrono::steady_clock::time_point autoCloseDeadline;
sigc::connection autoCloseConnection;
};

View File

@@ -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 {

View File

@@ -84,6 +84,7 @@ void NotificationService::handle_notify(const Glib::VariantContainerBase &parame
notify.app_icon = app_icon;
notify.summary = static_cast<std::string>(summary);
notify.body = static_cast<std::string>(body);
notify.actionInvoked = std::make_shared<bool>(false);
std::vector<std::string> actions_converted;
actions_converted.reserve(actions.size());

View File

@@ -7,8 +7,9 @@
#include "widgets/notification/notificationWindow.hpp"
#include "widgets/notification/spotifyNotification.hpp"
#include <algorithm>
#include "gdkmm/display.h"
#include "glibmm/main.h"
#include "sigc++/adaptors/bind.h"
std::shared_ptr<NotificationController> NotificationController::instance = nullptr;
@@ -36,32 +37,6 @@ NotificationController::NotificationController() {
}
}
void NotificationController::showSpotifyNotification(MprisPlayer2Message mpris) {
return;
std::vector<std::shared_ptr<BaseNotification>> notifications;
uint64_t id = this->globalNotificationId++;
for (const auto &monitor : this->activeMonitors) {
auto notification = std::make_shared<SpotifyNotification>(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) {
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<std::shared_ptr<BaseNotification>> notifications;
uint64_t id = this->globalNotificationId++;
for (const auto &monitor : this->activeMonitors) {
auto notification = std::make_shared<SpotifyNotification>(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(DEFAULT_NOTIFICATION_TIMEOUT);
}
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 &notifications = this->activeNotifications[notificationId];
if (count > 0) {
for (const auto &notification : notifications) {
notification->pauseAutoClose();
}
} else {
for (const auto &notification : 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);
}

View File

@@ -1,13 +1,19 @@
#include "widgets/notification/baseNotification.hpp"
#include <algorithm>
#include <filesystem>
#include <string>
#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<Gdk::Monitor> monitor) {
ensure_notification_css_loaded();
@@ -25,8 +31,94 @@ BaseNotification::BaseNotification(uint64_t notificationId, std::shared_ptr<Gdk:
this->set_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<Gtk::Button *>(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<std::chrono::milliseconds>(autoCloseDeadline - now).count();
autoCloseRemainingMs = static_cast<int>(std::max<int64_t>(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) {

View File

@@ -12,19 +12,6 @@
NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr<Gdk::Monitor> 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<Gtk::Button *>(w) != nullptr) {
return;
}
}
this->signal_close.emit(this->notificationId);
});
add_controller(window_click);
// Main vertical box
auto vbox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 8);
@@ -88,8 +75,22 @@ NotificationWindow::NotificationWindow(uint64_t notificationId, std::shared_ptr<
btn->set_label(action_label);
btn->add_css_class("notification-button");
btn->signal_clicked().connect([this, action_id, cb = notify.on_action]() {
if (cb) {
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, guard = notify.actionInvoked]() {
if (cb && guard && !*guard) {
*guard = true;
cb(action_id);
this->signal_close.emit(this->notificationId);
}