Compare commits

..

8 commits

Author SHA1 Message Date
Lukas Brübach
047fa8a6cf [VisualDatabase] Refresh filter toolbar label icons on theme change
The Sort by and Filter by labels kept a one-shot pixmap snapshot, so they stayed
at the previous palette until the toolbar was rebuilt. Re-render both snapshots
when the palette or theme changes.
2026-10-20 19:31:58 +02:00
Lukas Brübach
459c7ca4df [PrintingSelector] Load the pin badge through the themed icon engine
The badge rasterized `theme:icons/pin` with QImageReader, which cannot resolve
the palette token and left the glyph at its first-render colors. Ask QIcon for a
64x64 logical pixmap instead, and re-render the badge when the palette or theme
changes so it stays legible against both card art and the surrounding UI.
2026-10-20 19:31:58 +02:00
Lukas Brübach
1a3142aa0c [Game] Follow the palette in judge message tags
Judge tags hardcoded a black prefix and a `theme:icons/scales` image inside rich
text, where `currentColor` cannot resolve and a palette change leaves the tag
unreadable. Render the scales glyph through the themed icon engine, register it
as a document image resource, and repaint the log when the palette or theme
changes.
2026-10-20 19:31:58 +02:00
Lukas Brübach
f6e271cfbf [Icons] Add an icon asset ratchet test
The icon rules were enforced by review only, so the next Inkscape export could
reintroduce a baked fill or a physical size without anyone noticing.

Add a test that rejects non-24 viewBoxes, physical sizes, editor metadata,
hardcoded paints, missing tokens, color-coded filenames, and rasters. Keep the
legacy baseline empty so the ratchet can only tighten.
2026-10-20 19:31:58 +02:00
Lukas Brübach
6844da1d2c [Settings] Delete update channels at shutdown
SettingsCache owns the stable and beta update channels as raw pointers, but
they have no parent and are never deleted. Each owns a QNetworkAccessManager,
so the test process leaves QtNetwork objects alive past QApplication teardown.

Delete the channels in the SettingsCache destructor so their network managers
are destroyed while the application object is still alive.
2026-10-20 19:27:30 +02:00
Lukas Brübach
8c2a1a90f4 [Icons] Migrate UI glyphs to palette-token SVGs
Runtime inversion is not a theming system: it rotates hue, breaks duotone art
and mishandles alpha, and a QPixmap snapshot cannot follow a palette change.
Redraw the top-level UI glyph set on a 24x24 grid with `currentColor` as the
paint token, drop Inkscape/Sodipodi cruft, and delete the icons whose metaphor
was duplicated (cogwheel/gear -> settings, scale_balanced -> scales,
update.png -> reload).

Load the glyphs through `loadThemedIcon` so the engine resolves the token from
the palette on every request, and let `themePixmap` use the same resolution for
one-shot pixmaps. `loadColorAdjustedPixmap` is gone; every caller now keeps a
QIcon, which `SettingsButtonWidget::setButtonIcon` accepts instead of a QPixmap.

Content art (mana, counters, countries) keeps its authored colors.

Add a render test covering aspect ratio, palette flips, icon modes and
high-DPI pixmaps.
2026-10-20 11:41:26 +02:00
Lukas Brübach
041d9387b2 [Icons] Add palette-aware themed icon engine
A QPixmap snapshot cannot follow palette changes, so an authored `currentColor`
glyph would freeze at whatever color it was first rendered with. Add a QIcon
engine that resolves the sentinel from the current palette on every pixmap
request, caches by resolved path/color/size/DPR, and falls back to Qt's normal
loader for authored multi-color art and raster overrides.

Purely additive. No behavior change yet.
2026-10-20 11:32:16 +02:00
Lukas Brübach
67d5cc1aab [Icons] Add SVG currentColor sentinel resolver
Qt's SVG renderer has no CSS cascade, so an authored `currentColor` never
resolves. Resolve the sentinel to a concrete palette color as plain bytes,
before the SVG reaches QSvgRenderer.
2026-10-20 11:24:03 +02:00
6 changed files with 22 additions and 85 deletions

View file

