diff --git a/plugins/application-tray/util.cpp b/plugins/application-tray/util.cpp index 5d152fd63..e5079149b 100644 --- a/plugins/application-tray/util.cpp +++ b/plugins/application-tray/util.cpp @@ -269,6 +269,24 @@ bool Util::isValidX11Window(const xcb_window_t& window) const void Util::setX11WindowInputShape(const xcb_window_t& window, const QSize& size) { + // Idempotency guard: if this window's input shape is already the value we + // set last time, return early. Setting a shape (especially a 0x0 input + // shape) triggers Xwayland's miSetShape -> WindowsRestructured -> + // enter/leave recomputation, which broadcasts events to clients that + // selected LEAVE_NOTIFY. If nativeEventFilter receives that event and + // sets the same shape again, it forms a storm of hundreds of thousands + // of events per second and spins the CPU. + auto it = m_inputShapes.find(window); + if (it != m_inputShapes.end() && it.value() == size) { + // The stack mode is derived from the shape (non-empty -> ABOVE, + // empty -> BELOW), so an unchanged shape also means an unchanged + // stack mode. There is no other code path that restacks this window + // independently of its shape, hence no separate stacking cache is + // required. + return; + } + m_inputShapes.insert(window, size); + xcb_rectangle_t rectangle; rectangle.x = 0; rectangle.y = 0; @@ -282,6 +300,11 @@ void Util::setX11WindowInputShape(const xcb_window_t& window, const QSize& size) xcb_flush(m_x11connection); } +void Util::removeX11WindowInputShapeRecord(const xcb_window_t& window) +{ + m_inputShapes.remove(window); +} + uint8_t Util::getWindowVisualDepth(const xcb_window_t& window) const { auto attrCookie = xcb_get_window_attributes(m_x11connection, window); diff --git a/plugins/application-tray/util.h b/plugins/application-tray/util.h index ba93639a4..5b5f6956a 100644 --- a/plugins/application-tray/util.h +++ b/plugins/application-tray/util.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ class Util : public QObject QString getX11WindowName(const xcb_window_t& window); bool isValidX11Window(const xcb_window_t& window) const; void setX11WindowInputShape(const xcb_window_t& widnow, const QSize& size); + void removeX11WindowInputShapeRecord(const xcb_window_t& window); uint8_t getWindowVisualDepth(const xcb_window_t& window) const; QImage getX11WindowImageNonComposite(const xcb_window_t& window); bool getX11WindowPixmapData(const xcb_window_t& window, QByteArray *data); @@ -84,6 +86,11 @@ class Util : public QObject QSet m_currentIds; QMutex m_idMutex; + // Records the last input shape set per window, used for idempotency: + // setting the same shape again triggers Xwayland to recompute enter/leave + // and emit new LEAVE_NOTIFY events, forming an event storm (see + // XembedProtocol::nativeEventFilter). + QHash m_inputShapes; }; } diff --git a/plugins/application-tray/xembedprotocolhandler.cpp b/plugins/application-tray/xembedprotocolhandler.cpp index 11459e787..daf814590 100644 --- a/plugins/application-tray/xembedprotocolhandler.cpp +++ b/plugins/application-tray/xembedprotocolhandler.cpp @@ -69,8 +69,44 @@ bool XembedProtocol::nativeEventFilter(const QByteArray &eventType, void *messag const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev); if (responseType == XCB_LEAVE_NOTIFY) { xcb_leave_notify_event_t *lE = (xcb_leave_notify_event_t *)ev; - UTIL->setX11WindowInputShape(lE->event, QSize(0, 0)); + + // Only handle LEAVE_NOTIFY for windows we actually manage (created or + // reparented). Previously every client window's LEAVE_NOTIFY was handled, + // so a leave event from any X11 client (wine WeCom, nm-applet, etc.) + // triggered setX11WindowInputShape, which in turn caused another + // enter/leave recomputation, forming an event-storm infinite loop. + const xcb_window_t eventWindow = lE->event; + bool managed = false; + for (auto it = m_registedItem.cbegin(); it != m_registedItem.cend(); ++it) { + auto handler = it.value().dynamicCast(); + if (handler && handler->ownsX11Window(eventWindow)) { + managed = true; + break; + } + } + if (!managed) { + return false; + } + + UTIL->setX11WindowInputShape(eventWindow, QSize(0, 0)); return true; + } else if (responseType == XCB_DESTROY_NOTIFY) { + // The embedded client may destroy its icon window asynchronously; the + // DBus notification that removes the handler can lag behind by up to + // 200 ms. During that window X11 may recycle the destroyed window ID, + // so drop ownership synchronously here instead of waiting for the + // delayed tray-change callback. + auto *dE = reinterpret_cast(ev); + const xcb_window_t destroyedWindow = dE->window; + for (auto it = m_registedItem.cbegin(); it != m_registedItem.cend(); ++it) { + auto handler = it.value().dynamicCast(); + if (handler && handler->ownsX11Window(destroyedWindow)) { + handler->invalidate(); + break; + } + } + // Fall through without consuming the event so FdoSelectionManager can + // still observe the destroy and undock the icon. } return false; @@ -102,6 +138,10 @@ void XembedProtocol::onTrayIconsChanged() for (auto alreadyRegistedItem : m_registedItem.keys()) { if (!currentRegistedItems.contains(alreadyRegistedItem)) { if (auto value = m_registedItem.value(alreadyRegistedItem, nullptr)) { + auto handler = value.dynamicCast(); + if (handler) { + handler->invalidate(); + } uint pid = m_item2Pid[alreadyRegistedItem]; registeredMap.remove(pid); m_item2Pid.remove(alreadyRegistedItem); @@ -118,6 +158,9 @@ void XembedProtocol::onRemoveItemByPid(uint pid) const auto keys = m_registedItem.keys(); auto it = std::find_if(keys.begin(), keys.end(), [this, pid] (uint id) { return pid == m_item2Pid[id]; }); if (it != keys.end()) { + if (auto handler = m_registedItem.value(*it).dynamicCast()) { + handler->invalidate(); + } m_item2Pid.remove(*it); m_registedItem.remove(*it); } @@ -126,6 +169,7 @@ void XembedProtocol::onRemoveItemByPid(uint pid) XembedProtocolHandler::XembedProtocolHandler(const uint32_t& id, QObject* parent) : AbstractTrayProtocolHandler(parent) , m_enabled(false) + , m_owned(true) , m_windowId(id) , m_containerWid(0) , m_hoverTimer(new QTimer(this)) @@ -162,7 +206,12 @@ XembedProtocolHandler::~XembedProtocolHandler() { if (m_containerWid) { xcb_destroy_window(Util::instance()->getX11Connection(), m_containerWid); + Util::instance()->removeX11WindowInputShapeRecord(m_containerWid); } + // The icon window itself also gets its input shape cached when + // nativeEventFilter processes its LEAVE_NOTIFY. Drop that record too, so + // a recycled X11 window ID is never mistaken for an already-shaped window. + Util::instance()->removeX11WindowInputShapeRecord(m_windowId); UTIL->removeUniqueId(m_id); } @@ -177,6 +226,30 @@ uint32_t XembedProtocolHandler::windowId() const return m_windowId; } +bool XembedProtocolHandler::ownsX11Window(const xcb_window_t& window) const +{ + // Only recognize the tray icon window we reparented and the container + // window we created for it, and only while the handler still owns them. + // After invalidate() the embedded icon is gone and X11 may recycle the + // window ID, so we must stop claiming ownership. + return m_owned && (window == m_windowId || window == m_containerWid); +} + +void XembedProtocolHandler::invalidate() +{ + if (!m_owned) { + return; + } + m_owned = false; + // Drop the cached input shapes now: X11 may reuse these window IDs, and a + // stale "already 0x0" record would make the idempotency guard skip real + // shape requests for a new, unrelated window. + Util::instance()->removeX11WindowInputShapeRecord(m_windowId); + if (m_containerWid) { + Util::instance()->removeX11WindowInputShapeRecord(m_containerWid); + } +} + QString XembedProtocolHandler::id() const { return m_id; diff --git a/plugins/application-tray/xembedprotocolhandler.h b/plugins/application-tray/xembedprotocolhandler.h index 8fb1c6704..a171eaef0 100644 --- a/plugins/application-tray/xembedprotocolhandler.h +++ b/plugins/application-tray/xembedprotocolhandler.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -52,6 +52,17 @@ class XembedProtocolHandler : public AbstractTrayProtocolHandler virtual uint32_t windowId() const override; + // Checks whether an X11 window is managed by this handler (the reparented + // icon window or its container). Used by nativeEventFilter to only handle + // LEAVE_NOTIFY of its own windows, avoiding shape event storms triggered by + // other clients' windows. + bool ownsX11Window(const xcb_window_t& window) const; + + // Invalidates ownership when the embedded icon window is destroyed. X11 may + // reuse the window ID, so we must stop claiming it and drop the cached + // input shape to avoid mismatching a recycled ID. + void invalidate(); + virtual QString id() const override; virtual QString title() const override; @@ -88,6 +99,7 @@ public Q_SLOTS: }; bool m_enabled; + bool m_owned; uint32_t m_windowId; xcb_window_t m_containerWid; QPixmap m_icon;