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:
Lukas Brübach 2026-11-08 22:11:47 +01:00
parent b089a51be4
commit e506fb4ad4
3 changed files with 64 additions and 52 deletions

View file

@ -316,7 +316,7 @@ private:
{"GameSelector/aQuickHostGame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Host Game"),
parseSequenceString("Ctrl+N"),
ShortcutGroup::Game_Lobby)},
{"GameSelector/aReviewGame", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Review Game Settings"),
{"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)"),

View file

@ -184,20 +184,22 @@ GameSelector::GameSelector(AbstractClient *_client,
// 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 setHostCommandPending().
// 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);
reviewGameAction = hostGameMenu->addAction(QString());
reviewGameAction->setObjectName("reviewGameAction");
reviewGameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(reviewGameAction, &QAction::triggered, this, &GameSelector::actCreate);
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. It is hidden when
// the active style draws a native pull-down instead (see applyHostGameStyle()).
// 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);
@ -205,6 +207,10 @@ GameSelector::GameSelector(AbstractClient *_client,
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);
@ -216,13 +222,13 @@ GameSelector::GameSelector(AbstractClient *_client,
// 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 review action needs its own shortcut for keyboard parity.
// so the settings action needs its own shortcut for keyboard parity.
if (auto *roomTab = qobject_cast<TabRoom *>(parentWidget())) {
roomTab->addAction(hostGameAction);
roomTab->addAction(reviewGameAction);
roomTab->addAction(gameSettingsAction);
} else {
addAction(hostGameAction);
addAction(reviewGameAction);
addAction(gameSettingsAction);
}
refreshShortcuts();
}
@ -239,6 +245,7 @@ GameSelector::GameSelector(AbstractClient *_client,
buttonLayout->addStretch();
if (room) {
buttonLayout->addWidget(hostGameButtonWidget);
buttonLayout->addWidget(hostGameSettingsButton);
}
buttonLayout->addWidget(joinButton);
if (tabSupervisor->getUserInfo()->user_level() & ServerInfo_User::IsJudge) {
@ -428,31 +435,25 @@ void GameSelector::clearHostError()
void GameSelector::applyHostGameStyle()
{
if (hostGameButton == nullptr || hostGameMenu == nullptr) {
if (hostGameButton == nullptr || hostGameMenu == nullptr || hostGameMenuButton == nullptr ||
hostGameSettingsButton == nullptr) {
return;
}
// The appearance settings can switch the application style while the room is open, so
// the primary click is wired from scratch instead of once: disconnect first, otherwise
// a second pass would quick-host twice.
disconnect(hostGameButton, &QPushButton::clicked, hostGameAction, &QAction::trigger);
// 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()) {
// A native pull-down: one bezel with the menu indicator inside it. The separate
// arrow is hidden because QMacStyle would draw it as a second square bezel.
hostGameButton->setMenu(hostGameMenu);
hostGameMenuButton->setMenu(nullptr);
hostGameMenuButton->setVisible(false);
hostGameSettingsButton->setVisible(true);
} else {
// The split button: the primary half quick-hosts, the flat arrow opens the menu.
hostGameButton->setMenu(nullptr);
connect(hostGameButton, &QPushButton::clicked, hostGameAction, &QAction::trigger);
hostGameMenuButton->setMenu(hostGameMenu);
hostGameMenuButton->setVisible(true);
hostGameSettingsButton->setVisible(false);
}
// The tooltip depends on the control type: on macOS a click opens the menu, elsewhere it
// quick-hosts, and the appearance settings can switch between the two while the room is open.
// The appearance settings can switch the style while the room is open.
updateHostGameToolTip();
}
@ -484,8 +485,11 @@ void GameSelector::updateHostControlsEnabled()
if (hostGameMenuButton) {
hostGameMenuButton->setEnabled(hostControlsEnabled);
}
if (reviewGameAction) {
reviewGameAction->setEnabled(hostControlsEnabled);
if (hostGameSettingsButton) {
hostGameSettingsButton->setEnabled(hostControlsEnabled);
}
if (gameSettingsAction) {
gameSettingsAction->setEnabled(hostControlsEnabled);
}
}
@ -744,8 +748,13 @@ void GameSelector::retranslateUi()
}
updateHostGameToolTip();
}
if (reviewGameAction) {
reviewGameAction->setText(tr("Review settings first…"));
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"));
@ -766,8 +775,8 @@ void GameSelector::refreshShortcuts()
hostGameAction->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("GameSelector/aQuickHostGame"));
updateHostGameToolTip();
}
if (reviewGameAction) {
reviewGameAction->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("GameSelector/aReviewGame"));
if (gameSettingsAction) {
gameSettingsAction->setShortcuts(SettingsCache::instance().shortcuts().getShortcut("GameSelector/aReviewGame"));
}
}
@ -783,9 +792,10 @@ void GameSelector::updateHostGameToolTip()
hostGameAction->setToolTip(quickHostTooltip);
}
if (hostGameButton) {
// Under the native macOS style the button is a pull-down, so a click opens the menu
// instead of quick-hosting and the tooltip names the menu rather than the quick host.
hostGameButton->setToolTip(usesMacStyle() ? tr("Open the host menu") : quickHostTooltip);
hostGameButton->setToolTip(quickHostTooltip);
}
if (hostGameSettingsButton) {
hostGameSettingsButton->setToolTip(tr("Open the game settings before hosting"));
}
}

View file

@ -162,19 +162,20 @@ private:
GameSelectorQuickFilterToolBar *quickFilterToolBar;
QPushButton *filterButton; /**< Button to open the filter dialog. */
QPushButton *clearFilterButton; /**< Button to clear active filters. */
QPushButton *hostGameButton = nullptr; /**< Host button, in the same family as the join buttons. */
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 arrow, or behind the pull-down button on macOS. */
QAction *hostGameAction = nullptr; /**< Quick-host action, carrying the keyboard shortcut. */
QAction *reviewGameAction = nullptr; /**< Menu action that opens the full create dialog. */
QLabel *hostErrorLabel; /**< Inline failure message for a quick-host request. */
QPushButton *joinButton; /**< Button to join the selected game. */
QPushButton *joinAsJudgeButton; /**< Button to join the selected game as a judge. */
QPushButton *spectateButton; /**< Button to spectate the selected game. */
QPushButton *joinAsJudgeSpectatorButton; /**< Button to join the selected game as a spectating judge. */
QPushButton *filterButton; /**< Button to open the filter dialog. */
QPushButton *clearFilterButton; /**< Button to clear active filters. */
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 *joinAsJudgeButton; /**< Button to join the selected game as a judge. */
QPushButton *spectateButton; /**< Button to spectate the selected game. */
QPushButton *joinAsJudgeSpectatorButton; /**< Button to join the selected game as a spectating judge. */
const bool showFilters; /**< Determines whether filter buttons are displayed. */
GameTypeMap gameTypeMap; /**< Mapping of game types for the current room. */
@ -192,15 +193,16 @@ private:
/**
* @brief Rebuilds the host control for the active style.
*
* The native macOS style draws a single pull-down button; every other style gets the
* split button, whose primary half quick-hosts and whose arrow opens the menu.
* 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 Enables or disables the quick-host and review controls while a command is in flight. */
/** @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 quick-host and review controls. */
/** @brief Applies the in-flight state of both host and join commands to the host controls. */
void updateHostControlsEnabled();
/** @brief Keeps the quick-host button tooltip in sync with the current shortcut. */
/** @brief Keeps the host control tooltips in sync with the current shortcut and style. */
void updateHostGameToolTip();
/**