From 247479e4118d7e870c8f31f695c3ad6347b99613 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 16:35:29 +0200 Subject: [PATCH] Show Linux message notifications on all X11 desktops --- tools/chatgui/CMakeLists.txt | 4 ++ tools/chatgui/src/message_notification.cpp | 71 +++++++++++++++++++++- tools/chatgui/src/message_notification.h | 1 + 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index a394fe3e..7e6ad3f0 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -266,6 +266,10 @@ elseif(UNIX AND NOT APPLE) find_package(X11 QUIET) endif() +if(TARGET X11::X11) + target_compile_definitions(chatgui_notifications PRIVATE UTUN_NOTIFICATION_X11) + target_link_libraries(chatgui_notifications PRIVATE X11::X11) +endif() if(TARGET X11::Xss) target_compile_definitions(chatgui_notifications PRIVATE UTUN_ACTIVITY_X11) target_link_libraries(chatgui_notifications PRIVATE X11::Xss) diff --git a/tools/chatgui/src/message_notification.cpp b/tools/chatgui/src/message_notification.cpp index fb198b00..b6a8d24e 100644 --- a/tools/chatgui/src/message_notification.cpp +++ b/tools/chatgui/src/message_notification.cpp @@ -13,12 +13,18 @@ #include #include +#ifdef UTUN_NOTIFICATION_X11 +#include +#include +#endif + MessageNotification::MessageNotification() : QWidget(nullptr, Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus) , m_activity(new UserActivityMonitor(this)) { setObjectName("messageNotification"); setAttribute(Qt::WA_ShowWithoutActivating); + setAttribute(Qt::WA_X11NetWmWindowTypeNotification); #ifdef Q_OS_MACOS setAttribute(Qt::WA_MacAlwaysShowToolWindow); #endif @@ -58,7 +64,7 @@ MessageNotification::MessageNotification() GUI_DEBUG("notification: initialized platform=%s hide_after_activity_ms=%d", qPrintable(QGuiApplication::platformName()), m_hideTimer.interval()); if (QGuiApplication::platformName().startsWith("wayland")) - GUI_WARN("notification: native Wayland compositor controls placement and stacking; position near tray is best effort"); + GUI_WARN("notification: native Wayland controls desktop, placement and stacking; remapping on current desktop is best effort"); } MessageNotification::~MessageNotification() { @@ -79,6 +85,11 @@ void MessageNotification::showMessage(const QString& target, const QString& send } const QRect area = screen->availableGeometry(); const bool replacing = isVisible(); + if (QGuiApplication::platformName().startsWith("wayland") && internalWinId()) { + hide(); + destroy(); + GUI_DEBUG("notification: recreated Wayland surface for current desktop target=%s", qPrintable(target)); + } m_hideTimer.stop(); m_target = target; m_body->setText(QCoreApplication::translate("ChatGui", "New message from %1").arg(sender) + '\n' + channel); @@ -92,12 +103,70 @@ void MessageNotification::showMessage(const QString& target, const QString& send x = qBound(area.left(), x, qMax(area.left(), area.right() - width() + 1)); y = qBound(area.top(), y, qMax(area.top(), area.bottom() - height() + 1)); move(x, y); + setDesktopScope(); show(); m_activity->start(); GUI_INFO("notification: %s target=%s screen=%s position=%d,%d tray_anchor=%d waiting_for_activity", replacing ? "updated" : "shown", qPrintable(target), qPrintable(screen->name()), x, y, int(!trayGeometry.isEmpty())); } +void MessageNotification::setDesktopScope() { +#ifdef UTUN_NOTIFICATION_X11 + if (QGuiApplication::platformName() != "xcb") return; + Display* display = nullptr; + bool ownDisplay = false; +#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0) +#if QT_CONFIG(xcb) + if (auto* native = qGuiApp->nativeInterface()) display = native->display(); +#endif +#endif + if (!display) { display = XOpenDisplay(nullptr); ownDisplay = true; } + if (!display) { GUI_ERROR("notification: cannot open X11 display to select all desktops"); return; } + const Window window = Window(winId()); + QGuiApplication::sync(); // Окно Qt должно существовать на сервере до Xlib-запросов. + XWindowAttributes attributes{}; + if (!XGetWindowAttributes(display, window, &attributes)) { + GUI_ERROR("notification: cannot read X11 window attributes window=0x%lx", window); + if (ownDisplay) XCloseDisplay(display); + return; + } + const Atom desktopAtom = XInternAtom(display, "_NET_WM_DESKTOP", False); + const Atom supportedAtom = XInternAtom(display, "_NET_SUPPORTED", False); + Atom type = None; + int format = 0; + unsigned long count = 0, remaining = 0; + unsigned char* data = nullptr; + const int result = XGetWindowProperty(display, attributes.root, supportedAtom, 0, 4096, False, + XA_ATOM, &type, &format, &count, &remaining, &data); + bool supported = false; + if (result == Success && type == XA_ATOM && format == 32 && data) { + const auto* atoms = reinterpret_cast(data); + for (unsigned long i = 0; i < count; ++i) if (atoms[i] == desktopAtom) { supported = true; break; } + } + if (data) XFree(data); + if (!supported) GUI_WARN("notification: window manager does not advertise desktop selection; all-desktop hint is best effort"); + const unsigned long allDesktops = 0xffffffffUL; + if (!isVisible()) { + XChangeProperty(display, window, desktopAtom, XA_CARDINAL, 32, PropModeReplace, + reinterpret_cast(&allDesktops), 1); + } else { + XEvent event{}; + event.xclient.type = ClientMessage; + event.xclient.window = window; + event.xclient.message_type = desktopAtom; + event.xclient.format = 32; + event.xclient.data.l[0] = long(allDesktops); + event.xclient.data.l[1] = 1; // Запрос обычного приложения, не pager. + if (!XSendEvent(display, attributes.root, False, SubstructureRedirectMask | SubstructureNotifyMask, &event)) + GUI_ERROR("notification: cannot request all desktops window=0x%lx", window); + } + XSync(display, False); + GUI_DEBUG("notification: all-desktop scope requested window=0x%lx mapped=%d wm_supported=%d", + window, int(isVisible()), int(supported)); + if (ownDisplay) XCloseDisplay(display); +#endif +} + void MessageNotification::dismiss(const char* reason) { if (isVisible()) GUI_INFO("notification: hidden target=%s reason=%s", qPrintable(m_target), reason); m_hideTimer.stop(); diff --git a/tools/chatgui/src/message_notification.h b/tools/chatgui/src/message_notification.h index 4ad98154..e0ccc6c9 100644 --- a/tools/chatgui/src/message_notification.h +++ b/tools/chatgui/src/message_notification.h @@ -27,6 +27,7 @@ protected: void closeEvent(QCloseEvent* event) override; private: + void setDesktopScope(); QString m_target; QLabel* m_body = nullptr; UserActivityMonitor* m_activity = nullptr;