[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
This commit is contained in:
Lukas Brübach 2026-08-15 15:39:25 +02:00
parent 75142e6e9f
commit 829f56ccf0
2 changed files with 48 additions and 1 deletions

View file

@ -22,6 +22,7 @@
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
@ -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<QWidget *>(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<const QMenu *>(w)) {
return true;
}
w = w->parentWidget();
}
return false;
}
void UserListWidget::clearSelectionAndClosePopup()
{
popupPinned = false;
hidePopup(true);
userTree->clearSelection();
}
void UserListWidget::retranslateUi()
{
userContextMenu->retranslateUi();

View file

@ -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();