[Interface] Give every hover panel action a semantic color

The quick actions are the panel's whole point, so a neutral glyph made the
most-used buttons the hardest to scan. Every action now names a game fact with
its hue: gain, loss, information, hidden information, a mark on the card or a
move. The line glyph stays identical, so the colour is the only difference
between neighbours.

The grid is also ordered by family so a row reads as one idea: the two state
toggles, the two gains, the two marks, the two hidden-information actions and
the two losses each share a row. Conditional actions (Peek, Unattach) sit at the
end of their pair, so a missing one leaves a hole at the section edge instead of
splitting a row. In a zone, Play and Play Face Down stay adjacent on purpose:
the same action, one of them hiding information. The move tab leads with the
three library placements (Movement), then the destinations.

actionGridIcon is removed; semanticActionIcon is now the only grid ink. The
preview renders the real pages so the whole map can be judged at once.
This commit is contained in:
Lukas Brübach 2026-11-08 21:03:06 +01:00
parent 2567672556
commit 44fbbb6906
2 changed files with 71 additions and 66 deletions

View file

@ -86,14 +86,10 @@ static QIcon categoryTabIcon(const QString &prefix)
return loadThemedIcon(prefix, IconTint::quiet()); return loadThemedIcon(prefix, IconTint::quiet());
} }
static QIcon actionGridIcon(const QString &prefix) // Every action carries a hue naming a game fact the glyph cannot: a gain, a
{ // loss, information, hidden information, a mark on the card or a move. Keeping
return loadThemedIcon(prefix, IconTint::primary()); // one line glyph per action means a row of different families still reads as a
} // single grid instead of a set of buttons from different toolbars.
// A semantic action is the exception the icon rules allow: the hue names an
// outcome (play, gain, loss, hidden information). It keeps the grid's line
// glyph so hue is the only difference from its neighbours.
static QIcon semanticActionIcon(const QString &prefix, AppColor::Role role) static QIcon semanticActionIcon(const QString &prefix, AppColor::Role role)
{ {
return loadThemedIcon(prefix, IconTint(role, IconEmphasis::Primary)); return loadThemedIcon(prefix, IconTint(role, IconEmphasis::Primary));
@ -850,17 +846,17 @@ void CardHoverWidget::buildMainTab(const QList<CardState *> &cardList, const QSt
ButtonGrid grid(layout); ButtonGrid grid(layout);
grid.addSection(createSectionHeader(tr("Revealed"), mainPage)); grid.addSection(createSectionHeader(tr("Revealed"), mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Hide"), actionGridIcon(QStringLiteral("icons/arrow_left")), tr("Hide"), semanticActionIcon(QStringLiteral("icons/arrow_left"), AppColor::Informational),
[actions, cardList]() { actions->actHide(cardList); }, mainPage)); [actions, cardList]() { actions->actHide(cardList); }, mainPage));
grid.addButton(createGridButton(tr("Clone"), grid.addButton(createGridButton(tr("Clone"),
semanticActionIcon(QStringLiteral("icons/increment"), AppColor::Positive), semanticActionIcon(QStringLiteral("icons/increment"), AppColor::Positive),
invoke(cmClone), mainPage)); invoke(cmClone), mainPage));
grid.addSection(createSectionHeader(tr("Select"), mainPage)); grid.addSection(createSectionHeader(tr("Select"), mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Select All"), actionGridIcon(QStringLiteral("icons/search")), tr("Select All"), semanticActionIcon(QStringLiteral("icons/search"), AppColor::Informational),
[gameScene, card]() { gameScene->selectAllInZone(card); }, mainPage)); [gameScene, card]() { gameScene->selectAllInZone(card); }, mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Select Column"), actionGridIcon(QStringLiteral("icons/filter")), tr("Select Column"), semanticActionIcon(QStringLiteral("icons/filter"), AppColor::Informational),
[gameScene, card]() { gameScene->selectSameColumn(card); }, mainPage)); [gameScene, card]() { gameScene->selectSameColumn(card); }, mainPage));
layout->addStretch(1); layout->addStretch(1);
return; return;
@ -869,44 +865,49 @@ void CardHoverWidget::buildMainTab(const QList<CardState *> &cardList, const QSt
if (zoneName == ZoneNames::TABLE) { if (zoneName == ZoneNames::TABLE) {
ButtonGrid grid(layout); ButtonGrid grid(layout);
grid.addSection(createSectionHeader(tr("Battlefield"), mainPage)); grid.addSection(createSectionHeader(tr("Battlefield"), mainPage));
grid.addButton( grid.addButton(createGridButton(tr("Tap / Untap"),
createGridButton(tr("Tap / Untap"), actionGridIcon(QStringLiteral("icons/sync")), invoke(cmTap), mainPage)); semanticActionIcon(QStringLiteral("icons/sync"), AppColor::Informational),
invoke(cmTap), mainPage));
auto *doesntUntap = createGridButton( auto *doesntUntap = createGridButton(
tr("Skip untapping"), actionGridIcon(QStringLiteral("icons/lock")), tr("Skip untapping"), semanticActionIcon(QStringLiteral("icons/lock"), AppColor::Informational),
[actions, cardList]() { actions->cardMenuAction(cardList, cmDoesntUntap); }, mainPage); [actions, cardList]() { actions->cardMenuAction(cardList, cmDoesntUntap); }, mainPage);
doesntUntap->setCheckable(true); doesntUntap->setCheckable(true);
doesntUntap->setChecked(card->getDoesntUntap()); doesntUntap->setChecked(card->getDoesntUntap());
grid.addButton(doesntUntap); grid.addButton(doesntUntap);
grid.addButton(
createGridButton(tr("Turn Over"), actionGridIcon(QStringLiteral("icons/swap")), invoke(cmFlip), mainPage));
if (card->getFaceDown()) {
grid.addButton(createGridButton(tr("Peek at card face"), actionGridIcon(QStringLiteral("icons/view")),
invoke(cmPeek), mainPage));
}
grid.addButton(createGridButton(tr("Clone"), grid.addButton(createGridButton(tr("Clone"),
semanticActionIcon(QStringLiteral("icons/increment"), AppColor::Positive), semanticActionIcon(QStringLiteral("icons/increment"), AppColor::Positive),
invoke(cmClone), mainPage)); invoke(cmClone), mainPage));
grid.addButton( grid.addButton(createGridButton(tr("Attach to card..."),
createGridButton(tr("Attach to card..."), actionGridIcon(QStringLiteral("icons/pin")), attach, mainPage)); semanticActionIcon(QStringLiteral("icons/pin"), AppColor::Positive), attach,
mainPage));
grid.addButton(createGridButton(tr("Draw arrow..."),
semanticActionIcon(QStringLiteral("icons/share"), AppColor::Marked), drawArrow,
mainPage));
grid.addButton(createGridButton(
tr("Set annotation..."), semanticActionIcon(QStringLiteral("icons/pencil"), AppColor::Marked),
[actions, cardList]() { actions->actRequestSetAnnotationDialog(cardList); }, mainPage));
grid.addButton(createGridButton(tr("Turn Over"),
semanticActionIcon(QStringLiteral("icons/swap"), AppColor::Hidden),
invoke(cmFlip), mainPage));
if (card->getFaceDown()) {
grid.addButton(createGridButton(tr("Peek at card face"),
semanticActionIcon(QStringLiteral("icons/view"), AppColor::Hidden),
invoke(cmPeek), mainPage));
}
if (card->getAttachedTo()) { if (card->getAttachedTo()) {
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Unattach"), actionGridIcon(QStringLiteral("icons/arrow_undo")), tr("Unattach"), semanticActionIcon(QStringLiteral("icons/arrow_undo"), AppColor::Negative),
[actions, cardList]() { actions->actUnattach(cardList); }, mainPage)); [actions, cardList]() { actions->actUnattach(cardList); }, mainPage));
} }
grid.addButton(
createGridButton(tr("Draw arrow..."), actionGridIcon(QStringLiteral("icons/share")), drawArrow, mainPage));
grid.addButton(createGridButton(
tr("Set annotation..."), actionGridIcon(QStringLiteral("icons/pencil")),
[actions, cardList]() { actions->actRequestSetAnnotationDialog(cardList); }, mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Reduce life"), semanticActionIcon(QStringLiteral("icons/arrow_down"), AppColor::Negative), tr("Reduce life"), semanticActionIcon(QStringLiteral("icons/arrow_down"), AppColor::Negative),
[actions, cardList]() { actions->actReduceLifeByPower(cardList); }, mainPage)); [actions, cardList]() { actions->actReduceLifeByPower(cardList); }, mainPage));
grid.addSection(createSectionHeader(tr("Select"), mainPage)); grid.addSection(createSectionHeader(tr("Select"), mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Select All"), actionGridIcon(QStringLiteral("icons/search")), tr("Select All"), semanticActionIcon(QStringLiteral("icons/search"), AppColor::Informational),
[gameScene, card]() { gameScene->selectAllInZone(card); }, mainPage)); [gameScene, card]() { gameScene->selectAllInZone(card); }, mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Select Row"), actionGridIcon(QStringLiteral("icons/filter")), tr("Select Row"), semanticActionIcon(QStringLiteral("icons/filter"), AppColor::Informational),
[gameScene, card]() { gameScene->selectSameRow(card); }, mainPage)); [gameScene, card]() { gameScene->selectSameRow(card); }, mainPage));
} else if (!zoneName.isEmpty()) { } else if (!zoneName.isEmpty()) {
ButtonGrid grid(layout); ButtonGrid grid(layout);
@ -915,25 +916,27 @@ void CardHoverWidget::buildMainTab(const QList<CardState *> &cardList, const QSt
tr("Play"), semanticActionIcon(QStringLiteral("icons/arrow_right"), AppColor::Positive), tr("Play"), semanticActionIcon(QStringLiteral("icons/arrow_right"), AppColor::Positive),
[actions, cardList]() { actions->actPlay(cardList); }, mainPage)); [actions, cardList]() { actions->actPlay(cardList); }, mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Play Face Down"), semanticActionIcon(QStringLiteral("icons/arrow_right"), AppColor::Informational), tr("Play Face Down"), semanticActionIcon(QStringLiteral("icons/arrow_right"), AppColor::Hidden),
[actions, cardList]() { actions->actPlayFacedown(cardList); }, mainPage)); [actions, cardList]() { actions->actPlayFacedown(cardList); }, mainPage));
grid.addButton(createGridButton(tr("Clone"), grid.addButton(createGridButton(tr("Clone"),
semanticActionIcon(QStringLiteral("icons/increment"), AppColor::Positive), semanticActionIcon(QStringLiteral("icons/increment"), AppColor::Positive),
invoke(cmClone), mainPage)); invoke(cmClone), mainPage));
if (zoneName == ZoneNames::STACK || zoneName == ZoneNames::HAND || zoneName == ZoneNames::GRAVE || if (zoneName == ZoneNames::STACK || zoneName == ZoneNames::HAND || zoneName == ZoneNames::GRAVE ||
zoneName == ZoneNames::EXILE) { zoneName == ZoneNames::EXILE) {
grid.addButton(createGridButton(tr("Attach to card..."), actionGridIcon(QStringLiteral("icons/pin")), grid.addButton(createGridButton(tr("Attach to card..."),
attach, mainPage)); semanticActionIcon(QStringLiteral("icons/pin"), AppColor::Positive), attach,
grid.addButton(createGridButton(tr("Draw arrow..."), actionGridIcon(QStringLiteral("icons/share")), mainPage));
grid.addButton(createGridButton(tr("Draw arrow..."),
semanticActionIcon(QStringLiteral("icons/share"), AppColor::Marked),
drawArrow, mainPage)); drawArrow, mainPage));
} }
grid.addSection(createSectionHeader(tr("Select"), mainPage)); grid.addSection(createSectionHeader(tr("Select"), mainPage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Select All"), actionGridIcon(QStringLiteral("icons/search")), tr("Select All"), semanticActionIcon(QStringLiteral("icons/search"), AppColor::Informational),
[gameScene, card]() { gameScene->selectAllInZone(card); }, mainPage)); [gameScene, card]() { gameScene->selectAllInZone(card); }, mainPage));
if (qobject_cast<ZoneViewZoneLogic *>(card->getZone())) { if (qobject_cast<ZoneViewZoneLogic *>(card->getZone())) {
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("Select Column"), actionGridIcon(QStringLiteral("icons/filter")), tr("Select Column"), semanticActionIcon(QStringLiteral("icons/filter"), AppColor::Informational),
[gameScene, card]() { gameScene->selectSameColumn(card); }, mainPage)); [gameScene, card]() { gameScene->selectSameColumn(card); }, mainPage));
} }
} else { } else {
@ -960,15 +963,17 @@ void CardHoverWidget::buildMoveTab(const QList<CardState *> &cardList, const QSt
ButtonGrid grid(layout); ButtonGrid grid(layout);
grid.addSection(createSectionHeader(tr("Move to"), movePage)); grid.addSection(createSectionHeader(tr("Move to"), movePage));
grid.addButton(createGridButton(tr("Top of library"), actionGridIcon(QStringLiteral("icons/arrow_to_top")), grid.addButton(createGridButton(tr("Top of library"),
semanticActionIcon(QStringLiteral("icons/arrow_to_top"), AppColor::Movement),
invoke(cmMoveToTopLibrary), movePage)); invoke(cmMoveToTopLibrary), movePage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("X from top..."), actionGridIcon(QStringLiteral("icons/search")), tr("X from top..."), semanticActionIcon(QStringLiteral("icons/search"), AppColor::Movement),
[actions]() { actions->actRequestMoveCardXCardsFromTopDialog(); }, movePage, tr("X from top of library..."))); [actions]() { actions->actRequestMoveCardXCardsFromTopDialog(); }, movePage, tr("X from top of library...")));
grid.addButton(createGridButton(tr("Bottom of library"), actionGridIcon(QStringLiteral("icons/arrow_to_bottom")), grid.addButton(createGridButton(tr("Bottom of library"),
semanticActionIcon(QStringLiteral("icons/arrow_to_bottom"), AppColor::Movement),
invoke(cmMoveToBottomLibrary), movePage)); invoke(cmMoveToBottomLibrary), movePage));
grid.addButton( grid.addButton(createGridButton(tr("Table"), semanticActionIcon(QStringLiteral("icons/player"), AppColor::Positive),
createGridButton(tr("Table"), actionGridIcon(QStringLiteral("icons/player")), invoke(cmMoveToTable), movePage)); invoke(cmMoveToTable), movePage));
grid.addButton(createGridButton(tr("Hand"), grid.addButton(createGridButton(tr("Hand"),
semanticActionIcon(QStringLiteral("icons/arrow_left"), AppColor::Positive), semanticActionIcon(QStringLiteral("icons/arrow_left"), AppColor::Positive),
invoke(cmMoveToHand), movePage)); invoke(cmMoveToHand), movePage));
@ -986,7 +991,7 @@ void CardHoverWidget::buildMoveTab(const QList<CardState *> &cardList, const QSt
if (game && game->getPlayerManager()) { if (game && game->getPlayerManager()) {
grid.addSection(createSectionHeader(tr("Reveal to..."), movePage)); grid.addSection(createSectionHeader(tr("Reveal to..."), movePage));
grid.addButton(createGridButton( grid.addButton(createGridButton(
tr("All players"), actionGridIcon(QStringLiteral("icons/share")), tr("All players"), semanticActionIcon(QStringLiteral("icons/share"), AppColor::Informational),
[actions, cardList]() { [actions, cardList]() {
auto *fake = new QAction(nullptr); auto *fake = new QAction(nullptr);
fake->setData(-1); fake->setData(-1);
@ -1002,7 +1007,7 @@ void CardHoverWidget::buildMoveTab(const QList<CardState *> &cardList, const QSt
QString name = other->getPlayerInfo()->getName(); QString name = other->getPlayerInfo()->getName();
int id = other->getPlayerInfo()->getId(); int id = other->getPlayerInfo()->getId();
grid.addButton(createGridButton( grid.addButton(createGridButton(
name, actionGridIcon(QStringLiteral("icons/player")), name, semanticActionIcon(QStringLiteral("icons/player"), AppColor::Informational),
[actions, cardList, id]() { [actions, cardList, id]() {
auto *fake = new QAction(nullptr); auto *fake = new QAction(nullptr);
fake->setData(id); fake->setData(id);

View file

@ -323,8 +323,8 @@ TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested)
constexpr int GLYPH = 22; constexpr int GLYPH = 22;
constexpr qreal DPR = 2.0; constexpr qreal DPR = 2.0;
// The two pages a selected card shows, with every conditional action // The pages a selected card shows, with every conditional action present
// present so the whole map is visible at once. // so the whole map is visible at once.
const QList<PreviewPage> pages = { const QList<PreviewPage> pages = {
PreviewPage{.title = QStringLiteral("Main — on the battlefield"), PreviewPage{.title = QStringLiteral("Main — on the battlefield"),
.sections = .sections =
@ -334,31 +334,31 @@ TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested)
{ {
PreviewAction{.label = QStringLiteral("Tap / Untap"), PreviewAction{.label = QStringLiteral("Tap / Untap"),
.glyph = "sync", .glyph = "sync",
.role = AppColor::IconStrong}, .role = AppColor::InformationalStrong},
PreviewAction{.label = QStringLiteral("Skip untapping"), PreviewAction{.label = QStringLiteral("Skip untapping"),
.glyph = "lock", .glyph = "lock",
.role = AppColor::IconStrong}, .role = AppColor::InformationalStrong},
PreviewAction{.label = QStringLiteral("Turn Over"),
.glyph = "swap",
.role = AppColor::IconStrong},
PreviewAction{.label = QStringLiteral("Peek at card face"),
.glyph = "view",
.role = AppColor::HiddenStrong},
PreviewAction{.label = QStringLiteral("Clone"), PreviewAction{.label = QStringLiteral("Clone"),
.glyph = "increment", .glyph = "increment",
.role = AppColor::PositiveStrong}, .role = AppColor::PositiveStrong},
PreviewAction{.label = QStringLiteral("Attach to card..."), PreviewAction{.label = QStringLiteral("Attach to card..."),
.glyph = "pin", .glyph = "pin",
.role = AppColor::IconStrong}, .role = AppColor::PositiveStrong},
PreviewAction{.label = QStringLiteral("Unattach"),
.glyph = "arrow_undo",
.role = AppColor::NegativeStrong},
PreviewAction{.label = QStringLiteral("Draw arrow..."), PreviewAction{.label = QStringLiteral("Draw arrow..."),
.glyph = "share", .glyph = "share",
.role = AppColor::MarkedStrong}, .role = AppColor::MarkedStrong},
PreviewAction{.label = QStringLiteral("Set annotation..."), PreviewAction{.label = QStringLiteral("Set annotation..."),
.glyph = "pencil", .glyph = "pencil",
.role = AppColor::MarkedStrong}, .role = AppColor::MarkedStrong},
PreviewAction{.label = QStringLiteral("Turn Over"),
.glyph = "swap",
.role = AppColor::HiddenStrong},
PreviewAction{.label = QStringLiteral("Peek at card face"),
.glyph = "view",
.role = AppColor::HiddenStrong},
PreviewAction{.label = QStringLiteral("Unattach"),
.glyph = "arrow_undo",
.role = AppColor::NegativeStrong},
PreviewAction{.label = QStringLiteral("Reduce life"), PreviewAction{.label = QStringLiteral("Reduce life"),
.glyph = "arrow_down", .glyph = "arrow_down",
.role = AppColor::NegativeStrong}, .role = AppColor::NegativeStrong},
@ -368,10 +368,10 @@ TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested)
{ {
PreviewAction{.label = QStringLiteral("Select All"), PreviewAction{.label = QStringLiteral("Select All"),
.glyph = "search", .glyph = "search",
.role = AppColor::IconStrong}, .role = AppColor::InformationalStrong},
PreviewAction{.label = QStringLiteral("Select Row"), PreviewAction{.label = QStringLiteral("Select Row"),
.glyph = "filter", .glyph = "filter",
.role = AppColor::IconStrong}, .role = AppColor::InformationalStrong},
}}, }},
}}, }},
PreviewPage{.title = QStringLiteral("Main — in a zone"), PreviewPage{.title = QStringLiteral("Main — in a zone"),
@ -391,7 +391,7 @@ TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested)
.role = AppColor::PositiveStrong}, .role = AppColor::PositiveStrong},
PreviewAction{.label = QStringLiteral("Attach to card..."), PreviewAction{.label = QStringLiteral("Attach to card..."),
.glyph = "pin", .glyph = "pin",
.role = AppColor::IconStrong}, .role = AppColor::PositiveStrong},
PreviewAction{.label = QStringLiteral("Draw arrow..."), PreviewAction{.label = QStringLiteral("Draw arrow..."),
.glyph = "share", .glyph = "share",
.role = AppColor::MarkedStrong}, .role = AppColor::MarkedStrong},
@ -401,10 +401,10 @@ TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested)
{ {
PreviewAction{.label = QStringLiteral("Select All"), PreviewAction{.label = QStringLiteral("Select All"),
.glyph = "search", .glyph = "search",
.role = AppColor::IconStrong}, .role = AppColor::InformationalStrong},
PreviewAction{.label = QStringLiteral("Select Column"), PreviewAction{.label = QStringLiteral("Select Column"),
.glyph = "filter", .glyph = "filter",
.role = AppColor::IconStrong}, .role = AppColor::InformationalStrong},
}}, }},
}}, }},
PreviewPage{.title = QStringLiteral("Move — card in a zone"), PreviewPage{.title = QStringLiteral("Move — card in a zone"),
@ -415,16 +415,16 @@ TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested)
{ {
PreviewAction{.label = QStringLiteral("Top of library"), PreviewAction{.label = QStringLiteral("Top of library"),
.glyph = "arrow_to_top", .glyph = "arrow_to_top",
.role = AppColor::IconStrong}, .role = AppColor::MovementStrong},
PreviewAction{.label = QStringLiteral("X from top..."), PreviewAction{.label = QStringLiteral("X from top..."),
.glyph = "search", .glyph = "search",
.role = AppColor::IconStrong}, .role = AppColor::MovementStrong},
PreviewAction{.label = QStringLiteral("Bottom of library"), PreviewAction{.label = QStringLiteral("Bottom of library"),
.glyph = "arrow_to_bottom", .glyph = "arrow_to_bottom",
.role = AppColor::IconStrong}, .role = AppColor::MovementStrong},
PreviewAction{.label = QStringLiteral("Table"), PreviewAction{.label = QStringLiteral("Table"),
.glyph = "player", .glyph = "player",
.role = AppColor::IconStrong}, .role = AppColor::PositiveStrong},
PreviewAction{.label = QStringLiteral("Hand"), PreviewAction{.label = QStringLiteral("Hand"),
.glyph = "arrow_left", .glyph = "arrow_left",
.role = AppColor::PositiveStrong}, .role = AppColor::PositiveStrong},