mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-23 18:06:26 -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
|
|
@ -36,6 +36,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp
|
src/interface/widgets/dialogs/dlg_forgot_password_challenge.cpp
|
||||||
src/interface/widgets/dialogs/dlg_forgot_password_request.cpp
|
src/interface/widgets/dialogs/dlg_forgot_password_request.cpp
|
||||||
src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp
|
src/interface/widgets/dialogs/dlg_forgot_password_reset.cpp
|
||||||
|
src/interface/widgets/dialogs/dlg_invite_to_game.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_deck.cpp
|
src/interface/widgets/dialogs/dlg_load_deck.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp
|
src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp
|
||||||
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp
|
||||||
|
|
|
||||||
112
cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp
Normal file
112
cockatrice/src/interface/widgets/dialogs/dlg_invite_to_game.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
#include "dlg_invite_to_game.h"
|
||||||
|
|
||||||
|
#include "../server/user/user_list_manager.h"
|
||||||
|
#include "../server/user/user_list_widget.h"
|
||||||
|
#include "../tabs/tab_supervisor.h"
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUrlQuery>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
DlgInviteToGame::DlgInviteToGame(TabSupervisor *_tabSupervisor,
|
||||||
|
const QString &_inviteUrl,
|
||||||
|
bool _onlyBuddies,
|
||||||
|
const QStringList &_excludeUserNames,
|
||||||
|
QWidget *parent)
|
||||||
|
: QDialog(parent), tabSupervisor(_tabSupervisor), inviteUrl(_inviteUrl), onlyBuddies(_onlyBuddies),
|
||||||
|
excludeUserNames(_excludeUserNames)
|
||||||
|
{
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
searchEdit = new QLineEdit(this);
|
||||||
|
searchEdit->setClearButtonEnabled(true);
|
||||||
|
connect(searchEdit, &QLineEdit::textChanged, this, &DlgInviteToGame::searchTextChanged);
|
||||||
|
|
||||||
|
// The embedded list is the real room user list without the hover popup:
|
||||||
|
// same manager, same delegate/painter, same sections, live via manager
|
||||||
|
// signals while the modal loop runs.
|
||||||
|
UserListManager *manager = tabSupervisor->getUserListManager();
|
||||||
|
userList = new UserListWidget(tabSupervisor, tabSupervisor->getClient(), UserListWidget::RoomList, this,
|
||||||
|
/*hasUserInfoPopup=*/false);
|
||||||
|
userList->setUserFilter([this, manager](const QString &name, bool online) {
|
||||||
|
return !excludeUserNames.contains(name) && online && !manager->isUserIgnored(name);
|
||||||
|
});
|
||||||
|
if (onlyBuddies) {
|
||||||
|
userList->setSectioned({UserListWidget::Section::Buddy});
|
||||||
|
} else {
|
||||||
|
userList->setSectioned({UserListWidget::Section::Buddy, UserListWidget::Section::Online});
|
||||||
|
}
|
||||||
|
userList->bind(manager);
|
||||||
|
userList->rebuild();
|
||||||
|
|
||||||
|
connect(userList, &UserListWidget::userActivated, this, &DlgInviteToGame::inviteCurrentUser);
|
||||||
|
connect(userList, &UserListWidget::currentUserChanged, this, [this](const QString &userName) {
|
||||||
|
currentUserName = userName;
|
||||||
|
inviteButton->setEnabled(!userName.isEmpty());
|
||||||
|
});
|
||||||
|
|
||||||
|
inviteButton = new QPushButton(this);
|
||||||
|
inviteButton->setEnabled(false);
|
||||||
|
inviteButton->setDefault(true);
|
||||||
|
connect(inviteButton, &QPushButton::clicked, this, [this] { inviteCurrentUser(currentUserName); });
|
||||||
|
|
||||||
|
cancelButton = new QPushButton(this);
|
||||||
|
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
|
||||||
|
auto *buttonRow = new QHBoxLayout;
|
||||||
|
buttonRow->addStretch();
|
||||||
|
buttonRow->addWidget(inviteButton);
|
||||||
|
buttonRow->addWidget(cancelButton);
|
||||||
|
|
||||||
|
auto *layout = new QVBoxLayout(this);
|
||||||
|
layout->addWidget(searchEdit);
|
||||||
|
layout->addWidget(userList, 1);
|
||||||
|
layout->addLayout(buttonRow);
|
||||||
|
|
||||||
|
retranslateUi();
|
||||||
|
|
||||||
|
// Default to a comfortably tall dialog so the list has room to breathe,
|
||||||
|
// capped by the available screen. No minimum is enforced: small screens
|
||||||
|
// and manual resizing can go shorter than this.
|
||||||
|
const QRect availableScreen = QGuiApplication::primaryScreen()->availableGeometry();
|
||||||
|
resize(sizeHint().width(), qMin(sizeHint().height() * 3, availableScreen.height() * 4 / 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DlgInviteToGame::searchTextChanged(const QString &text)
|
||||||
|
{
|
||||||
|
userList->setFilterText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DlgInviteToGame::inviteCurrentUser(const QString &userName)
|
||||||
|
{
|
||||||
|
if (userName.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The invite link carries the game's id and, when the game has one, its
|
||||||
|
// description (makeGameJoinLink embeds both). Read them back so the prefix
|
||||||
|
// names the game by description first, then its id — identical to the
|
||||||
|
// context-menu invite so recipients see one consistent message style.
|
||||||
|
const QUrl inviteUrlObj(inviteUrl);
|
||||||
|
const QUrlQuery inviteQuery(inviteUrlObj);
|
||||||
|
const int gameId = inviteQuery.queryItemValue("gameid").toInt();
|
||||||
|
const QString gameDescription = inviteQuery.queryItemValue("game");
|
||||||
|
const QString prefix = gameDescription.isEmpty()
|
||||||
|
? tr("Join my game (#%1):").arg(gameId)
|
||||||
|
: tr("Join my game \"%1\" (#%2):").arg(gameDescription).arg(gameId);
|
||||||
|
tabSupervisor->sendInviteToUser(userName, prefix + " " + inviteUrl);
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DlgInviteToGame::retranslateUi()
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Invite to Game"));
|
||||||
|
searchEdit->setPlaceholderText(tr("Search users..."));
|
||||||
|
inviteButton->setText(tr("Invite"));
|
||||||
|
cancelButton->setText(tr("Cancel"));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* @file dlg_invite_to_game.h
|
||||||
|
* @ingroup RoomDialogs
|
||||||
|
*/
|
||||||
|
//! \todo Document this file.
|
||||||
|
|
||||||
|
#ifndef DLG_INVITE_TO_GAME_H
|
||||||
|
#define DLG_INVITE_TO_GAME_H
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
class QPushButton;
|
||||||
|
class TabSupervisor;
|
||||||
|
class UserListWidget;
|
||||||
|
|
||||||
|
class DlgInviteToGame : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
DlgInviteToGame(TabSupervisor *_tabSupervisor,
|
||||||
|
const QString &_inviteUrl,
|
||||||
|
bool _onlyBuddies,
|
||||||
|
const QStringList &_excludeUserNames,
|
||||||
|
QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void searchTextChanged(const QString &text);
|
||||||
|
void inviteCurrentUser(const QString &userName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
TabSupervisor *tabSupervisor;
|
||||||
|
QString inviteUrl;
|
||||||
|
bool onlyBuddies;
|
||||||
|
QStringList excludeUserNames;
|
||||||
|
QString currentUserName;
|
||||||
|
QLineEdit *searchEdit;
|
||||||
|
UserListWidget *userList;
|
||||||
|
QPushButton *inviteButton;
|
||||||
|
QPushButton *cancelButton;
|
||||||
|
|
||||||
|
void retranslateUi();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -354,7 +354,10 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
|
||||||
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
|
||||||
QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event);
|
QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event);
|
||||||
if (mouseEvent->button() == Qt::RightButton) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -578,8 +581,10 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
|
||||||
UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
AbstractClient *_client,
|
AbstractClient *_client,
|
||||||
UserListType _type,
|
UserListType _type,
|
||||||
QWidget *parent)
|
QWidget *parent,
|
||||||
: QGroupBox(parent), tabSupervisor(_tabSupervisor), client(_client), type(_type), onlineCount(0)
|
bool _hasUserInfoPopup)
|
||||||
|
: QGroupBox(parent), hasUserInfoPopup(_hasUserInfoPopup), tabSupervisor(_tabSupervisor), client(_client),
|
||||||
|
type(_type), onlineCount(0)
|
||||||
{
|
{
|
||||||
avatarProvider = new UserAvatarProvider(client, this);
|
avatarProvider = new UserAvatarProvider(client, this);
|
||||||
cardArtProvider = new UserCardArtProvider(this);
|
cardArtProvider = new UserCardArtProvider(this);
|
||||||
|
|
@ -605,15 +610,8 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
userTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
userTree->header()->setStretchLastSection(true);
|
userTree->header()->setStretchLastSection(true);
|
||||||
|
|
||||||
// ── Hover popup ───────────────────────────────────────────────────────────
|
// Always create timers so callers never segfault on a null deref;
|
||||||
userInfoPopup = new UserInfoPopup(tabSupervisor, tabSupervisor->getClient(), &avatarProvider->cache(),
|
// showPopupForUser / hidePopup already guard against a null userInfoPopup.
|
||||||
&cardArtProvider->cache(), &cardArtParamsMap,
|
|
||||||
window()); // parented to main window so it floats above siblings
|
|
||||||
|
|
||||||
userInfoPopup->hide();
|
|
||||||
userInfoPopup->setWindowOpacity(0.0);
|
|
||||||
userInfoPopup->installEventFilter(this);
|
|
||||||
|
|
||||||
showPopupTimer = new QTimer(this);
|
showPopupTimer = new QTimer(this);
|
||||||
showPopupTimer->setSingleShot(true);
|
showPopupTimer->setSingleShot(true);
|
||||||
showPopupTimer->setInterval(280);
|
showPopupTimer->setInterval(280);
|
||||||
|
|
@ -639,65 +637,104 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
// The hover ends when the cursor leaves the user row. Empty list
|
// The hover ends when the cursor leaves the user row. Empty list
|
||||||
// space, a section divider and anything outside the tree all close
|
// space, a section divider and anything outside the tree all close
|
||||||
// the popup, while the popup itself keeps it alive.
|
// 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();
|
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->setMouseTracking(true);
|
||||||
userTree->viewport()->setMouseTracking(true);
|
userTree->viewport()->setMouseTracking(true);
|
||||||
userTree->viewport()->installEventFilter(this);
|
userTree->viewport()->installEventFilter(this);
|
||||||
userTree->installEventFilter(this); // keyboard handling for section dividers
|
userTree->installEventFilter(this); // keyboard handling for section dividers
|
||||||
|
|
||||||
// Clicking anywhere outside the list clears its selection and closes the
|
if (hasUserInfoPopup) {
|
||||||
// popup. The filter watches all widgets because the press can land on any
|
// Clicking anywhere outside the list clears its selection and closes the
|
||||||
// part of the window, on another list or on the popup itself.
|
// popup. The filter watches all widgets because the press can land on any
|
||||||
qApp->installEventFilter(this);
|
// part of the window, on another list or on the popup itself.
|
||||||
|
qApp->installEventFilter(this);
|
||||||
|
|
||||||
// Pin on item click
|
// Pin on item click
|
||||||
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
connect(userTree, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem *item, int) {
|
||||||
// Clicking a section divider toggles it
|
// Clicking a section divider toggles it
|
||||||
if (sectioned && item->type() == SectionItemType) {
|
if (sectioned && item->type() == SectionItemType) {
|
||||||
setExpandedProgrammatically(item, !item->isExpanded());
|
setExpandedProgrammatically(item, !item->isExpanded());
|
||||||
handleSectionExpansion(item, item->isExpanded());
|
handleSectionExpansion(item, item->isExpanded());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item->type() != QTreeWidgetItem::Type) {
|
if (item->type() != QTreeWidgetItem::Type) {
|
||||||
return; // divider rows have no user popup
|
return; // divider rows have no user popup
|
||||||
}
|
}
|
||||||
popupPinned = false; // reset so showPopupForUser can update
|
popupPinned = false; // reset so showPopupForUser can update
|
||||||
showPopupForUser(static_cast<UserListTWI *>(item));
|
showPopupForUser(static_cast<UserListTWI *>(item));
|
||||||
popupPinned = true; // pin after showing
|
popupPinned = true; // pin after showing
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
connect(userTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||||
[this](const QItemSelection &sel, const QItemSelection &) {
|
[this](const QItemSelection &sel, const QItemSelection &) {
|
||||||
if (sel.isEmpty() && popupPinned) {
|
if (sel.isEmpty() && popupPinned) {
|
||||||
popupPinned = false;
|
popupPinned = false;
|
||||||
hidePopup();
|
hidePopup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keyboard selection: show the popup for the current row and hide it when
|
// Keyboard selection: the popup is a mouse surface, so keyboard
|
||||||
// the focus moves to a section divider or leaves the list entirely. The
|
// navigation shows no floating popup. A pinned (clicked) popup still
|
||||||
// popup therefore follows arrow key navigation exactly like mouse hover.
|
// follows the selection so it does not strand on a stale user while
|
||||||
// When it was pinned by a click it stays open and follows the selection.
|
// arrows move the cursor.
|
||||||
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
connect(userTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *current, QTreeWidgetItem *) {
|
||||||
if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) {
|
if (!popupPinned) {
|
||||||
return;
|
return; // keyboard navigation shows no popup
|
||||||
}
|
}
|
||||||
if (current && current->type() == QTreeWidgetItem::Type) {
|
if (!isVisible() || !SettingsCache::instance().appearance().getStyleUserList()) {
|
||||||
showPopupForUser(static_cast<UserListTWI *>(current));
|
return;
|
||||||
} else {
|
}
|
||||||
popupPinned = false;
|
if (current && current->type() == QTreeWidgetItem::Type) {
|
||||||
hidePopup();
|
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
|
// Section dividers can be collapsed/expanded by the user. Surface those
|
||||||
// changes only from real user interaction. Programmatic expansion is
|
// changes only from real user interaction. Programmatic expansion is
|
||||||
|
|
@ -707,16 +744,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
connect(userTree, &QTreeWidget::itemCollapsed, this,
|
connect(userTree, &QTreeWidget::itemCollapsed, this,
|
||||||
[this](QTreeWidgetItem *item) { handleSectionExpansion(item, false); });
|
[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(avatarProvider, &UserAvatarProvider::avatarUpdated, this, &UserListWidget::refreshVisibleUserHeader);
|
||||||
connect(cardArtProvider, &UserCardArtProvider::cardArtUpdated, 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)
|
void UserListWidget::refreshVisibleUserHeader(const QString &name)
|
||||||
{
|
{
|
||||||
userTree->viewport()->update();
|
userTree->viewport()->update();
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) {
|
if (userInfoPopup->isVisible() && userInfoPopup->getCurrentUser() == name) {
|
||||||
userInfoPopup->refreshHeader();
|
userInfoPopup->refreshHeader();
|
||||||
}
|
}
|
||||||
|
|
@ -846,6 +876,9 @@ void UserListWidget::refreshVisibleUserHeader(const QString &name)
|
||||||
|
|
||||||
void UserListWidget::refreshPopupButtons(const QString &userName)
|
void UserListWidget::refreshPopupButtons(const QString &userName)
|
||||||
{
|
{
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
UserListTWI *item = users.value(userName);
|
UserListTWI *item = users.value(userName);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -863,6 +896,9 @@ void UserListWidget::refreshPopupButtons(const QString &userName)
|
||||||
void UserListWidget::hideEvent(QHideEvent *e)
|
void UserListWidget::hideEvent(QHideEvent *e)
|
||||||
{
|
{
|
||||||
QGroupBox::hideEvent(e);
|
QGroupBox::hideEvent(e);
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
showPopupTimer->stop();
|
showPopupTimer->stop();
|
||||||
hidePopupTimer->stop();
|
hidePopupTimer->stop();
|
||||||
hidePopup(true);
|
hidePopup(true);
|
||||||
|
|
@ -871,6 +907,9 @@ void UserListWidget::hideEvent(QHideEvent *e)
|
||||||
void UserListWidget::showEvent(QShowEvent *e)
|
void UserListWidget::showEvent(QShowEvent *e)
|
||||||
{
|
{
|
||||||
QGroupBox::showEvent(e);
|
QGroupBox::showEvent(e);
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
requestAvatarsForVisibleItems();
|
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.
|
// Keyboard navigation of the section dividers.
|
||||||
// The dividers are selectable so arrow keys land on them. When one is the
|
// 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
|
// 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 (event->type() == QEvent::MouseMove) {
|
||||||
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
if (!SettingsCache::instance().appearance().getStyleUserList()) {
|
||||||
return QGroupBox::eventFilter(obj, event);
|
return QGroupBox::eventFilter(obj, event);
|
||||||
|
|
@ -1004,6 +1061,9 @@ bool UserListWidget::eventFilter(QObject *obj, QEvent *event)
|
||||||
|
|
||||||
void UserListWidget::showPopupForUser(UserListTWI *item)
|
void UserListWidget::showPopupForUser(UserListTWI *item)
|
||||||
{
|
{
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1062,6 +1122,9 @@ void UserListWidget::showPopupForUser(UserListTWI *item)
|
||||||
|
|
||||||
void UserListWidget::positionPopup(UserListTWI *item)
|
void UserListWidget::positionPopup(UserListTWI *item)
|
||||||
{
|
{
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1116,6 +1179,9 @@ void UserListWidget::positionPopup(UserListTWI *item)
|
||||||
|
|
||||||
void UserListWidget::hidePopup(bool immediate)
|
void UserListWidget::hidePopup(bool immediate)
|
||||||
{
|
{
|
||||||
|
if (!userInfoPopup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
showPopupTimer->stop();
|
showPopupTimer->stop();
|
||||||
hidePopupTimer->stop();
|
hidePopupTimer->stop();
|
||||||
if (!userInfoPopup->isVisible()) {
|
if (!userInfoPopup->isVisible()) {
|
||||||
|
|
@ -1476,8 +1542,10 @@ void UserListWidget::applyFilter()
|
||||||
int visible = 0;
|
int visible = 0;
|
||||||
for (int i = 0; i < divider->childCount(); ++i) {
|
for (int i = 0; i < divider->childCount(); ++i) {
|
||||||
auto *child = static_cast<UserListTWI *>(divider->child(i));
|
auto *child = static_cast<UserListTWI *>(divider->child(i));
|
||||||
const bool match =
|
const QString name = QString::fromStdString(child->getUserInfo().name());
|
||||||
!searching || QString::fromStdString(child->getUserInfo().name()).toLower().contains(lower);
|
const bool passesFilter =
|
||||||
|
!userFilter || userFilter(name, child->data(0, UserListRoles::Online).toBool());
|
||||||
|
const bool match = passesFilter && (!searching || name.toLower().contains(lower));
|
||||||
child->setHidden(!match);
|
child->setHidden(!match);
|
||||||
if (match) {
|
if (match) {
|
||||||
++visible;
|
++visible;
|
||||||
|
|
@ -1497,6 +1565,7 @@ void UserListWidget::applyFilter()
|
||||||
}
|
}
|
||||||
requestAvatarsForVisibleItems();
|
requestAvatarsForVisibleItems();
|
||||||
userTree->viewport()->update();
|
userTree->viewport()->update();
|
||||||
|
emit userListChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1514,6 +1583,7 @@ void UserListWidget::applyFilter()
|
||||||
|
|
||||||
requestAvatarsForVisibleItems();
|
requestAvatarsForVisibleItems();
|
||||||
userTree->viewport()->update();
|
userTree->viewport()->update();
|
||||||
|
emit userListChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
|
void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
|
||||||
|
|
@ -1521,7 +1591,38 @@ void UserListWidget::userClicked(QTreeWidgetItem *item, int /*column*/)
|
||||||
if (item->type() != QTreeWidgetItem::Type) {
|
if (item->type() != QTreeWidgetItem::Type) {
|
||||||
return; // divider rows open no chat
|
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)
|
void UserListWidget::showContextMenu(const QPoint &pos, const QModelIndex &index)
|
||||||
|
|
@ -1770,6 +1871,13 @@ UserListTWI *UserListWidget::ensureSectionMembership(Section section, const Serv
|
||||||
|
|
||||||
updateCardArtParams(user, userName);
|
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);
|
QTreeWidgetItem *divider = sectionItems.value(section);
|
||||||
if (!divider) {
|
if (!divider) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,8 @@ private:
|
||||||
QString hoveredUser;
|
QString hoveredUser;
|
||||||
bool popupPinned = false;
|
bool popupPinned = false;
|
||||||
bool bulkLoading = 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
|
* Popup functions are anchored on the row, not the user name. In sectioned
|
||||||
|
|
@ -242,12 +244,19 @@ signals:
|
||||||
void removeIgnore(const QString &userName);
|
void removeIgnore(const QString &userName);
|
||||||
void joinGameRequested(int gameId, int roomId, bool asSpectator);
|
void joinGameRequested(int gameId, int roomId, bool asSpectator);
|
||||||
void sectionExpanded(Section section, bool expanded);
|
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:
|
public:
|
||||||
UserListWidget(TabSupervisor *_tabSupervisor,
|
UserListWidget(TabSupervisor *_tabSupervisor,
|
||||||
AbstractClient *_client,
|
AbstractClient *_client,
|
||||||
UserListType _type,
|
UserListType _type,
|
||||||
QWidget *parent = nullptr);
|
QWidget *parent = nullptr,
|
||||||
|
bool hasUserInfoPopup = true);
|
||||||
~UserListWidget() override;
|
~UserListWidget() override;
|
||||||
void bind(UserListManager *mgr);
|
void bind(UserListManager *mgr);
|
||||||
void applyDisplayMode();
|
void applyDisplayMode();
|
||||||
|
|
@ -263,6 +272,16 @@ public:
|
||||||
void setShowTitle(bool showTitle);
|
void setShowTitle(bool showTitle);
|
||||||
void setSectioned(const QList<Section> &ids);
|
void setSectioned(const QList<Section> &ids);
|
||||||
void setSectionExpanded(Section section, bool expanded);
|
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
|
[[nodiscard]] const QList<Section> &getSectionIds() const
|
||||||
{
|
{
|
||||||
return sectionIds;
|
return sectionIds;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include "../interface/card_picture_loader/card_picture_loader.h"
|
#include "../interface/card_picture_loader/card_picture_loader.h"
|
||||||
#include "../interface/widgets/cards/card_info_frame_widget.h"
|
#include "../interface/widgets/cards/card_info_frame_widget.h"
|
||||||
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
#include "../interface/widgets/dialogs/dlg_create_game.h"
|
||||||
|
#include "../interface/widgets/dialogs/dlg_invite_to_game.h"
|
||||||
#include "../interface/widgets/server/game_link.h"
|
#include "../interface/widgets/server/game_link.h"
|
||||||
#include "../interface/widgets/server/user/user_list_manager.h"
|
#include "../interface/widgets/server/user/user_list_manager.h"
|
||||||
#include "../interface/widgets/utility/completer_utils.h"
|
#include "../interface/widgets/utility/completer_utils.h"
|
||||||
|
|
@ -43,10 +44,12 @@
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QStringListModel>
|
#include <QStringListModel>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QVBoxLayout>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <libcockatrice/card/database/card_database.h>
|
#include <libcockatrice/card/database/card_database.h>
|
||||||
#include <libcockatrice/card/database/card_database_manager.h>
|
#include <libcockatrice/card/database/card_database_manager.h>
|
||||||
|
|
@ -296,6 +299,9 @@ void TabGame::retranslateUi()
|
||||||
QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId());
|
QString tabText = " | " + type + " #" + QString::number(game->getGameMetaInfo()->gameId());
|
||||||
|
|
||||||
updatePlayerListDockTitle();
|
updatePlayerListDockTitle();
|
||||||
|
if (inviteButton) {
|
||||||
|
inviteButton->setText(tr("Invite"));
|
||||||
|
}
|
||||||
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
|
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
|
||||||
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
|
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
|
||||||
if (replayDock) {
|
if (replayDock) {
|
||||||
|
|
@ -337,6 +343,9 @@ void TabGame::retranslateUi()
|
||||||
if (aCopyGameLink) {
|
if (aCopyGameLink) {
|
||||||
aCopyGameLink->setText(tr("Cop&y game link"));
|
aCopyGameLink->setText(tr("Cop&y game link"));
|
||||||
}
|
}
|
||||||
|
if (aInviteToGame) {
|
||||||
|
aInviteToGame->setText(tr("Invite to Game..."));
|
||||||
|
}
|
||||||
if (aConcede) {
|
if (aConcede) {
|
||||||
if (game->getPlayerManager()->isMainPlayerConceded()) {
|
if (game->getPlayerManager()->isMainPlayerConceded()) {
|
||||||
aConcede->setText(tr("Un&concede"));
|
aConcede->setText(tr("Un&concede"));
|
||||||
|
|
@ -513,6 +522,47 @@ void TabGame::actCopyGameLink()
|
||||||
QApplication::clipboard()->setText(link);
|
QApplication::clipboard()->setText(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabGame::updateInviteButtonState()
|
||||||
|
{
|
||||||
|
// The dock button stays conservative (pre-start, not full); the menu action
|
||||||
|
// additionally covers started/full games, which are legitimate spectate
|
||||||
|
// invites, so it only needs the server-linked + not-closed conditions.
|
||||||
|
const bool canInvite = !tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed() &&
|
||||||
|
!game->getGameMetaInfo()->started() &&
|
||||||
|
game->getPlayerManager()->getPlayerCount() < game->getGameMetaInfo()->maxPlayers();
|
||||||
|
if (inviteButton) {
|
||||||
|
inviteButton->setVisible(canInvite);
|
||||||
|
}
|
||||||
|
if (aInviteToGame) {
|
||||||
|
aInviteToGame->setEnabled(!tabSupervisor->getIsLocalGame() && !game->getGameState()->isGameClosed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabGame::actInviteToGame()
|
||||||
|
{
|
||||||
|
if (!tabSupervisor || tabSupervisor->getIsLocalGame()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameMetaInfo *metaInfo = game->getGameMetaInfo();
|
||||||
|
const QString inviteUrl = makeGameJoinLink(
|
||||||
|
tabSupervisor->getClient()->serverName(), tabSupervisor->getClient()->serverPort(), metaInfo->proto().room_id(),
|
||||||
|
metaInfo->gameId(), QString::fromStdString(metaInfo->proto().description()));
|
||||||
|
|
||||||
|
QStringList excludeUserNames;
|
||||||
|
excludeUserNames << tabSupervisor->getUserListManager()->getOwnUsername();
|
||||||
|
for (auto player : game->getPlayerManager()->getPlayers()) {
|
||||||
|
excludeUserNames << player->getPlayerInfo()->getName();
|
||||||
|
}
|
||||||
|
for (auto it = game->getPlayerManager()->getSpectators().cbegin();
|
||||||
|
it != game->getPlayerManager()->getSpectators().cend(); ++it) {
|
||||||
|
excludeUserNames << QString::fromStdString(it.value().name());
|
||||||
|
}
|
||||||
|
|
||||||
|
DlgInviteToGame dlg(tabSupervisor, inviteUrl, metaInfo->proto().only_buddies(), excludeUserNames, this);
|
||||||
|
dlg.exec();
|
||||||
|
}
|
||||||
|
|
||||||
void TabGame::actConcede()
|
void TabGame::actConcede()
|
||||||
{
|
{
|
||||||
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
PlayerLogic *player = game->getPlayerManager()->getActiveLocalPlayer(game->getGameState()->getActivePlayer());
|
||||||
|
|
@ -1004,6 +1054,8 @@ void TabGame::createMenuItems()
|
||||||
aCopyGameLink = new QAction(this);
|
aCopyGameLink = new QAction(this);
|
||||||
aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty());
|
aCopyGameLink->setEnabled(!tabSupervisor->getIsLocalGame() && !tabSupervisor->getClient()->serverName().isEmpty());
|
||||||
connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink);
|
connect(aCopyGameLink, &QAction::triggered, this, &TabGame::actCopyGameLink);
|
||||||
|
aInviteToGame = new QAction(this);
|
||||||
|
connect(aInviteToGame, &QAction::triggered, this, &TabGame::actInviteToGame);
|
||||||
aConcede = new QAction(this);
|
aConcede = new QAction(this);
|
||||||
connect(aConcede, &QAction::triggered, this, &TabGame::actConcede);
|
connect(aConcede, &QAction::triggered, this, &TabGame::actConcede);
|
||||||
if (!game->getGameMetaInfo()->started()) {
|
if (!game->getGameMetaInfo()->started()) {
|
||||||
|
|
@ -1043,6 +1095,7 @@ void TabGame::createMenuItems()
|
||||||
gameMenu->addSeparator();
|
gameMenu->addSeparator();
|
||||||
gameMenu->addAction(aGameInfo);
|
gameMenu->addAction(aGameInfo);
|
||||||
gameMenu->addAction(aCopyGameLink);
|
gameMenu->addAction(aCopyGameLink);
|
||||||
|
gameMenu->addAction(aInviteToGame);
|
||||||
gameMenu->addAction(aConcede);
|
gameMenu->addAction(aConcede);
|
||||||
gameMenu->addAction(aFocusChat);
|
gameMenu->addAction(aFocusChat);
|
||||||
gameMenu->addAction(aLeaveGame);
|
gameMenu->addAction(aLeaveGame);
|
||||||
|
|
@ -1051,6 +1104,9 @@ void TabGame::createMenuItems()
|
||||||
|
|
||||||
aCardMenu = gameMenu->addMenu(new QMenu(this));
|
aCardMenu = gameMenu->addMenu(new QMenu(this));
|
||||||
|
|
||||||
|
// Sync the new action with the same state the dock button already shows.
|
||||||
|
updateInviteButtonState();
|
||||||
|
|
||||||
addTabMenu(gameMenu);
|
addTabMenu(gameMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1066,6 +1122,7 @@ void TabGame::createReplayMenuItems()
|
||||||
aResetLayout = nullptr;
|
aResetLayout = nullptr;
|
||||||
aGameInfo = nullptr;
|
aGameInfo = nullptr;
|
||||||
aCopyGameLink = nullptr;
|
aCopyGameLink = nullptr;
|
||||||
|
aInviteToGame = nullptr;
|
||||||
aConcede = nullptr;
|
aConcede = nullptr;
|
||||||
aFocusChat = nullptr;
|
aFocusChat = nullptr;
|
||||||
aLeaveGame = new QAction(this);
|
aLeaveGame = new QAction(this);
|
||||||
|
|
@ -1264,11 +1321,29 @@ void TabGame::createPlayerListDock(bool bReplay)
|
||||||
}
|
}
|
||||||
playerListWidget->setFocusPolicy(Qt::NoFocus);
|
playerListWidget->setFocusPolicy(Qt::NoFocus);
|
||||||
|
|
||||||
|
auto *playerListBox = new QWidget(this);
|
||||||
|
auto *vbox = new QVBoxLayout(playerListBox);
|
||||||
|
vbox->setContentsMargins(0, 0, 0, 0);
|
||||||
|
vbox->setSpacing(0);
|
||||||
|
|
||||||
|
vbox->addWidget(playerListWidget);
|
||||||
|
|
||||||
|
if (!bReplay) {
|
||||||
|
inviteButton = new QPushButton(tr("Invite"), playerListBox);
|
||||||
|
inviteButton->setVisible(false);
|
||||||
|
connect(inviteButton, &QPushButton::clicked, this, &TabGame::actInviteToGame);
|
||||||
|
vbox->addWidget(inviteButton);
|
||||||
|
|
||||||
|
connect(game->getGameMetaInfo(), &GameMetaInfo::startedChanged, this, &TabGame::updateInviteButtonState);
|
||||||
|
connect(game->getPlayerManager(), &PlayerManager::playerCountChanged, this, &TabGame::updateInviteButtonState);
|
||||||
|
updateInviteButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
playerListDock = new QDockWidget(this);
|
playerListDock = new QDockWidget(this);
|
||||||
playerListDock->setObjectName("playerListDock");
|
playerListDock->setObjectName("playerListDock");
|
||||||
playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
|
playerListDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetFloatable |
|
||||||
QDockWidget::DockWidgetMovable);
|
QDockWidget::DockWidgetMovable);
|
||||||
playerListDock->setWidget(playerListWidget);
|
playerListDock->setWidget(playerListBox);
|
||||||
playerListDock->setFloating(false);
|
playerListDock->setFloating(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class CardInfoFrameWidget;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
class QSplitter;
|
class QSplitter;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QPushButton;
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
class QMenu;
|
class QMenu;
|
||||||
class ZoneViewLayout;
|
class ZoneViewLayout;
|
||||||
|
|
@ -69,6 +70,7 @@ private:
|
||||||
|
|
||||||
CardInfoFrameWidget *cardInfoFrameWidget;
|
CardInfoFrameWidget *cardInfoFrameWidget;
|
||||||
PlayerListWidget *playerListWidget;
|
PlayerListWidget *playerListWidget;
|
||||||
|
QPushButton *inviteButton = nullptr;
|
||||||
QLabel *timeElapsedLabel;
|
QLabel *timeElapsedLabel;
|
||||||
MessageLogWidget *messageLog;
|
MessageLogWidget *messageLog;
|
||||||
QLabel *sayLabel;
|
QLabel *sayLabel;
|
||||||
|
|
@ -86,6 +88,7 @@ private:
|
||||||
QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn,
|
QAction *aGameInfo, *aConcede, *aCopyGameLink, *aLeaveGame, *aNextPhase, *aNextPhaseAction, *aNextTurn,
|
||||||
*aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout;
|
*aReverseTurn, *aRemoveLocalArrows, *aRotateViewCW, *aRotateViewCCW, *aResetLayout, *aResetReplayLayout;
|
||||||
QAction *aFocusChat;
|
QAction *aFocusChat;
|
||||||
|
QAction *aInviteToGame = nullptr;
|
||||||
QList<QAction *> phaseActions;
|
QList<QAction *> phaseActions;
|
||||||
QAction *aCardMenu;
|
QAction *aCardMenu;
|
||||||
|
|
||||||
|
|
@ -128,6 +131,7 @@ private:
|
||||||
void createPlayAreaWidget(bool bReplay = false);
|
void createPlayAreaWidget(bool bReplay = false);
|
||||||
void createDeckViewContainerWidget(bool bReplay = false);
|
void createDeckViewContainerWidget(bool bReplay = false);
|
||||||
void createReplayDock(GameReplay *replay);
|
void createReplayDock(GameReplay *replay);
|
||||||
|
void updateInviteButtonState();
|
||||||
signals:
|
signals:
|
||||||
void gameClosing(TabGame *tab);
|
void gameClosing(TabGame *tab);
|
||||||
void containerProcessingStarted(const GameEventContext &context);
|
void containerProcessingStarted(const GameEventContext &context);
|
||||||
|
|
@ -147,6 +151,7 @@ private slots:
|
||||||
void setCardMenu(CardMenu *menu);
|
void setCardMenu(CardMenu *menu);
|
||||||
|
|
||||||
void actGameInfo();
|
void actGameInfo();
|
||||||
|
void actInviteToGame();
|
||||||
void actConcede();
|
void actConcede();
|
||||||
void actCopyGameLink();
|
void actCopyGameLink();
|
||||||
void actRemoveLocalArrows();
|
void actRemoveLocalArrows();
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,12 @@ bool TabMessage::isUserOnline() const
|
||||||
return userOnline;
|
return userOnline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabMessage::sendInviteMessage(const QString &text)
|
||||||
|
{
|
||||||
|
sayEdit->setText(text);
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
void TabMessage::messageSent(const Response &response,
|
void TabMessage::messageSent(const Response &response,
|
||||||
const CommandContainer & /*commandContainer*/,
|
const CommandContainer & /*commandContainer*/,
|
||||||
const QVariant &extraData)
|
const QVariant &extraData)
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] bool isUserOnline() const;
|
[[nodiscard]] bool isUserOnline() const;
|
||||||
void sendPrivateMessage(const QString &text);
|
void sendPrivateMessage(const QString &text);
|
||||||
|
void sendInviteMessage(const QString &text);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool shouldShowSystemPopup(const Event_UserMessage &event);
|
bool shouldShowSystemPopup(const Event_UserMessage &event);
|
||||||
|
|
|
||||||
|
|
@ -993,8 +993,8 @@ QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) con
|
||||||
void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText)
|
void TabSupervisor::sendInviteToUser(const QString &userName, const QString &inviteText)
|
||||||
{
|
{
|
||||||
TabMessage *tab = addMessageTab(userName, true);
|
TabMessage *tab = addMessageTab(userName, true);
|
||||||
if (tab && tab->isUserOnline()) {
|
if (tab) {
|
||||||
tab->sendPrivateMessage(inviteText);
|
tab->sendInviteMessage(inviteText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue