Compare commits

..

7 commits

Author SHA1 Message Date
Lukas Brübach
63a8f6b10b
[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-21 05:11:13 +02:00
Lukas Brübach
8849c11f66
[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-21 05:11:13 +02:00
Lukas Brübach
eb7e866211
[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-21 05:11:12 +02:00
Lukas Brübach
9978be9ab7
[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-21 05:11:11 +02:00
BruebachL
0fe4fd0d8c
[CardDB] Guard guessCard and findPrintingWithId against missing cards (#7592)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Desktop / Prune caches (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / Ubuntu 26.04 cover (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
* [CardDB] Guard guessCard and findPrintingWithId against missing cards

guessCard() dereferenced a null CardInfoPtr through findPrintingWithId() when the name was unknown and a providerId was given. Return an empty ExactCard instead, and make findPrintingWithId() null-safe for every caller.

Add regression tests covering both paths.

* [CardDB] Rely on PrintingInfo's default null set in findPrintingWithId

The explicit nullptr was redundant: the constructor already defaults
the set to null.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-20 21:33:56 +02:00
BruebachL
9980c90cea
[Filter] Allow spaces around the colon in search queries (#7529)
* [Filter] Allow spaces around the colon in search queries

* [Filter] Keep whitespace out of the color-identity operator

Allowing `ws?` between `color` and the identity marker made `c i: g`
parse as identity green, changing the meaning of text that previously
read as a generic `c` plus a field query on `i`. Whitespace belongs
around the separator, not inside an operator token: `ci : g` and
`ci: g` stay valid, `c i: g` keeps its old parse.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-20 21:20:10 +02:00
BruebachL
9709fe4a1a
[FlowWidget] Restore arrow-key navigation for items in flow widgets (#7591)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-20 16:53:04 +02:00
8 changed files with 85 additions and 30 deletions

View file

@ -50,13 +50,6 @@ SettingsCache &SettingsCache::instance()
return *settingsCache; // returns a QT managed singleton reference
}
SettingsCache::~SettingsCache()
{
for (ReleaseChannel *releaseChannel : releaseChannels) {
delete releaseChannel;
}
}
QString SettingsCache::getDataPath()
{
if (isPortableBuild) {

View file

@ -99,7 +99,6 @@ private:
public:
SettingsCache();
~SettingsCache() override;
QString getDataPath();
QString getSettingsPath();
[[nodiscard]] QString getCachePath() const;

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 reach FlowWidget::keyPressEvent.
* 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 bubble up. 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,17 +207,12 @@ 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,7 +51,6 @@ 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,6 +133,10 @@ 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);
@ -173,10 +177,14 @@ 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.
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found or if cardInfo is null.
*/
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') SetQueryValue
SetQueryValue <- ([=:] FlexStringValue) / (<[!][=]?> FlexStringValue) / SetExpression
OracleQuery <- 'o' [:] MatcherString
SetQuery <- ('e'/'set') ws? SetQueryValue
SetQueryValue <- ([=:] ws? FlexStringValue) / (<[!][=]?> ws? FlexStringValue) / SetExpression
OracleQuery <- 'o' ws? [:] ws? MatcherString
CMCQuery <- ('cmc'/'mv') ws? NumericExpression
PowerQuery <- [Pp] 'ow' 'er'? ws? NumericExpression
ToughnessQuery <- [Tt] 'ou' 'ghness'? ws? NumericExpression
RarityQuery <- [rR] ':' Rarity
RarityQuery <- [rR] ws? ':' ws? Rarity
Rarity <- [Cc] 'ommon'? / [Uu] 'ncommon'? / [Rr] 'are'? / [Mm] 'ythic'? / [Ss] 'pecial'? / [a-zA-Z] [a-z]*
FormatQuery <- 'f' ':' Format / Legality ':' Format
FormatQuery <- 'f' ws? ':' ws? Format / Legality ws? ':' ws? Format
Format <- [a-zA-Z] [a-z]*
Legality <- [Ll] 'egal'? / [Bb] 'anned'? / [Rr] 'estricted'
TypeQuery <- [tT] 'ype'? [:] StringValue
TypeQuery <- [tT] 'ype'? ws? [:] ws? StringValue
Color <- < [Ww] 'hite'? / [Uu] / [Bb] 'lack'? / [Rr] 'ed'? / [Gg] 'reen'? / [Bb] 'lue'? >
ColorEx <- Color / [mc]
ColorQuery <- [cC] 'olor'? <[iI]?> <[:!]> ColorEx*
ColorQuery <- [cC] 'olor'? <[iI]?> ws? <[:!]> ws? ColorEx*
FieldQuery <- String [:] MatcherString / String ws? NumericExpression
FieldQuery <- String ws? [:] ws? MatcherString / String ws? NumericExpression
NonDoubleQuoteUnlessEscaped <- '\\\"'. / !["].
NonSingleQuoteUnlessEscaped <- "\\\'". / !['].

View file

@ -1,6 +1,7 @@
#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"
@ -29,6 +30,7 @@
#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
@ -60,6 +62,31 @@ 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,6 +99,17 @@ 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, {}, {}, {}, {}, {});
@ -107,6 +118,22 @@ 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();
@ -136,6 +163,13 @@ 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();