mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-22 09:35:08 -07:00
[Game] Add an invite button to non-started and not full games (#7143)
* [Client] Send game invites from the user context menu via a private message The user context menu gains an "Invite to Game" submenu listing the inviteable games in the room (the inviter's own games, honoring the buddy-only setting). Picking one opens a private message to the target user with a cockatrice://joingame link naming the game, so the target gets a clickable invite instead of a raw URL. Multi-game rooms offer a picker; a single inviteable game sends directly. Sending a message to an offline user no longer swallows the draft — it reports that the user is offline and keeps the typed text. Took 1 minute * [Client] Add invite-to-game dialog to the game window Took 15 seconds * [Client] Open the invite dialog taller by default without enforcing a minimum size * Move button to bottom Took 3 minutes * Address comments. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
08d6b51db9
commit
7c550ee505
10 changed files with 449 additions and 76 deletions
|
|
@ -354,7 +354,10 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
|
|||
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
||||
QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event);
|
||||
if (mouseEvent->button() == Qt::RightButton) {
|
||||
owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
|
||||
// Dialog mode has no context menu: consume the press, show nothing.
|
||||
if (owner->getHasUserInfoPopup()) {
|
||||
owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -578,8 +581,10 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
|
|||
UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
UserListType _type,
|
||||
QWidget *parent)
|
||||
: QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0)
|
||||
QWidget *parent,
|
||||
bool _hasUserInfoPopup)
|
||||
: QGroupBox(parent), hasUserInfoPopup(_hasUserInfoPopup), tabSupervisor(_tabSupervisor), client(_client),
|
||||
type(_type), onlineCount(0)
|
||||
{
|
||||
avatarProvider = new UserAvatarProvider(client, this);
|
||||
cardArtProvider = new UserCardArtProvider(this);
|
||||
|
|
@ -605,15 +610,8 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
userTree->header()->setStretchLastSection(true);
|
||||
|
||||
// ── Hover popup ───────────────────────────────────────────────────────────
|
||||
userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(),
|
||||
&cardArtProvider->cache(), &cardArtParamsMap,
|
||||
window()); // parented to main window so it floats above siblings
|
||||
|
||||
userInfoPopup->hide();
|
||||
userInfoPopup->setWindowOpacity(0.0);
|
||||
userInfoPopup->installEventFilter(this);
|
||||
|
||||
// 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);
|
||||
|
|
@ -639,65 +637,104 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
// 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())) {
|
||||
if (!popupPinned && userInfoPopup && !userInfoPopup->underMouse() &&
|
||||
(hoveredUser.isEmpty() || !userTree->underMouse())) {
|
||||
hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
connectPopupSignals();
|
||||
if (hasUserInfoPopup) {
|
||||
// ── Hover popup ───────────────────────────────────────────────────────
|
||||
userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(),
|
||||
&cardArtProvider->cache(), &cardArtParamsMap,
|
||||
window()); // parented to main window so it floats above siblings
|
||||
|
||||
userInfoPopup->hide();
|
||||
userInfoPopup->setWindowOpacity(0.0);
|
||||
userInfoPopup->installEventFilter(this);
|
||||
|
||||
connectPopupSignals();
|
||||
}
|
||||
|
||||
userTree->setMouseTracking(true);
|
||||
userTree->viewport()->setMouseTracking(true);
|
||||
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);
|
||||
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.
|
||||
qApp->installEventFilter(this);
|
||||
|
||||
// Pin on item click
|
||||
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
||||
// Clicking a section divider toggles it
|
||||
if (sectioned && item->type() == SectionItemType) {
|
||||
setExpandedProgrammatically(item, !item->isExpanded());
|
||||
handleSectionExpansion(item, item->isExpanded());
|
||||
return;
|
||||
}
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return;
|
||||
}
|
||||
if (item->type() != QTreeWidgetItem::Type) {
|
||||
return; // divider rows have no user popup
|
||||
}
|
||||
popupPinned = false; // reset so showPopupForUser can update
|
||||
showPopupForUser(static_cast<UserListTWI *>(item));
|
||||
popupPinned = true; // pin after showing
|
||||
});
|
||||
// Pin on item click
|
||||
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
||||
// Clicking a section divider toggles it
|
||||
if (sectioned && item->type() == SectionItemType) {
|
||||
setExpandedProgrammatically(item, !item->isExpanded());
|
||||
handleSectionExpansion(item, item->isExpanded());
|
||||
return;
|
||||
}
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return;
|
||||
}
|
||||
if (item->type() != QTreeWidgetItem::Type) {
|
||||
return; // divider rows have no user popup
|
||||
}
|
||||
popupPinned = false; // reset so showPopupForUser can update
|
||||
showPopupForUser(static_cast<UserListTWI *>(item));
|
||||
popupPinned = true; // pin after showing
|
||||
});
|
||||
|
||||
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
[this](const QItemSelection &sel, const QItemSelection &) {
|
||||
if (sel.isEmpty() && popupPinned) {
|
||||
popupPinned = false;
|
||||
hidePopup();
|
||||
}
|
||||
});
|
||||
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
[this](const QItemSelection &sel, const QItemSelection &) {
|
||||
if (sel.isEmpty() && popupPinned) {
|
||||
popupPinned = false;
|
||||
hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard selection: show the popup for the current row and hide it when
|
||||
// the focus moves to a section divider or leaves the list entirely. The
|
||||
// popup therefore follows arrow key navigation exactly like mouse hover.
|
||||
// When it was pinned by a click it stays open and follows the selection.
|
||||
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
||||
if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return;
|
||||
}
|
||||
if (current && current->type() == QTreeWidgetItem::Type) {
|
||||
showPopupForUser(static_cast<UserListTWI *>(current));
|
||||
} else {
|
||||
popupPinned = false;
|
||||
hidePopup();
|
||||
}
|
||||
});
|
||||
// Keyboard selection: the popup is a mouse surface, so keyboard
|
||||
// navigation shows no floating popup. A pinned (clicked) popup still
|
||||
// follows the selection so it does not strand on a stale user while
|
||||
// arrows move the cursor.
|
||||
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
||||
if (!popupPinned) {
|
||||
return; // keyboard navigation shows no popup
|
||||
}
|
||||
if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return;
|
||||
}
|
||||
if (current && current->type() == QTreeWidgetItem::Type) {
|
||||
showPopupForUser(static_cast<UserListTWI *>(current));
|
||||
} else {
|
||||
popupPinned = false;
|
||||
hidePopup();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide popup when list scrolls (reference row has moved)
|
||||
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
|
||||
showPopupTimer->stop();
|
||||
hidePopup(true);
|
||||
requestAvatarsForVisibleItems();
|
||||
});
|
||||
|
||||
// Forward join requests from popup upward
|
||||
connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested);
|
||||
} else {
|
||||
// Dialog mode: keyboard selection drives the Invite button.
|
||||
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
||||
const QString userName = (current && current->type() == QTreeWidgetItem::Type)
|
||||
? current->data(2, Qt::UserRole).toString()
|
||||
: QString();
|
||||
emit currentUserChanged(userName);
|
||||
});
|
||||
|
||||
// Keep the popup-less scroll path alive for avatar prefetch.
|
||||
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this,
|
||||
[this] { requestAvatarsForVisibleItems(); });
|
||||
}
|
||||
|
||||
// Section dividers can be collapsed/expanded by the user. Surface those
|
||||
// changes only from real user interaction. Programmatic expansion is
|
||||
|
|
@ -707,16 +744,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
|||
connect(userTree, &QTreeWidget::itemCollapsed, this,
|
||||
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); });
|
||||
|
||||
// Hide popup when list scrolls (reference row has moved)
|
||||
connect(userTree->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
|
||||
showPopupTimer->stop();
|
||||
hidePopup(true);
|
||||
requestAvatarsForVisibleItems();
|
||||
});
|
||||
|
||||
// Forward join requests from popup upward
|
||||
connect(userInfoPopup, &UserInfoPopup::joinGameRequested, this, &UserListWidget::joinGameRequested);
|
||||
|
||||
connect(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader);
|
||||
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, this, &UserListWidget::refreshVisibleUserHeader);
|
||||
|
||||
|
|
@ -839,6 +866,9 @@ void UserListWidget::bind(UserListManager *mgr)
|
|||
void UserListWidget::refreshVisibleUserHeader(const QString &name)
|
||||
{
|
||||
userTree->viewport()->update();
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) {
|
||||
userInfoPopup->refreshHeader();
|
||||
}
|
||||
|
|
@ -846,6 +876,9 @@ void UserListWidget::refreshVisibleUserHeader(const QString &name)
|
|||
|
||||
void UserListWidget::refreshPopupButtons(const QString &userName)
|
||||
{
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
UserListTWI *item = users.value(userName);
|
||||
if (!item) {
|
||||
return;
|
||||
|
|
@ -863,6 +896,9 @@ void UserListWidget::refreshPopupButtons(const QString &userName)
|
|||
void UserListWidget::hideEvent(QHideEvent *e)
|
||||
{
|
||||
QGroupBox::hideEvent(e);
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
showPopupTimer->stop();
|
||||
hidePopupTimer->stop();
|
||||
hidePopup(true);
|
||||
|
|
@ -871,6 +907,9 @@ void UserListWidget::hideEvent(QHideEvent *e)
|
|||
void UserListWidget::showEvent(QShowEvent *e)
|
||||
{
|
||||
QGroupBox::showEvent(e);
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
requestAvatarsForVisibleItems();
|
||||
}
|
||||
|
||||
|
|
@ -943,6 +982,24 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
// Keyboard entry to the user context menu: the Menu key (or Shift+F10)
|
||||
// pops the same menu the right-click shows, anchored to the focused row.
|
||||
// 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 (hasUserInfoPopup && (obj == userTree || obj == userTree->viewport()) && event->type() == QEvent::ContextMenu) {
|
||||
auto *contextEvent = static_cast<QContextMenuEvent *>(event);
|
||||
if (contextEvent->reason() == QContextMenuEvent::Keyboard) {
|
||||
QTreeWidgetItem *current = userTree->currentItem();
|
||||
if (current && current->type() == QTreeWidgetItem::Type) {
|
||||
const QPoint globalPos = userTree->viewport()->mapToGlobal(userTree->visualItemRect(current).center());
|
||||
showContextMenu(globalPos, userTree->indexFromItem(current));
|
||||
return true;
|
||||
}
|
||||
return false; // divider rows: no menu
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -964,7 +1021,7 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
if (obj == userTree->viewport()) {
|
||||
if (hasUserInfoPopup && obj == userTree->viewport()) {
|
||||
if (event->type() == QEvent::MouseMove) {
|
||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||
return QGroupBox::eventFilter(obj, event);
|
||||
|
|
@ -1004,6 +1061,9 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
|||
|
||||
void UserListWidget::showPopupForUser(UserListTWI *item)
|
||||
{
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1062,6 +1122,9 @@ void UserListWidget::showPopupForUser(UserListTWI *item)
|
|||
|
||||
void UserListWidget::positionPopup(UserListTWI *item)
|
||||
{
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1116,6 +1179,9 @@ void UserListWidget::positionPopup(UserListTWI *item)
|
|||
|
||||
void UserListWidget::hidePopup(bool immediate)
|
||||
{
|
||||
if (!userInfoPopup) {
|
||||
return;
|
||||
}
|
||||
showPopupTimer->stop();
|
||||
hidePopupTimer->stop();
|
||||
if (!userInfoPopup->isVisible()) {
|
||||
|
|
@ -1476,8 +1542,10 @@ void UserListWidget::applyFilter()
|
|||
int visible = 0;
|
||||
for (int i = 0; i < divider->childCount(); ++i) {
|
||||
auto *child = static_cast<UserListTWI *>(divider->child(i));
|
||||
const bool match =
|
||||
!searching || QString::fromStdString(child->getUserInfo().name()).toLower().contains(lower);
|
||||
const QString name = QString::fromStdString(child->getUserInfo().name());
|
||||
const bool passesFilter =
|
||||
!userFilter || userFilter(name, child->data(0, UserListRoles::Online).toBool());
|
||||
const bool match = passesFilter && (!searching || name.toLower().contains(lower));
|
||||
child->setHidden(!match);
|
||||
if (match) {
|
||||
++visible;
|
||||
|
|
@ -1497,6 +1565,7 @@ void UserListWidget::applyFilter()
|
|||
}
|
||||
requestAvatarsForVisibleItems();
|
||||
userTree->viewport()->update();
|
||||
emit userListChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1514,6 +1583,7 @@ void UserListWidget::applyFilter()
|
|||
|
||||
requestAvatarsForVisibleItems();
|
||||
userTree->viewport()->update();
|
||||
emit userListChanged();
|
||||
}
|
||||
|
||||
void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
|
||||
|
|
@ -1521,7 +1591,38 @@ void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
|
|||
if (item->type() != QTreeWidgetItem::Type) {
|
||||
return; // divider rows open no chat
|
||||
}
|
||||
emit openMessageDialog(item->data(2, Qt::UserRole).toString(), true);
|
||||
const QString userName = item->data(2, Qt::UserRole).toString();
|
||||
if (hasUserInfoPopup) {
|
||||
emit openMessageDialog(userName, true);
|
||||
} else {
|
||||
emit userActivated(userName);
|
||||
}
|
||||
}
|
||||
|
||||
int UserListWidget::visibleUserRowCount() const
|
||||
{
|
||||
int count = 0;
|
||||
if (sectioned) {
|
||||
for (const Section section : sectionIds) {
|
||||
QTreeWidgetItem *divider = sectionItems.value(section);
|
||||
if (!divider || divider->isHidden()) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < divider->childCount(); ++i) {
|
||||
if (!divider->child(i)->isHidden()) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
for (int i = 0; i < userTree->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem *item = userTree->topLevelItem(i);
|
||||
if (!item->isHidden() && item->type() == QTreeWidgetItem::Type) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index)
|
||||
|
|
@ -1770,6 +1871,13 @@ UserListTWI *UserListWidget::ensureSectionMembership(Section section, const Serv
|
|||
|
||||
updateCardArtParams(user, userName);
|
||||
|
||||
// Dialog mode: rows that fail the user filter never exist. applyFilter()
|
||||
// re-checks the predicate on every pass so a live state change (e.g. the
|
||||
// user being ignored mid-dialog) hides an already created row.
|
||||
if (userFilter && !userFilter(userName, online)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QTreeWidgetItem *divider = sectionItems.value(section);
|
||||
if (!divider) {
|
||||
return nullptr;
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ private:
|
|||
QString hoveredUser;
|
||||
bool popupPinned = false;
|
||||
bool bulkLoading = false;
|
||||
bool hasUserInfoPopup = true;
|
||||
std::function<bool(const QString &userName, bool online)> userFilter;
|
||||
|
||||
/**
|
||||
* Popup functions are anchored on the row, not the user name. In sectioned
|
||||
|
|
@ -242,12 +244,19 @@ signals:
|
|||
void removeIgnore(const QString &userName);
|
||||
void joinGameRequested(int gameId, int roomId, bool asSpectator);
|
||||
void sectionExpanded(Section section, bool expanded);
|
||||
/** Dialog mode: the user activated (Enter/double-click) the given row. */
|
||||
void userActivated(const QString &userName);
|
||||
/** Dialog mode: the current row changed; empty string means no user row. */
|
||||
void currentUserChanged(const QString &userName);
|
||||
/** The set of visible rows changed (filter, search or a live mutation). */
|
||||
void userListChanged();
|
||||
|
||||
public:
|
||||
UserListWidget(TabSupervisor *_tabSupervisor,
|
||||
AbstractClient *_client,
|
||||
UserListType _type,
|
||||
QWidget *parent = nullptr);
|
||||
QWidget *parent = nullptr,
|
||||
bool hasUserInfoPopup = true);
|
||||
~UserListWidget() override;
|
||||
void bind(UserListManager *mgr);
|
||||
void applyDisplayMode();
|
||||
|
|
@ -263,6 +272,16 @@ public:
|
|||
void setShowTitle(bool showTitle);
|
||||
void setSectioned(const QList<Section> &ids);
|
||||
void setSectionExpanded(Section section, bool expanded);
|
||||
/** Dialog mode: rows that fail the predicate are never shown. */
|
||||
void setUserFilter(std::function<bool(const QString &userName, bool online)> filter)
|
||||
{
|
||||
userFilter = std::move(filter);
|
||||
}
|
||||
[[nodiscard]] int visibleUserRowCount() const;
|
||||
[[nodiscard]] bool getHasUserInfoPopup() const
|
||||
{
|
||||
return hasUserInfoPopup;
|
||||
}
|
||||
[[nodiscard]] const QList<Section> &getSectionIds() const
|
||||
{
|
||||
return sectionIds;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue