mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-09-04 17:23:29 +00:00
set collection
This commit is contained in:
@@ -3,12 +3,15 @@
|
|||||||
// Pure helpers: Digi-Battle set-completion progress and per-set checklists.
|
// Pure helpers: Digi-Battle set-completion progress and per-set checklists.
|
||||||
// Ownership counts only when collection card.set.id matches the pack and the
|
// Ownership counts only when collection card.set.id matches the pack and the
|
||||||
// normalized setNo appears in that pack's catalog. Duplicates / amount do not
|
// normalized setNo appears in that pack's catalog. Duplicates / amount do not
|
||||||
// inflate the numerator.
|
// inflate the numerator. An optional languageFilter restricts ownership to
|
||||||
|
// cards of that language (packs with zero matches are omitted).
|
||||||
|
|
||||||
#include "ccm/domain/DigiBattle99Card.hpp"
|
#include "ccm/domain/DigiBattle99Card.hpp"
|
||||||
#include "ccm/domain/DigiBattle99SetCatalog.hpp"
|
#include "ccm/domain/DigiBattle99SetCatalog.hpp"
|
||||||
|
#include "ccm/domain/Enums.hpp"
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -33,16 +36,24 @@ struct DigiBattle99ChecklistEntry {
|
|||||||
bool owned{false};
|
bool owned{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Distinct languages present in the collection, in allLanguages() order.
|
||||||
|
[[nodiscard]] std::vector<Language>
|
||||||
|
digiBattle99LanguagesInCollection(const std::vector<DigiBattle99Card>& collection);
|
||||||
|
|
||||||
// Packs where the collection owns ≥1 card with matching set.id, ordered by
|
// Packs where the collection owns ≥1 card with matching set.id, ordered by
|
||||||
// setName. Packs absent from the catalog are skipped.
|
// setName. Packs absent from the catalog are skipped. When languageFilter is
|
||||||
|
// set, only cards of that language count toward ownership.
|
||||||
[[nodiscard]] std::vector<DigiBattle99SetCompletionProgress>
|
[[nodiscard]] std::vector<DigiBattle99SetCompletionProgress>
|
||||||
computeDigiBattle99SetCompletion(const std::vector<DigiBattle99Card>& collection,
|
computeDigiBattle99SetCompletion(const std::vector<DigiBattle99Card>& collection,
|
||||||
const DigiBattle99SetCatalog& catalog);
|
const DigiBattle99SetCatalog& catalog,
|
||||||
|
std::optional<Language> languageFilter = std::nullopt);
|
||||||
|
|
||||||
// Full catalog checklist for one pack; owned flags from the collection.
|
// Full catalog checklist for one pack; owned flags from the collection.
|
||||||
|
// When languageFilter is set, only cards of that language count as owned.
|
||||||
[[nodiscard]] std::vector<DigiBattle99ChecklistEntry>
|
[[nodiscard]] std::vector<DigiBattle99ChecklistEntry>
|
||||||
digiBattle99ChecklistForSet(const std::vector<DigiBattle99Card>& collection,
|
digiBattle99ChecklistForSet(const std::vector<DigiBattle99Card>& collection,
|
||||||
const DigiBattle99SetCatalog& catalog,
|
const DigiBattle99SetCatalog& catalog,
|
||||||
std::string_view setId);
|
std::string_view setId,
|
||||||
|
std::optional<Language> languageFilter = std::nullopt);
|
||||||
|
|
||||||
} // namespace ccm
|
} // namespace ccm
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "ccm/games/digibattle99/DigiBattle99CardPreviewSource.hpp"
|
#include "ccm/games/digibattle99/DigiBattle99CardPreviewSource.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@@ -12,9 +13,16 @@ namespace {
|
|||||||
|
|
||||||
using OwnedBySet = std::unordered_map<std::string, std::unordered_set<std::string>>;
|
using OwnedBySet = std::unordered_map<std::string, std::unordered_set<std::string>>;
|
||||||
|
|
||||||
OwnedBySet ownedSetNosBySetId(const std::vector<DigiBattle99Card>& collection) {
|
bool passesLanguageFilter(const DigiBattle99Card& card,
|
||||||
|
std::optional<Language> languageFilter) {
|
||||||
|
return !languageFilter.has_value() || card.language == *languageFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
OwnedBySet ownedSetNosBySetId(const std::vector<DigiBattle99Card>& collection,
|
||||||
|
std::optional<Language> languageFilter) {
|
||||||
OwnedBySet out;
|
OwnedBySet out;
|
||||||
for (const auto& card : collection) {
|
for (const auto& card : collection) {
|
||||||
|
if (!passesLanguageFilter(card, languageFilter)) continue;
|
||||||
if (card.set.id.empty()) continue;
|
if (card.set.id.empty()) continue;
|
||||||
const std::string setNo =
|
const std::string setNo =
|
||||||
DigiBattle99CardPreviewSource::normalizeCardNumber(card.setNo);
|
DigiBattle99CardPreviewSource::normalizeCardNumber(card.setNo);
|
||||||
@@ -26,10 +34,31 @@ OwnedBySet ownedSetNosBySetId(const std::vector<DigiBattle99Card>& collection) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<Language>
|
||||||
|
digiBattle99LanguagesInCollection(const std::vector<DigiBattle99Card>& collection) {
|
||||||
|
const auto& langs = allLanguages();
|
||||||
|
std::array<bool, 10> present{};
|
||||||
|
for (const auto& card : collection) {
|
||||||
|
for (std::size_t i = 0; i < langs.size(); ++i) {
|
||||||
|
if (langs[i] == card.language) {
|
||||||
|
present[i] = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Language> out;
|
||||||
|
for (std::size_t i = 0; i < langs.size(); ++i) {
|
||||||
|
if (present[i]) out.push_back(langs[i]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<DigiBattle99SetCompletionProgress>
|
std::vector<DigiBattle99SetCompletionProgress>
|
||||||
computeDigiBattle99SetCompletion(const std::vector<DigiBattle99Card>& collection,
|
computeDigiBattle99SetCompletion(const std::vector<DigiBattle99Card>& collection,
|
||||||
const DigiBattle99SetCatalog& catalog) {
|
const DigiBattle99SetCatalog& catalog,
|
||||||
const OwnedBySet owned = ownedSetNosBySetId(collection);
|
std::optional<Language> languageFilter) {
|
||||||
|
const OwnedBySet owned = ownedSetNosBySetId(collection, languageFilter);
|
||||||
|
|
||||||
std::vector<DigiBattle99SetCompletionProgress> out;
|
std::vector<DigiBattle99SetCompletionProgress> out;
|
||||||
out.reserve(owned.size());
|
out.reserve(owned.size());
|
||||||
@@ -64,12 +93,14 @@ computeDigiBattle99SetCompletion(const std::vector<DigiBattle99Card>& collection
|
|||||||
std::vector<DigiBattle99ChecklistEntry>
|
std::vector<DigiBattle99ChecklistEntry>
|
||||||
digiBattle99ChecklistForSet(const std::vector<DigiBattle99Card>& collection,
|
digiBattle99ChecklistForSet(const std::vector<DigiBattle99Card>& collection,
|
||||||
const DigiBattle99SetCatalog& catalog,
|
const DigiBattle99SetCatalog& catalog,
|
||||||
std::string_view setId) {
|
std::string_view setId,
|
||||||
|
std::optional<Language> languageFilter) {
|
||||||
const auto* pack = catalog.findPack(setId);
|
const auto* pack = catalog.findPack(setId);
|
||||||
if (pack == nullptr) return {};
|
if (pack == nullptr) return {};
|
||||||
|
|
||||||
std::unordered_set<std::string> ownedNos;
|
std::unordered_set<std::string> ownedNos;
|
||||||
for (const auto& card : collection) {
|
for (const auto& card : collection) {
|
||||||
|
if (!passesLanguageFilter(card, languageFilter)) continue;
|
||||||
if (card.set.id != setId) continue;
|
if (card.set.id != setId) continue;
|
||||||
const std::string setNo =
|
const std::string setNo =
|
||||||
DigiBattle99CardPreviewSource::normalizeCardNumber(card.setNo);
|
DigiBattle99CardPreviewSource::normalizeCardNumber(card.setNo);
|
||||||
|
|||||||
@@ -119,6 +119,54 @@ TEST_SUITE("computeDigiBattle99SetCompletion") {
|
|||||||
};
|
};
|
||||||
CHECK(computeDigiBattle99SetCompletion(collection, catalog).empty());
|
CHECK(computeDigiBattle99SetCompletion(collection, catalog).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("language filter hides packs with no cards in that language") {
|
||||||
|
const auto catalog = sampleCatalog();
|
||||||
|
DigiBattle99Card en =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-01");
|
||||||
|
en.language = Language::English;
|
||||||
|
|
||||||
|
const auto allRows = computeDigiBattle99SetCompletion({en}, catalog);
|
||||||
|
REQUIRE(allRows.size() == 1);
|
||||||
|
|
||||||
|
const auto deRows =
|
||||||
|
computeDigiBattle99SetCompletion({en}, catalog, Language::German);
|
||||||
|
CHECK(deRows.empty());
|
||||||
|
|
||||||
|
const auto enRows =
|
||||||
|
computeDigiBattle99SetCompletion({en}, catalog, Language::English);
|
||||||
|
REQUIRE(enRows.size() == 1);
|
||||||
|
CHECK(enRows[0].ownedUnique == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("same setNo in two languages counts once aggregated; filter is exclusive") {
|
||||||
|
const auto catalog = sampleCatalog();
|
||||||
|
DigiBattle99Card en =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-01");
|
||||||
|
en.language = Language::English;
|
||||||
|
DigiBattle99Card de =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-01");
|
||||||
|
de.id = 2;
|
||||||
|
de.language = Language::German;
|
||||||
|
|
||||||
|
const auto allRows = computeDigiBattle99SetCompletion({en, de}, catalog);
|
||||||
|
REQUIRE(allRows.size() == 1);
|
||||||
|
CHECK(allRows[0].ownedUnique == 1);
|
||||||
|
|
||||||
|
const auto enRows =
|
||||||
|
computeDigiBattle99SetCompletion({en, de}, catalog, Language::English);
|
||||||
|
REQUIRE(enRows.size() == 1);
|
||||||
|
CHECK(enRows[0].ownedUnique == 1);
|
||||||
|
|
||||||
|
DigiBattle99Card deOnly =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-02");
|
||||||
|
deOnly.id = 3;
|
||||||
|
deOnly.language = Language::German;
|
||||||
|
const auto deRows = computeDigiBattle99SetCompletion({en, de, deOnly}, catalog,
|
||||||
|
Language::German);
|
||||||
|
REQUIRE(deRows.size() == 1);
|
||||||
|
CHECK(deRows[0].ownedUnique == 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_SUITE("digiBattle99ChecklistForSet") {
|
TEST_SUITE("digiBattle99ChecklistForSet") {
|
||||||
@@ -142,6 +190,57 @@ TEST_SUITE("digiBattle99ChecklistForSet") {
|
|||||||
const auto catalog = sampleCatalog();
|
const auto catalog = sampleCatalog();
|
||||||
CHECK(digiBattle99ChecklistForSet({}, catalog, "missing").empty());
|
CHECK(digiBattle99ChecklistForSet({}, catalog, "missing").empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("owned flags respect language filter") {
|
||||||
|
const auto catalog = sampleCatalog();
|
||||||
|
DigiBattle99Card en =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-02");
|
||||||
|
en.language = Language::English;
|
||||||
|
|
||||||
|
const auto filtered =
|
||||||
|
digiBattle99ChecklistForSet({en}, catalog, "series-1-starter-set",
|
||||||
|
Language::German);
|
||||||
|
REQUIRE(filtered.size() == 3);
|
||||||
|
CHECK(filtered[0].owned == false);
|
||||||
|
CHECK(filtered[1].owned == false);
|
||||||
|
CHECK(filtered[2].owned == false);
|
||||||
|
|
||||||
|
const auto english =
|
||||||
|
digiBattle99ChecklistForSet({en}, catalog, "series-1-starter-set",
|
||||||
|
Language::English);
|
||||||
|
REQUIRE(english.size() == 3);
|
||||||
|
CHECK(english[1].owned == true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_SUITE("digiBattle99LanguagesInCollection") {
|
||||||
|
TEST_CASE("empty collection yields empty") {
|
||||||
|
CHECK(digiBattle99LanguagesInCollection({}).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("returns distinct languages in allLanguages order") {
|
||||||
|
DigiBattle99Card jp =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-01");
|
||||||
|
jp.language = Language::Japanese;
|
||||||
|
DigiBattle99Card en =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-02");
|
||||||
|
en.id = 2;
|
||||||
|
en.language = Language::English;
|
||||||
|
DigiBattle99Card enDup =
|
||||||
|
makeOwned("series-1-booster-pack", "Series 1 Booster Pack", "BO-01");
|
||||||
|
enDup.id = 3;
|
||||||
|
enDup.language = Language::English;
|
||||||
|
DigiBattle99Card de =
|
||||||
|
makeOwned("series-1-starter-set", "Series 1 Starter Set", "ST-03");
|
||||||
|
de.id = 4;
|
||||||
|
de.language = Language::German;
|
||||||
|
|
||||||
|
const auto langs = digiBattle99LanguagesInCollection({jp, en, enDup, de});
|
||||||
|
REQUIRE(langs.size() == 3);
|
||||||
|
CHECK(langs[0] == Language::English);
|
||||||
|
CHECK(langs[1] == Language::German);
|
||||||
|
CHECK(langs[2] == Language::Japanese);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_SUITE("DigiBattle99SetCatalogService") {
|
TEST_SUITE("DigiBattle99SetCatalogService") {
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@
|
|||||||
- `include/ccm/ui/SwitchCtrl.hpp` + `src/SwitchCtrl.cpp` — custom pill-track + thumb switch for small modal rows (Yu-Gi-Oh! set picker); fires `EVT_CCM_SWITCH` on user toggle and reads colors from `inferThemeFromWindow` / `paletteForTheme`.
|
- `include/ccm/ui/SwitchCtrl.hpp` + `src/SwitchCtrl.cpp` — custom pill-track + thumb switch for small modal rows (Yu-Gi-Oh! set picker); fires `EVT_CCM_SWITCH` on user toggle and reads colors from `inferThemeFromWindow` / `paletteForTheme`.
|
||||||
- `include/ccm/ui/Magic*.hpp` + `src/Magic*.cpp` — Magic implementations: `MagicCardListPanel`, `MagicSelectedCardPanel`, `MagicCardEditDialog`, `MagicGameView`. Each is ~50–100 lines of hook overrides on top of the matching base template.
|
- `include/ccm/ui/Magic*.hpp` + `src/Magic*.cpp` — Magic implementations: `MagicCardListPanel`, `MagicSelectedCardPanel`, `MagicCardEditDialog`, `MagicGameView`. Each is ~50–100 lines of hook overrides on top of the matching base template.
|
||||||
- `include/ccm/ui/Pokemon*.hpp` + `src/Pokemon*.cpp` — Pokemon implementations: `PokemonCardListPanel`, `PokemonSelectedCardPanel`, `PokemonCardEditDialog`, `PokemonGameView`. Same shape as the Magic ones; differences are limited to the Set # field, the Holo / 1. Edition flags, and the Pokemon TCG preview lookup key (which includes `setNo`).
|
- `include/ccm/ui/Pokemon*.hpp` + `src/Pokemon*.cpp` — Pokemon implementations: `PokemonCardListPanel`, `PokemonSelectedCardPanel`, `PokemonCardEditDialog`, `PokemonGameView`. Same shape as the Magic ones; differences are limited to the Set # field, the Holo / 1. Edition flags, and the Pokemon TCG preview lookup key (which includes `setNo`).
|
||||||
- `include/ccm/ui/DigiBattle99*.hpp` + `src/DigiBattle99*.cpp` — Digimon Digi-Battle: list/selected/edit plus `DigiBattle99GameView` notebook (**Single Cards** | **Set Completion**) via `contentPanel`, and `DigiBattle99SetCompletionPanel` (pack progress tiles + greyed checklist). Catalog from `DigiBattle99SetCatalogService` (`set-catalog.json`), filled on Update Sets. The Add/Edit/Delete + filter toolbar lives **inside** the Single Cards tab (under the notebook); MainFrame hides its shared toolbar while Digimon is active (`hostsOwnLayout`).
|
- `include/ccm/ui/DigiBattle99*.hpp` + `src/DigiBattle99*.cpp` — Digimon Digi-Battle: list/selected/edit plus `DigiBattle99GameView` via `contentPanel` with a **palette-painted tab strip** + `wxSimplebook` (**Single Cards** | **Set Completion**) — not native `wxNotebook`, which stays light on MSW dark mode — and `DigiBattle99SetCompletionPanel` (pack progress tiles + greyed checklist). Catalog from `DigiBattle99SetCatalogService` (`set-catalog.json`), filled on Update Sets. The Add/Edit/Delete + filter toolbar lives **inside** the Single Cards page; MainFrame hides its shared toolbar while Digimon is active (`hostsOwnLayout`).
|
||||||
- `include/ccm/ui/SvgIcons.hpp` + `src/SvgIcons.cpp` — embedded SVG templates with a `@FILL@` placeholder. Magic flags: `kSvgFoil` / `kSvgSigned` / `kSvgAltered`. Pokemon flags: `kSvgHolo` (sparkle, mirroring the original `IconHolo` from `PokemonTable.tsx`) and `kSvgFirstEdition` (themed "1" inside an outlined badge, rebuilt from the original `IconPokemonFirstEdition.tsx` — every fill/stroke uses `@FILL@` so the icon themes alongside the others). Toolbar glyphs: `kSvgToolbarAdd` / `kSvgToolbarEdit` / `kSvgToolbarDelete` (vscode-codicons). `svgIconBitmap` / `paddedSvgIcon` helpers backed by `wxBitmapBundle::FromSVG`. Bitmaps from `svgIconBitmap` go straight to `wxStaticBitmap` / `wxBitmapButton::SetBitmap` cleanly; for the row-icon path `IconListCtrl` packs them into a private premultiplied-BGRA `HIMAGELIST` and draws with `ImageList_Draw`. See convention 11 for the full pitfall write-up.
|
- `include/ccm/ui/SvgIcons.hpp` + `src/SvgIcons.cpp` — embedded SVG templates with a `@FILL@` placeholder. Magic flags: `kSvgFoil` / `kSvgSigned` / `kSvgAltered`. Pokemon flags: `kSvgHolo` (sparkle, mirroring the original `IconHolo` from `PokemonTable.tsx`) and `kSvgFirstEdition` (themed "1" inside an outlined badge, rebuilt from the original `IconPokemonFirstEdition.tsx` — every fill/stroke uses `@FILL@` so the icon themes alongside the others). Toolbar glyphs: `kSvgToolbarAdd` / `kSvgToolbarEdit` / `kSvgToolbarDelete` (vscode-codicons). `svgIconBitmap` / `paddedSvgIcon` helpers backed by `wxBitmapBundle::FromSVG`. Bitmaps from `svgIconBitmap` go straight to `wxStaticBitmap` / `wxBitmapButton::SetBitmap` cleanly; for the row-icon path `IconListCtrl` packs them into a private premultiplied-BGRA `HIMAGELIST` and draws with `ImageList_Draw`. See convention 11 for the full pitfall write-up.
|
||||||
- `src/BaseEvents.cpp` — single-translation-unit definitions for `EVT_CARD_SELECTED` and `EVT_PREVIEW_STATUS`. Both events are template-instantiation-agnostic so all per-game panels share the same event types.
|
- `src/BaseEvents.cpp` — single-translation-unit definitions for `EVT_CARD_SELECTED` and `EVT_PREVIEW_STATUS`. Both events are template-instantiation-agnostic so all per-game panels share the same event types.
|
||||||
- `include/ccm/ui/SettingsDialog.hpp` + `src/SettingsDialog.cpp` — edits `Configuration` via `ConfigService::store`.
|
- `include/ccm/ui/SettingsDialog.hpp` + `src/SettingsDialog.cpp` — edits `Configuration` via `ConfigService::store`.
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
3. **Ownership**: dialogs and panels are heap-allocated and parented to a `wxWindow`. wxWidgets owns the lifetime — do **not** wrap them in `unique_ptr`. `IGameView` instances themselves are owned by `app/main.cpp` (`std::unique_ptr<>`); the panels owned by the views become children of the `MainFrame` splitter on first mount.
|
3. **Ownership**: dialogs and panels are heap-allocated and parented to a `wxWindow`. wxWidgets owns the lifetime — do **not** wrap them in `unique_ptr`. `IGameView` instances themselves are owned by `app/main.cpp` (`std::unique_ptr<>`); the panels owned by the views become children of the `MainFrame` splitter on first mount.
|
||||||
4. **Custom events**: `EVT_CARD_SELECTED` is fired by the list panel on itself (not its parent). Each `IGameView` binds it on its typed list panel inside the panel's first construction so the typed selection flows directly into the typed selected panel — `MainFrame` never sees a `MagicCard` or a `PokemonCard`. Do not move that binding back into `MainFrame`.
|
4. **Custom events**: `EVT_CARD_SELECTED` is fired by the list panel on itself (not its parent). Each `IGameView` binds it on its typed list panel inside the panel's first construction so the typed selection flows directly into the typed selected panel — `MainFrame` never sees a `MagicCard` or a `PokemonCard`. Do not move that binding back into `MainFrame`.
|
||||||
5. **wxFont modifications** mutate in place: `font.MakeBold().MakeLarger()` — do not call `Scale` (it does not exist on wxFont 3.2; use `MakeLarger` / `SetPointSize`).
|
5. **wxFont modifications** mutate in place: `font.MakeBold().MakeLarger()` — do not call `Scale` (it does not exist on wxFont 3.2; use `MakeLarger` / `SetPointSize`).
|
||||||
6. **Single-active-game UX.** `MainFrame` only ever shows one game's panels at a time; the content host swaps either the shared `listPanel()` / `selectedPanel()` splitter or a game’s `contentPanel()` when the user picks a different `Game` menu entry. Do not stand up parallel side-by-side tabs for different games. Digimon’s Single Cards / Set Completion notebook is an in-game mode switch, not multi-game tabs.
|
6. **Single-active-game UX.** `MainFrame` only ever shows one game's panels at a time; the content host swaps either the shared `listPanel()` / `selectedPanel()` splitter or a game’s `contentPanel()` when the user picks a different `Game` menu entry. Do not stand up parallel side-by-side tabs for different games. Digimon’s Single Cards / Set Completion switch is an in-game mode switch (themed tab strip + `wxSimplebook`), not multi-game tabs.
|
||||||
7. **No `ccm_warnings`.** This target intentionally does **not** link the strict warning interface — wxWidgets headers trip `-Wpedantic` / `-Wshadow`. Keep it that way; do not add the link.
|
7. **No `ccm_warnings`.** This target intentionally does **not** link the strict warning interface — wxWidgets headers trip `-Wpedantic` / `-Wshadow`. Keep it that way; do not add the link.
|
||||||
8. **Async background work** must not capture `this` raw. Use the pattern from `BaseSelectedCardPanel`: a `std::shared_ptr<State>` holding `std::atomic<bool> alive`, `std::atomic<unsigned> currentGen`, and a back-pointer to the panel; spawn a detached `std::thread`, then deliver the result with `wxTheApp->CallAfter([state, gen, ...]() { if (!state->alive) return; if (state->currentGen != gen) return; ... })`. Flip `alive=false` in the panel destructor so late callbacks become no-ops.
|
8. **Async background work** must not capture `this` raw. Use the pattern from `BaseSelectedCardPanel`: a `std::shared_ptr<State>` holding `std::atomic<bool> alive`, `std::atomic<unsigned> currentGen`, and a back-pointer to the panel; spawn a detached `std::thread`, then deliver the result with `wxTheApp->CallAfter([state, gen, ...]() { if (!state->alive) return; if (state->currentGen != gen) return; ... })`. Flip `alive=false` in the panel destructor so late callbacks become no-ops.
|
||||||
9. **Icons come from `SvgIcons.hpp`.** Don't inline new SVG strings in panel sources; add them to `SvgIcons.{hpp,cpp}` so all panels stay in sync. Always pass a runtime fill color (`wxSystemSettings::GetColour(...).GetAsString(wxC2S_HTML_SYNTAX)`); never bake one into the SVG.
|
9. **Icons come from `SvgIcons.hpp`.** Don't inline new SVG strings in panel sources; add them to `SvgIcons.{hpp,cpp}` so all panels stay in sync. Always pass a runtime fill color (`wxSystemSettings::GetColour(...).GetAsString(wxC2S_HTML_SYNTAX)`); never bake one into the SVG.
|
||||||
|
|||||||
@@ -17,8 +17,10 @@
|
|||||||
|
|
||||||
class wxBitmapButton;
|
class wxBitmapButton;
|
||||||
class wxBoxSizer;
|
class wxBoxSizer;
|
||||||
class wxNotebook;
|
class wxPanel;
|
||||||
|
class wxSimplebook;
|
||||||
class wxSplitterWindow;
|
class wxSplitterWindow;
|
||||||
|
class wxStaticText;
|
||||||
class wxTextCtrl;
|
class wxTextCtrl;
|
||||||
|
|
||||||
namespace ccm::ui {
|
namespace ccm::ui {
|
||||||
@@ -64,7 +66,10 @@ private:
|
|||||||
const std::vector<Set>& setsForDialog();
|
const std::vector<Set>& setsForDialog();
|
||||||
void ensureSingleCardsMounted(wxWindow* splitterParent);
|
void ensureSingleCardsMounted(wxWindow* splitterParent);
|
||||||
void buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer);
|
void buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer);
|
||||||
|
void buildTabBar(wxWindow* parent, wxBoxSizer* rootSizer);
|
||||||
|
void selectTab(int index);
|
||||||
void refreshToolbarIcons(const ThemePalette& palette);
|
void refreshToolbarIcons(const ThemePalette& palette);
|
||||||
|
void refreshTabBarTheme(const ThemePalette& palette);
|
||||||
|
|
||||||
ConfigService& config_;
|
ConfigService& config_;
|
||||||
CollectionService<DigiBattle99Card>& collection_;
|
CollectionService<DigiBattle99Card>& collection_;
|
||||||
@@ -75,11 +80,15 @@ private:
|
|||||||
DigiBattle99SetCatalogService& catalogStore_;
|
DigiBattle99SetCatalogService& catalogStore_;
|
||||||
|
|
||||||
wxPanel* contentPanel_{nullptr};
|
wxPanel* contentPanel_{nullptr};
|
||||||
wxNotebook* notebook_{nullptr};
|
wxPanel* tabBar_{nullptr};
|
||||||
|
wxSimplebook* book_{nullptr};
|
||||||
wxSplitterWindow* singleSplitter_{nullptr};
|
wxSplitterWindow* singleSplitter_{nullptr};
|
||||||
DigiBattle99CardListPanel* listPanel_{nullptr};
|
DigiBattle99CardListPanel* listPanel_{nullptr};
|
||||||
DigiBattle99SelectedCardPanel* selectedPanel_{nullptr};
|
DigiBattle99SelectedCardPanel* selectedPanel_{nullptr};
|
||||||
DigiBattle99SetCompletionPanel* setCompletionPanel_{nullptr};
|
DigiBattle99SetCompletionPanel* setCompletionPanel_{nullptr};
|
||||||
|
std::array<wxPanel*, 2> tabPanels_{{nullptr, nullptr}};
|
||||||
|
std::array<wxStaticText*, 2> tabLabels_{{nullptr, nullptr}};
|
||||||
|
int activeTab_{0};
|
||||||
std::array<wxBitmapButton*, 3> toolbarButtons_{{nullptr, nullptr, nullptr}};
|
std::array<wxBitmapButton*, 3> toolbarButtons_{{nullptr, nullptr, nullptr}};
|
||||||
wxTextCtrl* filterInput_{nullptr};
|
wxTextCtrl* filterInput_{nullptr};
|
||||||
std::vector<Set> setsCache_;
|
std::vector<Set> setsCache_;
|
||||||
|
|||||||
@@ -3,18 +3,23 @@
|
|||||||
// DigiBattle99SetCompletionPanel: Set Completion tab — pack tiles with
|
// DigiBattle99SetCompletionPanel: Set Completion tab — pack tiles with
|
||||||
// progress bars for sets the user owns ≥1 card of, plus an in-tab checklist
|
// progress bars for sets the user owns ≥1 card of, plus an in-tab checklist
|
||||||
// drill-down (unowned rows greyed). Catalog is offline (set-catalog.json).
|
// drill-down (unowned rows greyed). Catalog is offline (set-catalog.json).
|
||||||
|
// Optional language filter restricts ownership to one language and labels
|
||||||
|
// set titles as "{setName} ({language})".
|
||||||
|
|
||||||
#include "ccm/domain/DigiBattle99Card.hpp"
|
#include "ccm/domain/DigiBattle99Card.hpp"
|
||||||
#include "ccm/domain/DigiBattle99SetCatalog.hpp"
|
#include "ccm/domain/DigiBattle99SetCatalog.hpp"
|
||||||
|
#include "ccm/domain/Enums.hpp"
|
||||||
#include "ccm/services/DigiBattle99SetCatalogService.hpp"
|
#include "ccm/services/DigiBattle99SetCatalogService.hpp"
|
||||||
#include "ccm/ui/Theme.hpp"
|
#include "ccm/ui/Theme.hpp"
|
||||||
|
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class wxBoxSizer;
|
class wxBoxSizer;
|
||||||
|
class wxChoice;
|
||||||
class wxListCtrl;
|
class wxListCtrl;
|
||||||
class wxScrolledWindow;
|
class wxScrolledWindow;
|
||||||
class wxSimplebook;
|
class wxSimplebook;
|
||||||
@@ -37,13 +42,19 @@ private:
|
|||||||
void rebuildChecklist(const std::string& setId);
|
void rebuildChecklist(const std::string& setId);
|
||||||
void setEmptyMessage(const wxString& message);
|
void setEmptyMessage(const wxString& message);
|
||||||
void clearGridTiles();
|
void clearGridTiles();
|
||||||
|
void refreshLanguageChoice();
|
||||||
|
void onLanguageChoice(wxCommandEvent& event);
|
||||||
|
void rebuildCurrentView();
|
||||||
|
[[nodiscard]] std::string displaySetName(const std::string& setName) const;
|
||||||
|
|
||||||
DigiBattle99SetCatalogService& catalogStore_;
|
DigiBattle99SetCatalogService& catalogStore_;
|
||||||
DigiBattle99SetCatalog catalog_;
|
DigiBattle99SetCatalog catalog_;
|
||||||
bool catalogLoaded_{false};
|
bool catalogLoaded_{false};
|
||||||
std::vector<DigiBattle99Card> collection_;
|
std::vector<DigiBattle99Card> collection_;
|
||||||
ThemePalette palette_{};
|
ThemePalette palette_{};
|
||||||
|
std::optional<Language> languageFilter_;
|
||||||
|
|
||||||
|
wxChoice* languageChoice_{nullptr};
|
||||||
wxSimplebook* book_{nullptr};
|
wxSimplebook* book_{nullptr};
|
||||||
wxPanel* gridPage_{nullptr};
|
wxPanel* gridPage_{nullptr};
|
||||||
wxScrolledWindow* scroll_{nullptr};
|
wxScrolledWindow* scroll_{nullptr};
|
||||||
@@ -54,6 +65,7 @@ private:
|
|||||||
wxStaticText* detailTitle_{nullptr};
|
wxStaticText* detailTitle_{nullptr};
|
||||||
wxListCtrl* checklist_{nullptr};
|
wxListCtrl* checklist_{nullptr};
|
||||||
std::string detailSetId_;
|
std::string detailSetId_;
|
||||||
|
std::string detailSetName_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ccm::ui
|
} // namespace ccm::ui
|
||||||
|
|||||||
@@ -10,10 +10,12 @@
|
|||||||
#include "ccm/ui/Theme.hpp"
|
#include "ccm/ui/Theme.hpp"
|
||||||
|
|
||||||
#include <wx/bmpbuttn.h>
|
#include <wx/bmpbuttn.h>
|
||||||
#include <wx/notebook.h>
|
#include <wx/dcclient.h>
|
||||||
#include <wx/panel.h>
|
#include <wx/panel.h>
|
||||||
|
#include <wx/simplebook.h>
|
||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
#include <wx/splitter.h>
|
#include <wx/splitter.h>
|
||||||
|
#include <wx/stattext.h>
|
||||||
#include <wx/textctrl.h>
|
#include <wx/textctrl.h>
|
||||||
#include <wx/window.h>
|
#include <wx/window.h>
|
||||||
|
|
||||||
@@ -24,6 +26,22 @@ namespace ccm::ui {
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr int kDigiToolbarIconPx = 18;
|
constexpr int kDigiToolbarIconPx = 18;
|
||||||
constexpr const char kDigiFilterHint[] = "Filter";
|
constexpr const char kDigiFilterHint[] = "Filter";
|
||||||
|
|
||||||
|
wxColour lighten(const wxColour& c, int amount) {
|
||||||
|
auto lift = [amount](unsigned char channel) -> unsigned char {
|
||||||
|
const int raised = static_cast<int>(channel) + amount;
|
||||||
|
return static_cast<unsigned char>(raised > 255 ? 255 : raised);
|
||||||
|
};
|
||||||
|
return wxColour(lift(c.Red()), lift(c.Green()), lift(c.Blue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
wxColour darken(const wxColour& c, int amount) {
|
||||||
|
auto drop = [amount](unsigned char channel) -> unsigned char {
|
||||||
|
const int lowered = static_cast<int>(channel) - amount;
|
||||||
|
return static_cast<unsigned char>(lowered < 0 ? 0 : lowered);
|
||||||
|
};
|
||||||
|
return wxColour(drop(c.Red()), drop(c.Green()), drop(c.Blue()));
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
DigiBattle99GameView::DigiBattle99GameView(ConfigService& config,
|
DigiBattle99GameView::DigiBattle99GameView(ConfigService& config,
|
||||||
@@ -128,28 +146,141 @@ void DigiBattle99GameView::refreshToolbarIcons(const ThemePalette& palette) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DigiBattle99GameView::selectTab(int index) {
|
||||||
|
if (index < 0 || index > 1 || book_ == nullptr) return;
|
||||||
|
activeTab_ = index;
|
||||||
|
book_->SetSelection(index);
|
||||||
|
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DigiBattle99GameView::refreshTabBarTheme(const ThemePalette& palette) {
|
||||||
|
if (tabBar_ == nullptr) return;
|
||||||
|
|
||||||
|
const wxColour barBg = palette.panelBg;
|
||||||
|
// Match toolbar button plate (Add/Edit/Delete), not a darker inset fill.
|
||||||
|
const wxColour tabBg = palette.buttonBg;
|
||||||
|
|
||||||
|
tabBar_->SetBackgroundColour(barBg);
|
||||||
|
tabBar_->SetOwnBackgroundColour(barBg);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
auto* tab = tabPanels_[i];
|
||||||
|
auto* label = tabLabels_[i];
|
||||||
|
if (tab == nullptr || label == nullptr) continue;
|
||||||
|
const bool selected = (i == activeTab_);
|
||||||
|
tab->SetBackgroundColour(tabBg);
|
||||||
|
tab->SetOwnBackgroundColour(tabBg);
|
||||||
|
// Keep the label plate identical to the tab fill so a late theme pass
|
||||||
|
// cannot leave a darker box around the caption.
|
||||||
|
label->SetBackgroundColour(tabBg);
|
||||||
|
label->SetOwnBackgroundColour(tabBg);
|
||||||
|
label->SetForegroundColour(palette.text);
|
||||||
|
label->SetOwnForegroundColour(palette.text);
|
||||||
|
wxFont font = label->GetFont();
|
||||||
|
font.SetWeight(selected ? wxFONTWEIGHT_BOLD : wxFONTWEIGHT_NORMAL);
|
||||||
|
label->SetFont(font);
|
||||||
|
tab->Refresh();
|
||||||
|
label->Refresh();
|
||||||
|
}
|
||||||
|
tabBar_->Layout();
|
||||||
|
tabBar_->Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DigiBattle99GameView::buildTabBar(wxWindow* parent, wxBoxSizer* rootSizer) {
|
||||||
|
tabBar_ = new wxPanel(parent, wxID_ANY);
|
||||||
|
tabBar_->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||||
|
auto* tabSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
tabSizer->AddSpacer(4);
|
||||||
|
|
||||||
|
const char* labels[2] = {"Single Cards", "Set Completion"};
|
||||||
|
for (int i = 0; i < 2; ++i) {
|
||||||
|
auto* tab = new wxPanel(tabBar_, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||||
|
tab->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||||
|
tab->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||||
|
auto* label = new wxStaticText(tab, wxID_ANY, wxString::FromUTF8(labels[i]));
|
||||||
|
auto* inner = new wxBoxSizer(wxVERTICAL);
|
||||||
|
// Compact padding so the strip stays short; frame is drawn in paint.
|
||||||
|
inner->Add(label, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 5);
|
||||||
|
tab->SetSizer(inner);
|
||||||
|
|
||||||
|
auto onClick = [this, i](wxMouseEvent&) { selectTab(i); };
|
||||||
|
tab->Bind(wxEVT_LEFT_DOWN, onClick);
|
||||||
|
label->Bind(wxEVT_LEFT_DOWN, onClick);
|
||||||
|
tab->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
|
||||||
|
tab->Bind(wxEVT_PAINT, [this, tab, i](wxPaintEvent&) {
|
||||||
|
wxPaintDC dc(tab);
|
||||||
|
const ThemePalette palette = paletteForTheme(config_.current().theme);
|
||||||
|
const bool dark = config_.current().theme == Theme::Dark;
|
||||||
|
const bool selected = (i == activeTab_);
|
||||||
|
// Same plate as toolbar bitmap buttons.
|
||||||
|
const wxColour bg = palette.buttonBg;
|
||||||
|
const wxColour frame =
|
||||||
|
dark ? lighten(palette.panelBg, 55) : darken(palette.panelBg, 45);
|
||||||
|
const wxColour frameSel = dark ? lighten(palette.panelBg, 85) : darken(palette.panelBg, 70);
|
||||||
|
const wxRect r = tab->GetClientRect();
|
||||||
|
dc.SetPen(wxPen(selected ? frameSel : frame, 1));
|
||||||
|
dc.SetBrush(wxBrush(bg));
|
||||||
|
dc.DrawRectangle(r.x, r.y, r.width, r.height);
|
||||||
|
if (selected) {
|
||||||
|
dc.SetPen(wxPen(palette.text, 2));
|
||||||
|
dc.DrawLine(r.GetLeft() + 4, r.GetBottom() - 1, r.GetRight() - 4,
|
||||||
|
r.GetBottom() - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tabPanels_[i] = tab;
|
||||||
|
tabLabels_[i] = label;
|
||||||
|
if (i > 0) tabSizer->AddSpacer(4);
|
||||||
|
tabSizer->Add(tab, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 3);
|
||||||
|
}
|
||||||
|
tabSizer->AddStretchSpacer(1);
|
||||||
|
|
||||||
|
tabBar_->Bind(wxEVT_PAINT, [this](wxPaintEvent&) {
|
||||||
|
wxPaintDC dc(tabBar_);
|
||||||
|
const ThemePalette palette = paletteForTheme(config_.current().theme);
|
||||||
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||||
|
dc.SetBrush(wxBrush(palette.panelBg));
|
||||||
|
dc.DrawRectangle(tabBar_->GetClientRect());
|
||||||
|
dc.SetPen(wxPen(darken(palette.text, 120), 1));
|
||||||
|
const wxRect r = tabBar_->GetClientRect();
|
||||||
|
dc.DrawLine(r.GetLeft(), r.GetBottom(), r.GetRight(), r.GetBottom());
|
||||||
|
});
|
||||||
|
tabBar_->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
|
||||||
|
|
||||||
|
tabBar_->SetSizer(tabSizer);
|
||||||
|
rootSizer->Add(tabBar_, 0, wxEXPAND);
|
||||||
|
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||||
|
}
|
||||||
|
|
||||||
wxPanel* DigiBattle99GameView::contentPanel(wxWindow* parent) {
|
wxPanel* DigiBattle99GameView::contentPanel(wxWindow* parent) {
|
||||||
if (contentPanel_ == nullptr) {
|
if (contentPanel_ == nullptr) {
|
||||||
contentPanel_ = new wxPanel(parent);
|
contentPanel_ = new wxPanel(parent);
|
||||||
auto* root = new wxBoxSizer(wxVERTICAL);
|
auto* root = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
notebook_ = new wxNotebook(contentPanel_, wxID_ANY);
|
buildTabBar(contentPanel_, root);
|
||||||
auto* singlePage = new wxPanel(notebook_);
|
|
||||||
|
book_ = new wxSimplebook(contentPanel_, wxID_ANY);
|
||||||
|
auto* singlePage = new wxPanel(book_);
|
||||||
auto* singleSizer = new wxBoxSizer(wxVERTICAL);
|
auto* singleSizer = new wxBoxSizer(wxVERTICAL);
|
||||||
buildSingleCardsToolbar(singlePage, singleSizer);
|
buildSingleCardsToolbar(singlePage, singleSizer);
|
||||||
ensureSingleCardsMounted(singlePage);
|
ensureSingleCardsMounted(singlePage);
|
||||||
singleSizer->Add(singleSplitter_, 1, wxEXPAND);
|
singleSizer->Add(singleSplitter_, 1, wxEXPAND);
|
||||||
singlePage->SetSizer(singleSizer);
|
singlePage->SetSizer(singleSizer);
|
||||||
notebook_->AddPage(singlePage, "Single Cards");
|
book_->AddPage(singlePage, "Single Cards");
|
||||||
|
|
||||||
setCompletionPanel_ = new DigiBattle99SetCompletionPanel(notebook_, catalogStore_);
|
setCompletionPanel_ = new DigiBattle99SetCompletionPanel(book_, catalogStore_);
|
||||||
setCompletionPanel_->reloadFromStore();
|
setCompletionPanel_->reloadFromStore();
|
||||||
notebook_->AddPage(setCompletionPanel_, "Set Completion");
|
book_->AddPage(setCompletionPanel_, "Set Completion");
|
||||||
|
|
||||||
root->Add(notebook_, 1, wxEXPAND);
|
root->Add(book_, 1, wxEXPAND | wxTOP, 5);
|
||||||
contentPanel_->SetSizer(root);
|
contentPanel_->SetSizer(root);
|
||||||
|
|
||||||
|
selectTab(0);
|
||||||
refreshToolbarIcons(paletteForTheme(config_.current().theme));
|
refreshToolbarIcons(paletteForTheme(config_.current().theme));
|
||||||
|
// First mount: re-assert tab plate colors after the initial layout paint.
|
||||||
|
contentPanel_->CallAfter([this]() {
|
||||||
|
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return contentPanel_;
|
return contentPanel_;
|
||||||
}
|
}
|
||||||
@@ -374,6 +505,7 @@ void DigiBattle99GameView::applyTheme(const ThemePalette& palette) {
|
|||||||
if (selectedPanel_) selectedPanel_->applyTheme(palette);
|
if (selectedPanel_) selectedPanel_->applyTheme(palette);
|
||||||
if (setCompletionPanel_) setCompletionPanel_->applyTheme(palette);
|
if (setCompletionPanel_) setCompletionPanel_->applyTheme(palette);
|
||||||
refreshToolbarIcons(palette);
|
refreshToolbarIcons(palette);
|
||||||
|
refreshTabBarTheme(palette);
|
||||||
if (filterInput_ != nullptr) {
|
if (filterInput_ != nullptr) {
|
||||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||||
filterInput_->SetForegroundColour(palette.inputText);
|
filterInput_->SetForegroundColour(palette.inputText);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "ccm/services/DigiBattle99SetCompletion.hpp"
|
#include "ccm/services/DigiBattle99SetCompletion.hpp"
|
||||||
|
|
||||||
#include <wx/button.h>
|
#include <wx/button.h>
|
||||||
|
#include <wx/choice.h>
|
||||||
#include <wx/cursor.h>
|
#include <wx/cursor.h>
|
||||||
#include <wx/gauge.h>
|
#include <wx/gauge.h>
|
||||||
#include <wx/listctrl.h>
|
#include <wx/listctrl.h>
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
#include <wx/sizer.h>
|
#include <wx/sizer.h>
|
||||||
#include <wx/stattext.h>
|
#include <wx/stattext.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace ccm::ui {
|
namespace ccm::ui {
|
||||||
@@ -34,6 +36,16 @@ DigiBattle99SetCompletionPanel::DigiBattle99SetCompletionPanel(
|
|||||||
: wxPanel(parent), catalogStore_(catalogStore) {
|
: wxPanel(parent), catalogStore_(catalogStore) {
|
||||||
palette_ = paletteForTheme(inferThemeFromWindow(this));
|
palette_ = paletteForTheme(inferThemeFromWindow(this));
|
||||||
|
|
||||||
|
auto* langRow = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
auto* langLabel = new wxStaticText(this, wxID_ANY, "Language");
|
||||||
|
languageChoice_ = new wxChoice(this, wxID_ANY);
|
||||||
|
languageChoice_->Append("All languages");
|
||||||
|
languageChoice_->SetSelection(0);
|
||||||
|
languageChoice_->Bind(wxEVT_CHOICE, &DigiBattle99SetCompletionPanel::onLanguageChoice,
|
||||||
|
this);
|
||||||
|
langRow->Add(langLabel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
|
||||||
|
langRow->Add(languageChoice_, 0, wxALIGN_CENTER_VERTICAL);
|
||||||
|
|
||||||
book_ = new wxSimplebook(this, wxID_ANY);
|
book_ = new wxSimplebook(this, wxID_ANY);
|
||||||
|
|
||||||
gridPage_ = new wxPanel(book_);
|
gridPage_ = new wxPanel(book_);
|
||||||
@@ -72,6 +84,7 @@ DigiBattle99SetCompletionPanel::DigiBattle99SetCompletionPanel(
|
|||||||
book_->AddPage(detailPage_, "Detail");
|
book_->AddPage(detailPage_, "Detail");
|
||||||
|
|
||||||
auto* root = new wxBoxSizer(wxVERTICAL);
|
auto* root = new wxBoxSizer(wxVERTICAL);
|
||||||
|
root->Add(langRow, 0, wxEXPAND | wxALL, 8);
|
||||||
root->Add(book_, 1, wxEXPAND);
|
root->Add(book_, 1, wxEXPAND);
|
||||||
SetSizer(root);
|
SetSizer(root);
|
||||||
|
|
||||||
@@ -80,11 +93,8 @@ DigiBattle99SetCompletionPanel::DigiBattle99SetCompletionPanel(
|
|||||||
|
|
||||||
void DigiBattle99SetCompletionPanel::setCollection(std::vector<DigiBattle99Card> cards) {
|
void DigiBattle99SetCompletionPanel::setCollection(std::vector<DigiBattle99Card> cards) {
|
||||||
collection_ = std::move(cards);
|
collection_ = std::move(cards);
|
||||||
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
refreshLanguageChoice();
|
||||||
rebuildChecklist(detailSetId_);
|
rebuildCurrentView();
|
||||||
} else {
|
|
||||||
rebuildGrid();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DigiBattle99SetCompletionPanel::reloadFromStore() {
|
void DigiBattle99SetCompletionPanel::reloadFromStore() {
|
||||||
@@ -103,26 +113,94 @@ void DigiBattle99SetCompletionPanel::reloadFromStore() {
|
|||||||
void DigiBattle99SetCompletionPanel::applyTheme(const ThemePalette& palette) {
|
void DigiBattle99SetCompletionPanel::applyTheme(const ThemePalette& palette) {
|
||||||
palette_ = palette;
|
palette_ = palette;
|
||||||
applyThemeToWindowTree(this, palette, inferThemeFromWindow(this));
|
applyThemeToWindowTree(this, palette, inferThemeFromWindow(this));
|
||||||
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
rebuildCurrentView();
|
||||||
rebuildChecklist(detailSetId_);
|
|
||||||
} else {
|
|
||||||
rebuildGrid();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DigiBattle99SetCompletionPanel::showGridPage() {
|
void DigiBattle99SetCompletionPanel::showGridPage() {
|
||||||
detailSetId_.clear();
|
detailSetId_.clear();
|
||||||
|
detailSetName_.clear();
|
||||||
book_->SetSelection(0);
|
book_->SetSelection(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DigiBattle99SetCompletionPanel::showChecklistPage(const std::string& setId,
|
void DigiBattle99SetCompletionPanel::showChecklistPage(const std::string& setId,
|
||||||
const std::string& setName) {
|
const std::string& setName) {
|
||||||
detailSetId_ = setId;
|
detailSetId_ = setId;
|
||||||
detailTitle_->SetLabelText(wxString::FromUTF8(setName.c_str()));
|
detailSetName_ = setName;
|
||||||
|
detailTitle_->SetLabelText(wxString::FromUTF8(displaySetName(setName).c_str()));
|
||||||
rebuildChecklist(setId);
|
rebuildChecklist(setId);
|
||||||
book_->SetSelection(1);
|
book_->SetSelection(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string DigiBattle99SetCompletionPanel::displaySetName(const std::string& setName) const {
|
||||||
|
if (!languageFilter_.has_value()) return setName;
|
||||||
|
return setName + " (" + std::string(to_string(*languageFilter_)) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
void DigiBattle99SetCompletionPanel::refreshLanguageChoice() {
|
||||||
|
const auto previous = languageFilter_;
|
||||||
|
const auto present = digiBattle99LanguagesInCollection(collection_);
|
||||||
|
|
||||||
|
languageChoice_->Clear();
|
||||||
|
languageChoice_->Append("All languages");
|
||||||
|
for (const Language lang : present) {
|
||||||
|
languageChoice_->Append(wxString::FromUTF8(std::string(to_string(lang)).c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int selection = 0;
|
||||||
|
languageFilter_ = std::nullopt;
|
||||||
|
if (previous.has_value()) {
|
||||||
|
for (std::size_t i = 0; i < present.size(); ++i) {
|
||||||
|
if (present[i] == *previous) {
|
||||||
|
selection = static_cast<int>(i + 1);
|
||||||
|
languageFilter_ = previous;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
languageChoice_->SetSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DigiBattle99SetCompletionPanel::onLanguageChoice(wxCommandEvent& /*event*/) {
|
||||||
|
const int sel = languageChoice_->GetSelection();
|
||||||
|
if (sel <= 0) {
|
||||||
|
languageFilter_ = std::nullopt;
|
||||||
|
} else {
|
||||||
|
const auto present = digiBattle99LanguagesInCollection(collection_);
|
||||||
|
const auto idx = static_cast<std::size_t>(sel - 1);
|
||||||
|
if (idx < present.size()) {
|
||||||
|
languageFilter_ = present[idx];
|
||||||
|
} else {
|
||||||
|
languageFilter_ = std::nullopt;
|
||||||
|
languageChoice_->SetSelection(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rebuildCurrentView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DigiBattle99SetCompletionPanel::rebuildCurrentView() {
|
||||||
|
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
||||||
|
const auto rows =
|
||||||
|
computeDigiBattle99SetCompletion(collection_, catalog_, languageFilter_);
|
||||||
|
bool stillVisible = false;
|
||||||
|
for (const auto& row : rows) {
|
||||||
|
if (row.setId == detailSetId_) {
|
||||||
|
stillVisible = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!stillVisible) {
|
||||||
|
showGridPage();
|
||||||
|
rebuildGrid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detailTitle_->SetLabelText(
|
||||||
|
wxString::FromUTF8(displaySetName(detailSetName_).c_str()));
|
||||||
|
rebuildChecklist(detailSetId_);
|
||||||
|
} else {
|
||||||
|
rebuildGrid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DigiBattle99SetCompletionPanel::setEmptyMessage(const wxString& message) {
|
void DigiBattle99SetCompletionPanel::setEmptyMessage(const wxString& message) {
|
||||||
clearGridTiles();
|
clearGridTiles();
|
||||||
emptyLabel_->SetLabelText(message);
|
emptyLabel_->SetLabelText(message);
|
||||||
@@ -145,7 +223,8 @@ void DigiBattle99SetCompletionPanel::rebuildGrid() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto rows = computeDigiBattle99SetCompletion(collection_, catalog_);
|
const auto rows =
|
||||||
|
computeDigiBattle99SetCompletion(collection_, catalog_, languageFilter_);
|
||||||
if (rows.empty()) {
|
if (rows.empty()) {
|
||||||
setEmptyMessage(wxString::FromUTF8(
|
setEmptyMessage(wxString::FromUTF8(
|
||||||
"No Digimon (Digi-Battle) sets in progress yet.\n"
|
"No Digimon (Digi-Battle) sets in progress yet.\n"
|
||||||
@@ -163,7 +242,8 @@ void DigiBattle99SetCompletionPanel::rebuildGrid() {
|
|||||||
tile->SetBackgroundColour(palette_.panelBg);
|
tile->SetBackgroundColour(palette_.panelBg);
|
||||||
auto* tileSizer = new wxBoxSizer(wxVERTICAL);
|
auto* tileSizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
|
||||||
auto* nameLbl = new wxStaticText(tile, wxID_ANY, wxString::FromUTF8(row.setName.c_str()));
|
const std::string title = displaySetName(row.setName);
|
||||||
|
auto* nameLbl = new wxStaticText(tile, wxID_ANY, wxString::FromUTF8(title.c_str()));
|
||||||
auto nameFont = nameLbl->GetFont();
|
auto nameFont = nameLbl->GetFont();
|
||||||
nameFont.MakeBold();
|
nameFont.MakeBold();
|
||||||
nameLbl->SetFont(nameFont);
|
nameLbl->SetFont(nameFont);
|
||||||
@@ -207,16 +287,21 @@ void DigiBattle99SetCompletionPanel::rebuildGrid() {
|
|||||||
|
|
||||||
void DigiBattle99SetCompletionPanel::rebuildChecklist(const std::string& setId) {
|
void DigiBattle99SetCompletionPanel::rebuildChecklist(const std::string& setId) {
|
||||||
checklist_->DeleteAllItems();
|
checklist_->DeleteAllItems();
|
||||||
const auto entries = digiBattle99ChecklistForSet(collection_, catalog_, setId);
|
const auto entries =
|
||||||
|
digiBattle99ChecklistForSet(collection_, catalog_, setId, languageFilter_);
|
||||||
const wxColour muted = mutedTextColour(palette_);
|
const wxColour muted = mutedTextColour(palette_);
|
||||||
|
// Fixed green so owned checkmarks stay readable in both light and dark themes.
|
||||||
|
const wxColour ownedGreen(46, 160, 67);
|
||||||
|
|
||||||
long idx = 0;
|
long idx = 0;
|
||||||
for (const auto& entry : entries) {
|
for (const auto& entry : entries) {
|
||||||
const std::string line = entry.setNo + " — " + entry.name;
|
// Align names: checkmark + two spaces vs four spaces for missing cards.
|
||||||
|
const std::string line =
|
||||||
|
(entry.owned ? "✓ " : " ") + entry.setNo + " — " + entry.name;
|
||||||
const long row = checklist_->InsertItem(idx++, wxString::FromUTF8(line.c_str()));
|
const long row = checklist_->InsertItem(idx++, wxString::FromUTF8(line.c_str()));
|
||||||
if (row < 0) continue;
|
if (row < 0) continue;
|
||||||
if (entry.owned) {
|
if (entry.owned) {
|
||||||
checklist_->SetItemTextColour(row, palette_.text);
|
checklist_->SetItemTextColour(row, ownedGreen);
|
||||||
} else {
|
} else {
|
||||||
checklist_->SetItemTextColour(row, muted);
|
checklist_->SetItemTextColour(row, muted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,8 +239,10 @@ void MainFrame::mountActiveView() {
|
|||||||
hostSizer->Add(custom, 1, wxEXPAND);
|
hostSizer->Add(custom, 1, wxEXPAND);
|
||||||
contentHost_->Layout();
|
contentHost_->Layout();
|
||||||
|
|
||||||
|
// Digimon (and other hostsOwnLayout views) apply their own tree theme
|
||||||
|
// and then restore tab-strip colors; a follow-up applyThemeToWindowTree
|
||||||
|
// here would reset tab labels to panelBg and leave a dark box around text.
|
||||||
view->applyTheme(palette);
|
view->applyTheme(palette);
|
||||||
applyThemeToWindowTree(custom, palette, ctx_.config.current().theme);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user