mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-08-28 23:01:09 +00:00
Minor: Add Asian Pokemon Card Support (#18)
This commit is contained in:
+3
-3
@@ -9,8 +9,8 @@
|
||||
- `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 `CallAfter`s 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/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 West use fixed HTTPS URLs (`fallbackImageUrlForGame`, CCM2-aligned); **Pokémon Asia** uses the Japanese TCG back via `previewGameFor(card)` → `Game::JapanesePokemon`; **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; override `previewGameFor` when preview routing differs from collection `gameId()` (Pokemon West/Asia).
|
||||
- `include/ccm/ui/BaseCardEditDialog.hpp` — header-only template `BaseCardEditDialog<TCard>` that owns the standard Add/Edit form: Name, optional `appendPreSetRows` (Pokemon West/Asia region), Set picker (read-only `wxComboBox` with typeahead — prefix first, then substring, ASCII-fold so `Pokemon`/`Jungle` match `Pokémon Jungle` — and case-insensitive id matching for legacy data), Amount spin, Language and Condition choices (`languagesForChoice()` hook; Pokemon filters by region), 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 `CallAfter`s 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 ~50–100 lines of hook overrides on top of the matching base template.
|
||||
- `include/ccm/ui/Pokemon*.hpp` + `src/Pokemon*.cpp` — Pokemon implementations: `PokemonCardListPanel`, `PokemonSelectedCardPanel`, `PokemonCardEditDialog`, `PokemonGameView`. Same shape as the Magic ones; differences are limited to the Set # field, the Holo / 1. Edition flags, and the Pokemon TCG preview lookup key (which includes `setNo`).
|
||||
@@ -67,7 +67,7 @@
|
||||
- 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.
|
||||
- Magic / Pokémon use single fixed HTTPS URLs (`Magic_card_back.jpg`, Bulbagarden `Cardback.jpg`). Japanese Pokémon uses the Japanese TCG Bulbagarden back (`TCG_Card_Back_Japanese.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.
|
||||
|
||||
@@ -27,6 +27,7 @@ add_library(ccm_ui_wx STATIC
|
||||
src/SettingsDialog.cpp
|
||||
src/SwitchCtrl.cpp
|
||||
src/ImageViewerDialog.cpp
|
||||
src/VariantImagePreviewDialog.cpp
|
||||
src/IconListCtrl.cpp
|
||||
src/SvgIcons.cpp
|
||||
src/Theme.cpp
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,8 @@ struct AppContext {
|
||||
IGameModule& pokemonModule;
|
||||
IGameModule& yuGiOhModule;
|
||||
IGameModule& digiBattle99Module;
|
||||
// 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
|
||||
// Game menu; the composition root constructs them and hands raw
|
||||
// pointers in. `MainFrame` does not own these — `app/main.cpp` does.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// BaseCardEditDialog<TCard>
|
||||
//
|
||||
// Header-only template for the modal create/edit form. Owns the parts every
|
||||
// game shares — Name, Set picker (read-only combo with prefix typeahead),
|
||||
// game shares — Name, Set picker (read-only combo with typeahead: prefix first,
|
||||
// then substring, with ASCII-fold so "Pokemon"/"Jungle" match "Pokémon Jungle"),
|
||||
// Amount spin, Language and Condition choices, Note, Image list with
|
||||
// Add/Remove/double-click-to-view, OK/Cancel — and exposes hooks the
|
||||
// subclass uses to:
|
||||
@@ -48,6 +49,7 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -102,6 +104,9 @@ protected:
|
||||
// wants; the base only owns the surrounding label.
|
||||
virtual void buildFlagsRow(wxBoxSizer* flagsBox) = 0;
|
||||
|
||||
// Subclass adds any labelled rows between Name and Set. Default does nothing.
|
||||
virtual void appendPreSetRows(wxFlexGridSizer* /*grid*/) {}
|
||||
|
||||
// Subclass adds any extra game-specific labelled rows just below the
|
||||
// standard rows but above the Note row, by calling `appendRow(label, ctrl)`
|
||||
// (provided as a parameter). Default does nothing.
|
||||
@@ -114,6 +119,11 @@ protected:
|
||||
// Subclass copies the extra fields it owns from its widgets back into `card_`.
|
||||
virtual void writeExtraToCard() {}
|
||||
|
||||
// Languages offered in the Language choice. Default: allLanguages().
|
||||
[[nodiscard]] virtual std::span<const Language> languagesForChoice() const {
|
||||
return allLanguages();
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual std::string updateMenuName() const { return "Update Sets"; }
|
||||
|
||||
// Display name passed into errors and the dialog title hints.
|
||||
@@ -151,6 +161,12 @@ protected:
|
||||
return preloadedSets_ != nullptr ? *preloadedSets_ : sets_;
|
||||
}
|
||||
|
||||
void setPreloadedSetsPointer(const std::vector<Set>* sets) noexcept {
|
||||
preloadedSets_ = sets;
|
||||
}
|
||||
|
||||
void refreshSetAndLanguageChoices() { populateChoices(); }
|
||||
|
||||
// Default: combo only. Yu-Gi-Oh! overrides to add set-code entry + toggle.
|
||||
virtual void customizeSetPickerRow(wxBoxSizer& row, wxComboBox* combo) {
|
||||
row.Add(combo, 1, wxEXPAND);
|
||||
@@ -193,6 +209,9 @@ private:
|
||||
});
|
||||
appendRow(grid, "Name", nameCtrl_);
|
||||
|
||||
// Optional rows between Name and Set (e.g. Pokemon West/Asia region).
|
||||
appendPreSetRows(grid);
|
||||
|
||||
// `setCombo_` must be parented to `setHost` so every control in the Set row
|
||||
// shares the same `wxPanel`; otherwise the combo stays a direct child of the
|
||||
// dialog while the sizer lives on `setHost`, which corrupts layout on MSW.
|
||||
@@ -311,9 +330,10 @@ private:
|
||||
languageChoice_->Clear();
|
||||
int langIdx = 0;
|
||||
int i = 0;
|
||||
const auto langsForChoice = languagesForChoice();
|
||||
wxArrayString langs;
|
||||
langs.Alloc(allLanguages().size());
|
||||
for (auto l : allLanguages()) {
|
||||
langs.Alloc(langsForChoice.size());
|
||||
for (auto l : langsForChoice) {
|
||||
const std::string lang = std::string(to_string(l));
|
||||
langs.Add(wxString::FromUTF8(lang.c_str()));
|
||||
if (l == card_.language) langIdx = i;
|
||||
@@ -486,20 +506,43 @@ private:
|
||||
#endif
|
||||
}
|
||||
|
||||
// Fold a few Latin-1 diacritics so typing ASCII "Pokemon" matches "Pokémon".
|
||||
[[nodiscard]] static wxString foldAsciiForTypeahead(wxString s) {
|
||||
s.MakeLower();
|
||||
s.Replace(wxString::FromUTF8("\xc3\xa9"), wxT("e")); // é
|
||||
s.Replace(wxString::FromUTF8("\xc3\x89"), wxT("e")); // É (after lower: é)
|
||||
s.Replace(wxString::FromUTF8("\xc3\xa8"), wxT("e")); // è
|
||||
s.Replace(wxString::FromUTF8("\xc3\xaa"), wxT("e")); // ê
|
||||
s.Replace(wxString::FromUTF8("\xc3\xa0"), wxT("a")); // à
|
||||
s.Replace(wxString::FromUTF8("\xc3\xa1"), wxT("a")); // á
|
||||
s.Replace(wxString::FromUTF8("\xc3\xb1"), wxT("n")); // ñ
|
||||
s.Replace(wxString::FromUTF8("\xc3\xbc"), wxT("u")); // ü
|
||||
s.Replace(wxString::FromUTF8("\xc3\xb6"), wxT("o")); // ö
|
||||
return s;
|
||||
}
|
||||
|
||||
void applySetTypeaheadSelection() {
|
||||
const auto& available = availableSets();
|
||||
if (!setCombo_ || available.empty()) return;
|
||||
wxString pref = setTypeaheadPrefix_;
|
||||
pref.MakeLower();
|
||||
const wxString pref = foldAsciiForTypeahead(setTypeaheadPrefix_);
|
||||
if (pref.empty()) return;
|
||||
// Prefer prefix matches, then substring (so "Jungle" finds "Pokémon Jungle").
|
||||
for (std::size_t i = 0; i < available.size(); ++i) {
|
||||
wxString name(wxString::FromUTF8(available[i].name));
|
||||
name.MakeLower();
|
||||
const wxString name =
|
||||
foldAsciiForTypeahead(wxString::FromUTF8(available[i].name));
|
||||
if (name.StartsWith(pref)) {
|
||||
setCombo_->SetSelection(static_cast<int>(i));
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < available.size(); ++i) {
|
||||
const wxString name =
|
||||
foldAsciiForTypeahead(wxString::FromUTF8(available[i].name));
|
||||
if (name.Contains(pref)) {
|
||||
setCombo_->SetSelection(static_cast<int>(i));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onSetComboChar(wxKeyEvent& ev) {
|
||||
|
||||
@@ -217,6 +217,12 @@ protected:
|
||||
|
||||
[[nodiscard]] virtual Game gameId() const noexcept = 0;
|
||||
|
||||
// Preview / card-back routing. Defaults to `gameId()`; Pokemon overrides
|
||||
// so Asia cards use the JapanesePokemon preview source + card back.
|
||||
[[nodiscard]] virtual Game previewGameFor(const TCard& /*card*/) const noexcept {
|
||||
return gameId();
|
||||
}
|
||||
|
||||
// Construction ------------------------------------------------------------
|
||||
|
||||
BaseSelectedCardPanel(wxWindow* parent,
|
||||
@@ -292,6 +298,9 @@ private:
|
||||
case Game::Pokemon:
|
||||
// Mirrors CCM2's unresolved-preview fallback image.
|
||||
return "https://archives.bulbagarden.net/media/upload/1/17/Cardback.jpg";
|
||||
case Game::JapanesePokemon:
|
||||
// Japanese TCG back (distinct from the Western Cardback.jpg).
|
||||
return "https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg";
|
||||
case Game::YuGiOh:
|
||||
// Yugipedia English TCG backing (thumbnail — smaller than full scan).
|
||||
return "https://ms.yugipedia.com/thumb/e/e5/Back-EN.png/250px-Back-EN.png";
|
||||
@@ -379,7 +388,7 @@ private:
|
||||
auto state = state_;
|
||||
CardPreviewService* svcPtr = &cardPreview_;
|
||||
auto [name, setId, setNo] = previewKey(card);
|
||||
const Game game = gameId();
|
||||
const Game game = previewGameFor(card);
|
||||
const std::string exeDirCopy = exeDirForBundledAssets_;
|
||||
|
||||
std::thread([state, gen, svcPtr, name = std::move(name),
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
// PokemonCardEditDialog: typed Add/Edit form for a `PokemonCard`. Inherits
|
||||
// the shared layout, set picker, and image management from
|
||||
// `BaseCardEditDialog<PokemonCard>` and adds:
|
||||
// - a `Set #` text input (between the Set picker and the Amount spin)
|
||||
// - `Holo`, `1. Edition`, `Signed`, `Altered` check boxes in the flags row
|
||||
// PokemonCardEditDialog: Add/Edit form for a unified West/Asia PokemonCard.
|
||||
// West/Asia switch drives set lists, language choices, preview APIs, and the
|
||||
// Asia-only UnnumberedPromo print UX (from the former Japanese dialog).
|
||||
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/ports/ICardPreviewSource.hpp"
|
||||
#include "ccm/services/CardPreviewService.hpp"
|
||||
#include "ccm/ui/BaseCardEditDialog.hpp"
|
||||
#include "ccm/ui/SwitchCtrl.hpp"
|
||||
|
||||
#include <wx/button.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
class VariantImagePreviewDialog;
|
||||
|
||||
class PokemonCardEditDialog final : public BaseCardEditDialog<PokemonCard> {
|
||||
public:
|
||||
PokemonCardEditDialog(wxWindow* parent,
|
||||
@@ -27,22 +31,27 @@ public:
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
PokemonCard initial,
|
||||
const std::vector<Set>* preloadedSets = nullptr);
|
||||
const std::vector<Set>* westSets = nullptr,
|
||||
const std::vector<Set>* asiaSets = nullptr);
|
||||
~PokemonCardEditDialog() override;
|
||||
|
||||
protected:
|
||||
void appendPreSetRows(wxFlexGridSizer* grid) override;
|
||||
void buildFlagsRow(wxBoxSizer* flagsBox) override;
|
||||
void appendExtraRows(wxFlexGridSizer* grid) override;
|
||||
void readExtraFromCard() override;
|
||||
void writeExtraToCard() override;
|
||||
[[nodiscard]] std::string updateMenuName() const override { return "Update Pokemon"; }
|
||||
void onCardLookupContextChanged() override;
|
||||
[[nodiscard]] std::span<const Language> languagesForChoice() const override;
|
||||
|
||||
private:
|
||||
struct VariantFetchState {
|
||||
std::atomic<bool> alive{true};
|
||||
};
|
||||
|
||||
void onRegionSwitch(wxCommandEvent&);
|
||||
void applyRegion(PokemonRegion region, bool clearSetIfMissing);
|
||||
void onAutoDetectSetNo(wxCommandEvent&);
|
||||
void onNextSetNo(wxCommandEvent&);
|
||||
void onSetSelectionChanged(wxCommandEvent&);
|
||||
@@ -60,15 +69,37 @@ private:
|
||||
void rebuildVariantRingFromCache();
|
||||
void syncRingPositionToControls();
|
||||
void refreshVariantNextControls();
|
||||
void refreshSetNoRowMode();
|
||||
void applySelectedSetNo(std::string setNo);
|
||||
void stepVariantRing(int delta);
|
||||
void scheduleDeferredVariantPrefetch();
|
||||
void prefetchVariantsForCurrentCardSilent(unsigned capturedEpoch);
|
||||
void closeUnnumberedPreview();
|
||||
void ensureUnnumberedPreviewOpen();
|
||||
void refreshUnnumberedPreview();
|
||||
void requestUnnumberedPreviewAsync(unsigned capturedEpoch,
|
||||
std::string name,
|
||||
std::string setId,
|
||||
std::string setNo,
|
||||
std::size_t ringIndex,
|
||||
std::size_t ringCount);
|
||||
[[nodiscard]] bool isUnnumberedPromoSelected() const;
|
||||
[[nodiscard]] std::string currentRingSetNo() const;
|
||||
[[nodiscard]] Game backendGame() const noexcept;
|
||||
[[nodiscard]] PokemonRegion currentRegion() const noexcept;
|
||||
[[nodiscard]] static std::string storedSetNoFromControls(const wxTextCtrl* ctrl);
|
||||
[[nodiscard]] static std::string normalizedStoredSetNo(std::string_view setNo);
|
||||
|
||||
EditMode dialogMode_;
|
||||
unsigned variantFetchEpoch_{0};
|
||||
unsigned previewFetchEpoch_{0};
|
||||
CardPreviewService& cardPreview_;
|
||||
const std::vector<Set>* westSets_{nullptr};
|
||||
const std::vector<Set>* asiaSets_{nullptr};
|
||||
std::shared_ptr<VariantFetchState> variantFetchState_;
|
||||
|
||||
SwitchCtrl* regionSwitch_{nullptr};
|
||||
wxStaticText* setNoLabel_{nullptr};
|
||||
wxTextCtrl* setNoCtrl_{nullptr};
|
||||
wxButton* autoSetNoBtn_{nullptr};
|
||||
wxButton* nextSetNoBtn_{nullptr};
|
||||
@@ -76,7 +107,9 @@ private:
|
||||
wxCheckBox* firstEditionCheck_{nullptr};
|
||||
wxCheckBox* signedCheck_{nullptr};
|
||||
wxCheckBox* alteredCheck_{nullptr};
|
||||
VariantImagePreviewDialog* unnumberedPreview_{nullptr};
|
||||
|
||||
std::string selectedSetNo_;
|
||||
std::vector<AutoDetectedPrint> cachedVariants_;
|
||||
std::vector<std::string> uniqueSetNos_;
|
||||
std::size_t setNoRingPos_{0};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
// PokemonGameView: IGameView for the Pokemon TCG. Mirrors `MagicGameView` —
|
||||
// owns the Pokemon-typed list, selected, and edit-dialog widgets and
|
||||
// delegates persistence to a `CollectionService<PokemonCard>` reference
|
||||
// supplied by the composition root.
|
||||
// PokemonGameView: unified West + Asia Pokemon UI. One collection file;
|
||||
// separate West/Asia set caches; Sets > Update Pokemon refreshes both.
|
||||
|
||||
#include "ccm/domain/PokemonCard.hpp"
|
||||
#include "ccm/games/IGameModule.hpp"
|
||||
@@ -49,7 +47,7 @@ public:
|
||||
|
||||
private:
|
||||
void ensureSetsLoaded();
|
||||
const std::vector<Set>& setsForDialog();
|
||||
const std::vector<Set>& setsForDialog(PokemonRegion region);
|
||||
|
||||
ConfigService& config_;
|
||||
CollectionService<PokemonCard>& collection_;
|
||||
@@ -60,7 +58,8 @@ private:
|
||||
|
||||
PokemonCardListPanel* listPanel_{nullptr};
|
||||
PokemonSelectedCardPanel* selectedPanel_{nullptr};
|
||||
std::vector<Set> setsCache_;
|
||||
std::vector<Set> setsCacheWest_;
|
||||
std::vector<Set> setsCacheAsia_;
|
||||
bool attemptedInitialSetLoad_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ protected:
|
||||
[[nodiscard]] std::tuple<std::string, std::string, std::string>
|
||||
previewKey(const PokemonCard& card) const override;
|
||||
[[nodiscard]] Game gameId() const noexcept override { return Game::Pokemon; }
|
||||
[[nodiscard]] Game previewGameFor(const PokemonCard& card) const noexcept override {
|
||||
return pokemonBackendGame(card.region);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
// VariantImagePreviewDialog: small modeless popup that shows a single card
|
||||
// preview image (bytes decoded as wxImage). Used by Japanese Pokémon Add/Edit
|
||||
// when cycling UnnumberedPromo prints. Prev/Next fire custom events so the
|
||||
// edit dialog owns the variant ring.
|
||||
|
||||
#include <wx/button.h>
|
||||
#include <wx/dialog.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/string.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
wxDECLARE_EVENT(EVT_VARIANT_PREVIEW_PREV, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_VARIANT_PREVIEW_NEXT, wxCommandEvent);
|
||||
|
||||
class VariantImagePreviewDialog : public wxDialog {
|
||||
public:
|
||||
explicit VariantImagePreviewDialog(wxWindow* parent);
|
||||
|
||||
void setCaption(const wxString& caption);
|
||||
void setImageBytes(std::string_view bytes);
|
||||
void clearImage();
|
||||
void setNavigationEnabled(bool enabled);
|
||||
void repositionBesideParent();
|
||||
|
||||
private:
|
||||
class ImageCanvas;
|
||||
|
||||
void onPrev(wxCommandEvent&);
|
||||
void onNext(wxCommandEvent&);
|
||||
|
||||
ImageCanvas* imageHost_{nullptr};
|
||||
wxStaticText* caption_{nullptr};
|
||||
wxButton* prevButton_{nullptr};
|
||||
wxButton* nextButton_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace ccm::ui
|
||||
@@ -41,10 +41,11 @@ constexpr const char kFilterInputHint[] = "Filter";
|
||||
|
||||
std::string dirNameForGame(Game g) {
|
||||
switch (g) {
|
||||
case Game::Magic: return "magic";
|
||||
case Game::Pokemon: return "pokemon";
|
||||
case Game::YuGiOh: return "yugioh";
|
||||
case Game::DigiBattle99: return "digibattle99";
|
||||
case Game::Magic: return "magic";
|
||||
case Game::Pokemon: return "pokemon";
|
||||
case Game::YuGiOh: return "yugioh";
|
||||
case Game::DigiBattle99: return "digibattle99";
|
||||
case Game::JapanesePokemon: return "pokemon";
|
||||
}
|
||||
return "magic";
|
||||
}
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
#include "ccm/ui/PokemonCardEditDialog.hpp"
|
||||
|
||||
#include "ccm/domain/Enums.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonCardPreviewSource.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
#include "ccm/ui/VariantImagePreviewDialog.hpp"
|
||||
|
||||
#include <wx/app.h>
|
||||
#include <wx/panel.h>
|
||||
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ccm::ui {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kUnnumberedPromoSetId = "UnnumberedPromo";
|
||||
|
||||
constexpr const char* kJapanesePokemonCardBackUrl =
|
||||
"https://archives.bulbagarden.net/media/upload/2/2a/TCG_Card_Back_Japanese.jpg";
|
||||
|
||||
const std::vector<Set> kEmptySets;
|
||||
|
||||
bool languageAllowedForRegion(Language lang, PokemonRegion region) {
|
||||
for (const auto l : languagesForPokemonRegion(region)) {
|
||||
if (l == lang) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PokemonCardEditDialog::PokemonCardEditDialog(wxWindow* parent,
|
||||
ImageService& imageService,
|
||||
@@ -14,15 +36,22 @@ PokemonCardEditDialog::PokemonCardEditDialog(wxWindow* parent,
|
||||
CardPreviewService& cardPreview,
|
||||
EditMode mode,
|
||||
PokemonCard initial,
|
||||
const std::vector<Set>* preloadedSets)
|
||||
const std::vector<Set>* westSets,
|
||||
const std::vector<Set>* asiaSets)
|
||||
: BaseCardEditDialog<PokemonCard>(
|
||||
parent,
|
||||
mode == EditMode::Create ? "Add Pokemon Card" : "Edit Pokemon Card",
|
||||
imageService, setService, mode, std::move(initial), Game::Pokemon, preloadedSets),
|
||||
imageService, setService, mode, PokemonCard{}, Game::Pokemon, nullptr),
|
||||
dialogMode_(mode),
|
||||
cardPreview_(cardPreview),
|
||||
westSets_(westSets),
|
||||
asiaSets_(asiaSets),
|
||||
variantFetchState_(std::make_shared<VariantFetchState>()) {
|
||||
const PokemonRegion region = initial.region;
|
||||
mutableCard() = std::move(initial);
|
||||
setPreloadedSetsPointer(region == PokemonRegion::Asia ? asiaSets_ : westSets_);
|
||||
buildAndPopulate();
|
||||
refreshSetNoRowMode();
|
||||
if (dialogMode_ == EditMode::Edit) {
|
||||
scheduleDeferredVariantPrefetch();
|
||||
}
|
||||
@@ -32,12 +61,40 @@ PokemonCardEditDialog::~PokemonCardEditDialog() {
|
||||
if (variantFetchState_) {
|
||||
variantFetchState_->alive.store(false);
|
||||
}
|
||||
closeUnnumberedPreview();
|
||||
}
|
||||
|
||||
PokemonRegion PokemonCardEditDialog::currentRegion() const noexcept {
|
||||
return constCard().region;
|
||||
}
|
||||
|
||||
Game PokemonCardEditDialog::backendGame() const noexcept {
|
||||
return pokemonBackendGame(currentRegion());
|
||||
}
|
||||
|
||||
std::span<const Language> PokemonCardEditDialog::languagesForChoice() const {
|
||||
return languagesForPokemonRegion(currentRegion());
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::onCardLookupContextChanged() {
|
||||
clearCachedPrintVariants();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::appendPreSetRows(wxFlexGridSizer* grid) {
|
||||
auto* regionPanel = new wxPanel(this, wxID_ANY);
|
||||
auto* row = new wxBoxSizer(wxHORIZONTAL);
|
||||
regionSwitch_ = new SwitchCtrl(regionPanel, wxID_ANY,
|
||||
constCard().region == PokemonRegion::Asia);
|
||||
row->Add(new wxStaticText(regionPanel, wxID_ANY, wxString::FromUTF8("West")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
row->Add(regionSwitch_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
|
||||
row->Add(new wxStaticText(regionPanel, wxID_ANY, wxString::FromUTF8("Asia")),
|
||||
0, wxALIGN_CENTER_VERTICAL);
|
||||
regionPanel->SetSizer(row);
|
||||
regionSwitch_->Bind(EVT_CCM_SWITCH, &PokemonCardEditDialog::onRegionSwitch, this);
|
||||
appendRow(grid, "Region", regionPanel);
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
|
||||
holoCheck_ = new wxCheckBox(this, wxID_ANY, "Holo");
|
||||
firstEditionCheck_ = new wxCheckBox(this, wxID_ANY, "1. Edition");
|
||||
@@ -50,6 +107,7 @@ void PokemonCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
|
||||
setNoLabel_ = new wxStaticText(this, wxID_ANY, "Set #");
|
||||
auto* setNoPanel = new wxPanel(this, wxID_ANY);
|
||||
setNoCtrl_ = new wxTextCtrl(setNoPanel, wxID_ANY);
|
||||
autoSetNoBtn_ = new wxButton(setNoPanel, wxID_ANY, "Auto detect");
|
||||
@@ -63,20 +121,63 @@ void PokemonCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
|
||||
setNoRow->Add(nextSetNoBtn_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
setNoPanel->SetSizer(setNoRow);
|
||||
|
||||
appendRow(grid, "Set #", setNoPanel);
|
||||
grid->Add(setNoLabel_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
grid->Add(setNoPanel, 1, wxEXPAND);
|
||||
|
||||
if (auto* setCombo = setComboControl()) {
|
||||
setCombo->Bind(wxEVT_COMBOBOX, &PokemonCardEditDialog::onSetSelectionChanged, this);
|
||||
}
|
||||
}
|
||||
|
||||
std::string PokemonCardEditDialog::normalizedStoredSetNo(std::string_view setNo) {
|
||||
std::string out(setNo);
|
||||
const auto slash = out.find('/');
|
||||
if (slash != std::string::npos) {
|
||||
out.resize(slash);
|
||||
void PokemonCardEditDialog::onRegionSwitch(wxCommandEvent&) {
|
||||
const PokemonRegion next =
|
||||
(regionSwitch_ && regionSwitch_->GetValue()) ? PokemonRegion::Asia
|
||||
: PokemonRegion::West;
|
||||
applyRegion(next, true);
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::applyRegion(PokemonRegion region, bool clearSetIfMissing) {
|
||||
mutableCard().region = region;
|
||||
if (regionSwitch_ && regionSwitch_->GetValue() != (region == PokemonRegion::Asia)) {
|
||||
regionSwitch_->SetValue(region == PokemonRegion::Asia, false);
|
||||
}
|
||||
return out;
|
||||
|
||||
if (!languageAllowedForRegion(mutableCard().language, region)) {
|
||||
mutableCard().language = defaultLanguageForPokemonRegion(region);
|
||||
}
|
||||
|
||||
const std::vector<Set>* sets =
|
||||
region == PokemonRegion::Asia
|
||||
? (asiaSets_ != nullptr ? asiaSets_ : &kEmptySets)
|
||||
: (westSets_ != nullptr ? westSets_ : &kEmptySets);
|
||||
setPreloadedSetsPointer(sets);
|
||||
|
||||
const std::string prevSetId = mutableCard().set.id;
|
||||
bool setStillValid = false;
|
||||
for (const auto& s : availableSets()) {
|
||||
if (s.id == prevSetId) {
|
||||
setStillValid = true;
|
||||
mutableCard().set = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (clearSetIfMissing && !setStillValid) {
|
||||
mutableCard().set = Set{};
|
||||
mutableCard().setNo.clear();
|
||||
selectedSetNo_.clear();
|
||||
if (setNoCtrl_) setNoCtrl_->ChangeValue(wxEmptyString);
|
||||
}
|
||||
|
||||
refreshSetAndLanguageChoices();
|
||||
clearCachedPrintVariants();
|
||||
refreshSetNoRowMode();
|
||||
scheduleDeferredVariantPrefetch();
|
||||
Layout();
|
||||
if (GetSizer()) Fit();
|
||||
}
|
||||
|
||||
std::string PokemonCardEditDialog::normalizedStoredSetNo(std::string_view setNo) {
|
||||
return JapanesePokemonCardPreviewSource::normalizeLocalId(setNo);
|
||||
}
|
||||
|
||||
std::string PokemonCardEditDialog::storedSetNoFromControls(const wxTextCtrl* ctrl) {
|
||||
@@ -84,20 +185,78 @@ std::string PokemonCardEditDialog::storedSetNoFromControls(const wxTextCtrl* ctr
|
||||
return normalizedStoredSetNo(ctrl->GetValue().ToStdString(wxConvUTF8));
|
||||
}
|
||||
|
||||
bool PokemonCardEditDialog::isUnnumberedPromoSelected() const {
|
||||
if (currentRegion() != PokemonRegion::Asia) return false;
|
||||
if (const auto* set = selectedSetFromControls()) {
|
||||
return set->id == kUnnumberedPromoSetId;
|
||||
}
|
||||
return constCard().set.id == kUnnumberedPromoSetId;
|
||||
}
|
||||
|
||||
std::string PokemonCardEditDialog::currentRingSetNo() const {
|
||||
if (!uniqueSetNos_.empty() && setNoRingPos_ < uniqueSetNos_.size()) {
|
||||
return uniqueSetNos_[setNoRingPos_];
|
||||
}
|
||||
return selectedSetNo_;
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::applySelectedSetNo(std::string setNo) {
|
||||
selectedSetNo_ = normalizedStoredSetNo(setNo);
|
||||
if (setNoCtrl_ && setNoCtrl_->IsShown()) {
|
||||
setNoCtrl_->ChangeValue(wxString::FromUTF8(selectedSetNo_.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::refreshSetNoRowMode() {
|
||||
const bool unnumbered = isUnnumberedPromoSelected();
|
||||
if (setNoLabel_) {
|
||||
setNoLabel_->SetLabelText(unnumbered ? wxString::FromUTF8("Print")
|
||||
: wxString::FromUTF8("Set #"));
|
||||
}
|
||||
if (setNoCtrl_) {
|
||||
setNoCtrl_->Show(!unnumbered);
|
||||
if (!unnumbered && !selectedSetNo_.empty()) {
|
||||
setNoCtrl_->ChangeValue(wxString::FromUTF8(selectedSetNo_.c_str()));
|
||||
}
|
||||
if (auto* parent = setNoCtrl_->GetParent()) {
|
||||
parent->Layout();
|
||||
}
|
||||
}
|
||||
if (!unnumbered) {
|
||||
closeUnnumberedPreview();
|
||||
}
|
||||
Layout();
|
||||
if (GetSizer()) Fit();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::readExtraFromCard() {
|
||||
clearCachedPrintVariants();
|
||||
selectedSetNo_ = normalizedStoredSetNo(constCard().setNo);
|
||||
if (setNoCtrl_) {
|
||||
setNoCtrl_->ChangeValue(
|
||||
wxString::FromUTF8(normalizedStoredSetNo(constCard().setNo).c_str()));
|
||||
setNoCtrl_->ChangeValue(wxString::FromUTF8(selectedSetNo_.c_str()));
|
||||
}
|
||||
if (holoCheck_) holoCheck_->SetValue(constCard().holo);
|
||||
if (firstEditionCheck_) firstEditionCheck_->SetValue(constCard().firstEdition);
|
||||
if (signedCheck_) signedCheck_->SetValue(constCard().signed_);
|
||||
if (alteredCheck_) alteredCheck_->SetValue(constCard().altered);
|
||||
if (regionSwitch_) {
|
||||
regionSwitch_->SetValue(constCard().region == PokemonRegion::Asia, false);
|
||||
}
|
||||
refreshSetNoRowMode();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::writeExtraToCard() {
|
||||
if (setNoCtrl_) mutableCard().setNo = storedSetNoFromControls(setNoCtrl_);
|
||||
mutableCard().region =
|
||||
(regionSwitch_ && regionSwitch_->GetValue()) ? PokemonRegion::Asia
|
||||
: PokemonRegion::West;
|
||||
if (isUnnumberedPromoSelected()) {
|
||||
mutableCard().setNo = normalizedStoredSetNo(selectedSetNo_);
|
||||
} else if (setNoCtrl_) {
|
||||
selectedSetNo_ = storedSetNoFromControls(setNoCtrl_);
|
||||
mutableCard().setNo = selectedSetNo_;
|
||||
} else {
|
||||
mutableCard().setNo = normalizedStoredSetNo(selectedSetNo_);
|
||||
}
|
||||
if (holoCheck_) mutableCard().holo = holoCheck_->IsChecked();
|
||||
if (firstEditionCheck_) mutableCard().firstEdition = firstEditionCheck_->IsChecked();
|
||||
if (signedCheck_) mutableCard().signed_ = signedCheck_->IsChecked();
|
||||
@@ -106,9 +265,11 @@ void PokemonCardEditDialog::writeExtraToCard() {
|
||||
|
||||
void PokemonCardEditDialog::clearCachedPrintVariants() {
|
||||
++variantFetchEpoch_;
|
||||
++previewFetchEpoch_;
|
||||
cachedVariants_.clear();
|
||||
uniqueSetNos_.clear();
|
||||
setNoRingPos_ = 0;
|
||||
closeUnnumberedPreview();
|
||||
refreshVariantNextControls();
|
||||
}
|
||||
|
||||
@@ -142,9 +303,10 @@ void PokemonCardEditDialog::requestVariantsAsync(unsigned capturedEpoch,
|
||||
auto state = variantFetchState_;
|
||||
CardPreviewService* svc = &cardPreview_;
|
||||
PokemonCardEditDialog* self = this;
|
||||
const Game game = backendGame();
|
||||
std::thread([state, svc, self, capturedEpoch, name = std::move(name),
|
||||
setId = std::move(setId), fillSetNoOnSuccess, showFailureDialog]() {
|
||||
auto detected = svc->detectPrintVariants(Game::Pokemon, name, setId);
|
||||
setId = std::move(setId), fillSetNoOnSuccess, showFailureDialog, game]() {
|
||||
auto detected = svc->detectPrintVariants(game, name, setId);
|
||||
wxTheApp->CallAfter([state, self, capturedEpoch, detected = std::move(detected),
|
||||
fillSetNoOnSuccess, showFailureDialog]() mutable {
|
||||
if (!state->alive.load()) return;
|
||||
@@ -173,14 +335,20 @@ void PokemonCardEditDialog::applyDetectedVariants(unsigned capturedEpoch,
|
||||
}
|
||||
|
||||
cachedVariants_ = std::move(detected).value();
|
||||
if (fillSetNoOnSuccess && setNoCtrl_ && !cachedVariants_.empty()) {
|
||||
setNoCtrl_->ChangeValue(
|
||||
wxString::FromUTF8(cachedVariants_.front().setNo.c_str()));
|
||||
if (fillSetNoOnSuccess && !cachedVariants_.empty()) {
|
||||
applySelectedSetNo(cachedVariants_.front().setNo);
|
||||
}
|
||||
|
||||
rebuildVariantRingFromCache();
|
||||
syncRingPositionToControls();
|
||||
refreshVariantNextControls();
|
||||
if (isUnnumberedPromoSelected() && !uniqueSetNos_.empty()) {
|
||||
ensureUnnumberedPreviewOpen();
|
||||
refreshUnnumberedPreview();
|
||||
if (unnumberedPreview_ != nullptr) {
|
||||
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::rebuildVariantRingFromCache() {
|
||||
@@ -197,8 +365,12 @@ void PokemonCardEditDialog::rebuildVariantRingFromCache() {
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::syncRingPositionToControls() {
|
||||
if (!setNoCtrl_) return;
|
||||
const std::string current = storedSetNoFromControls(setNoCtrl_);
|
||||
const std::string current = isUnnumberedPromoSelected()
|
||||
? normalizedStoredSetNo(selectedSetNo_)
|
||||
: storedSetNoFromControls(setNoCtrl_);
|
||||
if (!isUnnumberedPromoSelected() && setNoCtrl_) {
|
||||
selectedSetNo_ = current;
|
||||
}
|
||||
setNoRingPos_ = 0;
|
||||
for (std::size_t i = 0; i < uniqueSetNos_.size(); ++i) {
|
||||
if (uniqueSetNos_[i] == current) {
|
||||
@@ -206,26 +378,66 @@ void PokemonCardEditDialog::syncRingPositionToControls() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!uniqueSetNos_.empty() && selectedSetNo_.empty()) {
|
||||
applySelectedSetNo(uniqueSetNos_[setNoRingPos_]);
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::refreshVariantNextControls() {
|
||||
if (!nextSetNoBtn_) return;
|
||||
nextSetNoBtn_->Show(uniqueSetNos_.size() > 1);
|
||||
const bool showNext = uniqueSetNos_.size() > 1;
|
||||
nextSetNoBtn_->Show(showNext);
|
||||
if (showNext) {
|
||||
if (isUnnumberedPromoSelected()) {
|
||||
const std::size_t i = setNoRingPos_ + 1;
|
||||
const std::size_t n = uniqueSetNos_.size();
|
||||
nextSetNoBtn_->SetLabel(wxString::Format("Next (%zu/%zu)", i, n));
|
||||
} else {
|
||||
const std::string setNo = currentRingSetNo();
|
||||
if (setNo.empty()) {
|
||||
nextSetNoBtn_->SetLabel("Next");
|
||||
} else {
|
||||
nextSetNoBtn_->SetLabel(
|
||||
wxString::Format("Next (%s)", wxString::FromUTF8(setNo.c_str())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nextSetNoBtn_->SetLabel("Next");
|
||||
}
|
||||
if (auto* parent = nextSetNoBtn_->GetParent()) {
|
||||
parent->Layout();
|
||||
}
|
||||
Layout();
|
||||
if (GetSizer()) Fit();
|
||||
if (unnumberedPreview_ != nullptr) {
|
||||
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::onAutoDetectSetNo(wxCommandEvent&) {
|
||||
autoDetectFromApi();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::onNextSetNo(wxCommandEvent&) {
|
||||
if (uniqueSetNos_.size() <= 1) return;
|
||||
setNoRingPos_ = (setNoRingPos_ + 1) % uniqueSetNos_.size();
|
||||
if (setNoCtrl_) {
|
||||
setNoCtrl_->ChangeValue(wxString::FromUTF8(uniqueSetNos_[setNoRingPos_].c_str()));
|
||||
}
|
||||
void PokemonCardEditDialog::stepVariantRing(int delta) {
|
||||
if (uniqueSetNos_.size() <= 1 || delta == 0) return;
|
||||
const auto n = static_cast<int>(uniqueSetNos_.size());
|
||||
auto pos = static_cast<int>(setNoRingPos_) + delta;
|
||||
pos %= n;
|
||||
if (pos < 0) pos += n;
|
||||
setNoRingPos_ = static_cast<std::size_t>(pos);
|
||||
applySelectedSetNo(uniqueSetNos_[setNoRingPos_]);
|
||||
refreshVariantNextControls();
|
||||
if (isUnnumberedPromoSelected()) {
|
||||
ensureUnnumberedPreviewOpen();
|
||||
refreshUnnumberedPreview();
|
||||
if (unnumberedPreview_ != nullptr) {
|
||||
unnumberedPreview_->setNavigationEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::onNextSetNo(wxCommandEvent&) {
|
||||
stepVariantRing(1);
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::autoDetectFromApi() {
|
||||
@@ -248,8 +460,115 @@ void PokemonCardEditDialog::autoDetectFromApi() {
|
||||
|
||||
void PokemonCardEditDialog::onSetSelectionChanged(wxCommandEvent& ev) {
|
||||
clearCachedPrintVariants();
|
||||
refreshSetNoRowMode();
|
||||
scheduleDeferredVariantPrefetch();
|
||||
ev.Skip();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::closeUnnumberedPreview() {
|
||||
++previewFetchEpoch_;
|
||||
if (unnumberedPreview_ != nullptr) {
|
||||
unnumberedPreview_->Destroy();
|
||||
unnumberedPreview_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::ensureUnnumberedPreviewOpen() {
|
||||
if (!isUnnumberedPromoSelected()) {
|
||||
closeUnnumberedPreview();
|
||||
return;
|
||||
}
|
||||
if (unnumberedPreview_ != nullptr) {
|
||||
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
|
||||
unnumberedPreview_->repositionBesideParent();
|
||||
return;
|
||||
}
|
||||
|
||||
unnumberedPreview_ = new VariantImagePreviewDialog(this);
|
||||
const Theme theme = inferThemeFromWindow(this);
|
||||
applyThemeToWindowTree(unnumberedPreview_, paletteForTheme(theme), theme);
|
||||
unnumberedPreview_->Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& ev) {
|
||||
unnumberedPreview_ = nullptr;
|
||||
ev.Skip();
|
||||
});
|
||||
unnumberedPreview_->Bind(EVT_VARIANT_PREVIEW_PREV, [this](wxCommandEvent&) {
|
||||
stepVariantRing(-1);
|
||||
});
|
||||
unnumberedPreview_->Bind(EVT_VARIANT_PREVIEW_NEXT, [this](wxCommandEvent&) {
|
||||
stepVariantRing(1);
|
||||
});
|
||||
unnumberedPreview_->setNavigationEnabled(uniqueSetNos_.size() > 1);
|
||||
unnumberedPreview_->Show(true);
|
||||
unnumberedPreview_->repositionBesideParent();
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::refreshUnnumberedPreview() {
|
||||
if (!isUnnumberedPromoSelected() || uniqueSetNos_.empty()) {
|
||||
closeUnnumberedPreview();
|
||||
return;
|
||||
}
|
||||
ensureUnnumberedPreviewOpen();
|
||||
if (unnumberedPreview_ == nullptr) return;
|
||||
|
||||
syncCardFromControls();
|
||||
const auto& card = constCard();
|
||||
const std::string setNo = currentRingSetNo();
|
||||
if (card.name.empty() || setNo.empty()) {
|
||||
unnumberedPreview_->clearImage();
|
||||
unnumberedPreview_->setCaption(wxString::FromUTF8("Enter a card name"));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t i = setNoRingPos_ + 1;
|
||||
const std::size_t n = uniqueSetNos_.size();
|
||||
unnumberedPreview_->setCaption(
|
||||
wxString::Format("%s (%zu/%zu)",
|
||||
wxString::FromUTF8(card.name.c_str()), i, n));
|
||||
|
||||
const unsigned epoch = ++previewFetchEpoch_;
|
||||
requestUnnumberedPreviewAsync(epoch, card.name, kUnnumberedPromoSetId, setNo, setNoRingPos_,
|
||||
uniqueSetNos_.size());
|
||||
}
|
||||
|
||||
void PokemonCardEditDialog::requestUnnumberedPreviewAsync(unsigned capturedEpoch,
|
||||
std::string name,
|
||||
std::string setId,
|
||||
std::string setNo,
|
||||
std::size_t ringIndex,
|
||||
std::size_t ringCount) {
|
||||
auto state = variantFetchState_;
|
||||
CardPreviewService* svc = &cardPreview_;
|
||||
PokemonCardEditDialog* self = this;
|
||||
std::thread([state, svc, self, capturedEpoch, name = std::move(name),
|
||||
setId = std::move(setId), setNo = std::move(setNo), ringIndex,
|
||||
ringCount]() {
|
||||
auto bytes = svc->fetchPreviewBytes(Game::JapanesePokemon, name, setId, setNo);
|
||||
std::string payload;
|
||||
bool usedFallback = false;
|
||||
if (bytes) {
|
||||
payload = std::move(bytes).value();
|
||||
} else {
|
||||
auto fallback = svc->fetchImageBytesByUrl(kJapanesePokemonCardBackUrl);
|
||||
if (fallback) {
|
||||
payload = std::move(fallback).value();
|
||||
usedFallback = true;
|
||||
}
|
||||
}
|
||||
wxTheApp->CallAfter([state, self, capturedEpoch, payload = std::move(payload),
|
||||
name, ringIndex, ringCount, usedFallback]() mutable {
|
||||
if (!state->alive.load()) return;
|
||||
if (capturedEpoch != self->previewFetchEpoch_) return;
|
||||
if (self->unnumberedPreview_ == nullptr) return;
|
||||
|
||||
self->unnumberedPreview_->setImageBytes(payload);
|
||||
if (usedFallback) {
|
||||
self->unnumberedPreview_->setCaption(
|
||||
wxString::Format("%s (%zu/%zu) — preview unavailable",
|
||||
wxString::FromUTF8(name.c_str()),
|
||||
ringIndex + 1, ringCount));
|
||||
}
|
||||
});
|
||||
}).detach();
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -31,18 +31,22 @@ void PokemonGameView::ensureSetsLoaded() {
|
||||
if (attemptedInitialSetLoad_) return;
|
||||
attemptedInitialSetLoad_ = true;
|
||||
|
||||
auto cached = sets_.getSets(Game::Pokemon);
|
||||
if (cached) {
|
||||
setsCache_ = std::move(cached).value();
|
||||
if (!setsCache_.empty()) return;
|
||||
} else {
|
||||
setsCache_.clear();
|
||||
}
|
||||
auto loadOrRefresh = [this](Game game, std::vector<Set>& cache) {
|
||||
auto cached = sets_.getSets(game);
|
||||
if (cached) {
|
||||
cache = std::move(cached).value();
|
||||
if (!cache.empty()) return;
|
||||
} else {
|
||||
cache.clear();
|
||||
}
|
||||
auto refreshed = sets_.updateSets(game);
|
||||
if (refreshed) {
|
||||
cache = std::move(refreshed).value();
|
||||
}
|
||||
};
|
||||
|
||||
auto refreshed = sets_.updateSets(Game::Pokemon);
|
||||
if (refreshed) {
|
||||
setsCache_ = std::move(refreshed).value();
|
||||
}
|
||||
loadOrRefresh(Game::Pokemon, setsCacheWest_);
|
||||
loadOrRefresh(Game::JapanesePokemon, setsCacheAsia_);
|
||||
}
|
||||
|
||||
wxPanel* PokemonGameView::listPanel(wxWindow* parent) {
|
||||
@@ -81,13 +85,20 @@ void PokemonGameView::refreshCollection() {
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
|
||||
const std::vector<Set>& PokemonGameView::setsForDialog() {
|
||||
const std::vector<Set>& PokemonGameView::setsForDialog(PokemonRegion region) {
|
||||
ensureSetsLoaded();
|
||||
if (!setsCache_.empty()) return setsCache_;
|
||||
if (region == PokemonRegion::Asia) {
|
||||
if (!setsCacheAsia_.empty()) return setsCacheAsia_;
|
||||
auto loaded = sets_.getSets(Game::JapanesePokemon);
|
||||
if (loaded) setsCacheAsia_ = std::move(loaded).value();
|
||||
else setsCacheAsia_.clear();
|
||||
return setsCacheAsia_;
|
||||
}
|
||||
if (!setsCacheWest_.empty()) return setsCacheWest_;
|
||||
auto loaded = sets_.getSets(Game::Pokemon);
|
||||
if (loaded) setsCache_ = std::move(loaded).value();
|
||||
else setsCache_.clear();
|
||||
return setsCache_;
|
||||
if (loaded) setsCacheWest_ = std::move(loaded).value();
|
||||
else setsCacheWest_.clear();
|
||||
return setsCacheWest_;
|
||||
}
|
||||
|
||||
void PokemonGameView::onAddCard(wxWindow* parentWindow) {
|
||||
@@ -98,11 +109,13 @@ void PokemonGameView::onAddCard(wxWindow* parentWindow) {
|
||||
}
|
||||
PokemonCard fresh;
|
||||
fresh.amount = 1;
|
||||
fresh.region = PokemonRegion::West;
|
||||
fresh.language = Language::English;
|
||||
fresh.condition = Condition::NearMint;
|
||||
|
||||
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create, fresh,
|
||||
&setsForDialog());
|
||||
&setsForDialog(PokemonRegion::West),
|
||||
&setsForDialog(PokemonRegion::Asia));
|
||||
themeModalDialog(&dlg, config_.current().theme);
|
||||
CardEditModalGuard modalGuard;
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
@@ -147,7 +160,8 @@ void PokemonGameView::onEditCard(wxWindow* parentWindow) {
|
||||
return;
|
||||
}
|
||||
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit, *sel,
|
||||
&setsForDialog());
|
||||
&setsForDialog(PokemonRegion::West),
|
||||
&setsForDialog(PokemonRegion::Asia));
|
||||
themeModalDialog(&dlg, config_.current().theme);
|
||||
CardEditModalGuard modalGuard;
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
@@ -181,15 +195,46 @@ void PokemonGameView::onDeleteCard(wxWindow* parentWindow) {
|
||||
}
|
||||
|
||||
std::string PokemonGameView::onUpdateSets(wxWindow* parentWindow) {
|
||||
auto out = sets_.updateSets(Game::Pokemon);
|
||||
if (!out) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update sets: " + out.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
auto westOut = sets_.updateSets(Game::Pokemon);
|
||||
auto asiaOut = sets_.updateSets(Game::JapanesePokemon);
|
||||
|
||||
if (westOut) {
|
||||
setsCacheWest_ = westOut.value();
|
||||
}
|
||||
if (asiaOut) {
|
||||
setsCacheAsia_ = asiaOut.value();
|
||||
}
|
||||
|
||||
if (!westOut && !asiaOut) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Failed to update West sets: " + westOut.error() +
|
||||
"\nFailed to update Asia sets: " + asiaOut.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
setsCache_ = out.value();
|
||||
showThemedMessageDialog(parentWindow, "Updated " + std::to_string(out.value().size()) + " Pokemon sets.",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
if (!westOut) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(asiaOut.value().size()) +
|
||||
" Asia Pokemon sets, but West failed: " + westOut.error(),
|
||||
"Sets partially updated", wxOK | wxICON_WARNING);
|
||||
return "Pokemon sets partially updated.";
|
||||
}
|
||||
if (!asiaOut) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(westOut.value().size()) +
|
||||
" West Pokemon sets, but Asia failed: " + asiaOut.error(),
|
||||
"Sets partially updated", wxOK | wxICON_WARNING);
|
||||
return "Pokemon sets partially updated.";
|
||||
}
|
||||
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(westOut.value().size()) + " West and " +
|
||||
std::to_string(asiaOut.value().size()) + " Asia Pokemon sets.",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
return "Pokemon sets updated.";
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ enum PokemonDetailKey : int {
|
||||
kName = 0,
|
||||
kSet,
|
||||
kSetNo,
|
||||
kRegion,
|
||||
kLanguage,
|
||||
kCondition,
|
||||
kAmount,
|
||||
@@ -34,6 +35,7 @@ PokemonSelectedCardPanel::declareDetailRows() const {
|
||||
{"Name", kName, "(no card selected)"},
|
||||
{"Set", kSet, ""},
|
||||
{"Set #", kSetNo, ""},
|
||||
{"Region", kRegion, ""},
|
||||
{"Language", kLanguage, ""},
|
||||
{"Condition", kCondition, ""},
|
||||
{"Amount", kAmount, ""},
|
||||
@@ -56,6 +58,7 @@ std::string PokemonSelectedCardPanel::detailValueFor(const PokemonCard& card,
|
||||
case kName: return card.name;
|
||||
case kSet: return card.set.name;
|
||||
case kSetNo: return card.setNo;
|
||||
case kRegion: return std::string(to_string(card.region));
|
||||
case kLanguage: return std::string(to_string(card.language));
|
||||
case kCondition: return std::string(to_string(card.condition));
|
||||
case kAmount: return std::to_string(card.amount);
|
||||
|
||||
@@ -14,10 +14,11 @@ namespace {
|
||||
|
||||
wxString displayLabelForGame(Game g) {
|
||||
switch (g) {
|
||||
case Game::Magic: return "Magic";
|
||||
case Game::Pokemon: return "Pokemon";
|
||||
case Game::YuGiOh: return "Yu-Gi-Oh!";
|
||||
case Game::DigiBattle99: return "Digimon (Digi-Battle)";
|
||||
case Game::Magic: return "Magic";
|
||||
case Game::Pokemon: return "Pokemon";
|
||||
case Game::YuGiOh: return "Yu-Gi-Oh!";
|
||||
case Game::DigiBattle99: return "Digimon (Digi-Battle)";
|
||||
case Game::JapanesePokemon: return "Pokemon"; // internal; not in allGames()
|
||||
}
|
||||
return wxString::FromUTF8(to_string(g).data());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "ccm/ui/VariantImagePreviewDialog.hpp"
|
||||
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/display.h>
|
||||
#include <wx/log.h>
|
||||
#include <wx/mstream.h>
|
||||
#include <wx/sizer.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
wxDEFINE_EVENT(EVT_VARIANT_PREVIEW_PREV, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_VARIANT_PREVIEW_NEXT, wxCommandEvent);
|
||||
|
||||
class VariantImagePreviewDialog::ImageCanvas : public wxPanel {
|
||||
public:
|
||||
explicit ImageCanvas(wxWindow* parent) : wxPanel(parent, wxID_ANY) {
|
||||
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
Bind(wxEVT_PAINT, &ImageCanvas::onPaint, this);
|
||||
Bind(wxEVT_SIZE, [this](wxSizeEvent& ev) {
|
||||
Refresh();
|
||||
ev.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
void setImage(const wxImage& img) {
|
||||
original_ = img;
|
||||
cachedScaled_ = wxBitmap();
|
||||
cachedScaledFor_ = wxSize(-1, -1);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
original_ = wxImage();
|
||||
cachedScaled_ = wxBitmap();
|
||||
cachedScaledFor_ = wxSize(-1, -1);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private:
|
||||
void onPaint(wxPaintEvent&) {
|
||||
wxPaintDC dc(this);
|
||||
dc.Clear();
|
||||
if (!original_.IsOk()) return;
|
||||
|
||||
const wxSize ws = GetClientSize();
|
||||
if (ws.GetWidth() <= 0 || ws.GetHeight() <= 0) return;
|
||||
|
||||
const double scale = std::min(
|
||||
static_cast<double>(ws.GetWidth()) / original_.GetWidth(),
|
||||
static_cast<double>(ws.GetHeight()) / original_.GetHeight());
|
||||
const int w = std::max(1, static_cast<int>(original_.GetWidth() * scale));
|
||||
const int h = std::max(1, static_cast<int>(original_.GetHeight() * scale));
|
||||
const wxSize scaledSize(w, h);
|
||||
if (!cachedScaled_.IsOk() || cachedScaledFor_ != scaledSize) {
|
||||
wxImage scaled = original_.Scale(w, h, wxIMAGE_QUALITY_HIGH);
|
||||
cachedScaled_ = wxBitmap(scaled);
|
||||
cachedScaledFor_ = scaledSize;
|
||||
}
|
||||
dc.DrawBitmap(cachedScaled_,
|
||||
(ws.GetWidth() - w) / 2,
|
||||
(ws.GetHeight() - h) / 2,
|
||||
true);
|
||||
}
|
||||
|
||||
wxImage original_;
|
||||
wxBitmap cachedScaled_;
|
||||
wxSize cachedScaledFor_{-1, -1};
|
||||
};
|
||||
|
||||
VariantImagePreviewDialog::VariantImagePreviewDialog(wxWindow* parent)
|
||||
: wxDialog(parent, wxID_ANY, "Print preview",
|
||||
wxDefaultPosition, wxSize(280, 420),
|
||||
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxSTAY_ON_TOP) {
|
||||
auto* root = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
imageHost_ = new ImageCanvas(this);
|
||||
root->Add(imageHost_, 1, wxEXPAND | wxALL, 6);
|
||||
|
||||
caption_ = new wxStaticText(this, wxID_ANY, "");
|
||||
root->Add(caption_, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 6);
|
||||
|
||||
auto* nav = new wxBoxSizer(wxHORIZONTAL);
|
||||
prevButton_ = new wxButton(this, wxID_ANY, "<< Prev");
|
||||
nextButton_ = new wxButton(this, wxID_ANY, "Next >>");
|
||||
nav->Add(prevButton_, 0, wxRIGHT, 6);
|
||||
nav->Add(nextButton_, 0);
|
||||
root->Add(nav, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxBOTTOM, 6);
|
||||
|
||||
prevButton_->Bind(wxEVT_BUTTON, &VariantImagePreviewDialog::onPrev, this);
|
||||
nextButton_->Bind(wxEVT_BUTTON, &VariantImagePreviewDialog::onNext, this);
|
||||
|
||||
SetSizer(root);
|
||||
Layout();
|
||||
repositionBesideParent();
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::repositionBesideParent() {
|
||||
wxWindow* parent = GetParent();
|
||||
if (parent == nullptr) return;
|
||||
|
||||
const wxRect parentScreen = parent->GetScreenRect();
|
||||
const wxSize size = GetSize();
|
||||
int x = parentScreen.GetRight() + 20;
|
||||
int y = parentScreen.GetTop();
|
||||
|
||||
const int displayIdx = wxDisplay::GetFromWindow(parent);
|
||||
if (displayIdx != wxNOT_FOUND) {
|
||||
const wxRect work = wxDisplay(displayIdx).GetClientArea();
|
||||
if (x + size.GetWidth() > work.GetRight()) {
|
||||
x = std::max(work.GetLeft(), work.GetRight() - size.GetWidth());
|
||||
}
|
||||
if (y + size.GetHeight() > work.GetBottom()) {
|
||||
y = std::max(work.GetTop(), work.GetBottom() - size.GetHeight());
|
||||
}
|
||||
if (x < work.GetLeft()) x = work.GetLeft();
|
||||
if (y < work.GetTop()) y = work.GetTop();
|
||||
}
|
||||
|
||||
SetPosition(wxPoint(x, y));
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::setCaption(const wxString& caption) {
|
||||
if (caption_) {
|
||||
caption_->SetLabelText(caption);
|
||||
Layout();
|
||||
}
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::setImageBytes(std::string_view bytes) {
|
||||
if (!imageHost_) return;
|
||||
if (bytes.empty()) {
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
|
||||
wxMemoryInputStream stream(bytes.data(), bytes.size());
|
||||
wxImage img;
|
||||
bool decoded = false;
|
||||
{
|
||||
wxLogNull suppressPngWarnings;
|
||||
decoded = img.LoadFile(stream, wxBITMAP_TYPE_ANY);
|
||||
}
|
||||
if (!decoded || !img.IsOk()) {
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
imageHost_->setImage(img);
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::clearImage() {
|
||||
if (imageHost_) {
|
||||
imageHost_->clear();
|
||||
}
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::setNavigationEnabled(bool enabled) {
|
||||
if (prevButton_) prevButton_->Enable(enabled);
|
||||
if (nextButton_) nextButton_->Enable(enabled);
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::onPrev(wxCommandEvent&) {
|
||||
wxCommandEvent ev(EVT_VARIANT_PREVIEW_PREV, GetId());
|
||||
ev.SetEventObject(this);
|
||||
ProcessWindowEvent(ev);
|
||||
}
|
||||
|
||||
void VariantImagePreviewDialog::onNext(wxCommandEvent&) {
|
||||
wxCommandEvent ev(EVT_VARIANT_PREVIEW_NEXT, GetId());
|
||||
ev.SetEventObject(this);
|
||||
ProcessWindowEvent(ev);
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
Reference in New Issue
Block a user