diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp index 4ce2b5fdc..b5451bb2c 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp @@ -33,7 +33,7 @@ DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor, // signals while the modal loop runs. UserListManager *manager = tabSupervisor->getUserListManager(); userList = new UserListWidget(tabSupervisor, tabSupervisor->getClient(), UserListWidget::RoomList, this, - /*withUserInfoPopup=*/false); + /*hasUserInfoPopup=*/false); userList->setUserFilter([this, manager](const QString &name, bool online) { return !excludeUserNames.contains(name) && online && !manager->isUserIgnored(name); }); @@ -50,11 +50,6 @@ DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor, currentUserName = userName; inviteButton->setEnabled(!userName.isEmpty()); }); - connect(userList, &UserListWidget::userListChanged, this, &DlgInviteToGame::updateEmptyLabel); - - emptyLabel = new QLabel(this); - emptyLabel->setAlignment(Qt::AlignCenter); - emptyLabel->setVisible(false); inviteButton = new QPushButton(this); inviteButton->setEnabled(false); @@ -72,10 +67,8 @@ DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor, auto *layout = new QVBoxLayout(this); layout->addWidget(searchEdit); layout->addWidget(userList, 1); - layout->addWidget(emptyLabel); layout->addLayout(buttonRow); - updateEmptyLabel(); retranslateUi(); // Default to a comfortably tall dialog so the list has room to breathe, @@ -88,12 +81,6 @@ DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor, void DlgInviteToGame::searchTextChanged(const QString &text) { userList->setFilterText(text); - updateEmptyLabel(); -} - -void DlgInviteToGame::updateEmptyLabel() -{ - emptyLabel->setVisible(userList->visibleUserRowCount() == 0); } void DlgInviteToGame::inviteCurrentUser(const QString &userName) @@ -122,5 +109,4 @@ void DlgInviteToGame::retranslateUi() searchEdit->setPlaceholderText(tr("Search users...")); inviteButton->setText(tr("Invite")); cancelButton->setText(tr("Cancel")); - emptyLabel->setText(tr("No matching users.")); } diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h index a64a924c3..bbb589bdb 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.h @@ -10,7 +10,6 @@ #include #include -class QLabel; class QLineEdit; class QPushButton; class TabSupervisor; @@ -38,11 +37,9 @@ private: QString currentUserName; QLineEdit *searchEdit; UserListWidget *userList; - QLabel *emptyLabel; QPushButton *inviteButton; QPushButton *cancelButton; - void updateEmptyLabel(); void retranslateUi(); }; 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 02d055c77..685325f50 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -355,7 +355,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event, QMouseEvent *const mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::RightButton) { // Dialog mode has no context menu: consume the press, show nothing. - if (owner->getWithUserInfoPopup()) { + if (owner->getHasUserInfoPopup()) { owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index); } return true; @@ -582,8 +582,8 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, AbstractClient *_client, UserListType _type, QWidget *parent, - bool _withUserInfoPopup) - : QGroupBox(parent), withUserInfoPopup(_withUserInfoPopup), tabSupervisor(_tabSupervisor), client(_client), + bool _hasUserInfoPopup) + : QGroupBox(parent), hasUserInfoPopup(_hasUserInfoPopup), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0) { avatarProvider = new UserAvatarProvider(client, this); @@ -610,7 +610,40 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); userTree->header()->setStretchLastSection(true); - if (withUserInfoPopup) { + // Always create timers so callers never segfault on a null deref; + // showPopupForUser / hidePopup already guard against a null userInfoPopup. + showPopupTimer = new QTimer(this); + showPopupTimer->setSingleShot(true); + showPopupTimer->setInterval(280); + connect(showPopupTimer, &QTimer::timeout, this, [this] { + if (hoveredUser.isEmpty()) { + return; + } + // Resolve the row under the cursor again. In sectioned mode a user can + // own several rows (online + buddy), so the popup must anchor to the + // exact hovered row instead of a lookup by name. + const QPoint viewportPos = userTree->viewport()->mapFromGlobal(QCursor::pos()); + QTreeWidgetItem *item = userTree->itemAt(viewportPos); + if (item && item->type() == QTreeWidgetItem::Type && + QString::fromStdString(static_cast(item)->getUserInfo().name()) == hoveredUser) { + showPopupForUser(static_cast(item)); + } + }); + + hidePopupTimer = new QTimer(this); + hidePopupTimer->setSingleShot(true); + hidePopupTimer->setInterval(160); + connect(hidePopupTimer, &QTimer::timeout, this, [this] { + // 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 && !userInfoPopup->underMouse() && + (hoveredUser.isEmpty() || !userTree->underMouse())) { + hidePopup(); + } + }); + + if (hasUserInfoPopup) { // ── Hover popup ─────────────────────────────────────────────────────── userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap, @@ -620,36 +653,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userInfoPopup->setWindowOpacity(0.0); userInfoPopup->installEventFilter(this); - showPopupTimer = new QTimer(this); - showPopupTimer->setSingleShot(true); - showPopupTimer->setInterval(280); - connect(showPopupTimer, &QTimer::timeout, this, [this] { - if (hoveredUser.isEmpty()) { - return; - } - // Resolve the row under the cursor again. In sectioned mode a user can - // own several rows (online + buddy), so the popup must anchor to the - // exact hovered row instead of a lookup by name. - const QPoint viewportPos = userTree->viewport()->mapFromGlobal(QCursor::pos()); - QTreeWidgetItem *item = userTree->itemAt(viewportPos); - if (item && item->type() == QTreeWidgetItem::Type && - QString::fromStdString(static_cast(item)->getUserInfo().name()) == hoveredUser) { - showPopupForUser(static_cast(item)); - } - }); - - hidePopupTimer = new QTimer(this); - hidePopupTimer->setSingleShot(true); - hidePopupTimer->setInterval(160); - connect(hidePopupTimer, &QTimer::timeout, this, [this] { - // 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(); - } - }); - connectPopupSignals(); } @@ -658,7 +661,7 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, userTree->viewport()->installEventFilter(this); userTree->installEventFilter(this); // keyboard handling for section dividers - if (withUserInfoPopup) { + if (hasUserInfoPopup) { // 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. @@ -984,7 +987,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) // Divider rows have no menu. Mouse-triggered context events are NOT handled // here — the delegate's right-press path already pops the menu, and // handling both would open two menus on one right-click. - if (withUserInfoPopup && (obj == userTree || obj == userTree->viewport()) && event->type() == QEvent::ContextMenu) { + if (hasUserInfoPopup && (obj == userTree || obj == userTree->viewport()) && event->type() == QEvent::ContextMenu) { auto *contextEvent = static_cast(event); if (contextEvent->reason() == QContextMenuEvent::Keyboard) { QTreeWidgetItem *current = userTree->currentItem(); @@ -1018,7 +1021,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event) } } - if (withUserInfoPopup && obj == userTree->viewport()) { + if (hasUserInfoPopup && obj == userTree->viewport()) { if (event->type() == QEvent::MouseMove) { if (!SettingsCache::instance().appearance().getStyleUserList()) { return QGroupBox::eventFilter(obj, event); @@ -1589,7 +1592,7 @@ void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/) return; // divider rows open no chat } const QString userName = item->data(2, Qt::UserRole).toString(); - if (withUserInfoPopup) { + if (hasUserInfoPopup) { emit openMessageDialog(userName, true); } else { emit userActivated(userName); 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 6c20c5e89..0407ad8ca 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -173,7 +173,7 @@ private: QString hoveredUser; bool popupPinned = false; bool bulkLoading = false; - bool withUserInfoPopup = true; + bool hasUserInfoPopup = true; std::function userFilter; /** @@ -256,7 +256,7 @@ public: AbstractClient *_client, UserListType _type, QWidget *parent = nullptr, - bool withUserInfoPopup = true); + bool hasUserInfoPopup = true); ~UserListWidget() override; void bind(UserListManager *mgr); void applyDisplayMode(); @@ -278,9 +278,9 @@ public: userFilter = std::move(filter); } [[nodiscard]] int visibleUserRowCount() const; - [[nodiscard]] bool getWithUserInfoPopup() const + [[nodiscard]] bool getHasUserInfoPopup() const { - return withUserInfoPopup; + return hasUserInfoPopup; } [[nodiscard]] const QList
&getSectionIds() const { diff --git a/cockatrice/src/interface/widgets/tabs/tab_game.cpp b/cockatrice/src/interface/widgets/tabs/tab_game.cpp index c7e0d260b..dbf4a5a4a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_game.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_game.cpp @@ -1324,6 +1324,7 @@ void TabGame::createPlayerListDock(bool bReplay) auto *playerListBox = new QWidget(this); auto *vbox = new QVBoxLayout(playerListBox); vbox->setContentsMargins(0, 0, 0, 0); + vbox->setSpacing(0); vbox->addWidget(playerListWidget);