[Lobby] Add instant quick host to the game room (#7802)

* Add instant quick host to the game lobby

* Use a native pull-down on macOS and a split button elsewhere

- Gate on the active application style instead of the platform, so
  choosing Fusion on macOS still gets the split button: QMacStyle ignores
  autoRaise/flat and draws a QToolButton menu as a separate square.
- Rebuild the host control on QEvent::StyleChange, because the appearance
  settings switch styles while the room is open.
- Keep the review action, its arrow and the host button disabled while a
  host or join command is still in flight.
- Announce inline quick-host failures to screen readers.

* Address review findings

* Replace the macOS pull-down with a sibling settings button

QMacStyle has no split-button rendering: a menu-bearing QToolButton is drawn as a second square bezel. Keep the primary Host game button as the one-click quick host on every platform, and show a plain Game settings button as the secondary action on macOS instead; other styles keep the split arrow.

Rename the secondary action from "Review settings first..." to "Game settings..." so the label says what it opens, and update the shortcuts page label to match.
This commit is contained in:
BruebachL 2026-11-08 22:12:29 +01:00 • committed by GitHub
parent b1c8f156dc
commit 40ebd22549
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 388 additions and 21 deletions

View file

@ -313,6 +313,12 @@ private:
{"DeckViewContainer/forceStartGameButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Force Start"), {"DeckViewContainer/forceStartGameButton", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Force Start"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Game_Lobby)}, ShortcutGroup::Game_Lobby)},
{"GameSelector/aQuickHostGame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Host Game"),
parseSequenceString("Ctrl+N"),
ShortcutGroup::Game_Lobby)},
{"GameSelector/aReviewGame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Game Settings"),
parseSequenceString("Ctrl+Shift+N"),
ShortcutGroup::Game_Lobby)},
{"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"), {"Player/aCCMagenta", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Add Card Counter (F)"),
parseSequenceString(""), parseSequenceString(""),
ShortcutGroup::Card_Counters)}, ShortcutGroup::Card_Counters)},

View file