@ -105,9 +105,9 @@ void FlowWidget::addWidget(QWidget *widget_to_add)
*
* The widget is filtered for arrow-key events so keyboard navigation between
* the flow items keeps working even when the flow sits inside a QScrollArea,
* which swallows arrow keys before they can bubble up. Only widgets added
* through this method are affected; anything that needs its own arrow keys
* should use plain addWidget.
* which swallows arrow keys before they can reach FlowWidget::keyPressEvent.
* Only widgets added through this method are affected; anything that needs its
* own arrow keys should use plain addWidget.
*
* @param widget_to_add The widget to add to the flow layout.
*/
@ -207,12 +207,17 @@ QLayoutItem *FlowWidget::itemAt(int index) const
return flowLayout->itemAt(index);
}
void FlowWidget::keyPressEvent(QKeyEvent *event)
{
if (moveFocus(event)) {
event->accept();
return;
}
QWidget::keyPressEvent(event);
}
bool FlowWidget::eventFilter(QObject *watched, QEvent *event)
{
// Arrow-key navigation is deliberately scoped to widgets that opted in via
// addNavigableWidget(). Handling arrow keys in keyPressEvent() as well would
// also catch the events ordinary children (QLineEdit, ...) ignore and move
// focus away from the control the user is typing in.
if (event->type() == QEvent::KeyPress && moveFocus(static_cast<QKeyEvent *>(event))) {
return true;
}

View file

@ -51,6 +51,7 @@ public slots:
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private:
/// @brief Moves keyboard focus to an adjacent flow item for an arrow-key event.

View file

@ -133,10 +133,6 @@ ExactCard CardDatabaseQuerier::getCard(const CardRef &cardRef) const
ExactCard CardDatabaseQuerier::guessCard(const CardRef &cardRef) const
{
auto card = lookupCardByName(cardRef.name);
if (!card) {
return {};
}
auto printing =
cardRef.providerId.isEmpty() ? getPreferredPrinting(card) : findPrintingWithId(card, cardRef.providerId);
@ -177,14 +173,10 @@ ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const
*
* @param cardInfo The CardInfo to search
* @param providerId The uuid to look for
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found or if cardInfo is null.
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found.
*/
PrintingInfo CardDatabaseQuerier::findPrintingWithId(const CardInfoPtr &cardInfo, const QString &providerId) const
{
if (!cardInfo) {
return PrintingInfo();
}
for (const auto &printings : cardInfo->getSets()) {
for (const auto &printing : printings) {
if (printing.getUuid() == providerId) {

View file

@ -43,31 +43,31 @@ SomewhatComplexQueryPart <- [(] QueryPartList [)] / QueryPart
QueryPart <- NotQuery / SetQuery / RarityQuery / CMCQuery / FormatQuery / PowerQuery / ToughnessQuery / ColorQuery / TypeQuery / OracleQuery / FieldQuery / GenericQuery
NotQuery <- ('NOT' ws/'-') SomewhatComplexQueryPart
SetQuery <- ('e'/'set') ws? SetQueryValue
SetQueryValue <- ([=:] ws? FlexStringValue) / (<[!][=]?> ws? FlexStringValue) / SetExpression
OracleQuery <- 'o' ws? [:] ws? MatcherString
SetQuery <- ('e'/'set') SetQueryValue
SetQueryValue <- ([=:] FlexStringValue) / (<[!][=]?> FlexStringValue) / SetExpression
OracleQuery <- 'o' [:] MatcherString
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
PowerQuery <- [Pp] 'ow' 'er'? ws? NumericExpression
ToughnessQuery <- [Tt] 'ou' 'ghness'? ws? NumericExpression
RarityQuery <- [rR] ws? ':' ws? Rarity
RarityQuery <- [rR] ':' Rarity
Rarity <- [Cc] 'ommon'? / [Uu] 'ncommon'? / [Rr] 'are'? / [Mm] 'ythic'? / [Ss] 'pecial'? / [a-zA-Z] [a-z]*
FormatQuery <- 'f' ws? ':' ws? Format / Legality ws? ':' ws? Format
FormatQuery <- 'f' ':' Format / Legality ':' Format
Format <- [a-zA-Z] [a-z]*
Legality <- [Ll] 'egal'? / [Bb] 'anned'? / [Rr] 'estricted'
TypeQuery <- [tT] 'ype'? ws? [:] ws? StringValue
TypeQuery <- [tT] 'ype'? [:] StringValue
Color <- < [Ww] 'hite'? / [Uu] / [Bb] 'lack'? / [Rr] 'ed'? / [Gg] 'reen'? / [Bb] 'lue'? >
ColorEx <- Color / [mc]
ColorQuery <- [cC] 'olor'? <[iI]?> ws? <[:!]> ws? ColorEx*
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
FieldQuery <- String ws? [:] ws? MatcherString / String ws? NumericExpression
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].

View file

@ -1,7 +1,6 @@
#include "libcockatrice/card/database/card_database.h"
#include "libcockatrice/card/database/card_database_loader.h"
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/card/printing/exact_card.h"
#include "libcockatrice/card/set/card_set.h"
#include "libcockatrice/card/set/card_set_list.h"
#include "test_card_database_path_provider.h"
@ -30,7 +29,6 @@
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <libcockatrice/utility/card_ref.h>
#include <string>
namespace
@ -62,31 +60,6 @@ TEST(CardDatabaseTest, LoadXml)
ASSERT_EQ(NotLoaded, db->getLoadStatus()) << "Incorrect status after clear";
}
TEST(CardDatabaseTest, GuessCardWithProviderIdOnUnknownCardReturnsEmpty)
{
// An unknown name plus a providerId used to dereference the missing card
// inside findPrintingWithId(); the lookup must come back empty instead.
NoopCardPreferenceProvider prefs;
TestCardDatabasePathProvider pathProvider;
NoopCardSetPriorityController controller;
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
const ExactCard guessed = db.query()->guessCard({"Not a Card", "card_Not a Card_fixture"});
EXPECT_TRUE(guessed.isEmpty());
EXPECT_FALSE(guessed);
}
TEST(CardDatabaseTest, FindPrintingWithIdOnNullCardInfoReturnsEmpty)
{
NoopCardPreferenceProvider prefs;
TestCardDatabasePathProvider pathProvider;
NoopCardSetPriorityController controller;
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
EXPECT_TRUE(db.query()->findPrintingWithId(nullptr, "card_Not a Card_fixture").isEmpty());
}
TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
{
NoopCardSetPriorityController controller;

View file

@ -99,17 +99,6 @@ QUERY(SetCodeWrongSet2, doctor, "set:cat", false)
QUERY(BracketNextToUnquotedString, cat, "(o:woof OR o:meow)", true)
// Spaces around the colon are optional for all field queries (e.g. "o: flying")
QUERY(OracleSpaceAfterColon, cat, "o: meow", true)
QUERY(OracleSpaceBeforeColon, cat, "o :meow", true)
QUERY(OracleSpacesAroundColon, cat, "o : meow", true)
QUERY(OracleSpaceAfterColonNoMatch, cat, "o: woof", false)
QUERY(TypeSpaceAfterColon, cat, "t: creature", true)
QUERY(FieldSpaceAfterColon, cat, "pt: \"3/3\"", true)
QUERY(ColorSpaceAfterColon, cat, "c: g", true)
QUERY(ColorSpaceBeforeOperator, cat, "c !g", true)
QUERY(SetSpaceAfterColon, cat, "set: cat", true)
CardInfoPtr localizedCat()
{
CardInfoPtr localized = CardInfo::newInstance("Cat", "Meow!", false, {}, {}, {}, {}, {});
@ -118,22 +107,6 @@ CardInfoPtr localizedCat()
return localized;
}
CardInfoPtr greenIdentityCard()
{
return CardInfo::newInstance("Identity Cat", "", false, {{"colors", "G"}, {"coloridentity", "G"}}, {}, {}, {}, {});
}
// Whitespace belongs around the separator, never inside an operator token: "c i: g" must not
// be read as the color-identity operator "ci" (it is a generic "c" AND a field query on "i").
TEST_F(CardQuery, ColorIdentityOperatorIsNotSplitByWhitespace)
{
const CardData identityCard = greenIdentityCard();
ASSERT_TRUE(FilterString("ci:g").check(identityCard));
ASSERT_TRUE(FilterString("ci: g").check(identityCard));
ASSERT_TRUE(FilterString("ci : g").check(identityCard));
ASSERT_FALSE(FilterString("c i: g").check(identityCard));
}
TEST_F(CardQuery, SearchLanguageEnglishMatchesOnlyEnglish)
{
const CardData localized = localizedCat();
@ -163,13 +136,6 @@ TEST_F(CardQuery, SearchLanguageBothMatchesEitherLanguage)
ASSERT_TRUE(FilterString("Kater", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
}
TEST_F(CardQuery, SearchLanguageBothMatchesOracleTextWithSpaceAfterColon)
{
const CardData localized = localizedCat();
ASSERT_TRUE(FilterString("o: Meow", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
ASSERT_TRUE(FilterString("o: miaut", CardSearchLanguage{"de", SearchLanguageMode::Both}).check(localized));
}
TEST_F(CardQuery, SearchLanguageIsBoundPerInstance)
{
const CardData localized = localizedCat();