From 829f56ccf02c7475bc58a441002303d8c21512d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sat, 15 Aug 2026 15:39:25 +0200 Subject: [PATCH] [UserList] Deselect the list and close the popup on outside clicks Clicking anywhere outside the tree, the popup or an open menu now clears the selection and hides the popup, so a pinned popup does not stay open when the list loses focus. - The application-wide event filter watches every mouse press and treats a press as inside the list UI only when its target is the tree, the popup or an open menu (parent-chain walk), so a click on another list, a tab or the window background deselects. - A hover popup now also closes when the cursor leaves the hovered row. The hide timer previously checked whether the cursor was over the tree, which is always true over empty list space and section dividers, so the popup stayed open after moving off the user. - Deselection keeps the current item so keyboard navigation is not disturbed, and the pinned flag is dropped before hiding so the selection-changed handler does not hide twice. Took 15 minutes --- .../widgets/server/user/user_list_widget.cpp | 45 ++++++++++++++++++- .../widgets/server/user/user_list_widget.h | 4 ++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index 8b0e187f8..a0d7c8a4c 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -634,7 +635,10 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, hidePopupTimer->setSingleShot(true); hidePopupTimer->setInterval(160); connect(hidePopupTimer, &QTimer::timeout, this, [this] { - if (!popupPinned && !userInfoPopup->underMouse() && !userTree->underMouse()) { + // The hover ends when the cursor leaves the user row. Empty list + // space, a section divider and anything outside the tree all close + // the popup, while the popup itself keeps it alive. + if (!popupPinned && !userInfoPopup->underMouse() && (hoveredUser.isEmpty() || !userTree->underMouse())) { hidePopup(); } }); @@ -646,6 +650,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->viewport()->installEventFilter(this); userTree->installEventFilter(this); // keyboard handling for section dividers + // Clicking anywhere outside the list clears its selection and closes the + // popup. The filter watches all widgets because the press can land on any + // part of the window, on another list or on the popup itself. + qApp->installEventFilter(this); + // Pin on item click connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) { // Clicking a section divider toggles it @@ -733,6 +742,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, retranslateUi(); } +UserListWidget::~UserListWidget() +{ + qApp->removeEventFilter(this); +} + void UserListWidget::bind(UserListManager *mgr) { manager = mgr; @@ -918,6 +932,16 @@ void UserListWidget::connectPopupSignals() bool UserListWidget::eventFilter(QObject *obj, QEvent *event) { + // A press outside the tree, the popup and any open menu deselects the + // list and closes the popup. The filter is installed application-wide, so + // the target can be any widget in the window or another list. + if (event->type() == QEvent::MouseButtonPress) { + auto *pressTarget = qobject_cast(obj); + if (pressTarget && !isPressInsideListUi(pressTarget)) { + clearSelectionAndClosePopup(); + } + } + // Keyboard navigation of the section dividers. // The dividers are selectable so arrow keys land on them. When one is the // current item, Enter/Space toggle it (like a button) and Left/Right follow @@ -1111,6 +1135,25 @@ void UserListWidget::hidePopup(bool immediate) fade->start(QAbstractAnimation::DeleteWhenStopped); } +bool UserListWidget::isPressInsideListUi(const QWidget *widget) const +{ + const QWidget *w = widget; + while (w) { + if (w == userTree || w == userInfoPopup || qobject_cast(w)) { + return true; + } + w = w->parentWidget(); + } + return false; +} + +void UserListWidget::clearSelectionAndClosePopup() +{ + popupPinned = false; + hidePopup(true); + userTree->clearSelection(); +} + void UserListWidget::retranslateUi() { userContextMenu->retranslateUi(); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 3721892c5..d7aeb3762 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -173,6 +173,9 @@ private: void hidePopup(bool immediate = false); void positionPopup(UserListTWI *item); void connectPopupSignals(); + /** True when @p widget is the tree, the popup or an open menu. */ + bool isPressInsideListUi(const QWidget *widget) const; + void clearSelectionAndClosePopup(); bool isItemNearViewport(const UserListTWI *item) const; void requestAvatarsForVisibleItems(); @@ -235,6 +238,7 @@ public: AbstractClient *_client, UserListType _type, QWidget *parent = nullptr); + ~UserListWidget() override; void bind(UserListManager *mgr); void applyDisplayMode(); void beginBulkLoad();