@ -36,6 +36,13 @@ GameCreationOptions GameCreationOptions::fromSettings(const IGameSettingsProvide
return options; return options;
} }
GameCreationOptions GameCreationOptions::defaultsFor(bool registeredUser)
{
GameCreationOptions options;
options.onlyRegistered = registeredUser;
return options;
}
GameCreationOptions GameCreationOptions::fromGameInfo(const ServerInfo_Game &gameInfo, GameCreationOptions GameCreationOptions::fromGameInfo(const ServerInfo_Game &gameInfo,
const QMap<int, QString> &gameTypes) const QMap<int, QString> &gameTypes)
{ {

View file

@ -49,6 +49,14 @@ struct GameCreationOptions
[[nodiscard]] static GameCreationOptions [[nodiscard]] static GameCreationOptions
fromSettings(const IGameSettingsProvider &settings, const QMap<int, QString> &gameTypes, bool registeredUser); fromSettings(const IGameSettingsProvider &settings, const QMap<int, QString> &gameTypes, bool registeredUser);
/**
* @brief The options the host form shows when nothing is remembered.
*
* A registered host defaults to a registered-only game, matching the form's
* ``Clear`` action.
*/
[[nodiscard]] static GameCreationOptions defaultsFor(bool registeredUser);
/** /**
* @brief Reads the read-only fields shown in the game information view. * @brief Reads the read-only fields shown in the game information view.
*/ */

View file

@ -1,9 +1,11 @@
#include "game_selector.h" #include "game_selector.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../../../client/settings/shortcuts_settings.h"
#include "../../theme_icon.h" #include "../../theme_icon.h"
#include "../interface/widgets/dialogs/dlg_create_game.h" #include "../interface/widgets/dialogs/dlg_create_game.h"
#include "../interface/widgets/dialogs/dlg_filter_games.h" #include "../interface/widgets/dialogs/dlg_filter_games.h"
#include "../interface/widgets/game_hosting/game_creation_options.h"
#include "../interface/widgets/tabs/tab_room.h" #include "../interface/widgets/tabs/tab_room.h"
#include "../interface/widgets/tabs/tab_supervisor.h" #include "../interface/widgets/tabs/tab_supervisor.h"
#include "../interface/widgets/utility/get_text_with_max.h" #include "../interface/widgets/utility/get_text_with_max.h"
@ -15,13 +17,18 @@
#include "libcockatrice/protocol/pb/serverinfo_user.pb.h" #include "libcockatrice/protocol/pb/serverinfo_user.pb.h"
#include "user/user_list_manager.h" #include "user/user_list_manager.h"
#include <QAccessible>
#include <QAccessibleEvent>
#include <QAction> #include <QAction>
#include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QDebug> #include <QDebug>
#include <QEvent>
#include <QGuiApplication> #include <QGuiApplication>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QItemSelectionModel> #include <QItemSelectionModel>
#include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QMap> #include <QMap>
#include <QMenu> #include <QMenu>
@ -31,15 +38,19 @@
#include <QSize> #include <QSize>
#include <QString> #include <QString>
#include <QStringLiteral> #include <QStringLiteral>
#include <QStyle>
#include <QToolButton>
#include <QTreeView> #include <QTreeView>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QVariant> #include <QVariant>
#include <QWidget>
#include <QtGlobal> #include <QtGlobal>
#include <libcockatrice/network/client/abstract/abstract_client.h> #include <libcockatrice/network/client/abstract/abstract_client.h>
#include <libcockatrice/protocol/pb/response.pb.h> #include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h> #include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h> #include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
#include <libcockatrice/settings/game_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/qt_utils.h> #include <libcockatrice/utility/qt_utils.h>
#include <qlogging.h> #include <qlogging.h>
@ -51,6 +62,33 @@ class QPoint;
class QWidget; class QWidget;
template <typename T> class QList; template <typename T> class QList;
namespace
{
/**
* @brief Returns whether the application paints with the native macOS style.
*
* QMacStyle ignores autoRaise and flat, and draws a QToolButton menu as a second square
* bezel, so the split host control cannot be composed there. A global style sheet wraps the
* real application style in a QStyleSheetStyle that takes it as a child, so that layer is
* unwrapped before the style is identified.
*/
bool usesMacStyle()
{
QStyle *style = QApplication::style();
if (style == nullptr) {
return false;
}
if (style->inherits("QStyleSheetStyle")) {
if (QStyle *baseStyle = style->findChild<QStyle *>(QString(), Qt::FindDirectChildrenOnly)) {
style = baseStyle;
}
}
return style->inherits("QMacStyle");
}
} // namespace
GameSelector::GameSelector(AbstractClient *_client, GameSelector::GameSelector(AbstractClient *_client,
TabSupervisor *_tabSupervisor, TabSupervisor *_tabSupervisor,
TabRoom *_room, TabRoom *_room,
@ -140,10 +178,59 @@ GameSelector::GameSelector(AbstractClient *_client,
connect(gameListProxyModel, &GamesProxyModel::filtersChanged, this, &GameSelector::checkClearFilterButtonState); connect(gameListProxyModel, &GamesProxyModel::filtersChanged, this, &GameSelector::checkClearFilterButtonState);
if (room) { if (room) {
createButton = new QPushButton; hostGameAction = new QAction(this);
connect(createButton, &QPushButton::clicked, this, &GameSelector::actCreate); hostGameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(hostGameAction, &QAction::triggered, this, &GameSelector::actQuickHost);
// A plain QPushButton, so the quick host sits in the same control family as the
// join buttons. A QPushButton does not take its text, tooltip or enabled state from
// an action, so those are mirrored by hand below and in updateHostControlsEnabled().
hostGameButton = new QPushButton(this);
hostGameButton->setObjectName("hostGameButton");
connect(hostGameButton, &QPushButton::clicked, hostGameAction, &QAction::trigger);
hostGameMenu = new QMenu(this);
hostGameMenu->setObjectName("hostGameMenu");
hostGameMenu->addAction(hostGameAction);
gameSettingsAction = hostGameMenu->addAction(QString());
gameSettingsAction->setObjectName("gameSettingsAction");
gameSettingsAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(gameSettingsAction, &QAction::triggered, this, &GameSelector::actCreate);
// The secondary control: a flat arrow next to the primary button on styles that
// support a split control. The native macOS style does not, so there it is a plain
// sibling button instead (see applyHostGameStyle()).
hostGameMenuButton = new QToolButton(this);
hostGameMenuButton->setObjectName("hostGameMenuButton");
hostGameMenuButton->setArrowType(Qt::DownArrow);
hostGameMenuButton->setAutoRaise(true);
hostGameMenuButton->setPopupMode(QToolButton::InstantPopup);
hostGameMenuButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
hostGameSettingsButton = new QPushButton(this);
hostGameSettingsButton->setObjectName("hostGameSettingsButton");
connect(hostGameSettingsButton, &QPushButton::clicked, gameSettingsAction, &QAction::trigger);
hostGameButtonWidget = new QWidget(this);
auto *hostGameButtonLayout = new QHBoxLayout(hostGameButtonWidget);
hostGameButtonLayout->setContentsMargins(0, 0, 0, 0);
hostGameButtonLayout->setSpacing(0);
hostGameButtonLayout->addWidget(hostGameButton);
hostGameButtonLayout->addWidget(hostGameMenuButton);
applyHostGameStyle();
// The shortcuts belong to the whole room tab, not just the game list, so they also
// fire while the chat or the user list has focus. A QToolButton menu is mouse-only,
// so the settings action needs its own shortcut for keyboard parity.
if (auto *roomTab = qobject_cast<TabRoom *>(parentWidget())) {
roomTab->addAction(hostGameAction);
roomTab->addAction(gameSettingsAction);
} else { } else {
createButton = nullptr; addAction(hostGameAction);
addAction(gameSettingsAction);
}
refreshShortcuts();
} }
joinButton = new QPushButton; joinButton = new QPushButton;
joinAsJudgeButton = new QPushButton; joinAsJudgeButton = new QPushButton;
@ -157,7 +244,8 @@ GameSelector::GameSelector(AbstractClient *_client,
} }
buttonLayout->addStretch(); buttonLayout->addStretch();
if (room) { if (room) {
buttonLayout->addWidget(createButton); buttonLayout->addWidget(hostGameButtonWidget);
buttonLayout->addWidget(hostGameSettingsButton);
} }
buttonLayout->addWidget(joinButton); buttonLayout->addWidget(joinButton);
if (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsJudge) { if (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
@ -173,10 +261,16 @@ GameSelector::GameSelector(AbstractClient *_client,
} }
buttonLayout->setAlignment(Qt::AlignTop); buttonLayout->setAlignment(Qt::AlignTop);
hostErrorLabel = new QLabel(this);
hostErrorLabel->setObjectName("hostGameErrorLabel");
hostErrorLabel->setWordWrap(true);
hostErrorLabel->setVisible(false);
QVBoxLayout *mainLayout = new QVBoxLayout; QVBoxLayout *mainLayout = new QVBoxLayout;
if (showFilters && restoresettings) { if (showFilters && restoresettings) {
mainLayout->addWidget(quickFilterToolBar); mainLayout->addWidget(quickFilterToolBar);
} }
mainLayout->addWidget(hostErrorLabel);
mainLayout->addWidget(gameListView); mainLayout->addWidget(gameListView);
mainLayout->addLayout(buttonLayout); mainLayout->addLayout(buttonLayout);
@ -266,6 +360,8 @@ void GameSelector::actCreate()
return; return;
} }
clearHostError();
// Heap-allocate the dialog: if this selector (or its room tab) is destroyed while the // Heap-allocate the dialog: if this selector (or its room tab) is destroyed while the
// dialog is open, e.g. on server disconnect, Qt deletes its children. A stack-allocated // dialog is open, e.g. on server disconnect, Qt deletes its children. A stack-allocated
// child would then be deleted by Qt and crash. // child would then be deleted by Qt and crash.
@ -275,6 +371,128 @@ void GameSelector::actCreate()
dlg->exec(); dlg->exec();
} }
void GameSelector::actQuickHost()
{
if (room == nullptr) {
qWarning() << "Attempted to create a game, but the room was null";
return;
}
clearHostError();
auto &gameSettings = SettingsCache::instance().game();
const bool registered = tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsRegistered;
const GameCreationOptions options =
gameSettings.getRememberGameSettings()
? GameCreationOptions::fromSettings(gameSettings, room->getGameTypes(), registered)
: GameCreationOptions::defaultsFor(registered);
PendingCommand *pend = room->prepareRoomCommand(options.toCreateGameCommand());
connect(pend, &PendingCommand::finished, this, &GameSelector::checkQuickHostResponse);
room->sendRoomCommand(pend);
setHostCommandPending(true);
}
void GameSelector::checkQuickHostResponse(const Response &response)
{
setHostCommandPending(false);
switch (response.response_code()) {
case Response::RespOk:
clearHostError();
break;
case Response::RespContextError:
showHostError(tr("You already host the maximum number of games."));
break;
case Response::RespInternalError:
showHostError(tr("The server could not create the game."));
break;
default:
showHostError(tr("Could not create the game."));
break;
}
}
void GameSelector::showHostError(const QString &message)
{
hostErrorLabel->setText(message);
hostErrorLabel->setAccessibleName(message);
hostErrorLabel->setVisible(true);
// The label appears above the game list without taking focus, so announce it: a screen
// reader would otherwise never reach the message after the button press.
QAccessibleEvent alertEvent(hostErrorLabel, QAccessible::Alert);
QAccessible::updateAccessibility(&alertEvent);
}
void GameSelector::clearHostError()
{
hostErrorLabel->clear();
hostErrorLabel->setAccessibleName(QString());
hostErrorLabel->setVisible(false);
}
void GameSelector::applyHostGameStyle()
{
if (hostGameButton == nullptr || hostGameMenu == nullptr || hostGameMenuButton == nullptr ||
hostGameSettingsButton == nullptr) {
return;
}
// The primary button always quick-hosts. The native macOS style has no split-button
// rendering (a QToolButton menu becomes a second square bezel), so the secondary
// action is a plain sibling button there; every other style gets the split arrow.
if (usesMacStyle()) {
hostGameMenuButton->setMenu(nullptr);
hostGameMenuButton->setVisible(false);
hostGameSettingsButton->setVisible(true);
} else {
hostGameMenuButton->setMenu(hostGameMenu);
hostGameMenuButton->setVisible(true);
hostGameSettingsButton->setVisible(false);
}
// The appearance settings can switch the style while the room is open.
updateHostGameToolTip();
}
void GameSelector::changeEvent(QEvent *event)
{
if (event->type() == QEvent::StyleChange) {
applyHostGameStyle();
}
QGroupBox::changeEvent(event);
}
void GameSelector::setHostCommandPending(bool pending)
{
hostCommandPending = pending;
// A join may still be in flight: its response enables these controls again, so keep them
// disabled until that response arrives instead of re-enabling on the host response alone.
updateHostControlsEnabled();
}
void GameSelector::updateHostControlsEnabled()
{
const bool hostControlsEnabled = !hostCommandPending && !joinCommandPending;
if (hostGameAction) {
hostGameAction->setEnabled(hostControlsEnabled);
}
if (hostGameButton) {
hostGameButton->setEnabled(hostControlsEnabled);
}
if (hostGameMenuButton) {
hostGameMenuButton->setEnabled(hostControlsEnabled);
}
if (hostGameSettingsButton) {
hostGameSettingsButton->setEnabled(hostControlsEnabled);
}
if (gameSettingsAction) {
gameSettingsAction->setEnabled(hostControlsEnabled);
}
}
void GameSelector::checkResponse(const Response &response) void GameSelector::checkResponse(const Response &response)
{ {
// NB: We re-enable buttons for the currently selected game, which may not // NB: We re-enable buttons for the currently selected game, which may not
@ -289,6 +507,9 @@ void GameSelector::checkResponse(const Response &response)
// Ideally we should have a way to figure out if the current game is the // Ideally we should have a way to figure out if the current game is the
// same as the one we are getting a response for, but it's probably not // same as the one we are getting a response for, but it's probably not
// worth the trouble. // worth the trouble.
// The join response is the one that re-enables the buttons; clear the join flag first so
// the host controls become usable again unless a quick host is still in flight.
joinCommandPending = false;
enableButtons(); enableButtons();
switch (response.response_code()) { switch (response.response_code()) {
@ -486,9 +707,8 @@ bool GameSelector::joinGameById(const int gameId, const bool asSpectator)
void GameSelector::disableButtons() void GameSelector::disableButtons()
{ {
if (createButton) { joinCommandPending = true;
createButton->setEnabled(false); updateHostControlsEnabled();
}
joinButton->setEnabled(false); joinButton->setEnabled(false);
spectateButton->setEnabled(false); spectateButton->setEnabled(false);
@ -496,9 +716,7 @@ void GameSelector::disableButtons()
void GameSelector::enableButtons() void GameSelector::enableButtons()
{ {
if (createButton) { updateHostControlsEnabled();
createButton->setEnabled(true);
}
// Enable buttons for the currently selected game // Enable buttons for the currently selected game
enableButtonsForIndex(gameListView->currentIndex()); enableButtonsForIndex(gameListView->currentIndex());
@ -521,8 +739,27 @@ void GameSelector::retranslateUi()
{ {
filterButton->setText(tr("&Filter games")); filterButton->setText(tr("&Filter games"));
clearFilterButton->setText(tr("C&lear filter")); clearFilterButton->setText(tr("C&lear filter"));
if (createButton) { if (hostGameAction) {
createButton->setText(tr("C&reate")); hostGameAction->setText(tr("Host with last settings"));
if (hostGameButton) {
// A QPushButton does not draw text from an action; the button keeps the short
// label while the menu item spells out what the remembered settings do.
hostGameButton->setText(tr("Host game"));
}
updateHostGameToolTip();
}
if (gameSettingsAction) {
gameSettingsAction->setText(tr("Game settings…"));
if (hostGameSettingsButton) {
// A QPushButton does not draw text from an action; on macOS this button is the
// secondary action, so it carries the same label.
hostGameSettingsButton->setText(tr("Game settings…"));
}
if (hostGameMenuButton) {
// The split arrow has no text of its own; name it for the menu it opens.
hostGameMenuButton->setToolTip(tr("More host options"));
hostGameMenuButton->setAccessibleName(tr("More host options"));
}
} }
joinButton->setText(tr("&Join")); joinButton->setText(tr("&Join"));
joinAsJudgeButton->setText(tr("Join as judge")); joinAsJudgeButton->setText(tr("Join as judge"));
@ -532,6 +769,36 @@ void GameSelector::retranslateUi()
updateTitle(); updateTitle();
} }
void GameSelector::refreshShortcuts()
{
if (hostGameAction) {
hostGameAction->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("GameSelector/aQuickHostGame"));
updateHostGameToolTip();
}
if (gameSettingsAction) {
gameSettingsAction->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("GameSelector/aReviewGame"));
}
}
void GameSelector::updateHostGameToolTip()
{
const QString shortcut = SettingsCache::instance().shortcuts().getShortcutString("GameSelector/aQuickHostGame");
const QString quickHostTooltip = shortcut.isEmpty()
? tr("Host a game with your last-used settings")
: tr("Host a game with your last-used settings (%1)").arg(shortcut);
// The tooltip lives on the action for the shortcut and on the button for the mouse,
// since a QPushButton does not inherit an action's tooltip.
if (hostGameAction) {
hostGameAction->setToolTip(quickHostTooltip);
}
if (hostGameButton) {
hostGameButton->setToolTip(quickHostTooltip);
}
if (hostGameSettingsButton) {
hostGameSettingsButton->setToolTip(tr("Open the game settings before hosting"));
}
}
void GameSelector::processGameInfo(const ServerInfo_Game &info) void GameSelector::processGameInfo(const ServerInfo_Game &info)
{ {
gameListModel->updateGameList(info); gameListModel->updateGameList(info);

View file

@ -14,7 +14,11 @@
class QTreeView; class QTreeView;
class GamesModel; class GamesModel;
class GamesProxyModel; class GamesProxyModel;
class QLabel;
class QMenu;
class QPushButton; class QPushButton;
class QToolButton;
class QAction;
class AbstractClient; class AbstractClient;
class TabSupervisor; class TabSupervisor;
class TabRoom; class TabRoom;
@ -64,6 +68,17 @@ private slots:
*/ */
void actCreate(); void actCreate();
/**
* @brief Creates a game immediately from the remembered settings, without a dialog.
*/
void actQuickHost();
/**
* @brief Reports a failed quick-host request inline instead of as a modal box.
* @param response The server response to the create command.
*/
void checkQuickHostResponse(const Response &response);
/** /**
* @brief Joins the currently selected game as a player. * @brief Joins the currently selected game as a player.
*/ */
@ -127,6 +142,15 @@ signals:
*/ */
void gameJoined(int gameId); void gameJoined(int gameId);
protected:
/**
* @brief Rebuilds the host control when the application style changes.
*
* The appearance settings can switch between the native macOS style and a
* cross-platform style while the room is open, and the host control follows.
*/
void changeEvent(QEvent *event) override;
private: private:
AbstractClient *client; /**< The network client used to communicate with the server. */ AbstractClient *client; /**< The network client used to communicate with the server. */
TabSupervisor *tabSupervisor; /**< Reference to TabSupervisor for managing tabs and rooms. */ TabSupervisor *tabSupervisor; /**< Reference to TabSupervisor for managing tabs and rooms. */
@ -140,7 +164,14 @@ private:
QPushButton *filterButton; /**< Button to open the filter dialog. */ QPushButton *filterButton; /**< Button to open the filter dialog. */
QPushButton *clearFilterButton; /**< Button to clear active filters. */ QPushButton *clearFilterButton; /**< Button to clear active filters. */
QPushButton *createButton; /**< Button to create a new game (only if room is set). */ QPushButton *hostGameButton = nullptr; /**< Host button, in the same family as the join buttons. */
QPushButton *hostGameSettingsButton = nullptr; /**< Opens the host form; a sibling button on macOS. */
QToolButton *hostGameMenuButton = nullptr; /**< Split arrow that opens the host menu (non-macOS styles only). */
QWidget *hostGameButtonWidget = nullptr; /**< Container that keeps the split button and its arrow together. */
QMenu *hostGameMenu = nullptr; /**< Menu behind the split arrow (non-macOS styles only). */
QAction *hostGameAction = nullptr; /**< Quick-host action, carrying the keyboard shortcut. */
QAction *gameSettingsAction = nullptr; /**< Opens the host form with the current settings. */
QLabel *hostErrorLabel; /**< Inline failure message for a quick-host request. */
QPushButton *joinButton; /**< Button to join the selected game. */ QPushButton *joinButton; /**< Button to join the selected game. */
QPushButton *joinAsJudgeButton; /**< Button to join the selected game as a judge. */ QPushButton *joinAsJudgeButton; /**< Button to join the selected game as a judge. */
QPushButton *spectateButton; /**< Button to spectate the selected game. */ QPushButton *spectateButton; /**< Button to spectate the selected game. */
@ -152,6 +183,28 @@ private:
QTimer filterSaveDebounceTimer; /**< Delays persisting quick-filter edits so typing does not rewrite the settings QTimer filterSaveDebounceTimer; /**< Delays persisting quick-filter edits so typing does not rewrite the settings
file on every keystroke. */ file on every keystroke. */
bool hostCommandPending = false; /**< True while a quick-host command is in flight. */
bool joinCommandPending = false; /**< True while a join/spectate command is in flight. */
/** @brief Shows a quick-host failure above the game list. */
void showHostError(const QString &message);
/** @brief Hides the quick-host failure message. */
void clearHostError();
/**
* @brief Rebuilds the host control for the active style.
*
* The primary button always quick-hosts. Every other style gets the split button,
* whose arrow opens the menu; the native macOS style has no split control, so it
* gets a plain sibling button for the secondary action instead.
*/
void applyHostGameStyle();
/** @brief Marks a quick-host command as in flight or finished, then updates the host controls. */
void setHostCommandPending(bool pending);
/** @brief Applies the in-flight state of both host and join commands to the host controls. */
void updateHostControlsEnabled();
/** @brief Keeps the host control tooltips in sync with the current shortcut and style. */
void updateHostGameToolTip();
/** /**
* @brief Updates the widget title to reflect the current number of displayed games. * @brief Updates the widget title to reflect the current number of displayed games.
* *
@ -222,6 +275,11 @@ public:
*/ */
void retranslateUi(); void retranslateUi();
/**
* @brief Re-reads the quick-host and review shortcuts after the user rebinds them.
*/
void refreshShortcuts();
/** /**
* @brief Updates or adds a game entry in the list. * @brief Updates or adds a game entry in the list.
* @param info The ServerInfo_Game object containing information about the game to update. * @param info The ServerInfo_Game object containing information about the game to update.

View file

@ -113,7 +113,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
QMap<int, GameTypeMap> tempMap; QMap<int, GameTypeMap> tempMap;
tempMap.insert(info.room_id(), gameTypes); tempMap.insert(info.room_id(), gameTypes);
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true); gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true, this);
userListPanel = new UserListPanelWidget(tabSupervisor, client, this); userListPanel = new UserListPanelWidget(tabSupervisor, client, this);
userListPanel->bind(tabSupervisor->getUserListManager()); userListPanel->bind(tabSupervisor->getUserListManager());
@ -720,6 +720,7 @@ void TabRoom::processRemoveMessagesEvent(const Event_RemoveMessages &event)
void TabRoom::refreshShortcuts() void TabRoom::refreshShortcuts()
{ {
aClearChat->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("tab_room/aClearChat")); aClearChat->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("tab_room/aClearChat"));
gameSelector->refreshShortcuts();
} }
void TabRoom::addMentionTag(QString mentionTag) void TabRoom::addMentionTag(QString mentionTag)

View file

@ -185,6 +185,26 @@ TEST(GameCreationOptions, DescriptionWhitespaceIsSimplified)
EXPECT_EQ(cmd.description(), "a b"); EXPECT_EQ(cmd.description(), "a b");
} }
TEST(GameCreationOptions, DefaultsForRegisteredHostEnforceRegistration)
{
const GameCreationOptions registered = GameCreationOptions::defaultsFor(true);
EXPECT_TRUE(registered.onlyRegistered);
EXPECT_FALSE(registered.onlyBuddies);
EXPECT_EQ(registered.maxPlayers, 2);
EXPECT_TRUE(registered.spectatorsAllowed);
EXPECT_EQ(registered.startingLifeTotal, 20);
EXPECT_TRUE(registered.gameTypeIds.isEmpty());
}
TEST(GameCreationOptions, DefaultsForUnregisteredHostAllowOpenJoining)
{
const GameCreationOptions unregistered = GameCreationOptions::defaultsFor(false);
EXPECT_FALSE(unregistered.onlyRegistered);
EXPECT_FALSE(unregistered.onlyBuddies);
}
TEST(GameCreationOptions, FromSettingsCopiesEveryRememberedField) TEST(GameCreationOptions, FromSettingsCopiesEveryRememberedField)
{ {
const FakeGameSettings settings{GameCreationOptions{.description = "Remembered game", const FakeGameSettings settings{GameCreationOptions{.description = "Remembered game",