mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 17:01:02 +00:00
Minor: New Game Yu-Gi-Oh! Bandai (#22)
This commit is contained in:
+3
-2
@@ -16,6 +16,7 @@
|
||||
- `include/ccm/ui/Pokemon*.hpp` + `src/Pokemon*.cpp` — Pokemon implementations: `PokemonCardListPanel`, `PokemonSelectedCardPanel`, `PokemonCardEditDialog`, `PokemonGameView`, `PokemonSetCompletionPanel`. Same Add/Edit shape as Magic for the card form; the game view hosts **Single Cards | Set Completion** via `contentPanel` / `hostsOwnLayout` (like Digimon/Yu-Gi-Oh!). Catalog from `PokemonSetCatalogService` (`set-catalog-west.json` / `set-catalog-asia.json`), filled on Update Pokemon. The Add/Edit/Delete + filter toolbar lives inside the Single Cards tab; MainFrame hides its shared toolbar while Pokemon is active.
|
||||
- `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/YuGiOh*.hpp` + `src/YuGiOh*.cpp` — Yu-Gi-Oh!: list/selected/edit plus `YuGiOhGameView` notebook (**Single Cards** | **Set Completion**) via the same `hostsOwnLayout` / `contentPanel` pattern as Digimon, and `YuGiOhSetCompletionPanel`. Catalog from `YuGiOhSetCatalogService` (`yugioh/set-catalog.json`), filled on Update Sets from YGOPRODeck `cardinfo.php`.
|
||||
- `include/ccm/ui/YuGiOhBandai*.hpp` + `src/YuGiOhBandai*.cpp` — Yu-Gi-Oh! (Bandai): same notebook layout as Digimon/YGO; dual auto-detect (name or Bandai number) via Yugipedia SMW ask; catalog from `YuGiOhBandaiSetCatalogService` (`yugiohbandai/set-catalog.json`).
|
||||
- `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.
|
||||
- `include/ccm/ui/SettingsDialog.hpp` + `src/SettingsDialog.cpp` — edits `Configuration` via `ConfigService::store`.
|
||||
@@ -61,7 +62,7 @@
|
||||
- Center popup dialogs on the app window (`CentreOnParent()`) so confirmations/info boxes open relative to the current app window.
|
||||
- Include `wxSpinCtrl` in themed input controls (Amount field) or it will keep a mismatched native background.
|
||||
- Do not call `applyNativeClassTheme(..., "DarkMode_Explorer", "Explorer")` for `wxTextCtrl`; on some Windows builds this causes black typed text in dark mode. Keep text inputs palette-driven (`SetThemeEnabled(false)` in dark/high-contrast as needed).
|
||||
- If a specific text field still renders wrong while typing (notably `MainFrame`'s filter box), enforce text/background in `MainFrame::MSWWindowProc` via `WM_CTLCOLOREDIT` for that control handle.
|
||||
- Text inputs are hardened in `Theme.cpp` via `applyPaletteToTextCtrl` / `hardenTextCtrlNativeTheme`: opt the EDIT HWND out of immersive dark mode, clear its visual style, and subclass the **parent** to answer `WM_CTLCOLOREDIT` (that message goes to the parent, not the frame — an earlier frame-level handler never ran for the toolbar filter).
|
||||
- Keep toolbar button behavior stable under dark/high-contrast: avoid changes that break click/tooltip affordances while experimenting with hover contrast fixes.
|
||||
- For dark/high-contrast button readability, do not trust native hover/pressed rendering on Windows; custom state painting in `Theme.cpp` is allowed when native visuals ignore configured colors.
|
||||
- Button event handlers must use per-button state that is refreshed when theme changes. Avoid one-time captures of theme colors/mode in lambdas; these can leak dark-mode behavior into light mode.
|
||||
@@ -90,7 +91,7 @@
|
||||
1. Implement three derived classes under `include/ccm/ui/` mirroring the Magic / Pokemon trio:
|
||||
- `<Name>CardListPanel : public BaseCardListPanel<<Name>Card, <Name>SortColumn>` — override `declareTextColumns()`, `declareIconColumns()`, `renderTextCell()`, `isIconColumnSet()`, `sortBy()`, `matchesFilter()`.
|
||||
- `<Name>SelectedCardPanel : public BaseSelectedCardPanel<<Name>Card>` — override `declareDetailRows()`, `declareFlagIcons()`, `detailValueFor()`, `isFlagSet()`, `previewKey()`, `gameId()`. Define a local `enum` of `DetailKey` constants for clarity.
|
||||
- `<Name>CardEditDialog : public BaseCardEditDialog<<Name>Card>` — override `buildFlagsRow()`, optionally `appendExtraRows()`, `readExtraFromCard()`, `writeExtraToCard()`, `updateMenuName()`.
|
||||
- `<Name>CardEditDialog : public BaseCardEditDialog<<Name>Card>` — override `buildFlagsRow()`, optionally `appendExtraRows()`, `readExtraFromCard()`, `writeExtraToCard()`, `updateMenuName()`, and optionally `validateExtraFields()` (Bandai requires set number).
|
||||
2. Add a `<Name>GameView : public IGameView` that owns those panels and the typed `CollectionService<<Name>Card>&`. Bind `EVT_CARD_SELECTED` on the list panel inside `listPanel(parent)` to push the typed selection into the selected panel. The `MagicGameView` / `PokemonGameView` pair is the canonical reference.
|
||||
3. Re-add the new view to `AppContext::gameViews` in the composition root (`app/main.cpp`). The `Game` and `Sets` menus pick it up automatically.
|
||||
4. Add SVG glyphs for any new flag columns to `SvgIcons.{hpp,cpp}` (with the `@FILL@` placeholder).
|
||||
|
||||
@@ -26,6 +26,11 @@ add_library(ccm_ui_wx STATIC
|
||||
src/DigiBattle99CardEditDialog.cpp
|
||||
src/DigiBattle99GameView.cpp
|
||||
src/DigiBattle99SetCompletionPanel.cpp
|
||||
src/YuGiOhBandaiCardListPanel.cpp
|
||||
src/YuGiOhBandaiSelectedCardPanel.cpp
|
||||
src/YuGiOhBandaiCardEditDialog.cpp
|
||||
src/YuGiOhBandaiGameView.cpp
|
||||
src/YuGiOhBandaiSetCompletionPanel.cpp
|
||||
|
||||
src/SettingsDialog.cpp
|
||||
src/SwitchCtrl.cpp
|
||||
@@ -61,8 +66,10 @@ target_link_libraries(ccm_ui_wx
|
||||
# the per-row flag glyphs with proper transparency. Without msimg32 linked
|
||||
# explicitly the linker fails on `AlphaBlend@44` even though gdi32 is pulled
|
||||
# in transitively by wxWidgets.
|
||||
# Theme.cpp subclasses EDIT parents via SetWindowSubclass / DefSubclassProc /
|
||||
# RemoveWindowSubclass (comctl32); those symbols are not pulled in by wx alone.
|
||||
if (WIN32)
|
||||
target_link_libraries(ccm_ui_wx PRIVATE msimg32)
|
||||
target_link_libraries(ccm_ui_wx PRIVATE msimg32 comctl32)
|
||||
endif()
|
||||
|
||||
target_compile_features(ccm_ui_wx PUBLIC cxx_std_20)
|
||||
|
||||
@@ -27,6 +27,7 @@ struct AppContext {
|
||||
IGameModule& pokemonModule;
|
||||
IGameModule& yuGiOhModule;
|
||||
IGameModule& digiBattle99Module;
|
||||
IGameModule& yuGiOhBandaiModule;
|
||||
// Asia Pokemon sets/preview backend (not a separate Game menu entry).
|
||||
IGameModule& japanesePokemonModule;
|
||||
// Active per-game UI bundles. The order is the order shown in the
|
||||
|
||||
@@ -136,6 +136,10 @@ protected:
|
||||
// controls cannot outlive the lookup identity.
|
||||
virtual void onCardLookupContextChanged() {}
|
||||
|
||||
// Extra validation after name/set checks and writeFromControls(). Return
|
||||
// false to block OK (subclass should show its own themed dialog).
|
||||
[[nodiscard]] virtual bool validateExtraFields() { return true; }
|
||||
|
||||
// Common helpers ----------------------------------------------------------
|
||||
|
||||
void appendRow(wxFlexGridSizer* grid, const wxString& label, wxWindow* ctrl) {
|
||||
@@ -148,6 +152,8 @@ protected:
|
||||
[[nodiscard]] const TCard& constCard() const noexcept { return card_; }
|
||||
void syncCardFromControls() { writeFromControls(); }
|
||||
[[nodiscard]] wxComboBox* setComboControl() const noexcept { return setCombo_; }
|
||||
[[nodiscard]] wxTextCtrl* nameControl() const noexcept { return nameCtrl_; }
|
||||
[[nodiscard]] wxChoice* languageChoiceControl() const noexcept { return languageChoice_; }
|
||||
|
||||
[[nodiscard]] const Set* selectedSetFromControls() const {
|
||||
const auto& available = availableSets();
|
||||
@@ -471,6 +477,7 @@ private:
|
||||
"Add card", wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (!validateExtraFields()) return;
|
||||
if (mode_ == EditMode::Edit && !(card_ == openingSnapshot_)) {
|
||||
if (showThemedConfirmDialog(
|
||||
this,
|
||||
|
||||
@@ -307,6 +307,8 @@ private:
|
||||
case Game::DigiBattle99:
|
||||
// No stable public Digi-Battle back URL; UI uses bundled PNG.
|
||||
return {};
|
||||
case Game::YuGiOhBandai:
|
||||
return "https://ms.yugipedia.com//3/34/Back-BAN-JP-1999.png";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@ private:
|
||||
|
||||
[[nodiscard]] IGameView* activeView();
|
||||
|
||||
#ifdef __WXMSW__
|
||||
WXLRESULT MSWWindowProc(WXUINT message, WXWPARAM wParam, WXLPARAM lParam) override;
|
||||
#endif
|
||||
|
||||
AppContext& ctx_;
|
||||
Game activeGame_{Game::Magic};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
class wxDialog;
|
||||
class wxWindow;
|
||||
class wxString;
|
||||
class wxTextCtrl;
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
@@ -23,6 +24,8 @@ struct ThemePalette {
|
||||
ThemePalette paletteForTheme(Theme theme);
|
||||
Theme inferThemeFromWindow(const wxWindow* window);
|
||||
void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme theme);
|
||||
// Force palette colors onto a text input (incl. MSW dark-mode typed-text fix).
|
||||
void applyPaletteToTextCtrl(wxTextCtrl* text, const ThemePalette& palette, Theme theme);
|
||||
void themeModalDialog(wxDialog* dlg, Theme theme);
|
||||
int showThemedMessageDialog(wxWindow* parent, const wxString& message, const wxString& caption, long style);
|
||||
int showThemedConfirmDialog(wxWindow* parent, const wxString& message, const wxString& caption);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
#include "ccm/ui/BaseCardEditDialog.hpp"
|
||||
#include <wx/button.h>
|
||||
#include <wx/checkbox.h>
|
||||
#include <wx/choice.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhBandaiCardEditDialog final : public BaseCardEditDialog<YuGiOhBandaiCard> {
|
||||
public:
|
||||
YuGiOhBandaiCardEditDialog(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
SetService& setService,
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
YuGiOhBandaiCard initial,
|
||||
const std::vector<Set>* preloadedSets = nullptr);
|
||||
~YuGiOhBandaiCardEditDialog() override;
|
||||
|
||||
protected:
|
||||
void buildFlagsRow(wxBoxSizer* flagsBox) override;
|
||||
void appendExtraRows(wxFlexGridSizer* grid) override;
|
||||
void readExtraFromCard() override;
|
||||
void writeExtraToCard() override;
|
||||
[[nodiscard]] std::span<const Language> languagesForChoice() const override;
|
||||
[[nodiscard]] std::string updateMenuName() const override {
|
||||
return "Update Yu-Gi-Oh! (Bandai)";
|
||||
}
|
||||
void onCardLookupContextChanged() override;
|
||||
[[nodiscard]] bool validateExtraFields() override;
|
||||
|
||||
private:
|
||||
struct VariantFetchState {
|
||||
std::atomic<bool> alive{true};
|
||||
};
|
||||
|
||||
void onAutoDetectBySetNo(wxCommandEvent&);
|
||||
void onNextSetNo(wxCommandEvent&);
|
||||
void onAutoDetectByName(wxCommandEvent&);
|
||||
void onRarityChoiceChanged(wxCommandEvent&);
|
||||
void onSetSelectionChanged(wxCommandEvent&);
|
||||
|
||||
void scheduleDeferredVariantPrefetch();
|
||||
void prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch);
|
||||
void requestByNameAsync(unsigned capturedEpoch, std::string name, std::string setId,
|
||||
bool showFailureDialog);
|
||||
void requestByNoAsync(unsigned capturedEpoch, std::string setNo, bool showFailureDialog);
|
||||
void applyDetectedList(unsigned capturedEpoch,
|
||||
Result<std::vector<AutoDetectedPrint>> detected,
|
||||
bool showFailureDialog, bool applyFirst);
|
||||
void clearCachedPrintVariants();
|
||||
void applyDetectedPrint(const AutoDetectedPrint& print);
|
||||
void applyRarityStringToChoice(const std::string& rarity);
|
||||
void maybeAutoCheckHoloForRarity(const std::string& rarity);
|
||||
void refreshVariantNextControls();
|
||||
[[nodiscard]] std::size_t findAvailableSetIndex(const std::string& setId) const;
|
||||
|
||||
EditMode dialogMode_;
|
||||
unsigned variantFetchEpoch_{0};
|
||||
CardPreviewService& cardPreview_;
|
||||
std::shared_ptr<VariantFetchState> variantFetchState_;
|
||||
|
||||
wxTextCtrl* setNoCtrl_{nullptr};
|
||||
wxButton* autoSetNoBtn_{nullptr};
|
||||
wxButton* nextSetNoBtn_{nullptr};
|
||||
wxChoice* rarityChoice_{nullptr};
|
||||
wxButton* autoRarityBtn_{nullptr};
|
||||
wxCheckBox* holoCheck_{nullptr};
|
||||
wxCheckBox* signedCheck_{nullptr};
|
||||
wxCheckBox* alteredCheck_{nullptr};
|
||||
|
||||
std::vector<AutoDetectedPrint> cachedVariants_;
|
||||
std::size_t variantRingPos_{0};
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/services/CardSorter.hpp"
|
||||
#include "ccm/ui/BaseCardListPanel.hpp"
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhBandaiCardListPanel final
|
||||
: public BaseCardListPanel<YuGiOhBandaiCard, YuGiOhBandaiSortColumn> {
|
||||
public:
|
||||
explicit YuGiOhBandaiCardListPanel(wxWindow* parent);
|
||||
|
||||
protected:
|
||||
[[nodiscard]] std::vector<TextColumnSpec> declareTextColumns() const override;
|
||||
[[nodiscard]] std::vector<IconColumnSpec> declareIconColumns() const override;
|
||||
[[nodiscard]] std::string renderTextCell(const YuGiOhBandaiCard& card,
|
||||
std::size_t idx) const override;
|
||||
[[nodiscard]] bool isIconColumnSet(const YuGiOhBandaiCard& card,
|
||||
std::size_t idx) const override;
|
||||
void sortBy(YuGiOhBandaiSortColumn column, bool ascending) override;
|
||||
[[nodiscard]] bool matchesFilter(const YuGiOhBandaiCard& card,
|
||||
std::string_view filter) const override;
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
#include "ccm/services/CollectionService.hpp"
|
||||
#include "ccm/services/ConfigService.hpp"
|
||||
#include "ccm/services/ImageService.hpp"
|
||||
#include "ccm/services/SetService.hpp"
|
||||
#include "ccm/services/YuGiOhBandaiSetCatalogService.hpp"
|
||||
#include "ccm/ui/IGameView.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
class wxBitmapButton;
|
||||
class wxBoxSizer;
|
||||
class wxPanel;
|
||||
class wxSimplebook;
|
||||
class wxSplitterWindow;
|
||||
class wxStaticText;
|
||||
class wxTextCtrl;
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhBandaiCardListPanel;
|
||||
class YuGiOhBandaiSelectedCardPanel;
|
||||
class YuGiOhBandaiSetCompletionPanel;
|
||||
|
||||
class YuGiOhBandaiGameView final : public IGameView {
|
||||
public:
|
||||
YuGiOhBandaiGameView(ConfigService& config,
|
||||
CollectionService<YuGiOhBandaiCard>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module,
|
||||
YuGiOhBandaiSetCatalogService& catalogStore);
|
||||
|
||||
[[nodiscard]] Game gameId() const noexcept override { return Game::YuGiOhBandai; }
|
||||
[[nodiscard]] std::string displayName() const override { return "Yu-Gi-Oh! (Bandai)"; }
|
||||
|
||||
wxPanel* listPanel(wxWindow* parent) override;
|
||||
wxPanel* selectedPanel(wxWindow* parent) override;
|
||||
wxPanel* contentPanel(wxWindow* parent) override;
|
||||
[[nodiscard]] wxPanel* contentPanelIfCreated() const noexcept override {
|
||||
return contentPanel_;
|
||||
}
|
||||
[[nodiscard]] bool hostsOwnLayout() const noexcept override { return true; }
|
||||
|
||||
void refreshCollection(std::optional<std::uint32_t> selectId = std::nullopt) override;
|
||||
void onAddCard(wxWindow* parentWindow) override;
|
||||
void onEditCard(wxWindow* parentWindow) override;
|
||||
void onDeleteCard(wxWindow* parentWindow) override;
|
||||
std::string onUpdateSets(wxWindow* parentWindow) override;
|
||||
void setFilter(std::string_view filter) override;
|
||||
void nudgeSelection(int delta) override;
|
||||
void applyTheme(const ThemePalette& palette) override;
|
||||
[[nodiscard]] std::string updateSetsMenuLabel() const override {
|
||||
return "Update Yu-Gi-Oh! (Bandai)";
|
||||
}
|
||||
|
||||
private:
|
||||
void ensureSetsLoaded();
|
||||
// Fetches sets + checklist catalog from Yugipedia and persists both.
|
||||
// Returns false on failure (error dialogs already shown).
|
||||
[[nodiscard]] bool downloadSetsAndCatalog(wxWindow* parentWindow,
|
||||
std::size_t* setCountOut = nullptr,
|
||||
std::size_t* packCountOut = nullptr);
|
||||
// Fetches sets + checklist catalog when set-catalog.json is missing.
|
||||
// Returns true if the catalog exists afterward. Shows error dialogs on failure.
|
||||
[[nodiscard]] bool ensureCatalogLoaded(wxWindow* parentWindow);
|
||||
void refreshSetCompletionFromStore();
|
||||
const std::vector<Set>& setsForDialog();
|
||||
void ensureSingleCardsMounted(wxWindow* splitterParent);
|
||||
void buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer);
|
||||
void buildTabBar(wxWindow* parent, wxBoxSizer* rootSizer);
|
||||
void selectTab(int index);
|
||||
void refreshToolbarIcons(const ThemePalette& palette);
|
||||
void refreshTabBarTheme(const ThemePalette& palette);
|
||||
|
||||
ConfigService& config_;
|
||||
CollectionService<YuGiOhBandaiCard>& collection_;
|
||||
SetService& sets_;
|
||||
ImageService& images_;
|
||||
CardPreviewService& cardPreview_;
|
||||
IGameModule& module_;
|
||||
YuGiOhBandaiSetCatalogService& catalogStore_;
|
||||
|
||||
wxPanel* contentPanel_{nullptr};
|
||||
wxPanel* tabBar_{nullptr};
|
||||
wxSimplebook* book_{nullptr};
|
||||
wxSplitterWindow* singleSplitter_{nullptr};
|
||||
YuGiOhBandaiCardListPanel* listPanel_{nullptr};
|
||||
YuGiOhBandaiSelectedCardPanel* selectedPanel_{nullptr};
|
||||
YuGiOhBandaiSetCompletionPanel* 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}};
|
||||
wxTextCtrl* filterInput_{nullptr};
|
||||
std::vector<Set> setsCache_;
|
||||
bool attemptedInitialSetLoad_{false};
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/ui/BaseSelectedCardPanel.hpp"
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhBandaiSelectedCardPanel final : public BaseSelectedCardPanel<YuGiOhBandaiCard> {
|
||||
public:
|
||||
YuGiOhBandaiSelectedCardPanel(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
CardPreviewService& cardPreview);
|
||||
|
||||
protected:
|
||||
[[nodiscard]] std::vector<DetailRowSpec> declareDetailRows() const override;
|
||||
[[nodiscard]] std::vector<FlagIconSpec> declareFlagIcons() const override;
|
||||
[[nodiscard]] std::string detailValueFor(const YuGiOhBandaiCard& card,
|
||||
DetailKey key) const override;
|
||||
[[nodiscard]] bool isFlagSet(const YuGiOhBandaiCard& card, DetailKey key) const override;
|
||||
[[nodiscard]] std::tuple<std::string, std::string, std::string>
|
||||
previewKey(const YuGiOhBandaiCard& card) const override;
|
||||
[[nodiscard]] Game gameId() const noexcept override { return Game::YuGiOhBandai; }
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
// YuGiOhBandaiSetCompletionPanel: Set Completion tab — pack tiles with
|
||||
// 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).
|
||||
// Optional language filter restricts ownership to one language and labels
|
||||
// set titles as "{setName} ({language})".
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiCard.hpp"
|
||||
#include "ccm/domain/YuGiOhBandaiSetCatalog.hpp"
|
||||
#include "ccm/services/YuGiOhBandaiSetCatalogService.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
|
||||
#include <wx/panel.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class wxBoxSizer;
|
||||
class wxChoice;
|
||||
class wxListCtrl;
|
||||
class wxScrolledWindow;
|
||||
class wxSimplebook;
|
||||
class wxStaticText;
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class YuGiOhBandaiSetCompletionPanel : public wxPanel {
|
||||
public:
|
||||
YuGiOhBandaiSetCompletionPanel(wxWindow* parent, YuGiOhBandaiSetCatalogService& catalogStore);
|
||||
|
||||
void setCollection(std::vector<YuGiOhBandaiCard> cards);
|
||||
void reloadFromStore();
|
||||
void applyTheme(const ThemePalette& palette);
|
||||
|
||||
private:
|
||||
void showGridPage();
|
||||
void showChecklistPage(const std::string& setId, const std::string& setName);
|
||||
void rebuildGrid();
|
||||
void rebuildChecklist(const std::string& setId);
|
||||
void setEmptyMessage(const wxString& message);
|
||||
void clearGridTiles();
|
||||
void refreshLanguageChoice();
|
||||
void onLanguageChoice(wxCommandEvent& event);
|
||||
void rebuildCurrentView();
|
||||
[[nodiscard]] std::string displaySetName(const std::string& setName) const;
|
||||
|
||||
YuGiOhBandaiSetCatalogService& catalogStore_;
|
||||
YuGiOhBandaiSetCatalog catalog_;
|
||||
bool catalogLoaded_{false};
|
||||
std::vector<YuGiOhBandaiCard> collection_;
|
||||
ThemePalette palette_{};
|
||||
std::optional<Language> languageFilter_;
|
||||
|
||||
wxChoice* languageChoice_{nullptr};
|
||||
wxSimplebook* book_{nullptr};
|
||||
wxPanel* gridPage_{nullptr};
|
||||
wxScrolledWindow* scroll_{nullptr};
|
||||
wxBoxSizer* gridSizer_{nullptr};
|
||||
wxStaticText* emptyLabel_{nullptr};
|
||||
|
||||
wxPanel* detailPage_{nullptr};
|
||||
wxStaticText* detailTitle_{nullptr};
|
||||
wxListCtrl* checklist_{nullptr};
|
||||
std::string detailSetId_;
|
||||
std::string detailSetName_;
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -518,13 +518,7 @@ void DigiBattle99GameView::applyTheme(const ThemePalette& palette) {
|
||||
if (setCompletionPanel_) setCompletionPanel_->applyTheme(palette);
|
||||
refreshToolbarIcons(palette);
|
||||
refreshTabBarTheme(palette);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
applyPaletteToTextCtrl(filterInput_, palette, config_.current().theme);
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
+2
-30
@@ -29,10 +29,6 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#ifdef __WXMSW__
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
@@ -44,6 +40,7 @@ std::string dirNameForGame(Game g) {
|
||||
case Game::Magic: return "magic";
|
||||
case Game::Pokemon: return "pokemon";
|
||||
case Game::YuGiOh: return "yugioh";
|
||||
case Game::YuGiOhBandai: return "yugiohbandai";
|
||||
case Game::DigiBattle99: return "digibattle99";
|
||||
case Game::JapanesePokemon: return "pokemon";
|
||||
}
|
||||
@@ -314,11 +311,7 @@ void MainFrame::applyTheme() {
|
||||
SetBackgroundColour(palette.windowBg);
|
||||
SetForegroundColour(palette.text);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
filterInput_->Refresh();
|
||||
applyPaletteToTextCtrl(filterInput_, palette, currentTheme);
|
||||
}
|
||||
for (auto* view : ctx_.gameViews) {
|
||||
if (view != nullptr) view->applyTheme(palette);
|
||||
@@ -481,25 +474,4 @@ void MainFrame::onDelete(wxCommandEvent&) {
|
||||
if (auto* view = activeView()) view->onDeleteCard(this);
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
WXLRESULT MainFrame::MSWWindowProc(WXUINT message, WXWPARAM wParam, WXLPARAM lParam) {
|
||||
if (message == WM_CTLCOLOREDIT && filterInput_ != nullptr) {
|
||||
const HWND target = reinterpret_cast<HWND>(lParam);
|
||||
const HWND filterHwnd = reinterpret_cast<HWND>(filterInput_->GetHandle());
|
||||
if (target != nullptr && filterHwnd != nullptr && target == filterHwnd) {
|
||||
const ThemePalette palette = paletteForTheme(ctx_.config.current().theme);
|
||||
HDC hdc = reinterpret_cast<HDC>(wParam);
|
||||
::SetTextColor(hdc, RGB(palette.inputText.Red(), palette.inputText.Green(),
|
||||
palette.inputText.Blue()));
|
||||
::SetBkColor(hdc, RGB(palette.inputBg.Red(), palette.inputBg.Green(),
|
||||
palette.inputBg.Blue()));
|
||||
::SetDCBrushColor(hdc, RGB(palette.inputBg.Red(), palette.inputBg.Green(),
|
||||
palette.inputBg.Blue()));
|
||||
return reinterpret_cast<WXLRESULT>(::GetStockObject(DC_BRUSH));
|
||||
}
|
||||
}
|
||||
return wxFrame::MSWWindowProc(message, wParam, lParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -601,12 +601,7 @@ void PokemonGameView::applyTheme(const ThemePalette& palette) {
|
||||
if (setCompletionPanel_) setCompletionPanel_->applyTheme(palette);
|
||||
refreshToolbarIcons(palette);
|
||||
refreshTabBarTheme(palette);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
}
|
||||
applyPaletteToTextCtrl(filterInput_, palette, config_.current().theme);
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -18,6 +18,7 @@ wxString displayLabelForGame(Game g) {
|
||||
case Game::Pokemon: return "Pokemon";
|
||||
case Game::YuGiOh: return "Yu-Gi-Oh!";
|
||||
case Game::DigiBattle99: return "Digimon (Digi-Battle)";
|
||||
case Game::YuGiOhBandai: return "Yu-Gi-Oh! (Bandai)";
|
||||
case Game::JapanesePokemon: return "Pokemon"; // internal; not in allGames()
|
||||
}
|
||||
return wxString::FromUTF8(to_string(g).data());
|
||||
|
||||
+95
-3
@@ -20,6 +20,7 @@
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#ifdef __WXMSW__
|
||||
@@ -216,6 +217,79 @@ void applyNativeClassTheme(wxWindow* window, Theme theme, const wchar_t* darkCla
|
||||
setWindowTheme(hwnd, dark ? darkClass : lightClass, nullptr);
|
||||
}
|
||||
|
||||
// WM_CTLCOLOREDIT is sent to the EDIT's parent, not the top-level frame. Immersive
|
||||
// dark mode can still paint black typed text even when wx colours are set, so we
|
||||
// subclass each parent once and force text/background from the wxTextCtrl palette.
|
||||
constexpr UINT_PTR kEditColorSubclassId = 0x43434d45; // 'CCME'
|
||||
std::unordered_set<HWND> gEditColorSubclassedParents;
|
||||
std::unordered_map<HWND, wxTextCtrl*> gPaletteTextCtrls;
|
||||
std::unordered_set<wxTextCtrl*> gPaletteTextCtrlDestroyBound;
|
||||
|
||||
LRESULT CALLBACK editColorParentSubclass(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam,
|
||||
UINT_PTR /*subclassId*/, DWORD_PTR /*refData*/) {
|
||||
if (msg == WM_CTLCOLOREDIT) {
|
||||
const HWND editHwnd = reinterpret_cast<HWND>(lParam);
|
||||
const auto it = gPaletteTextCtrls.find(editHwnd);
|
||||
if (it != gPaletteTextCtrls.end() && it->second != nullptr) {
|
||||
wxTextCtrl* text = it->second;
|
||||
const wxColour fg = text->GetForegroundColour();
|
||||
const wxColour bg = text->GetBackgroundColour();
|
||||
if (fg.IsOk() && bg.IsOk()) {
|
||||
HDC hdc = reinterpret_cast<HDC>(wParam);
|
||||
::SetTextColor(hdc, RGB(fg.Red(), fg.Green(), fg.Blue()));
|
||||
::SetBkColor(hdc, RGB(bg.Red(), bg.Green(), bg.Blue()));
|
||||
::SetDCBrushColor(hdc, RGB(bg.Red(), bg.Green(), bg.Blue()));
|
||||
return reinterpret_cast<LRESULT>(::GetStockObject(DC_BRUSH));
|
||||
}
|
||||
}
|
||||
} else if (msg == WM_NCDESTROY) {
|
||||
gEditColorSubclassedParents.erase(hwnd);
|
||||
::RemoveWindowSubclass(hwnd, editColorParentSubclass, kEditColorSubclassId);
|
||||
}
|
||||
return ::DefSubclassProc(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
void ensureEditColorParentSubclass(wxTextCtrl* text) {
|
||||
if (text == nullptr) return;
|
||||
const HWND editHwnd = reinterpret_cast<HWND>(text->GetHandle());
|
||||
if (editHwnd == nullptr) return;
|
||||
gPaletteTextCtrls[editHwnd] = text;
|
||||
if (gPaletteTextCtrlDestroyBound.insert(text).second) {
|
||||
text->Bind(wxEVT_DESTROY, [text, editHwnd](wxWindowDestroyEvent& event) {
|
||||
gPaletteTextCtrls.erase(editHwnd);
|
||||
gPaletteTextCtrlDestroyBound.erase(text);
|
||||
event.Skip();
|
||||
});
|
||||
}
|
||||
const HWND parent = ::GetParent(editHwnd);
|
||||
if (parent == nullptr) return;
|
||||
if (gEditColorSubclassedParents.count(parent) != 0) return;
|
||||
if (::SetWindowSubclass(parent, editColorParentSubclass, kEditColorSubclassId, 0) != FALSE) {
|
||||
gEditColorSubclassedParents.insert(parent);
|
||||
}
|
||||
}
|
||||
|
||||
void hardenTextCtrlNativeTheme(wxTextCtrl* text, Theme theme) {
|
||||
if (text == nullptr) return;
|
||||
const bool darkLike = isDarkLikeTheme(theme);
|
||||
text->SetThemeEnabled(!darkLike);
|
||||
|
||||
const HWND hwnd = reinterpret_cast<HWND>(text->GetHandle());
|
||||
if (hwnd == nullptr) return;
|
||||
|
||||
// Opt this EDIT out of immersive dark mode so typed text uses our palette.
|
||||
if (auto allowDarkModeForWindow = resolveAllowDarkModeForWindow()) {
|
||||
allowDarkModeForWindow(hwnd, FALSE);
|
||||
}
|
||||
if (darkLike) {
|
||||
if (auto setWindowTheme = resolveSetWindowTheme()) {
|
||||
// Empty theme class disables visual-style painting of the EDIT contents.
|
||||
setWindowTheme(hwnd, L"", L"");
|
||||
}
|
||||
}
|
||||
ensureEditColorParentSubclass(text);
|
||||
}
|
||||
|
||||
COLORREF toColorRef(const wxColour& c) {
|
||||
return RGB(c.Red(), c.Green(), c.Blue());
|
||||
}
|
||||
@@ -412,9 +486,13 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
|
||||
dynamic_cast<wxSpinCtrl*>(root) != nullptr) {
|
||||
if (auto* text = dynamic_cast<wxTextCtrl*>(root)) {
|
||||
// On Windows, themed EDIT controls can ignore wx foreground color
|
||||
// while typing in dark mode; disable native theming there so the
|
||||
// control consistently uses palette-driven text/background colors.
|
||||
// while typing in dark mode; disable native theming and force
|
||||
// WM_CTLCOLOREDIT colours via the parent subclass helper.
|
||||
#ifdef __WXMSW__
|
||||
hardenTextCtrlNativeTheme(text, theme);
|
||||
#else
|
||||
text->SetThemeEnabled(!isDarkLikeTheme(theme));
|
||||
#endif
|
||||
}
|
||||
root->SetBackgroundColour(palette.inputBg);
|
||||
root->SetForegroundColour(palette.inputText);
|
||||
@@ -428,7 +506,7 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
|
||||
} else if (dynamic_cast<wxTextCtrl*>(root) != nullptr) {
|
||||
// Do not apply Explorer class theming to edit controls: on some
|
||||
// Windows builds it forces black typed text in dark mode.
|
||||
// Keep text fields palette-driven via wx colors instead.
|
||||
// Keep text fields palette-driven via wx colours + CTLCOLOR fix.
|
||||
} else {
|
||||
applyNativeClassTheme(root, theme, L"DarkMode_Explorer", L"Explorer");
|
||||
}
|
||||
@@ -637,6 +715,20 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
|
||||
}
|
||||
}
|
||||
|
||||
void applyPaletteToTextCtrl(wxTextCtrl* text, const ThemePalette& palette, Theme theme) {
|
||||
if (text == nullptr) return;
|
||||
#ifdef __WXMSW__
|
||||
hardenTextCtrlNativeTheme(text, theme);
|
||||
#else
|
||||
text->SetThemeEnabled(!isDarkLikeTheme(theme));
|
||||
#endif
|
||||
text->SetBackgroundColour(palette.inputBg);
|
||||
text->SetForegroundColour(palette.inputText);
|
||||
text->SetOwnBackgroundColour(palette.inputBg);
|
||||
text->SetOwnForegroundColour(palette.inputText);
|
||||
text->Refresh();
|
||||
}
|
||||
|
||||
void themeModalDialog(wxDialog* dlg, Theme theme) {
|
||||
if (dlg == nullptr) return;
|
||||
const ThemePalette palette = paletteForTheme(theme);
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
#include "ccm/ui/YuGiOhBandaiCardEditDialog.hpp"
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiSetSource.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
#include <wx/app.h>
|
||||
#include <wx/panel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
const char* const kRarityOptions[] = {
|
||||
"Common",
|
||||
"Rare",
|
||||
"Super Rare",
|
||||
"Ultra Rare",
|
||||
"Holo Seal",
|
||||
};
|
||||
} // namespace
|
||||
|
||||
YuGiOhBandaiCardEditDialog::YuGiOhBandaiCardEditDialog(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
SetService& setService,
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
YuGiOhBandaiCard initial,
|
||||
const std::vector<Set>* preloadedSets)
|
||||
: BaseCardEditDialog<YuGiOhBandaiCard>(
|
||||
parent,
|
||||
mode == EditMode::Create ? "Add Yu-Gi-Oh! (Bandai) Card"
|
||||
: "Edit Yu-Gi-Oh! (Bandai) Card",
|
||||
imageService, setService, mode, std::move(initial), Game::YuGiOhBandai,
|
||||
preloadedSets),
|
||||
dialogMode_(mode),
|
||||
cardPreview_(cardPreview),
|
||||
variantFetchState_(std::make_shared<VariantFetchState>()) {
|
||||
buildAndPopulate();
|
||||
if (dialogMode_ == EditMode::Edit) {
|
||||
scheduleDeferredVariantPrefetch();
|
||||
}
|
||||
}
|
||||
|
||||
YuGiOhBandaiCardEditDialog::~YuGiOhBandaiCardEditDialog() {
|
||||
if (variantFetchState_) {
|
||||
variantFetchState_->alive.store(false);
|
||||
}
|
||||
}
|
||||
|
||||
std::span<const Language> YuGiOhBandaiCardEditDialog::languagesForChoice() const {
|
||||
static constexpr Language kLangs[] = {Language::Japanese, Language::English};
|
||||
return kLangs;
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::onCardLookupContextChanged() {
|
||||
clearCachedPrintVariants();
|
||||
}
|
||||
|
||||
bool YuGiOhBandaiCardEditDialog::validateExtraFields() {
|
||||
const std::string setNo =
|
||||
YuGiOhBandaiSetSource::normalizeCardNumber(constCard().setNo);
|
||||
if (setNo.empty()) {
|
||||
showThemedMessageDialog(
|
||||
this,
|
||||
"Set number (No.) is required for set completion tracking.\n"
|
||||
"Enter a Bandai number or use Auto detect.",
|
||||
"Add card", wxOK | wxICON_INFORMATION);
|
||||
return false;
|
||||
}
|
||||
// Persist the normalized form so ownership keys stay stable.
|
||||
mutableCard().setNo = setNo;
|
||||
if (setNoCtrl_) setNoCtrl_->ChangeValue(wxString::FromUTF8(setNo.c_str()));
|
||||
return true;
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
|
||||
holoCheck_ = new wxCheckBox(this, wxID_ANY, "Holo");
|
||||
signedCheck_ = new wxCheckBox(this, wxID_ANY, "Signed");
|
||||
alteredCheck_ = new wxCheckBox(this, wxID_ANY, "Altered");
|
||||
flagsBox->Add(holoCheck_, 0, wxRIGHT, 12);
|
||||
flagsBox->Add(signedCheck_, 0, wxRIGHT, 12);
|
||||
flagsBox->Add(alteredCheck_, 0, wxRIGHT, 12);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
|
||||
auto* setNoPanel = new wxPanel(this, wxID_ANY);
|
||||
setNoCtrl_ = new wxTextCtrl(setNoPanel, wxID_ANY);
|
||||
autoSetNoBtn_ = new wxButton(setNoPanel, wxID_ANY, "Auto detect");
|
||||
autoSetNoBtn_->Bind(wxEVT_BUTTON, &YuGiOhBandaiCardEditDialog::onAutoDetectBySetNo, this);
|
||||
nextSetNoBtn_ = new wxButton(setNoPanel, wxID_ANY, "Next");
|
||||
nextSetNoBtn_->Bind(wxEVT_BUTTON, &YuGiOhBandaiCardEditDialog::onNextSetNo, this);
|
||||
nextSetNoBtn_->Show(false);
|
||||
auto* setNoRow = new wxBoxSizer(wxHORIZONTAL);
|
||||
setNoRow->Add(setNoCtrl_, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
setNoRow->Add(autoSetNoBtn_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
setNoRow->Add(nextSetNoBtn_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
setNoPanel->SetSizer(setNoRow);
|
||||
|
||||
auto* rarityPanel = new wxPanel(this, wxID_ANY);
|
||||
rarityChoice_ = new wxChoice(rarityPanel, wxID_ANY);
|
||||
wxArrayString rarityItems;
|
||||
rarityItems.Alloc(static_cast<int>(sizeof(kRarityOptions) / sizeof(kRarityOptions[0])));
|
||||
for (const char* rarity : kRarityOptions) {
|
||||
rarityItems.Add(wxString::FromUTF8(rarity));
|
||||
}
|
||||
rarityChoice_->Append(rarityItems);
|
||||
rarityChoice_->Bind(wxEVT_CHOICE, &YuGiOhBandaiCardEditDialog::onRarityChoiceChanged, this);
|
||||
autoRarityBtn_ = new wxButton(rarityPanel, wxID_ANY, "Auto detect");
|
||||
autoRarityBtn_->Bind(wxEVT_BUTTON, &YuGiOhBandaiCardEditDialog::onAutoDetectByName, this);
|
||||
auto* rarityRow = new wxBoxSizer(wxHORIZONTAL);
|
||||
rarityRow->Add(rarityChoice_, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
rarityRow->Add(autoRarityBtn_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
rarityPanel->SetSizer(rarityRow);
|
||||
|
||||
appendRow(grid, "No.", setNoPanel);
|
||||
appendRow(grid, "Rarity", rarityPanel);
|
||||
|
||||
if (auto* setCombo = setComboControl()) {
|
||||
setCombo->Bind(wxEVT_COMBOBOX, &YuGiOhBandaiCardEditDialog::onSetSelectionChanged, this);
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::readExtraFromCard() {
|
||||
clearCachedPrintVariants();
|
||||
if (setNoCtrl_) setNoCtrl_->ChangeValue(wxString::FromUTF8(constCard().setNo.c_str()));
|
||||
applyRarityStringToChoice(constCard().rarity);
|
||||
if (holoCheck_) holoCheck_->SetValue(constCard().holo);
|
||||
if (signedCheck_) signedCheck_->SetValue(constCard().signed_);
|
||||
if (alteredCheck_) alteredCheck_->SetValue(constCard().altered);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::writeExtraToCard() {
|
||||
if (setNoCtrl_) mutableCard().setNo = setNoCtrl_->GetValue().ToStdString(wxConvUTF8);
|
||||
if (rarityChoice_) mutableCard().rarity = rarityChoice_->GetStringSelection().ToStdString(wxConvUTF8);
|
||||
if (holoCheck_) mutableCard().holo = holoCheck_->IsChecked();
|
||||
if (signedCheck_) mutableCard().signed_ = signedCheck_->IsChecked();
|
||||
if (alteredCheck_) mutableCard().altered = alteredCheck_->IsChecked();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::applyRarityStringToChoice(const std::string& rarity) {
|
||||
if (!rarityChoice_) return;
|
||||
if (rarity.empty()) {
|
||||
rarityChoice_->SetSelection(0);
|
||||
return;
|
||||
}
|
||||
const wxString wxRare = wxString::FromUTF8(rarity.c_str());
|
||||
int idx = rarityChoice_->FindString(wxRare);
|
||||
if (idx == wxNOT_FOUND) {
|
||||
rarityChoice_->Append(wxRare);
|
||||
idx = rarityChoice_->GetCount() - 1;
|
||||
}
|
||||
if (idx != wxNOT_FOUND) rarityChoice_->SetSelection(idx);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::maybeAutoCheckHoloForRarity(const std::string& rarity) {
|
||||
if (rarity == "Holo Seal" && holoCheck_ != nullptr) {
|
||||
holoCheck_->SetValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::onRarityChoiceChanged(wxCommandEvent&) {
|
||||
if (!rarityChoice_) return;
|
||||
maybeAutoCheckHoloForRarity(rarityChoice_->GetStringSelection().ToStdString(wxConvUTF8));
|
||||
}
|
||||
|
||||
std::size_t YuGiOhBandaiCardEditDialog::findAvailableSetIndex(const std::string& setId) const {
|
||||
const auto& available = availableSets();
|
||||
auto lowerAscii = [](std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
return s;
|
||||
};
|
||||
const std::string target = lowerAscii(setId);
|
||||
for (std::size_t i = 0; i < available.size(); ++i) {
|
||||
if (lowerAscii(available[i].id) == target) return i;
|
||||
}
|
||||
return available.size();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::applyDetectedPrint(const AutoDetectedPrint& print) {
|
||||
if (!print.name.empty()) {
|
||||
if (auto* name = nameControl()) {
|
||||
name->ChangeValue(wxString::FromUTF8(print.name.c_str()));
|
||||
}
|
||||
}
|
||||
if (!print.setNo.empty() && setNoCtrl_) {
|
||||
setNoCtrl_->ChangeValue(wxString::FromUTF8(print.setNo.c_str()));
|
||||
}
|
||||
if (!print.rarity.empty()) {
|
||||
applyRarityStringToChoice(print.rarity);
|
||||
maybeAutoCheckHoloForRarity(print.rarity);
|
||||
}
|
||||
if (!print.language.empty()) {
|
||||
if (auto lang = languageFromString(print.language)) {
|
||||
for (const Language l : languagesForChoice()) {
|
||||
if (l != *lang) continue;
|
||||
if (auto* choice = languageChoiceControl()) {
|
||||
const wxString wanted =
|
||||
wxString::FromUTF8(std::string(to_string(*lang)).c_str());
|
||||
const int idx = choice->FindString(wanted);
|
||||
if (idx != wxNOT_FOUND) choice->SetSelection(idx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!print.setId.empty()) {
|
||||
const std::size_t idx = findAvailableSetIndex(print.setId);
|
||||
if (idx < availableSets().size()) {
|
||||
applySetSelectionByIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::clearCachedPrintVariants() {
|
||||
++variantFetchEpoch_;
|
||||
cachedVariants_.clear();
|
||||
variantRingPos_ = 0;
|
||||
refreshVariantNextControls();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::refreshVariantNextControls() {
|
||||
if (!nextSetNoBtn_) return;
|
||||
nextSetNoBtn_->Show(cachedVariants_.size() > 1);
|
||||
Layout();
|
||||
if (GetSizer()) Fit();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::scheduleDeferredVariantPrefetch() {
|
||||
const unsigned epoch = variantFetchEpoch_;
|
||||
wxTheApp->CallAfter([this, epoch]() {
|
||||
prefetchVariantsForCurrentCardSilent(epoch);
|
||||
});
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
if (!cachedVariants_.empty()) return;
|
||||
const auto& card = constCard();
|
||||
if (card.name.empty()) return;
|
||||
requestByNameAsync(capturedEpoch, card.name, card.set.id, false);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::requestByNameAsync(unsigned capturedEpoch, std::string name,
|
||||
std::string setId, bool showFailureDialog) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
if (showFailureDialog && autoRarityBtn_) autoRarityBtn_->Disable();
|
||||
|
||||
auto state = variantFetchState_;
|
||||
CardPreviewService* svc = &cardPreview_;
|
||||
YuGiOhBandaiCardEditDialog* self = this;
|
||||
std::thread([state, svc, self, capturedEpoch, name = std::move(name),
|
||||
setId = std::move(setId), showFailureDialog]() {
|
||||
auto detected = svc->detectPrintVariants(Game::YuGiOhBandai, name, setId);
|
||||
wxTheApp->CallAfter([state, self, capturedEpoch, detected = std::move(detected),
|
||||
showFailureDialog]() mutable {
|
||||
if (!state->alive.load()) return;
|
||||
self->applyDetectedList(capturedEpoch, std::move(detected), showFailureDialog,
|
||||
/*applyFirst=*/true);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::requestByNoAsync(unsigned capturedEpoch, std::string setNo,
|
||||
bool showFailureDialog) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
if (showFailureDialog && autoSetNoBtn_) autoSetNoBtn_->Disable();
|
||||
|
||||
auto state = variantFetchState_;
|
||||
CardPreviewService* svc = &cardPreview_;
|
||||
YuGiOhBandaiCardEditDialog* self = this;
|
||||
std::thread([state, svc, self, capturedEpoch, setNo = std::move(setNo),
|
||||
showFailureDialog]() {
|
||||
auto detected = svc->detectVariantsBySetNo(Game::YuGiOhBandai, setNo);
|
||||
wxTheApp->CallAfter([state, self, capturedEpoch, detected = std::move(detected),
|
||||
showFailureDialog]() mutable {
|
||||
if (!state->alive.load()) return;
|
||||
self->applyDetectedList(capturedEpoch, std::move(detected), showFailureDialog,
|
||||
/*applyFirst=*/true);
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::applyDetectedList(
|
||||
unsigned capturedEpoch,
|
||||
Result<std::vector<AutoDetectedPrint>> detected,
|
||||
bool showFailureDialog,
|
||||
bool applyFirst) {
|
||||
if (capturedEpoch != variantFetchEpoch_) return;
|
||||
|
||||
if (autoSetNoBtn_) autoSetNoBtn_->Enable();
|
||||
if (autoRarityBtn_) autoRarityBtn_->Enable();
|
||||
|
||||
if (!detected) {
|
||||
if (showFailureDialog) {
|
||||
showThemedMessageDialog(this, "Auto detect failed: " + detected.error(),
|
||||
"Auto detect", wxOK | wxICON_WARNING);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (detected.value().empty()) {
|
||||
if (showFailureDialog) {
|
||||
showThemedMessageDialog(this, "No matching Yu-Gi-Oh! (Bandai) card found.",
|
||||
"Auto detect", wxOK | wxICON_INFORMATION);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cachedVariants_ = std::move(detected).value();
|
||||
variantRingPos_ = 0;
|
||||
if (applyFirst) applyDetectedPrint(cachedVariants_.front());
|
||||
refreshVariantNextControls();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::onAutoDetectBySetNo(wxCommandEvent&) {
|
||||
syncCardFromControls();
|
||||
const std::string setNo =
|
||||
setNoCtrl_ ? setNoCtrl_->GetValue().ToStdString(wxConvUTF8) : std::string();
|
||||
if (setNo.empty()) {
|
||||
showThemedMessageDialog(this, "Enter a Bandai number first.", "Auto detect",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
const unsigned epoch = variantFetchEpoch_;
|
||||
requestByNoAsync(epoch, setNo, true);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::onNextSetNo(wxCommandEvent&) {
|
||||
if (cachedVariants_.size() <= 1) return;
|
||||
variantRingPos_ = (variantRingPos_ + 1) % cachedVariants_.size();
|
||||
applyDetectedPrint(cachedVariants_[variantRingPos_]);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::onAutoDetectByName(wxCommandEvent&) {
|
||||
syncCardFromControls();
|
||||
const auto& card = constCard();
|
||||
if (card.name.empty()) {
|
||||
showThemedMessageDialog(this, "Enter a card name first.", "Auto detect",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
std::string setId;
|
||||
if (const Set* set = selectedSetFromControls()) setId = set->id;
|
||||
|
||||
const unsigned epoch = variantFetchEpoch_;
|
||||
requestByNameAsync(epoch, card.name, setId, true);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardEditDialog::onSetSelectionChanged(wxCommandEvent& ev) {
|
||||
clearCachedPrintVariants();
|
||||
scheduleDeferredVariantPrefetch();
|
||||
ev.Skip();
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "ccm/ui/YuGiOhBandaiCardListPanel.hpp"
|
||||
|
||||
#include "ccm/services/CardFilter.hpp"
|
||||
#include "ccm/ui/SvgIcons.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
YuGiOhBandaiCardListPanel::YuGiOhBandaiCardListPanel(wxWindow* parent)
|
||||
: BaseCardListPanel<YuGiOhBandaiCard, YuGiOhBandaiSortColumn>(parent) {
|
||||
buildLayout();
|
||||
}
|
||||
|
||||
std::vector<YuGiOhBandaiCardListPanel::TextColumnSpec>
|
||||
YuGiOhBandaiCardListPanel::declareTextColumns() const {
|
||||
return {
|
||||
{"Name", 200, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::Name},
|
||||
{"Set", 150, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::SetReleaseDate},
|
||||
{"No.", 70, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::SetNo},
|
||||
{"Rarity", 100, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::Rarity},
|
||||
{"Amount", 70, wxLIST_FORMAT_RIGHT, YuGiOhBandaiSortColumn::Amount},
|
||||
{"Condition", 100, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::Condition},
|
||||
{"Language", 100, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::Language},
|
||||
{"Note", 180, wxLIST_FORMAT_LEFT, YuGiOhBandaiSortColumn::Note},
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<YuGiOhBandaiCardListPanel::IconColumnSpec>
|
||||
YuGiOhBandaiCardListPanel::declareIconColumns() const {
|
||||
constexpr int kFlagColWidth = 36;
|
||||
return {
|
||||
{kSvgHolo, kFlagColWidth, YuGiOhBandaiSortColumn::Holo},
|
||||
{kSvgSigned, kFlagColWidth, YuGiOhBandaiSortColumn::Signed},
|
||||
{kSvgAltered, kFlagColWidth, YuGiOhBandaiSortColumn::Altered},
|
||||
};
|
||||
}
|
||||
|
||||
std::string YuGiOhBandaiCardListPanel::renderTextCell(const YuGiOhBandaiCard& card,
|
||||
std::size_t idx) const {
|
||||
switch (idx) {
|
||||
case 0: return card.name;
|
||||
case 1: return card.set.name;
|
||||
case 2: return card.setNo;
|
||||
case 3: return card.rarity;
|
||||
case 4: return std::to_string(card.amount);
|
||||
case 5: return std::string(to_string(card.condition));
|
||||
case 6: return std::string(to_string(card.language));
|
||||
case 7: return card.note;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool YuGiOhBandaiCardListPanel::isIconColumnSet(const YuGiOhBandaiCard& card,
|
||||
std::size_t idx) const {
|
||||
switch (idx) {
|
||||
case 0: return card.holo;
|
||||
case 1: return card.signed_;
|
||||
case 2: return card.altered;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void YuGiOhBandaiCardListPanel::sortBy(YuGiOhBandaiSortColumn column, bool ascending) {
|
||||
sortYuGiOhBandaiCards(mutableCards(), column, ascending);
|
||||
}
|
||||
|
||||
bool YuGiOhBandaiCardListPanel::matchesFilter(const YuGiOhBandaiCard& card,
|
||||
std::string_view filter) const {
|
||||
return matchesYuGiOhBandaiFilter(card, filter);
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,552 @@
|
||||
#include "ccm/ui/YuGiOhBandaiGameView.hpp"
|
||||
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiSetSource.hpp"
|
||||
#include "ccm/ui/CardEditModalGuard.hpp"
|
||||
#include "ccm/ui/YuGiOhBandaiCardEditDialog.hpp"
|
||||
#include "ccm/ui/YuGiOhBandaiCardListPanel.hpp"
|
||||
#include "ccm/ui/YuGiOhBandaiSelectedCardPanel.hpp"
|
||||
#include "ccm/ui/YuGiOhBandaiSetCompletionPanel.hpp"
|
||||
#include "ccm/ui/SvgIcons.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
|
||||
#include <wx/bmpbuttn.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/simplebook.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/splitter.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/window.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
constexpr int kBandaiToolbarIconPx = 18;
|
||||
constexpr const char kBandaiFilterHint[] = "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
|
||||
|
||||
YuGiOhBandaiGameView::YuGiOhBandaiGameView(ConfigService& config,
|
||||
CollectionService<YuGiOhBandaiCard>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module,
|
||||
YuGiOhBandaiSetCatalogService& catalogStore)
|
||||
: config_(config),
|
||||
collection_(collection),
|
||||
sets_(sets),
|
||||
images_(images),
|
||||
cardPreview_(cardPreview),
|
||||
module_(module),
|
||||
catalogStore_(catalogStore) {}
|
||||
|
||||
void YuGiOhBandaiGameView::ensureSetsLoaded() {
|
||||
if (attemptedInitialSetLoad_) return;
|
||||
attemptedInitialSetLoad_ = true;
|
||||
|
||||
auto cached = sets_.getSets(Game::YuGiOhBandai);
|
||||
if (cached) {
|
||||
setsCache_ = std::move(cached).value();
|
||||
if (!setsCache_.empty()) return;
|
||||
} else {
|
||||
setsCache_.clear();
|
||||
}
|
||||
|
||||
auto refreshed = sets_.updateSets(Game::YuGiOhBandai);
|
||||
if (refreshed) {
|
||||
setsCache_ = std::move(refreshed).value();
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::refreshSetCompletionFromStore() {
|
||||
if (setCompletionPanel_ == nullptr) return;
|
||||
setCompletionPanel_->reloadFromStore();
|
||||
if (auto loaded = collection_.list(Game::YuGiOhBandai)) {
|
||||
setCompletionPanel_->setCollection(std::move(loaded).value());
|
||||
}
|
||||
}
|
||||
|
||||
bool YuGiOhBandaiGameView::downloadSetsAndCatalog(wxWindow* parentWindow,
|
||||
std::size_t* setCountOut,
|
||||
std::size_t* packCountOut) {
|
||||
auto* bandaiSrc = dynamic_cast<YuGiOhBandaiSetSource*>(&module_.setSource());
|
||||
if (bandaiSrc == nullptr) {
|
||||
showThemedMessageDialog(parentWindow, "Yu-Gi-Oh! (Bandai) set source unavailable.",
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto both = bandaiSrc->fetchAllWithCatalog();
|
||||
if (!both) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update sets: " + both.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto savedSets = sets_.saveSets(Game::YuGiOhBandai, both.value().sets);
|
||||
if (!savedSets) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to save sets: " + savedSets.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto savedCatalog = catalogStore_.save(both.value().catalog);
|
||||
if (!savedCatalog) {
|
||||
showThemedMessageDialog(parentWindow,
|
||||
"Sets saved, but set catalog failed: " + savedCatalog.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
setsCache_ = both.value().sets;
|
||||
if (setCountOut != nullptr) *setCountOut = both.value().sets.size();
|
||||
if (packCountOut != nullptr) *packCountOut = both.value().catalog.packs.size();
|
||||
refreshSetCompletionFromStore();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool YuGiOhBandaiGameView::ensureCatalogLoaded(wxWindow* parentWindow) {
|
||||
if (catalogStore_.exists()) return true;
|
||||
return downloadSetsAndCatalog(parentWindow, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::ensureSingleCardsMounted(wxWindow* splitterParent) {
|
||||
if (singleSplitter_ == nullptr) {
|
||||
singleSplitter_ = new wxSplitterWindow(splitterParent, wxID_ANY, wxDefaultPosition,
|
||||
wxDefaultSize, wxSP_LIVE_UPDATE);
|
||||
singleSplitter_->SetMinimumPaneSize(280);
|
||||
}
|
||||
auto* list = listPanel(singleSplitter_);
|
||||
auto* selected = selectedPanel(singleSplitter_);
|
||||
if (!singleSplitter_->IsSplit()) {
|
||||
singleSplitter_->SplitVertically(selected, list, 360);
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer) {
|
||||
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto makeToolBtn = [&](const char* svg, const wxString& tip) {
|
||||
wxBitmap bmp = svgIconBitmap(svg, kBandaiToolbarIconPx, "#000000");
|
||||
auto* b = new wxBitmapButton(parent, wxID_ANY, bmp, wxDefaultPosition, wxDefaultSize,
|
||||
wxBU_EXACTFIT);
|
||||
b->SetToolTip(tip);
|
||||
return b;
|
||||
};
|
||||
toolbarButtons_[0] = makeToolBtn(kSvgToolbarAdd, "Add Card");
|
||||
toolbarButtons_[1] = makeToolBtn(kSvgToolbarEdit, "Edit");
|
||||
toolbarButtons_[2] = makeToolBtn(kSvgToolbarDelete, "Delete");
|
||||
toolbar->AddSpacer(4);
|
||||
toolbar->Add(toolbarButtons_[0], 0, wxALIGN_CENTER_VERTICAL | wxALL, 4);
|
||||
toolbar->Add(toolbarButtons_[1], 0, wxALIGN_CENTER_VERTICAL | wxALL, 4);
|
||||
toolbar->Add(toolbarButtons_[2], 0, wxALIGN_CENTER_VERTICAL | wxALL, 4);
|
||||
toolbar->AddStretchSpacer(1);
|
||||
filterInput_ = new wxTextCtrl(parent, wxID_ANY, "", wxDefaultPosition, wxSize(260, -1));
|
||||
filterInput_->SetHint(kBandaiFilterHint);
|
||||
toolbar->Add(filterInput_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxTOP | wxBOTTOM, 4);
|
||||
pageSizer->Add(toolbar, 0, wxEXPAND);
|
||||
|
||||
toolbarButtons_[0]->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
wxWindow* owner = wxGetTopLevelParent(contentPanel_);
|
||||
onAddCard(owner != nullptr ? owner : contentPanel_);
|
||||
});
|
||||
toolbarButtons_[1]->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
wxWindow* owner = wxGetTopLevelParent(contentPanel_);
|
||||
onEditCard(owner != nullptr ? owner : contentPanel_);
|
||||
});
|
||||
toolbarButtons_[2]->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
wxWindow* owner = wxGetTopLevelParent(contentPanel_);
|
||||
onDeleteCard(owner != nullptr ? owner : contentPanel_);
|
||||
});
|
||||
filterInput_->Bind(wxEVT_TEXT, [this](wxCommandEvent&) {
|
||||
if (filterInput_ == nullptr) return;
|
||||
setFilter(filterInput_->GetValue().ToStdString(wxConvUTF8));
|
||||
});
|
||||
filterInput_->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& ev) {
|
||||
const int code = ev.GetKeyCode();
|
||||
if (code == WXK_UP || code == WXK_DOWN) {
|
||||
nudgeSelection(code == WXK_UP ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
ev.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
const std::string tbHex = palette.buttonText.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
if (toolbarButtons_[0]) {
|
||||
toolbarButtons_[0]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarAdd, kBandaiToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[1]) {
|
||||
toolbarButtons_[1]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarEdit, kBandaiToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[2]) {
|
||||
toolbarButtons_[2]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarDelete, kBandaiToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::selectTab(int index) {
|
||||
if (index < 0 || index > 1 || book_ == nullptr) return;
|
||||
activeTab_ = index;
|
||||
book_->SetSelection(index);
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
|
||||
// Bandai sets.json can load offline from the hardcoded manifest; the
|
||||
// set-completion catalog needs a Yugipedia fetch. Pull it on first visit.
|
||||
if (index == 1 && !catalogStore_.exists()) {
|
||||
wxWindow* owner = wxGetTopLevelParent(contentPanel_);
|
||||
// Errors are shown inside ensureCatalogLoaded; empty-state UI remains if it fails.
|
||||
(void)ensureCatalogLoaded(owner != nullptr ? owner : contentPanel_);
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::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 YuGiOhBandaiGameView::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* YuGiOhBandaiGameView::contentPanel(wxWindow* parent) {
|
||||
if (contentPanel_ == nullptr) {
|
||||
contentPanel_ = new wxPanel(parent);
|
||||
auto* root = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
buildTabBar(contentPanel_, root);
|
||||
|
||||
book_ = new wxSimplebook(contentPanel_, wxID_ANY);
|
||||
auto* singlePage = new wxPanel(book_);
|
||||
auto* singleSizer = new wxBoxSizer(wxVERTICAL);
|
||||
buildSingleCardsToolbar(singlePage, singleSizer);
|
||||
ensureSingleCardsMounted(singlePage);
|
||||
singleSizer->Add(singleSplitter_, 1, wxEXPAND);
|
||||
singlePage->SetSizer(singleSizer);
|
||||
book_->AddPage(singlePage, "Single Cards");
|
||||
|
||||
setCompletionPanel_ = new YuGiOhBandaiSetCompletionPanel(book_, catalogStore_);
|
||||
setCompletionPanel_->reloadFromStore();
|
||||
book_->AddPage(setCompletionPanel_, "Set Completion");
|
||||
|
||||
root->Add(book_, 1, wxEXPAND | wxTOP, 5);
|
||||
contentPanel_->SetSizer(root);
|
||||
|
||||
selectTab(0);
|
||||
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_;
|
||||
}
|
||||
|
||||
wxPanel* YuGiOhBandaiGameView::listPanel(wxWindow* parent) {
|
||||
if (listPanel_ == nullptr) {
|
||||
listPanel_ = new YuGiOhBandaiCardListPanel(parent);
|
||||
listPanel_->Bind(EVT_CARD_SELECTED, [this](wxCommandEvent&) {
|
||||
if (selectedPanel_ != nullptr && listPanel_ != nullptr) {
|
||||
selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
});
|
||||
listPanel_->Bind(EVT_CARD_ACTIVATED, [this](wxCommandEvent&) {
|
||||
wxWindow* owner = wxGetTopLevelParent(listPanel_);
|
||||
onEditCard(owner != nullptr ? owner : static_cast<wxWindow*>(listPanel_));
|
||||
});
|
||||
}
|
||||
return listPanel_;
|
||||
}
|
||||
|
||||
wxPanel* YuGiOhBandaiGameView::selectedPanel(wxWindow* parent) {
|
||||
if (selectedPanel_ == nullptr) {
|
||||
selectedPanel_ = new YuGiOhBandaiSelectedCardPanel(parent, images_, cardPreview_);
|
||||
}
|
||||
return selectedPanel_;
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::refreshCollection(std::optional<std::uint32_t> selectId) {
|
||||
// Ensure the Bandai host (and list panel) exist even when MainFrame mounts
|
||||
// via contentPanel before an explicit listPanel call.
|
||||
if (contentPanel_ == nullptr && listPanel_ == nullptr) return;
|
||||
|
||||
auto loaded = collection_.list(Game::YuGiOhBandai);
|
||||
if (!loaded) {
|
||||
showThemedMessageDialog(
|
||||
nullptr,
|
||||
"Failed to load Yu-Gi-Oh! (Bandai) collection: " + loaded.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards, selectId);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->setCollection(std::move(cards));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<Set>& YuGiOhBandaiGameView::setsForDialog() {
|
||||
ensureSetsLoaded();
|
||||
if (!setsCache_.empty()) return setsCache_;
|
||||
auto loaded = sets_.getSets(Game::YuGiOhBandai);
|
||||
if (loaded) setsCache_ = std::move(loaded).value();
|
||||
else setsCache_.clear();
|
||||
return setsCache_;
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::onAddCard(wxWindow* parentWindow) {
|
||||
if (cardEditModalIsActive()) {
|
||||
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
|
||||
wxString::FromUTF8("Add card"), wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
YuGiOhBandaiCard fresh;
|
||||
fresh.amount = 1;
|
||||
fresh.condition = Condition::NearMint;
|
||||
// language defaults to Language::Japanese via the domain type.
|
||||
|
||||
YuGiOhBandaiCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create,
|
||||
fresh, &setsForDialog());
|
||||
themeModalDialog(&dlg, config_.current().theme);
|
||||
CardEditModalGuard modalGuard;
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
|
||||
auto added = collection_.add(Game::YuGiOhBandai, dlg.card());
|
||||
if (!added) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to add card: " + added.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
YuGiOhBandaiCard persisted = dlg.card();
|
||||
persisted.id = added.value();
|
||||
auto normalized = images_.normalizeNamesForPersistedCard(
|
||||
Game::YuGiOhBandai, persisted.id, persisted.set.name, persisted.name, persisted.images);
|
||||
if (normalized) {
|
||||
if (normalized.value() != persisted.images) {
|
||||
persisted.images = std::move(normalized).value();
|
||||
auto updated = collection_.update(Game::YuGiOhBandai, persisted);
|
||||
if (!updated) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Card added, but image name normalization failed to persist: " +
|
||||
updated.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Card added, but image rename to ID-prefixed format failed: " + normalized.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
refreshCollection(added.value());
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::onEditCard(wxWindow* parentWindow) {
|
||||
if (listPanel_ == nullptr) return;
|
||||
auto sel = listPanel_->selected();
|
||||
if (!sel) {
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (cardEditModalIsActive()) {
|
||||
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
|
||||
wxString::FromUTF8("Edit"), wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
YuGiOhBandaiCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit,
|
||||
*sel, &setsForDialog());
|
||||
themeModalDialog(&dlg, config_.current().theme);
|
||||
CardEditModalGuard modalGuard;
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
auto updated = collection_.update(Game::YuGiOhBandai, dlg.card());
|
||||
if (!updated) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update card: " + updated.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
refreshCollection();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::onDeleteCard(wxWindow* parentWindow) {
|
||||
if (listPanel_ == nullptr) return;
|
||||
auto sel = listPanel_->selected();
|
||||
if (!sel) {
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Delete",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (showThemedConfirmDialog(parentWindow, "Delete \"" + sel->name + "\"?",
|
||||
"Confirm") != wxID_YES) {
|
||||
return;
|
||||
}
|
||||
auto removed = collection_.remove(Game::YuGiOhBandai, sel->id);
|
||||
if (!removed) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to delete card: " + removed.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
refreshCollection();
|
||||
}
|
||||
|
||||
std::string YuGiOhBandaiGameView::onUpdateSets(wxWindow* parentWindow) {
|
||||
std::size_t setCount = 0;
|
||||
std::size_t packCount = 0;
|
||||
if (!downloadSetsAndCatalog(parentWindow, &setCount, &packCount)) {
|
||||
return "Update failed";
|
||||
}
|
||||
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(setCount) + " Yu-Gi-Oh! (Bandai) sets and " +
|
||||
std::to_string(packCount) + " set checklists.",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
return "Yu-Gi-Oh! (Bandai) sets updated.";
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::setFilter(std::string_view filter) {
|
||||
if (filterInput_ != nullptr) {
|
||||
const wxString wanted = wxString::FromUTF8(std::string(filter).c_str());
|
||||
if (filterInput_->GetValue() != wanted) {
|
||||
filterInput_->ChangeValue(wanted);
|
||||
if (filter.empty()) {
|
||||
filterInput_->SetHint(kBandaiFilterHint);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::nudgeSelection(int delta) {
|
||||
if (listPanel_) listPanel_->nudgeSelection(delta);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiGameView::applyTheme(const ThemePalette& palette) {
|
||||
if (contentPanel_) applyThemeToWindowTree(contentPanel_, palette, config_.current().theme);
|
||||
if (listPanel_) listPanel_->applyTheme(palette);
|
||||
if (selectedPanel_) selectedPanel_->applyTheme(palette);
|
||||
if (setCompletionPanel_) setCompletionPanel_->applyTheme(palette);
|
||||
refreshToolbarIcons(palette);
|
||||
refreshTabBarTheme(palette);
|
||||
applyPaletteToTextCtrl(filterInput_, palette, config_.current().theme);
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "ccm/ui/YuGiOhBandaiSelectedCardPanel.hpp"
|
||||
|
||||
#include "ccm/ui/SvgIcons.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
enum YuGiOhBandaiDetailKey : int {
|
||||
kName = 0,
|
||||
kSet,
|
||||
kSetNo,
|
||||
kRarity,
|
||||
kAmount,
|
||||
kCondition,
|
||||
kLanguage,
|
||||
kHolo,
|
||||
kSigned,
|
||||
kAltered,
|
||||
};
|
||||
} // namespace
|
||||
|
||||
YuGiOhBandaiSelectedCardPanel::YuGiOhBandaiSelectedCardPanel(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
CardPreviewService& cardPreview)
|
||||
: BaseSelectedCardPanel<YuGiOhBandaiCard>(parent, imageService, cardPreview) {
|
||||
buildLayout();
|
||||
}
|
||||
|
||||
std::vector<YuGiOhBandaiSelectedCardPanel::DetailRowSpec>
|
||||
YuGiOhBandaiSelectedCardPanel::declareDetailRows() const {
|
||||
return {
|
||||
{"Name", kName, "(no card selected)"},
|
||||
{"Set", kSet, ""},
|
||||
{"No.", kSetNo, ""},
|
||||
{"Rarity", kRarity, ""},
|
||||
{"Amount", kAmount, ""},
|
||||
{"Condition", kCondition, ""},
|
||||
{"Language", kLanguage, ""},
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<YuGiOhBandaiSelectedCardPanel::FlagIconSpec>
|
||||
YuGiOhBandaiSelectedCardPanel::declareFlagIcons() const {
|
||||
return {
|
||||
{kSvgHolo, "Holo", kHolo},
|
||||
{kSvgSigned, "Signed", kSigned},
|
||||
{kSvgAltered, "Altered", kAltered},
|
||||
};
|
||||
}
|
||||
|
||||
std::string YuGiOhBandaiSelectedCardPanel::detailValueFor(const YuGiOhBandaiCard& card,
|
||||
DetailKey key) const {
|
||||
switch (key) {
|
||||
case kName: return card.name;
|
||||
case kSet: return card.set.name;
|
||||
case kSetNo: return card.setNo;
|
||||
case kRarity: return card.rarity;
|
||||
case kAmount: return std::to_string(card.amount);
|
||||
case kCondition: return std::string(to_string(card.condition));
|
||||
case kLanguage: return std::string(to_string(card.language));
|
||||
case kNoteKey: return card.note;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool YuGiOhBandaiSelectedCardPanel::isFlagSet(const YuGiOhBandaiCard& card,
|
||||
DetailKey key) const {
|
||||
switch (key) {
|
||||
case kHolo: return card.holo;
|
||||
case kSigned: return card.signed_;
|
||||
case kAltered: return card.altered;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::tuple<std::string, std::string, std::string>
|
||||
YuGiOhBandaiSelectedCardPanel::previewKey(const YuGiOhBandaiCard& card) const {
|
||||
return {card.name, card.set.id, card.setNo};
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -0,0 +1,336 @@
|
||||
#include "ccm/ui/YuGiOhBandaiSetCompletionPanel.hpp"
|
||||
|
||||
#include "ccm/games/yugiohbandai/YuGiOhBandaiSetSource.hpp"
|
||||
#include "ccm/services/YuGiOhBandaiSetCompletion.hpp"
|
||||
|
||||
#include <wx/button.h>
|
||||
#include <wx/choice.h>
|
||||
#include <wx/cursor.h>
|
||||
#include <wx/gauge.h>
|
||||
#include <wx/listctrl.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/simplebook.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
|
||||
wxColour mutedTextColour(const ThemePalette& palette) {
|
||||
// Blend text toward panel background so missing checklist rows read as greyed.
|
||||
const auto blend = [](unsigned char a, unsigned char b) -> unsigned char {
|
||||
return static_cast<unsigned char>((static_cast<int>(a) * 2 + static_cast<int>(b)) / 3);
|
||||
};
|
||||
return wxColour(blend(palette.text.Red(), palette.panelBg.Red()),
|
||||
blend(palette.text.Green(), palette.panelBg.Green()),
|
||||
blend(palette.text.Blue(), palette.panelBg.Blue()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
YuGiOhBandaiSetCompletionPanel::YuGiOhBandaiSetCompletionPanel(
|
||||
wxWindow* parent, YuGiOhBandaiSetCatalogService& catalogStore)
|
||||
: wxPanel(parent), catalogStore_(catalogStore) {
|
||||
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, &YuGiOhBandaiSetCompletionPanel::onLanguageChoice,
|
||||
this);
|
||||
langRow->Add(langLabel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
|
||||
langRow->Add(languageChoice_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
book_ = new wxSimplebook(this, wxID_ANY);
|
||||
|
||||
gridPage_ = new wxPanel(book_);
|
||||
auto* gridRoot = new wxBoxSizer(wxVERTICAL);
|
||||
emptyLabel_ = new wxStaticText(gridPage_, wxID_ANY, "");
|
||||
emptyLabel_->Wrap(480);
|
||||
gridRoot->Add(emptyLabel_, 0, wxALL | wxEXPAND, 12);
|
||||
|
||||
scroll_ = new wxScrolledWindow(gridPage_, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxVSCROLL | wxTAB_TRAVERSAL);
|
||||
scroll_->SetScrollRate(0, 16);
|
||||
gridSizer_ = new wxBoxSizer(wxVERTICAL);
|
||||
scroll_->SetSizer(gridSizer_);
|
||||
gridRoot->Add(scroll_, 1, wxEXPAND);
|
||||
gridPage_->SetSizer(gridRoot);
|
||||
book_->AddPage(gridPage_, "Grid");
|
||||
|
||||
detailPage_ = new wxPanel(book_);
|
||||
auto* detailRoot = new wxBoxSizer(wxVERTICAL);
|
||||
auto* topRow = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* backBtn = new wxButton(detailPage_, wxID_ANY, "Back");
|
||||
backBtn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { showGridPage(); });
|
||||
detailTitle_ = new wxStaticText(detailPage_, wxID_ANY, "");
|
||||
auto titleFont = detailTitle_->GetFont();
|
||||
titleFont.MakeBold().MakeLarger();
|
||||
detailTitle_->SetFont(titleFont);
|
||||
topRow->Add(backBtn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
|
||||
topRow->Add(detailTitle_, 1, wxALIGN_CENTER_VERTICAL);
|
||||
detailRoot->Add(topRow, 0, wxEXPAND | wxALL, 8);
|
||||
|
||||
checklist_ = new wxListCtrl(detailPage_, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_NO_HEADER);
|
||||
checklist_->AppendColumn("Card", wxLIST_FORMAT_LEFT, 520);
|
||||
detailRoot->Add(checklist_, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 8);
|
||||
detailPage_->SetSizer(detailRoot);
|
||||
book_->AddPage(detailPage_, "Detail");
|
||||
|
||||
auto* root = new wxBoxSizer(wxVERTICAL);
|
||||
root->Add(langRow, 0, wxEXPAND | wxALL, 8);
|
||||
root->Add(book_, 1, wxEXPAND);
|
||||
SetSizer(root);
|
||||
|
||||
showGridPage();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::setCollection(std::vector<YuGiOhBandaiCard> cards) {
|
||||
collection_ = std::move(cards);
|
||||
refreshLanguageChoice();
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::reloadFromStore() {
|
||||
catalogLoaded_ = false;
|
||||
catalog_ = {};
|
||||
if (catalogStore_.exists()) {
|
||||
if (auto loaded = catalogStore_.load()) {
|
||||
catalog_ = std::move(loaded).value();
|
||||
catalogLoaded_ = true;
|
||||
}
|
||||
}
|
||||
showGridPage();
|
||||
rebuildGrid();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::applyTheme(const ThemePalette& palette) {
|
||||
palette_ = palette;
|
||||
applyThemeToWindowTree(this, palette, inferThemeFromWindow(this));
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::showGridPage() {
|
||||
detailSetId_.clear();
|
||||
detailSetName_.clear();
|
||||
book_->SetSelection(0);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::showChecklistPage(const std::string& setId,
|
||||
const std::string& setName) {
|
||||
detailSetId_ = setId;
|
||||
detailSetName_ = setName;
|
||||
detailTitle_->SetLabelText(wxString::FromUTF8(displaySetName(setName).c_str()));
|
||||
rebuildChecklist(setId);
|
||||
book_->SetSelection(1);
|
||||
}
|
||||
|
||||
std::string YuGiOhBandaiSetCompletionPanel::displaySetName(const std::string& setName) const {
|
||||
if (!languageFilter_.has_value()) return setName;
|
||||
return setName + " (" + std::string(to_string(*languageFilter_)) + ")";
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::refreshLanguageChoice() {
|
||||
const auto previous = languageFilter_;
|
||||
const auto present = yuGiOhBandaiLanguagesInCollection(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 YuGiOhBandaiSetCompletionPanel::onLanguageChoice(wxCommandEvent& /*event*/) {
|
||||
const int sel = languageChoice_->GetSelection();
|
||||
if (sel <= 0) {
|
||||
languageFilter_ = std::nullopt;
|
||||
} else {
|
||||
const auto present = yuGiOhBandaiLanguagesInCollection(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 YuGiOhBandaiSetCompletionPanel::rebuildCurrentView() {
|
||||
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
||||
const auto rows =
|
||||
computeYuGiOhBandaiSetCompletion(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 YuGiOhBandaiSetCompletionPanel::setEmptyMessage(const wxString& message) {
|
||||
clearGridTiles();
|
||||
emptyLabel_->SetLabelText(message);
|
||||
emptyLabel_->Wrap(480);
|
||||
emptyLabel_->Show();
|
||||
scroll_->Hide();
|
||||
gridPage_->Layout();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::clearGridTiles() {
|
||||
if (gridSizer_ == nullptr) return;
|
||||
gridSizer_->Clear(true);
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::rebuildGrid() {
|
||||
if (!catalogLoaded_) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"Set checklists are not downloaded yet.\n"
|
||||
"Run Sets \xE2\x86\x92 Update Yu-Gi-Oh! (Bandai) to enable Set Completion."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rows = computeYuGiOhBandaiSetCompletion(collection_, catalog_, languageFilter_);
|
||||
if (rows.empty()) {
|
||||
if (collection_.empty()) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"No Yu-Gi-Oh! (Bandai) sets in progress yet.\n"
|
||||
"Add cards on the Single Cards tab to track set completion here."));
|
||||
} else {
|
||||
bool anyCountable = false;
|
||||
for (const auto& card : collection_) {
|
||||
if (card.set.id.empty()) continue;
|
||||
if (YuGiOhBandaiSetSource::normalizeCardNumber(card.setNo).empty()) continue;
|
||||
anyCountable = true;
|
||||
break;
|
||||
}
|
||||
if (!anyCountable) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"Your cards need a set number (No.) to track set completion.\n"
|
||||
"Edit each card and enter its Bandai number, or use Auto detect."));
|
||||
} else {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"None of your cards match a downloaded set checklist.\n"
|
||||
"Run Sets \xE2\x86\x92 Update Yu-Gi-Oh! (Bandai), and confirm "
|
||||
"each card's set and No."));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
emptyLabel_->Hide();
|
||||
scroll_->Show();
|
||||
clearGridTiles();
|
||||
|
||||
for (const auto& row : rows) {
|
||||
auto* tile = new wxPanel(scroll_, wxID_ANY, wxDefaultPosition, wxDefaultSize,
|
||||
wxBORDER_SIMPLE);
|
||||
tile->SetBackgroundColour(palette_.panelBg);
|
||||
auto* tileSizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
const std::string title = displaySetName(row.setName);
|
||||
auto* nameLbl = new wxStaticText(tile, wxID_ANY, wxString::FromUTF8(title.c_str()));
|
||||
auto nameFont = nameLbl->GetFont();
|
||||
nameFont.MakeBold();
|
||||
nameLbl->SetFont(nameFont);
|
||||
nameLbl->SetForegroundColour(palette_.text);
|
||||
|
||||
const std::string counts =
|
||||
std::to_string(row.ownedUnique) + " / " + std::to_string(row.total) + " (" +
|
||||
std::to_string(row.percent()) + "%)";
|
||||
auto* countLbl = new wxStaticText(tile, wxID_ANY, wxString::FromUTF8(counts.c_str()));
|
||||
countLbl->SetForegroundColour(palette_.text);
|
||||
|
||||
auto* gauge = new wxGauge(tile, wxID_ANY, 100, wxDefaultPosition, wxSize(-1, 14),
|
||||
wxGA_HORIZONTAL | wxGA_SMOOTH);
|
||||
gauge->SetValue(row.percent());
|
||||
|
||||
tileSizer->Add(nameLbl, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 10);
|
||||
tileSizer->Add(countLbl, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 6);
|
||||
tileSizer->Add(gauge, 0, wxEXPAND | wxALL, 10);
|
||||
tile->SetSizer(tileSizer);
|
||||
|
||||
const std::string setId = row.setId;
|
||||
const std::string setName = row.setName;
|
||||
auto openDetail = [this, setId, setName](wxMouseEvent&) {
|
||||
showChecklistPage(setId, setName);
|
||||
};
|
||||
tile->Bind(wxEVT_LEFT_UP, openDetail);
|
||||
nameLbl->Bind(wxEVT_LEFT_UP, openDetail);
|
||||
countLbl->Bind(wxEVT_LEFT_UP, openDetail);
|
||||
gauge->Bind(wxEVT_LEFT_UP, openDetail);
|
||||
tile->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
nameLbl->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
countLbl->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
|
||||
gridSizer_->Add(tile, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 8);
|
||||
}
|
||||
gridSizer_->AddStretchSpacer(1);
|
||||
scroll_->FitInside();
|
||||
gridPage_->Layout();
|
||||
Layout();
|
||||
}
|
||||
|
||||
void YuGiOhBandaiSetCompletionPanel::rebuildChecklist(const std::string& setId) {
|
||||
checklist_->DeleteAllItems();
|
||||
const auto entries =
|
||||
yuGiOhBandaiChecklistForSet(collection_, catalog_, setId, languageFilter_);
|
||||
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;
|
||||
for (const auto& entry : entries) {
|
||||
// Align names: checkmark + two spaces vs four spaces for missing cards.
|
||||
std::string line =
|
||||
(entry.owned ? "\xE2\x9C\x93 " : " ") + entry.setNo + " \xE2\x80\x94 " + entry.name;
|
||||
if (!entry.rarity.empty()) {
|
||||
line += " (" + entry.rarity + ")";
|
||||
}
|
||||
const long row = checklist_->InsertItem(idx++, wxString::FromUTF8(line.c_str()));
|
||||
if (row < 0) continue;
|
||||
if (entry.owned) {
|
||||
checklist_->SetItemTextColour(row, ownedGreen);
|
||||
} else {
|
||||
checklist_->SetItemTextColour(row, muted);
|
||||
}
|
||||
}
|
||||
checklist_->SetColumnWidth(0, wxLIST_AUTOSIZE);
|
||||
detailPage_->Layout();
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -524,13 +524,7 @@ void YuGiOhGameView::applyTheme(const ThemePalette& palette) {
|
||||
if (setCompletionPanel_) setCompletionPanel_->applyTheme(palette);
|
||||
refreshToolbarIcons(palette);
|
||||
refreshTabBarTheme(palette);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
applyPaletteToTextCtrl(filterInput_, palette, config_.current().theme);
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
Reference in New Issue
Block a user