Files
Card-Collection-Manager-3/ui_wx/AGENTS.md
T
Sebastian Dine e5c830e945 minor: New Game Digimon Digi-Battle (#17)
* digimon digi battle added to supported games

* sonarqube update

* readme update

---------

Co-authored-by: sdine <sdine@sdine.com>
2026-07-19 12:06:57 +02:00

23 KiB
Raw Blame History

ui_wx/AGENTS.md

ccm_ui_wx static library — wxWidgets adapter. The only target that may include wx/... headers. Read the root AGENTS.md first.

Layer pointers

  • include/ccm/ui/AppContext.hpp — the boundary type. A struct of references to shared core services + per-game modules and a std::vector<IGameView*> of all UI bundles. UI code talks to core only through this struct (and the typed pointers go through IGameView, never directly).
  • include/ccm/ui/IGameView.hpp — abstract base class for per-game UI bundles. MainFrame only ever sees IGameView references; this is the seam that lets the frame swap between Magic, Pokemon, and any future TCG without knowing their card types.
  • include/ccm/ui/MainFrame.hpp + src/MainFrame.cpp — top-level window (default size 1210×770), menu strip (File / Game / Sets / Help), toolbar (Add / Edit / Delete + filter input), and the splitter that swaps the active IGameView's panels. The Game and Sets menus are built dynamically from AppContext::gameViews so adding a new game lights up its menu entries automatically. Filter and toolbar actions forward to activeView(). EVT_PREVIEW_STATUS (preview fetch outcome → status label; empty string resets to "Ready") is the only event the frame binds; EVT_CARD_SELECTED is bound per view (each IGameView connects its typed list panel to its typed selected panel internally). About is a custom themed dialog (not wxAboutBox) so dark mode behavior stays consistent.
  • include/ccm/ui/BaseCardListPanel.hpp — header-only template BaseCardListPanel<TCard, TSortColumn> that owns ALL the non-game-specific wxListCtrl machinery: hidden zero-width spacer column (legacy of the MSW comctl32 image-list gutter workaround, kept to preserve column-index math), themed header row (clickable to sort, edge-drag to resize, divider double-click to autosize), per-icon-column cached wxBitmap pairs (normal + selected color) consumed by IconListCtrl::MSWOnNotify so row icons are pixel-perfect centered under the themed-header icons, rebuild guard so DESELECTED/SELECTED storms collapse into a single bubbled EVT_CARD_SELECTED, case-insensitive substring filter via setFilter(...), per-column toggle-direction sort. Subclasses fill in column descriptors + per-row text + per-icon-column flag predicates + dispatch hooks (sortBy, matchesFilter).
  • include/ccm/ui/IconListCtrl.hpp + src/IconListCtrl.cpp — small wxListCtrl subclass that intercepts NM_CUSTOMDRAW on Windows and paints flag-icon sub-items at the exact center of each cell. It owns a HIMAGELIST (built from the cached wxBitmap pairs via straight-RGBA 32 bpp DIB sections) and draws each cell's icon with ImageList_Draw(ILD_TRANSPARENT) onto the native HDC from NMLVCUSTOMDRAW. This is the same low-level pixel path wxImageList uses internally, which is the only rendering path that has reliably preserved SVG transparency + correct fill color across light/dark themes on MSW. Two earlier attempts — wxGraphicsContext::DrawBitmap and a manually-premultiplied-DIB AlphaBlend — both rendered runtime-fill SVG icons as solid white in light mode and were abandoned (see convention 11). The custom-draw is purely about positioning; pixel format handling is delegated to comctl32.
  • include/ccm/ui/BaseSelectedCardPanel.hpp — header-only template BaseSelectedCardPanel<TCard> that owns the right-hand-side detail panel: preview image fetched via CardPreviewService (with the shared_ptr<State> + std::atomic alive/currentGen cancellation pattern), 2-column detail grid, flag-icon strip that collapses when no flags are set, image list with double-click viewer. If preview lookup fails or returns empty bytes, the panel loads a per-game card-back fallback: Magic and Pokémon use fixed HTTPS URLs (fallbackImageUrlForGame, CCM2-aligned); Yu-Gi-Oh! tries Yugipedia thumbnail URL, then full Back-EN.png on ms.yugipedia.com, then reads <exeDir>/assets/ygo_card_back.png; Digimon Digi-Battle reads <exeDir>/assets/digibattle99_card_back.png (both bundled assets copied by app/CMakeLists.txt on link). The constructor caches <exeDir>/ for that disk path. Subclasses describe the detail rows / flag icons / preview lookup (name, setId, setNo) and own a Game constant.
  • include/ccm/ui/BaseCardEditDialog.hpp — header-only template BaseCardEditDialog<TCard> that owns the standard Add/Edit form: Name, Set picker (read-only wxComboBox with prefix-match typeahead and case-insensitive id matching for legacy data), Amount spin, Language and Condition choices, Note, image management (Add multiple via wxFD_MULTIPLE, Remove, double-click to view), OK/Cancel + validation. The Set row is built on a host wxPanel with a horizontal wxBoxSizer; games may override customizeSetPickerRow(row, combo) to wrap the combo (default: combo only). After a programmatic selection, applySetSelectionByIndex updates card_.set and calls onSetSelectionApplied() (default no-op). After buildAndPopulate(), the template snapshots the loaded card into openingSnapshot_; in EditMode::Edit, OK asks Yes/No (“Save changes to this card?”) only when the card differs from that snapshot (dirty-only confirm). Create mode never prompts. Subclasses build the flags row (buildFlagsRow), append game-specific extra rows (e.g. Pokemon's Set #) via appendExtraRows, and copy values in/out of the typed card (readExtraFromCard / writeExtraToCard). The template binds EVT_TEXT on Name and invokes onCardLookupContextChanged() so games can drop stale keyed metadata when the user edits the lookup identity (Yu-Gi-Oh! clears its YGOPRODeck print-variant cache here). YuGiOhCardEditDialog overrides customizeSetPickerRow to add a SwitchCtrl pill switch plus a hint label (Set name / Set code), a text field, and Auto detect (resolves Set.id via ccm/util/YuGiOhSetLookup.hpp against availableSets(), then returns to the dropdown on success); it overrides onSetSelectionApplied to match manual set-change behavior. It additionally CallAfters a silent detectPrintVariants when opening Edit (and after changing Set) so multi-print Next buttons can appear without pressing Auto detect first, as long as name + display set are populated. The base also exposes helpers to sync current control values and inspect the currently-selected set when a subclass needs derived-field UI.
  • 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 ~50100 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/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.
  • include/ccm/ui/ImageViewerDialog.hpp + src/ImageViewerDialog.cpp — full-size viewer with prev/next.
  • include/ccm/ui/Theme.hpp + src/Theme.cpp — shared theme helpers and popup helpers (showThemedMessageDialog, showThemedConfirmDialog) for consistent dark/light dialogs. applyThemeToWindowTree paints wxButton, wxBitmapButton, and wxToggleButton in dark mode (custom wxEVT_PAINT + hover/focus) so native Win32 theming cannot flash a light hover plate; light mode leaves buttons native where possible. SwitchCtrl is palette-driven and self-painted (not native wxToggleButton).

Conventions

  1. Only consume core through AppContext. Do not include any header from ccm/infra/ here. The set of allowed ccm/... includes is domain/, services/, games/IGameModule.hpp, ports/ICardPreviewSource.hpp, and util/ headers that remain UI-agnostic (for example util/Result.hpp, util/YuGiOhPrintingSlot.hpp, util/YuGiOhSetLookup.hpp). Do not pull arbitrary util/ or games/ implementation headers beyond what a panel/dialog already needs for display or small shared helpers.
  2. Image decoding lives here, not in core. Use wxImage::LoadFile(path.string()) against the path returned by IImageStore::resolvePath. Core stays free of any image library.
  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.
  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 splitter swaps listPanel() / selectedPanel() when the user picks a different Game menu entry. Do not stand up parallel side-by-side tabs for different games.
  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.
  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.
  10. Sort key != display key. When you add a new column to a list panel, follow the existing pattern: the wxListCtrl cell text is one thing; the sort comparator lives in ccm::services::CardSorter and may key off a different field (the canonical case is set.name shown but set.releaseDate sorted, so collections list chronologically). New columns must extend MagicSortColumn / PokemonSortColumn and add a corresponding case in sortMagicCards / sortPokemonCards.
  11. wxListCtrl + flag-icon centering (MSW comctl32):
    • Native LVS_REPORT sub-item image rendering on MSW left-anchors the bitmap with a small built-in inset, regardless of wxLIST_FORMAT_CENTER. It can never align pixel-perfect with our wx-sizer-centered themed header icons, especially after column resize. Don't try to compensate by padding the image-list bitmap or nudging it horizontally — that path was tried and abandoned.
    • Authoritative path: flag-icon sub-items go through IconListCtrl::MSWOnNotify (NM_CUSTOMDRAW). It computes the live sub-item rect via LVM_GETSUBITEMRECT(LVIR_BOUNDS) and composites the cell's icon at the rect center with AlphaBlend(... AC_SRC_OVER | AC_SRC_ALPHA) straight onto cd->nmcd.hdc. Each (icon, selection-state) pair has its own pre-built premultiplied 32 bpp BGRA DIB section in dibBitmaps_; index i holds the normal variant and index i + iconColCount holds the selected variant. The cache rebuilds whenever the theme changes (via setIconBitmaps(...) from BaseCardListPanel::rebuildIconBitmaps).
    • We deliberately do not route through ImageList_Draw / HIMAGELIST here. On the verified MinGW-w64 + comctl32 v6 stack, ImageList_Draw on an ILC_COLOR32 list with ILD_TRANSPARENT ignored the alpha channel of the bitmap and the "transparent" canvas around each glyph painted as opaque black behind the icon — every row flag rendered as a black rectangle with a white glyph regardless of theme. AlphaBlend directly on the listctrl's HDC works in every case we've tested.
    • Bitmap format pitfall — AlphaBlend requires PREMULTIPLIED BGRA, not straight alpha. With straight RGBA the function returns FALSE (or, depending on the driver, paints garbage). makePremultipliedDib in IconListCtrl.cpp does the per-pixel premultiply with the rounded form (c * a + 127) / 255. Do not simplify that to c * a / 255 (loss of precision on c=0xFF, a=0xFF) and do not skip the divide-by-255 entirely (c * a overflows the byte and renders the icon as solid white — that was the original failure mode that made an earlier dev abandon premultiplication for a while). Hardcoded-fill SVGs (e.g. baked-in black/white badges) happen to look correct on every path and are not a useful sanity check on their own — always verify rendering against a runtime-fill icon (foil / signed / altered / holo) on both light and dark themes.
    • The hidden zero-width spacer column at index 0 stays. It's no longer load-bearing for any image-list gutter, but it keeps every other column index stable across the codebase. Start real columns at index 1.
    • Insert each row through the spacer column with a wxListItem whose mask includes wxLIST_MASK_IMAGE and image -1 so MSW doesn't try to render an item icon for column 0 if a public image list ever gets attached again.
    • AlphaBlend lives in msimg32.lib; ui_wx/CMakeLists.txt links msimg32 on WIN32. Don't rely on gdi32 being enough — AlphaBlend@44 is not in gdi32.
  12. Startup/dialog responsiveness rules:
    • Keep first paint fast: avoid heavy synchronous work in window/dialog constructors.
    • In MainFrame, defer initial collection load with CallAfter(...) so the frame paints before I/O/parsing.
    • Keep startup's "first row selected" behavior, but schedule initial selection with CallAfter(...) in BaseCardListPanel to avoid blocking first render.
    • Avoid reloading/reparsing sets on each Add/Edit open: each IGameView caches its own set list and passes it into the dialog by pointer.
    • Pass preloaded sets into BaseCardEditDialog by pointer/reference (not by value) to avoid vector copies per open.
    • For heavy dialog setup, wrap constructor-time UI population in Freeze() / Thaw() and append choice items in bulk via wxArrayString (BaseCardEditDialog::buildAndPopulate does this).
  13. String encoding on Windows (avoid mojibake):
    • Domain/service strings are UTF-8 std::string. Do not rely on implicit std::string <-> wxString conversions on Windows; those can route through the active ANSI codepage and render Pokémon as Pokémon.
    • UI display path (std::string -> wx control): always convert with wxString::FromUTF8(str.c_str()) before SetLabelText, SetItem, Append, control constructors, etc.
    • UI write-back path (wx control -> std::string): always convert with ToStdString(wxConvUTF8) so persisted/domain text stays UTF-8.
    • Apply this rule consistently in shared templates (BaseCardListPanel, BaseSelectedCardPanel, BaseCardEditDialog) because a single implicit conversion in those bases affects every game view.
  14. Theme consistency rules (Windows):
    • Treat dialog roots as panelBg, not a separate shade, otherwise label rows can look like mismatched darker boxes.
    • Theme dialogs before ShowModal() with applyThemeToWindowTree(...) (and root background/foreground colors as needed); this includes Settings, image viewer, About, and custom popup dialogs. Per-game Add/Edit flows use themeModalDialog(wxDialog*, Theme) from Theme.hpp so MagicGameView / PokemonGameView / YuGiOhGameView share one path instead of duplicating palette wiring.
    • Do not use native wxMessageBox / wxAboutBox for app-facing flows that must match dark mode. Use themed popup helpers (or a custom themed wxDialog) so body/buttons stay in sync with the app palette.
    • 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.
    • 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.
    • In High Contrast, use stronger hover/pressed deltas than regular dark mode and keep the button border in the foreground/text color for visibility (currently yellow in this palette).
    • When validating UI theming changes, rebuild and run ccm (the executable), not just ccm_ui_wx.
  15. Preview fallback behavior (CCM2 parity where applicable):
    • Keep unresolved external previews user-visible by showing a per-game card-back image in BaseSelectedCardPanel instead of a blank/transparent bitmap.
    • Magic / Pokémon use single fixed HTTPS URLs (Magic_card_back.jpg, Bulbagarden Cardback.jpg). Yu-Gi-Oh! uses Yugipedia-hosted backs plus a bundled PNG beside the exe (assets/ygo_card_back.png) when the network path fails. Digimon Digi-Battle uses a bundled PNG (assets/digibattle99_card_back.png) — keep those chains working when touching preview code.
    • If you change fallback sourcing (URLs or bundled asset), keep the "always show a reasonable card-back fallback" behavior intact for every game with remote previews.
  16. Per-game auto-detect controls:
    • Auto-detect actions in edit dialogs (e.g. detect set print number / rarity from API) are opt-in per game.
    • Keep shared templates game-agnostic: put buttons and detection behavior in <Name>CardEditDialog, not in BaseCardEditDialog. Yu-Gi-Oh!'s Set code entry (SwitchCtrl + text + Auto detect against cached sets) is wired through the template hook customizeSetPickerRow so Magic/Pokemon keep the default single-combo row unchanged.
    • For games that use composed print IDs (prefix + numeric suffix), allow user editing on the numeric portion and render the full code as a read-only derived label beside the input.

Required follow-ups

  • If you replace ui_wx/assets/ygo_card_back.png or ui_wx/assets/digibattle99_card_back.png, rebuild the ccm target so app/CMakeLists.txt's POST_BUILD copy refreshes <exeDir>/assets/; do not remove an asset without updating BaseSelectedCardPanel / docs/assets-and-info-apis.md.
  • After adding a new dialog/panel .cpp you must add it to ui_wx/CMakeLists.txt.
  • After adding a new menu action you must allocate an Ids::* value in MainFrame.hpp (don't reuse wxID_HIGHEST math inline) and Bind it in buildMenuBar. The dynamic Game / Sets menus consume the IdGameMenuBase / IdSetsMenuBase ranges; do not stomp on those id ranges.
  • After changing AppContext you must update app/main.cpp so the composition root populates the new field.
  • After adding a new icon to SvgIcons.{hpp,cpp} you must keep the @FILL@ placeholder so both light- and dark-variant rendering keeps working, and add a small unit-test-equivalent visual check by running the binary (no automated UI tests in this repo).
  • After changing one of the Base* template hooks (or adding a new one) you must keep docs/adding-a-new-game.md in sync — the per-game derived classes are the readers of that contract and the doc is what onboarding agents read first.

Adding a new game UI

  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().
  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).
  5. Register all new .cpp files in ui_wx/CMakeLists.txt.

Commands

Build UI only: cmake --build build --target ccm_ui_wx