Browse Source

Show Linux message notifications on all X11 desktops

master
evgeny 5 days ago
parent
commit
247479e411
  1. 4
      tools/chatgui/CMakeLists.txt
  2. 71
      tools/chatgui/src/message_notification.cpp
  3. 1
      tools/chatgui/src/message_notification.h

4
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)

71
tools/chatgui/src/message_notification.cpp

@ -13,12 +13,18 @@
#include <QHideEvent>
#include <QCloseEvent>
#ifdef UTUN_NOTIFICATION_X11
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#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<QNativeInterface::QX11Application>()) 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<const Atom*>(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<const unsigned char*>(&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();

1
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;

Loading…
Cancel
Save