mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-09-04 17:23:29 +00:00
Minor: Set completion tracking (#19)
This commit is contained in:
@@ -1,31 +1,63 @@
|
||||
#include "ccm/ui/DigiBattle99GameView.hpp"
|
||||
|
||||
#include "ccm/games/digibattle99/DigiBattle99SetSource.hpp"
|
||||
#include "ccm/ui/CardEditModalGuard.hpp"
|
||||
#include "ccm/ui/DigiBattle99CardEditDialog.hpp"
|
||||
#include "ccm/ui/DigiBattle99CardListPanel.hpp"
|
||||
#include "ccm/ui/DigiBattle99SelectedCardPanel.hpp"
|
||||
#include "ccm/ui/DigiBattle99SetCompletionPanel.hpp"
|
||||
#include "ccm/ui/SvgIcons.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
|
||||
#include <wx/msgdlg.h>
|
||||
#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 <optional>
|
||||
#include <string>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
constexpr int kDigiToolbarIconPx = 18;
|
||||
constexpr const char kDigiFilterHint[] = "Filter";
|
||||
|
||||
wxColour lighten(const wxColour& c, int amount) {
|
||||
auto lift = [amount](unsigned char channel) -> unsigned char {
|
||||
const int raised = static_cast<int>(channel) + amount;
|
||||
return static_cast<unsigned char>(raised > 255 ? 255 : raised);
|
||||
};
|
||||
return wxColour(lift(c.Red()), lift(c.Green()), lift(c.Blue()));
|
||||
}
|
||||
|
||||
wxColour darken(const wxColour& c, int amount) {
|
||||
auto drop = [amount](unsigned char channel) -> unsigned char {
|
||||
const int lowered = static_cast<int>(channel) - amount;
|
||||
return static_cast<unsigned char>(lowered < 0 ? 0 : lowered);
|
||||
};
|
||||
return wxColour(drop(c.Red()), drop(c.Green()), drop(c.Blue()));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DigiBattle99GameView::DigiBattle99GameView(ConfigService& config,
|
||||
CollectionService<DigiBattle99Card>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module)
|
||||
IGameModule& module,
|
||||
DigiBattle99SetCatalogService& catalogStore)
|
||||
: config_(config),
|
||||
collection_(collection),
|
||||
sets_(sets),
|
||||
images_(images),
|
||||
cardPreview_(cardPreview),
|
||||
module_(module) {}
|
||||
module_(module),
|
||||
catalogStore_(catalogStore) {}
|
||||
|
||||
void DigiBattle99GameView::ensureSetsLoaded() {
|
||||
if (attemptedInitialSetLoad_) return;
|
||||
@@ -45,6 +77,214 @@ void DigiBattle99GameView::ensureSetsLoaded() {
|
||||
}
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::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 DigiBattle99GameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer) {
|
||||
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto makeToolBtn = [&](const char* svg, const wxString& tip) {
|
||||
wxBitmap bmp = svgIconBitmap(svg, kDigiToolbarIconPx, "#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(kDigiFilterHint);
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
const std::string tbHex = palette.buttonText.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
if (toolbarButtons_[0]) {
|
||||
toolbarButtons_[0]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarAdd, kDigiToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[1]) {
|
||||
toolbarButtons_[1]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarEdit, kDigiToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[2]) {
|
||||
toolbarButtons_[2]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarDelete, kDigiToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::selectTab(int index) {
|
||||
if (index < 0 || index > 1 || book_ == nullptr) return;
|
||||
activeTab_ = index;
|
||||
book_->SetSelection(index);
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::refreshTabBarTheme(const ThemePalette& palette) {
|
||||
if (tabBar_ == nullptr) return;
|
||||
|
||||
const wxColour barBg = palette.panelBg;
|
||||
// Match toolbar button plate (Add/Edit/Delete), not a darker inset fill.
|
||||
const wxColour tabBg = palette.buttonBg;
|
||||
|
||||
tabBar_->SetBackgroundColour(barBg);
|
||||
tabBar_->SetOwnBackgroundColour(barBg);
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto* tab = tabPanels_[i];
|
||||
auto* label = tabLabels_[i];
|
||||
if (tab == nullptr || label == nullptr) continue;
|
||||
const bool selected = (i == activeTab_);
|
||||
tab->SetBackgroundColour(tabBg);
|
||||
tab->SetOwnBackgroundColour(tabBg);
|
||||
// Keep the label plate identical to the tab fill so a late theme pass
|
||||
// cannot leave a darker box around the caption.
|
||||
label->SetBackgroundColour(tabBg);
|
||||
label->SetOwnBackgroundColour(tabBg);
|
||||
label->SetForegroundColour(palette.text);
|
||||
label->SetOwnForegroundColour(palette.text);
|
||||
wxFont font = label->GetFont();
|
||||
font.SetWeight(selected ? wxFONTWEIGHT_BOLD : wxFONTWEIGHT_NORMAL);
|
||||
label->SetFont(font);
|
||||
tab->Refresh();
|
||||
label->Refresh();
|
||||
}
|
||||
tabBar_->Layout();
|
||||
tabBar_->Refresh();
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::buildTabBar(wxWindow* parent, wxBoxSizer* rootSizer) {
|
||||
tabBar_ = new wxPanel(parent, wxID_ANY);
|
||||
tabBar_->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
auto* tabSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
tabSizer->AddSpacer(4);
|
||||
|
||||
const char* labels[2] = {"Single Cards", "Set Completion"};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto* tab = new wxPanel(tabBar_, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
tab->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
tab->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
auto* label = new wxStaticText(tab, wxID_ANY, wxString::FromUTF8(labels[i]));
|
||||
auto* inner = new wxBoxSizer(wxVERTICAL);
|
||||
// Compact padding so the strip stays short; frame is drawn in paint.
|
||||
inner->Add(label, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 5);
|
||||
tab->SetSizer(inner);
|
||||
|
||||
auto onClick = [this, i](wxMouseEvent&) { selectTab(i); };
|
||||
tab->Bind(wxEVT_LEFT_DOWN, onClick);
|
||||
label->Bind(wxEVT_LEFT_DOWN, onClick);
|
||||
tab->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
|
||||
tab->Bind(wxEVT_PAINT, [this, tab, i](wxPaintEvent&) {
|
||||
wxPaintDC dc(tab);
|
||||
const ThemePalette palette = paletteForTheme(config_.current().theme);
|
||||
const bool dark = config_.current().theme == Theme::Dark;
|
||||
const bool selected = (i == activeTab_);
|
||||
// Same plate as toolbar bitmap buttons.
|
||||
const wxColour bg = palette.buttonBg;
|
||||
const wxColour frame =
|
||||
dark ? lighten(palette.panelBg, 55) : darken(palette.panelBg, 45);
|
||||
const wxColour frameSel = dark ? lighten(palette.panelBg, 85) : darken(palette.panelBg, 70);
|
||||
const wxRect r = tab->GetClientRect();
|
||||
dc.SetPen(wxPen(selected ? frameSel : frame, 1));
|
||||
dc.SetBrush(wxBrush(bg));
|
||||
dc.DrawRectangle(r.x, r.y, r.width, r.height);
|
||||
if (selected) {
|
||||
dc.SetPen(wxPen(palette.text, 2));
|
||||
dc.DrawLine(r.GetLeft() + 4, r.GetBottom() - 1, r.GetRight() - 4,
|
||||
r.GetBottom() - 1);
|
||||
}
|
||||
});
|
||||
|
||||
tabPanels_[i] = tab;
|
||||
tabLabels_[i] = label;
|
||||
if (i > 0) tabSizer->AddSpacer(4);
|
||||
tabSizer->Add(tab, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 3);
|
||||
}
|
||||
tabSizer->AddStretchSpacer(1);
|
||||
|
||||
tabBar_->Bind(wxEVT_PAINT, [this](wxPaintEvent&) {
|
||||
wxPaintDC dc(tabBar_);
|
||||
const ThemePalette palette = paletteForTheme(config_.current().theme);
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(palette.panelBg));
|
||||
dc.DrawRectangle(tabBar_->GetClientRect());
|
||||
dc.SetPen(wxPen(darken(palette.text, 120), 1));
|
||||
const wxRect r = tabBar_->GetClientRect();
|
||||
dc.DrawLine(r.GetLeft(), r.GetBottom(), r.GetRight(), r.GetBottom());
|
||||
});
|
||||
tabBar_->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
|
||||
|
||||
tabBar_->SetSizer(tabSizer);
|
||||
rootSizer->Add(tabBar_, 0, wxEXPAND);
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
}
|
||||
|
||||
wxPanel* DigiBattle99GameView::contentPanel(wxWindow* parent) {
|
||||
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 DigiBattle99SetCompletionPanel(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* DigiBattle99GameView::listPanel(wxWindow* parent) {
|
||||
if (listPanel_ == nullptr) {
|
||||
listPanel_ = new DigiBattle99CardListPanel(parent);
|
||||
@@ -69,7 +309,10 @@ wxPanel* DigiBattle99GameView::selectedPanel(wxWindow* parent) {
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::refreshCollection() {
|
||||
if (listPanel_ == nullptr) return;
|
||||
// Ensure the Digimon 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::DigiBattle99);
|
||||
if (!loaded) {
|
||||
showThemedMessageDialog(
|
||||
@@ -78,9 +321,15 @@ void DigiBattle99GameView::refreshCollection() {
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
listPanel_->setCards(std::move(loaded).value());
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->setCollection(std::move(cards));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<Set>& DigiBattle99GameView::setsForDialog() {
|
||||
@@ -190,27 +439,80 @@ void DigiBattle99GameView::onDeleteCard(wxWindow* parentWindow) {
|
||||
}
|
||||
|
||||
std::string DigiBattle99GameView::onUpdateSets(wxWindow* parentWindow) {
|
||||
auto out = sets_.updateSets(Game::DigiBattle99);
|
||||
if (!out) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update sets: " + out.error(),
|
||||
auto* digiSrc = dynamic_cast<DigiBattle99SetSource*>(&module_.setSource());
|
||||
if (digiSrc == nullptr) {
|
||||
showThemedMessageDialog(parentWindow, "Digimon Digi-Battle set source unavailable.",
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
setsCache_ = out.value();
|
||||
|
||||
auto both = digiSrc->fetchAllWithCatalog();
|
||||
if (!both) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update sets: " + both.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
|
||||
auto savedSets = sets_.saveSets(Game::DigiBattle99, both.value().sets);
|
||||
if (!savedSets) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to save sets: " + savedSets.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
|
||||
auto savedCatalog = catalogStore_.save(both.value().catalog);
|
||||
if (!savedCatalog) {
|
||||
showThemedMessageDialog(parentWindow,
|
||||
"Sets saved, but set catalog failed: " + savedCatalog.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
|
||||
setsCache_ = both.value().sets;
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->reloadFromStore();
|
||||
if (auto loaded = collection_.list(Game::DigiBattle99)) {
|
||||
setCompletionPanel_->setCollection(std::move(loaded).value());
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t setCount = both.value().sets.size();
|
||||
const std::size_t packCount = both.value().catalog.packs.size();
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(out.value().size()) + " Digimon (Digi-Battle) sets.",
|
||||
"Updated " + std::to_string(setCount) + " Digimon (Digi-Battle) sets and " +
|
||||
std::to_string(packCount) + " set checklists.",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
return "Digimon (Digi-Battle) sets updated.";
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::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(kDigiFilterHint);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::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);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
#include "ccm/ui/DigiBattle99SetCompletionPanel.hpp"
|
||||
|
||||
#include "ccm/services/DigiBattle99SetCompletion.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
|
||||
|
||||
DigiBattle99SetCompletionPanel::DigiBattle99SetCompletionPanel(
|
||||
wxWindow* parent, DigiBattle99SetCatalogService& 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, &DigiBattle99SetCompletionPanel::onLanguageChoice,
|
||||
this);
|
||||
langRow->Add(langLabel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
|
||||
langRow->Add(languageChoice_, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
book_ = new wxSimplebook(this, wxID_ANY);
|
||||
|
||||
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 DigiBattle99SetCompletionPanel::setCollection(std::vector<DigiBattle99Card> cards) {
|
||||
collection_ = std::move(cards);
|
||||
refreshLanguageChoice();
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::reloadFromStore() {
|
||||
catalogLoaded_ = false;
|
||||
catalog_ = {};
|
||||
if (catalogStore_.exists()) {
|
||||
if (auto loaded = catalogStore_.load()) {
|
||||
catalog_ = std::move(loaded).value();
|
||||
catalogLoaded_ = true;
|
||||
}
|
||||
}
|
||||
showGridPage();
|
||||
rebuildGrid();
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::applyTheme(const ThemePalette& palette) {
|
||||
palette_ = palette;
|
||||
applyThemeToWindowTree(this, palette, inferThemeFromWindow(this));
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::showGridPage() {
|
||||
detailSetId_.clear();
|
||||
detailSetName_.clear();
|
||||
book_->SetSelection(0);
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::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 DigiBattle99SetCompletionPanel::displaySetName(const std::string& setName) const {
|
||||
if (!languageFilter_.has_value()) return setName;
|
||||
return setName + " (" + std::string(to_string(*languageFilter_)) + ")";
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::refreshLanguageChoice() {
|
||||
const auto previous = languageFilter_;
|
||||
const auto present = digiBattle99LanguagesInCollection(collection_);
|
||||
|
||||
languageChoice_->Clear();
|
||||
languageChoice_->Append("All languages");
|
||||
for (const Language lang : present) {
|
||||
languageChoice_->Append(wxString::FromUTF8(std::string(to_string(lang)).c_str()));
|
||||
}
|
||||
|
||||
int selection = 0;
|
||||
languageFilter_ = std::nullopt;
|
||||
if (previous.has_value()) {
|
||||
for (std::size_t i = 0; i < present.size(); ++i) {
|
||||
if (present[i] == *previous) {
|
||||
selection = static_cast<int>(i + 1);
|
||||
languageFilter_ = previous;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
languageChoice_->SetSelection(selection);
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::onLanguageChoice(wxCommandEvent& /*event*/) {
|
||||
const int sel = languageChoice_->GetSelection();
|
||||
if (sel <= 0) {
|
||||
languageFilter_ = std::nullopt;
|
||||
} else {
|
||||
const auto present = digiBattle99LanguagesInCollection(collection_);
|
||||
const auto idx = static_cast<std::size_t>(sel - 1);
|
||||
if (idx < present.size()) {
|
||||
languageFilter_ = present[idx];
|
||||
} else {
|
||||
languageFilter_ = std::nullopt;
|
||||
languageChoice_->SetSelection(0);
|
||||
}
|
||||
}
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::rebuildCurrentView() {
|
||||
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
||||
const auto rows =
|
||||
computeDigiBattle99SetCompletion(collection_, catalog_, languageFilter_);
|
||||
bool stillVisible = false;
|
||||
for (const auto& row : rows) {
|
||||
if (row.setId == detailSetId_) {
|
||||
stillVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stillVisible) {
|
||||
showGridPage();
|
||||
rebuildGrid();
|
||||
return;
|
||||
}
|
||||
detailTitle_->SetLabelText(
|
||||
wxString::FromUTF8(displaySetName(detailSetName_).c_str()));
|
||||
rebuildChecklist(detailSetId_);
|
||||
} else {
|
||||
rebuildGrid();
|
||||
}
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::setEmptyMessage(const wxString& message) {
|
||||
clearGridTiles();
|
||||
emptyLabel_->SetLabelText(message);
|
||||
emptyLabel_->Wrap(480);
|
||||
emptyLabel_->Show();
|
||||
scroll_->Hide();
|
||||
gridPage_->Layout();
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::clearGridTiles() {
|
||||
if (gridSizer_ == nullptr) return;
|
||||
gridSizer_->Clear(true);
|
||||
}
|
||||
|
||||
void DigiBattle99SetCompletionPanel::rebuildGrid() {
|
||||
if (!catalogLoaded_) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"Set checklists are not downloaded yet.\n"
|
||||
"Run Sets → Update Digimon (Digi-Battle) to enable Set Completion."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rows =
|
||||
computeDigiBattle99SetCompletion(collection_, catalog_, languageFilter_);
|
||||
if (rows.empty()) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"No Digimon (Digi-Battle) sets in progress yet.\n"
|
||||
"Add cards on the Single Cards tab to track set completion here."));
|
||||
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 DigiBattle99SetCompletionPanel::rebuildChecklist(const std::string& setId) {
|
||||
checklist_->DeleteAllItems();
|
||||
const auto entries =
|
||||
digiBattle99ChecklistForSet(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.
|
||||
const std::string line =
|
||||
(entry.owned ? "✓ " : " ") + entry.setNo + " — " + entry.name;
|
||||
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
|
||||
+53
-7
@@ -133,10 +133,11 @@ void MainFrame::buildLayout() {
|
||||
menuStrip_->SetSizer(menuSizer);
|
||||
root->Add(menuStrip_, 0, wxEXPAND);
|
||||
|
||||
toolbarPanel_ = new wxPanel(this, wxID_ANY);
|
||||
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto makeToolBtn = [&](int id, const char* svg, const wxString& tip) {
|
||||
wxBitmap bmp = svgIconBitmap(svg, kToolbarIconPx, "#000000");
|
||||
auto* b = new wxBitmapButton(this, id, bmp, wxDefaultPosition,
|
||||
auto* b = new wxBitmapButton(toolbarPanel_, id, bmp, wxDefaultPosition,
|
||||
wxDefaultSize,
|
||||
wxBU_EXACTFIT);
|
||||
b->SetToolTip(tip);
|
||||
@@ -150,16 +151,21 @@ void MainFrame::buildLayout() {
|
||||
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(this, wxID_ANY, "", wxDefaultPosition,
|
||||
filterInput_ = new wxTextCtrl(toolbarPanel_, wxID_ANY, "", wxDefaultPosition,
|
||||
wxSize(260, -1));
|
||||
filterInput_->SetHint(kFilterInputHint);
|
||||
toolbar->Add(filterInput_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT | wxTOP | wxBOTTOM, 4);
|
||||
root->Add(toolbar, 0, wxEXPAND);
|
||||
toolbarPanel_->SetSizer(toolbar);
|
||||
root->Add(toolbarPanel_, 0, wxEXPAND);
|
||||
|
||||
splitter_ = new wxSplitterWindow(this, wxID_ANY, wxDefaultPosition,
|
||||
contentHost_ = new wxPanel(this, wxID_ANY);
|
||||
auto* hostSizer = new wxBoxSizer(wxVERTICAL);
|
||||
splitter_ = new wxSplitterWindow(contentHost_, wxID_ANY, wxDefaultPosition,
|
||||
wxDefaultSize, wxSP_LIVE_UPDATE);
|
||||
splitter_->SetMinimumPaneSize(280);
|
||||
root->Add(splitter_, 1, wxEXPAND);
|
||||
hostSizer->Add(splitter_, 1, wxEXPAND);
|
||||
contentHost_->SetSizer(hostSizer);
|
||||
root->Add(contentHost_, 1, wxEXPAND);
|
||||
|
||||
auto* statusPanel = new wxPanel(this, wxID_ANY);
|
||||
auto* statusSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
@@ -199,15 +205,55 @@ IGameView* MainFrame::activeView() {
|
||||
|
||||
void MainFrame::mountActiveView() {
|
||||
auto* view = activeView();
|
||||
if (view == nullptr || splitter_ == nullptr) return;
|
||||
if (view == nullptr || splitter_ == nullptr || contentHost_ == nullptr) return;
|
||||
|
||||
auto* hostSizer = contentHost_->GetSizer();
|
||||
if (hostSizer == nullptr) return;
|
||||
|
||||
// Hide every other view's panels so wx doesn't double-paint them.
|
||||
for (auto* other : ctx_.gameViews) {
|
||||
if (other == nullptr || other == view) continue;
|
||||
if (other->hostsOwnLayout()) {
|
||||
if (auto* cp = other->contentPanelIfCreated()) cp->Hide();
|
||||
continue;
|
||||
}
|
||||
if (auto* lp = other->listPanel(splitter_)) lp->Hide();
|
||||
if (auto* sp = other->selectedPanel(splitter_)) sp->Hide();
|
||||
}
|
||||
|
||||
const ThemePalette palette = paletteForTheme(ctx_.config.current().theme);
|
||||
|
||||
if (toolbarPanel_ != nullptr) {
|
||||
if (view->hostsOwnLayout()) toolbarPanel_->Hide();
|
||||
else toolbarPanel_->Show();
|
||||
Layout();
|
||||
}
|
||||
|
||||
if (view->hostsOwnLayout()) {
|
||||
auto* custom = view->contentPanel(contentHost_);
|
||||
if (custom == nullptr) return;
|
||||
|
||||
splitter_->Hide();
|
||||
hostSizer->Clear(false);
|
||||
custom->Show();
|
||||
hostSizer->Add(custom, 1, wxEXPAND);
|
||||
contentHost_->Layout();
|
||||
|
||||
// Digimon (and other hostsOwnLayout views) apply their own tree theme
|
||||
// and then restore tab-strip colors; a follow-up applyThemeToWindowTree
|
||||
// here would reset tab labels to panelBg and leave a dark box around text.
|
||||
view->applyTheme(palette);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* previousCustom = view->contentPanelIfCreated()) {
|
||||
previousCustom->Hide();
|
||||
}
|
||||
// Re-seat the shared splitter if a contentPanel game was showing.
|
||||
hostSizer->Clear(false);
|
||||
splitter_->Show();
|
||||
hostSizer->Add(splitter_, 1, wxEXPAND);
|
||||
|
||||
auto* listPanel = view->listPanel(splitter_);
|
||||
auto* selectedPanel = view->selectedPanel(splitter_);
|
||||
if (listPanel == nullptr || selectedPanel == nullptr) return;
|
||||
@@ -221,7 +267,7 @@ void MainFrame::mountActiveView() {
|
||||
splitter_->SplitVertically(selectedPanel, listPanel, 360);
|
||||
}
|
||||
|
||||
const ThemePalette palette = paletteForTheme(ctx_.config.current().theme);
|
||||
contentHost_->Layout();
|
||||
view->applyTheme(palette);
|
||||
applyThemeToWindowTree(selectedPanel, palette, ctx_.config.current().theme);
|
||||
applyThemeToWindowTree(listPanel, palette, ctx_.config.current().theme);
|
||||
|
||||
+342
-27
@@ -1,31 +1,66 @@
|
||||
#include "ccm/ui/PokemonGameView.hpp"
|
||||
|
||||
#include "ccm/games/pokemon/PokemonSetSource.hpp"
|
||||
#include "ccm/games/pokemonjp/JapanesePokemonSetSource.hpp"
|
||||
#include "ccm/ui/CardEditModalGuard.hpp"
|
||||
#include "ccm/ui/PokemonCardEditDialog.hpp"
|
||||
#include "ccm/ui/PokemonCardListPanel.hpp"
|
||||
#include "ccm/ui/PokemonSelectedCardPanel.hpp"
|
||||
#include "ccm/ui/PokemonSetCompletionPanel.hpp"
|
||||
#include "ccm/ui/SvgIcons.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
|
||||
#include <wx/msgdlg.h>
|
||||
#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 <optional>
|
||||
#include <string>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
namespace {
|
||||
constexpr int kPokeToolbarIconPx = 18;
|
||||
constexpr const char kPokeFilterHint[] = "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
|
||||
|
||||
PokemonGameView::PokemonGameView(ConfigService& config,
|
||||
CollectionService<PokemonCard>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module)
|
||||
IGameModule& westModule,
|
||||
IGameModule& asiaModule,
|
||||
PokemonSetCatalogService& catalogStore)
|
||||
: config_(config),
|
||||
collection_(collection),
|
||||
sets_(sets),
|
||||
images_(images),
|
||||
cardPreview_(cardPreview),
|
||||
module_(module) {}
|
||||
westModule_(westModule),
|
||||
asiaModule_(asiaModule),
|
||||
catalogStore_(catalogStore) {}
|
||||
|
||||
void PokemonGameView::ensureSetsLoaded() {
|
||||
if (attemptedInitialSetLoad_) return;
|
||||
@@ -49,6 +84,208 @@ void PokemonGameView::ensureSetsLoaded() {
|
||||
loadOrRefresh(Game::JapanesePokemon, setsCacheAsia_);
|
||||
}
|
||||
|
||||
void PokemonGameView::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 PokemonGameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer) {
|
||||
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto makeToolBtn = [&](const char* svg, const wxString& tip) {
|
||||
wxBitmap bmp = svgIconBitmap(svg, kPokeToolbarIconPx, "#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(kPokeFilterHint);
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
void PokemonGameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
const std::string tbHex = palette.buttonText.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
if (toolbarButtons_[0]) {
|
||||
toolbarButtons_[0]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarAdd, kPokeToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[1]) {
|
||||
toolbarButtons_[1]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarEdit, kPokeToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[2]) {
|
||||
toolbarButtons_[2]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarDelete, kPokeToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonGameView::selectTab(int index) {
|
||||
if (index < 0 || index > 1 || book_ == nullptr) return;
|
||||
activeTab_ = index;
|
||||
book_->SetSelection(index);
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
}
|
||||
|
||||
void PokemonGameView::refreshTabBarTheme(const ThemePalette& palette) {
|
||||
if (tabBar_ == nullptr) return;
|
||||
|
||||
const wxColour barBg = palette.panelBg;
|
||||
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);
|
||||
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 PokemonGameView::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);
|
||||
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_);
|
||||
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* PokemonGameView::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 PokemonSetCompletionPanel(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));
|
||||
contentPanel_->CallAfter([this]() {
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
});
|
||||
}
|
||||
return contentPanel_;
|
||||
}
|
||||
|
||||
wxPanel* PokemonGameView::listPanel(wxWindow* parent) {
|
||||
if (listPanel_ == nullptr) {
|
||||
listPanel_ = new PokemonCardListPanel(parent);
|
||||
@@ -73,16 +310,23 @@ wxPanel* PokemonGameView::selectedPanel(wxWindow* parent) {
|
||||
}
|
||||
|
||||
void PokemonGameView::refreshCollection() {
|
||||
if (listPanel_ == nullptr) return;
|
||||
if (contentPanel_ == nullptr && listPanel_ == nullptr) return;
|
||||
|
||||
auto loaded = collection_.list(Game::Pokemon);
|
||||
if (!loaded) {
|
||||
showThemedMessageDialog(nullptr, "Failed to load Pokemon collection: " + loaded.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
listPanel_->setCards(std::move(loaded).value());
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->setCollection(std::move(cards));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<Set>& PokemonGameView::setsForDialog(PokemonRegion region) {
|
||||
@@ -195,56 +439,127 @@ void PokemonGameView::onDeleteCard(wxWindow* parentWindow) {
|
||||
}
|
||||
|
||||
std::string PokemonGameView::onUpdateSets(wxWindow* parentWindow) {
|
||||
auto westOut = sets_.updateSets(Game::Pokemon);
|
||||
auto asiaOut = sets_.updateSets(Game::JapanesePokemon);
|
||||
|
||||
if (westOut) {
|
||||
setsCacheWest_ = westOut.value();
|
||||
}
|
||||
if (asiaOut) {
|
||||
setsCacheAsia_ = asiaOut.value();
|
||||
auto* westSrc = dynamic_cast<PokemonSetSource*>(&westModule_.setSource());
|
||||
auto* asiaSrc = dynamic_cast<JapanesePokemonSetSource*>(&asiaModule_.setSource());
|
||||
if (westSrc == nullptr || asiaSrc == nullptr) {
|
||||
showThemedMessageDialog(parentWindow, "Pokemon set source unavailable.",
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
|
||||
if (!westOut && !asiaOut) {
|
||||
auto westBoth = westSrc->fetchAllWithCatalog();
|
||||
auto asiaBoth = asiaSrc->fetchAllWithCatalog();
|
||||
|
||||
std::string westErr;
|
||||
std::string asiaErr;
|
||||
std::size_t westSets = 0;
|
||||
std::size_t asiaSets = 0;
|
||||
std::size_t westPacks = 0;
|
||||
std::size_t asiaPacks = 0;
|
||||
|
||||
if (westBoth) {
|
||||
auto savedSets = sets_.saveSets(Game::Pokemon, westBoth.value().sets);
|
||||
if (!savedSets) {
|
||||
westErr = savedSets.error();
|
||||
} else {
|
||||
auto savedCatalog =
|
||||
catalogStore_.save(PokemonRegion::West, westBoth.value().catalog);
|
||||
if (!savedCatalog) {
|
||||
westErr = "sets saved, but catalog failed: " + savedCatalog.error();
|
||||
}
|
||||
setsCacheWest_ = westBoth.value().sets;
|
||||
westSets = westBoth.value().sets.size();
|
||||
westPacks = westBoth.value().catalog.packs.size();
|
||||
}
|
||||
} else {
|
||||
westErr = westBoth.error();
|
||||
}
|
||||
|
||||
if (asiaBoth) {
|
||||
auto savedSets = sets_.saveSets(Game::JapanesePokemon, asiaBoth.value().sets);
|
||||
if (!savedSets) {
|
||||
asiaErr = savedSets.error();
|
||||
} else {
|
||||
auto savedCatalog =
|
||||
catalogStore_.save(PokemonRegion::Asia, asiaBoth.value().catalog);
|
||||
if (!savedCatalog) {
|
||||
asiaErr = "sets saved, but catalog failed: " + savedCatalog.error();
|
||||
}
|
||||
setsCacheAsia_ = asiaBoth.value().sets;
|
||||
asiaSets = asiaBoth.value().sets.size();
|
||||
asiaPacks = asiaBoth.value().catalog.packs.size();
|
||||
}
|
||||
} else {
|
||||
asiaErr = asiaBoth.error();
|
||||
}
|
||||
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->reloadFromStore();
|
||||
if (auto loaded = collection_.list(Game::Pokemon)) {
|
||||
setCompletionPanel_->setCollection(std::move(loaded).value());
|
||||
}
|
||||
}
|
||||
|
||||
if (!westErr.empty() && !asiaErr.empty()) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Failed to update West sets: " + westOut.error() +
|
||||
"\nFailed to update Asia sets: " + asiaOut.error(),
|
||||
"Failed to update West: " + westErr + "\nFailed to update Asia: " + asiaErr,
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
if (!westOut) {
|
||||
if (!westErr.empty()) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(asiaOut.value().size()) +
|
||||
" Asia Pokemon sets, but West failed: " + westOut.error(),
|
||||
"Updated " + std::to_string(asiaSets) + " Asia sets / " +
|
||||
std::to_string(asiaPacks) + " checklists, but West failed: " + westErr,
|
||||
"Sets partially updated", wxOK | wxICON_WARNING);
|
||||
return "Pokemon sets partially updated.";
|
||||
}
|
||||
if (!asiaOut) {
|
||||
if (!asiaErr.empty()) {
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(westOut.value().size()) +
|
||||
" West Pokemon sets, but Asia failed: " + asiaOut.error(),
|
||||
"Updated " + std::to_string(westSets) + " West sets / " +
|
||||
std::to_string(westPacks) + " checklists, but Asia failed: " + asiaErr,
|
||||
"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.",
|
||||
"Updated " + std::to_string(westSets) + " West sets (" +
|
||||
std::to_string(westPacks) + " checklists) and " + std::to_string(asiaSets) +
|
||||
" Asia sets (" + std::to_string(asiaPacks) + " checklists).",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
return "Pokemon sets updated.";
|
||||
}
|
||||
|
||||
void PokemonGameView::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(kPokeFilterHint);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void PokemonGameView::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);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
#include "ccm/ui/PokemonSetCompletionPanel.hpp"
|
||||
|
||||
#include "ccm/services/PokemonSetCompletion.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) {
|
||||
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
|
||||
|
||||
PokemonSetCompletionPanel::PokemonSetCompletionPanel(wxWindow* parent,
|
||||
PokemonSetCatalogService& catalogStore)
|
||||
: wxPanel(parent), catalogStore_(catalogStore) {
|
||||
palette_ = paletteForTheme(inferThemeFromWindow(this));
|
||||
|
||||
auto* filterRow = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto* regionLabel = new wxStaticText(this, wxID_ANY, "Region");
|
||||
regionChoice_ = new wxChoice(this, wxID_ANY);
|
||||
regionChoice_->Append("All regions");
|
||||
regionChoice_->SetSelection(0);
|
||||
regionChoice_->Bind(wxEVT_CHOICE, &PokemonSetCompletionPanel::onRegionChoice, this);
|
||||
filterRow->Add(regionLabel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
|
||||
filterRow->Add(regionChoice_, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 16);
|
||||
|
||||
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, &PokemonSetCompletionPanel::onLanguageChoice, this);
|
||||
filterRow->Add(langLabel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
|
||||
filterRow->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(filterRow, 0, wxEXPAND | wxALL, 8);
|
||||
root->Add(book_, 1, wxEXPAND);
|
||||
SetSizer(root);
|
||||
|
||||
showGridPage();
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::setCollection(std::vector<PokemonCard> cards) {
|
||||
collection_ = std::move(cards);
|
||||
refreshRegionChoice();
|
||||
refreshLanguageChoice();
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::reloadFromStore() {
|
||||
westCatalogLoaded_ = false;
|
||||
asiaCatalogLoaded_ = false;
|
||||
westCatalog_ = {};
|
||||
asiaCatalog_ = {};
|
||||
if (catalogStore_.exists(PokemonRegion::West)) {
|
||||
if (auto loaded = catalogStore_.load(PokemonRegion::West)) {
|
||||
westCatalog_ = std::move(loaded).value();
|
||||
westCatalogLoaded_ = true;
|
||||
}
|
||||
}
|
||||
if (catalogStore_.exists(PokemonRegion::Asia)) {
|
||||
if (auto loaded = catalogStore_.load(PokemonRegion::Asia)) {
|
||||
asiaCatalog_ = std::move(loaded).value();
|
||||
asiaCatalogLoaded_ = true;
|
||||
}
|
||||
}
|
||||
showGridPage();
|
||||
rebuildGrid();
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::applyTheme(const ThemePalette& palette) {
|
||||
palette_ = palette;
|
||||
applyThemeToWindowTree(this, palette, inferThemeFromWindow(this));
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::showGridPage() {
|
||||
detailSetId_.clear();
|
||||
detailSetName_.clear();
|
||||
book_->SetSelection(0);
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::showChecklistPage(PokemonRegion region,
|
||||
const std::string& setId,
|
||||
const std::string& setName) {
|
||||
detailRegion_ = region;
|
||||
detailSetId_ = setId;
|
||||
detailSetName_ = setName;
|
||||
detailTitle_->SetLabelText(
|
||||
wxString::FromUTF8(displaySetName(setName, region).c_str()));
|
||||
rebuildChecklist(region, setId);
|
||||
book_->SetSelection(1);
|
||||
}
|
||||
|
||||
std::string PokemonSetCompletionPanel::displaySetName(const std::string& setName,
|
||||
PokemonRegion region) const {
|
||||
std::string out = setName;
|
||||
if (!regionFilter_.has_value()) {
|
||||
out += " (";
|
||||
out += std::string(to_string(region));
|
||||
out += ")";
|
||||
}
|
||||
if (languageFilter_.has_value()) {
|
||||
out += " (";
|
||||
out += std::string(to_string(*languageFilter_));
|
||||
out += ")";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool PokemonSetCompletionPanel::catalogsReadyForFilter() const {
|
||||
if (!regionFilter_.has_value()) {
|
||||
return westCatalogLoaded_ || asiaCatalogLoaded_;
|
||||
}
|
||||
if (*regionFilter_ == PokemonRegion::West) return westCatalogLoaded_;
|
||||
return asiaCatalogLoaded_;
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::refreshRegionChoice() {
|
||||
const auto previous = regionFilter_;
|
||||
const auto present =
|
||||
pokemonRegionsInCollection(collection_, westCatalog_, asiaCatalog_);
|
||||
|
||||
regionChoice_->Clear();
|
||||
regionChoice_->Append("All regions");
|
||||
for (const PokemonRegion region : present) {
|
||||
regionChoice_->Append(wxString::FromUTF8(std::string(to_string(region)).c_str()));
|
||||
}
|
||||
|
||||
int selection = 0;
|
||||
regionFilter_ = 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);
|
||||
regionFilter_ = previous;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
regionChoice_->SetSelection(selection);
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::refreshLanguageChoice() {
|
||||
const auto previous = languageFilter_;
|
||||
const auto present = pokemonLanguagesInCollection(collection_, regionFilter_);
|
||||
|
||||
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 PokemonSetCompletionPanel::onRegionChoice(wxCommandEvent& /*event*/) {
|
||||
const int sel = regionChoice_->GetSelection();
|
||||
if (sel <= 0) {
|
||||
regionFilter_ = std::nullopt;
|
||||
} else {
|
||||
const auto present =
|
||||
pokemonRegionsInCollection(collection_, westCatalog_, asiaCatalog_);
|
||||
const auto idx = static_cast<std::size_t>(sel - 1);
|
||||
if (idx < present.size()) {
|
||||
regionFilter_ = present[idx];
|
||||
} else {
|
||||
regionFilter_ = std::nullopt;
|
||||
regionChoice_->SetSelection(0);
|
||||
}
|
||||
}
|
||||
refreshLanguageChoice();
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::onLanguageChoice(wxCommandEvent& /*event*/) {
|
||||
const int sel = languageChoice_->GetSelection();
|
||||
if (sel <= 0) {
|
||||
languageFilter_ = std::nullopt;
|
||||
} else {
|
||||
const auto present = pokemonLanguagesInCollection(collection_, regionFilter_);
|
||||
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 PokemonSetCompletionPanel::rebuildCurrentView() {
|
||||
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
||||
const auto rows = computePokemonSetCompletion(
|
||||
collection_, westCatalog_, asiaCatalog_, regionFilter_, languageFilter_);
|
||||
bool stillVisible = false;
|
||||
for (const auto& row : rows) {
|
||||
if (row.setId == detailSetId_ && row.region == detailRegion_) {
|
||||
stillVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stillVisible) {
|
||||
showGridPage();
|
||||
rebuildGrid();
|
||||
return;
|
||||
}
|
||||
detailTitle_->SetLabelText(
|
||||
wxString::FromUTF8(displaySetName(detailSetName_, detailRegion_).c_str()));
|
||||
rebuildChecklist(detailRegion_, detailSetId_);
|
||||
} else {
|
||||
rebuildGrid();
|
||||
}
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::setEmptyMessage(const wxString& message) {
|
||||
clearGridTiles();
|
||||
emptyLabel_->SetLabelText(message);
|
||||
emptyLabel_->Wrap(480);
|
||||
emptyLabel_->Show();
|
||||
scroll_->Hide();
|
||||
gridPage_->Layout();
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::clearGridTiles() {
|
||||
if (gridSizer_ == nullptr) return;
|
||||
gridSizer_->Clear(true);
|
||||
}
|
||||
|
||||
void PokemonSetCompletionPanel::rebuildGrid() {
|
||||
if (!catalogsReadyForFilter()) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"Set checklists are not downloaded yet.\n"
|
||||
"Run Sets → Update Pokemon to enable Set Completion."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rows = computePokemonSetCompletion(
|
||||
collection_, westCatalog_, asiaCatalog_, regionFilter_, languageFilter_);
|
||||
if (rows.empty()) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"No Pokemon sets in progress yet.\n"
|
||||
"Add cards on the Single Cards tab to track set completion here."));
|
||||
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, row.region);
|
||||
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 PokemonRegion region = row.region;
|
||||
const std::string setId = row.setId;
|
||||
const std::string setName = row.setName;
|
||||
auto openDetail = [this, region, setId, setName](wxMouseEvent&) {
|
||||
showChecklistPage(region, 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 PokemonSetCompletionPanel::rebuildChecklist(PokemonRegion region,
|
||||
const std::string& setId) {
|
||||
checklist_->DeleteAllItems();
|
||||
const auto entries = pokemonChecklistForSet(
|
||||
collection_, westCatalog_, asiaCatalog_, region, setId, languageFilter_);
|
||||
const wxColour muted = mutedTextColour(palette_);
|
||||
const wxColour ownedGreen(46, 160, 67);
|
||||
|
||||
long idx = 0;
|
||||
for (const auto& entry : entries) {
|
||||
const std::string line =
|
||||
(entry.owned ? "✓ " : " ") + entry.setNo + " — " + entry.name;
|
||||
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
|
||||
+332
-27
@@ -1,32 +1,65 @@
|
||||
#include "ccm/ui/YuGiOhGameView.hpp"
|
||||
|
||||
#include "ccm/games/yugioh/YuGiOhSetSource.hpp"
|
||||
#include "ccm/ui/CardEditModalGuard.hpp"
|
||||
#include "ccm/ui/SvgIcons.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
#include "ccm/ui/YuGiOhCardEditDialog.hpp"
|
||||
#include "ccm/ui/YuGiOhCardListPanel.hpp"
|
||||
#include "ccm/ui/YuGiOhSelectedCardPanel.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
#include "ccm/ui/YuGiOhSetCompletionPanel.hpp"
|
||||
|
||||
#include <wx/msgdlg.h>
|
||||
#include <wx/bmpbuttn.h>
|
||||
#include <wx/cursor.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 <optional>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace ccm::ui {
|
||||
|
||||
YuGiOhGameView::YuGiOhGameView(ConfigService& config,
|
||||
CollectionService<YuGiOhCard>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module)
|
||||
namespace {
|
||||
constexpr int kYgoToolbarIconPx = 18;
|
||||
constexpr const char kYgoFilterHint[] = "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
|
||||
|
||||
YuGiOhGameView::YuGiOhGameView(ConfigService& config,
|
||||
CollectionService<YuGiOhCard>& collection,
|
||||
SetService& sets,
|
||||
ImageService& images,
|
||||
CardPreviewService& cardPreview,
|
||||
IGameModule& module,
|
||||
YuGiOhSetCatalogService& catalogStore)
|
||||
: config_(config),
|
||||
collection_(collection),
|
||||
sets_(sets),
|
||||
images_(images),
|
||||
cardPreview_(cardPreview),
|
||||
module_(module) {}
|
||||
module_(module),
|
||||
catalogStore_(catalogStore) {}
|
||||
|
||||
void YuGiOhGameView::ensureSetsLoaded() {
|
||||
if (attemptedInitialSetLoad_) return;
|
||||
@@ -50,6 +83,208 @@ void YuGiOhGameView::ensureSetsLoaded() {
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhGameView::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 YuGiOhGameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageSizer) {
|
||||
auto* toolbar = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto makeToolBtn = [&](const char* svg, const wxString& tip) {
|
||||
wxBitmap bmp = svgIconBitmap(svg, kYgoToolbarIconPx, "#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(kYgoFilterHint);
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
void YuGiOhGameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
const std::string tbHex = palette.buttonText.GetAsString(wxC2S_HTML_SYNTAX).ToStdString();
|
||||
if (toolbarButtons_[0]) {
|
||||
toolbarButtons_[0]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarAdd, kYgoToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[1]) {
|
||||
toolbarButtons_[1]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarEdit, kYgoToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
if (toolbarButtons_[2]) {
|
||||
toolbarButtons_[2]->SetBitmap(
|
||||
svgIconBitmap(kSvgToolbarDelete, kYgoToolbarIconPx, tbHex.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void YuGiOhGameView::selectTab(int index) {
|
||||
if (index < 0 || index > 1 || book_ == nullptr) return;
|
||||
activeTab_ = index;
|
||||
book_->SetSelection(index);
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
}
|
||||
|
||||
void YuGiOhGameView::refreshTabBarTheme(const ThemePalette& palette) {
|
||||
if (tabBar_ == nullptr) return;
|
||||
|
||||
const wxColour barBg = palette.panelBg;
|
||||
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);
|
||||
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 YuGiOhGameView::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);
|
||||
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_);
|
||||
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* YuGiOhGameView::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 YuGiOhSetCompletionPanel(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));
|
||||
contentPanel_->CallAfter([this]() {
|
||||
refreshTabBarTheme(paletteForTheme(config_.current().theme));
|
||||
});
|
||||
}
|
||||
return contentPanel_;
|
||||
}
|
||||
|
||||
wxPanel* YuGiOhGameView::listPanel(wxWindow* parent) {
|
||||
if (listPanel_ == nullptr) {
|
||||
listPanel_ = new YuGiOhCardListPanel(parent);
|
||||
@@ -74,16 +309,23 @@ wxPanel* YuGiOhGameView::selectedPanel(wxWindow* parent) {
|
||||
}
|
||||
|
||||
void YuGiOhGameView::refreshCollection() {
|
||||
if (listPanel_ == nullptr) return;
|
||||
if (contentPanel_ == nullptr && listPanel_ == nullptr) return;
|
||||
|
||||
auto loaded = collection_.list(Game::YuGiOh);
|
||||
if (!loaded) {
|
||||
showThemedMessageDialog(nullptr, "Failed to load Yu-Gi-Oh! collection: " + loaded.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
listPanel_->setCards(std::move(loaded).value());
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->setCollection(std::move(cards));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<Set>& YuGiOhGameView::setsForDialog() {
|
||||
@@ -94,8 +336,9 @@ const std::vector<Set>& YuGiOhGameView::setsForDialog() {
|
||||
setsCache_ = std::move(loaded).value();
|
||||
std::sort(setsCache_.begin(), setsCache_.end(),
|
||||
[](const Set& a, const Set& b) { return a.releaseDate < b.releaseDate; });
|
||||
} else {
|
||||
setsCache_.clear();
|
||||
}
|
||||
else setsCache_.clear();
|
||||
return setsCache_;
|
||||
}
|
||||
|
||||
@@ -135,13 +378,18 @@ void YuGiOhGameView::onAddCard(wxWindow* parentWindow) {
|
||||
persisted.images = std::move(normalized).value();
|
||||
auto updated = collection_.update(Game::YuGiOh, persisted);
|
||||
if (!updated) {
|
||||
showThemedMessageDialog(parentWindow, "Card added, but image name normalization failed to persist: " + updated.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
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);
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Card added, but image rename to ID-prefixed format failed: " + normalized.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
refreshCollection();
|
||||
}
|
||||
@@ -150,7 +398,8 @@ void YuGiOhGameView::onEditCard(wxWindow* parentWindow) {
|
||||
if (listPanel_ == nullptr) return;
|
||||
auto sel = listPanel_->selected();
|
||||
if (!sel) {
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit", wxOK | wxICON_INFORMATION);
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (cardEditModalIsActive()) {
|
||||
@@ -176,7 +425,8 @@ void YuGiOhGameView::onDeleteCard(wxWindow* parentWindow) {
|
||||
if (listPanel_ == nullptr) return;
|
||||
auto sel = listPanel_->selected();
|
||||
if (!sel) {
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Delete", wxOK | wxICON_INFORMATION);
|
||||
showThemedMessageDialog(parentWindow, "Select a card first.", "Delete",
|
||||
wxOK | wxICON_INFORMATION);
|
||||
return;
|
||||
}
|
||||
if (showThemedConfirmDialog(parentWindow, "Delete \"" + sel->name + "\"?",
|
||||
@@ -193,27 +443,82 @@ void YuGiOhGameView::onDeleteCard(wxWindow* parentWindow) {
|
||||
}
|
||||
|
||||
std::string YuGiOhGameView::onUpdateSets(wxWindow* parentWindow) {
|
||||
auto out = sets_.updateSets(Game::YuGiOh);
|
||||
if (!out) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update sets: " + out.error(),
|
||||
auto* ygoSrc = dynamic_cast<YuGiOhSetSource*>(&module_.setSource());
|
||||
if (ygoSrc == nullptr) {
|
||||
showThemedMessageDialog(parentWindow, "Yu-Gi-Oh! set source unavailable.",
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
setsCache_ = out.value();
|
||||
|
||||
auto both = ygoSrc->fetchAllWithCatalog();
|
||||
if (!both) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to update sets: " + both.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
|
||||
auto savedSets = sets_.saveSets(Game::YuGiOh, both.value().sets);
|
||||
if (!savedSets) {
|
||||
showThemedMessageDialog(parentWindow, "Failed to save sets: " + savedSets.error(),
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return "Update failed";
|
||||
}
|
||||
|
||||
auto savedCatalog = catalogStore_.save(both.value().catalog);
|
||||
if (!savedCatalog) {
|
||||
showThemedMessageDialog(parentWindow,
|
||||
"Sets saved, but set catalog failed: " + savedCatalog.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
|
||||
setsCache_ = both.value().sets;
|
||||
std::sort(setsCache_.begin(), setsCache_.end(),
|
||||
[](const Set& a, const Set& b) { return a.releaseDate < b.releaseDate; });
|
||||
showThemedMessageDialog(parentWindow, "Updated " + std::to_string(out.value().size()) + " Yu-Gi-Oh! sets.",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
if (setCompletionPanel_ != nullptr) {
|
||||
setCompletionPanel_->reloadFromStore();
|
||||
if (auto loaded = collection_.list(Game::YuGiOh)) {
|
||||
setCompletionPanel_->setCollection(std::move(loaded).value());
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t setCount = both.value().sets.size();
|
||||
const std::size_t packCount = both.value().catalog.packs.size();
|
||||
showThemedMessageDialog(
|
||||
parentWindow,
|
||||
"Updated " + std::to_string(setCount) + " Yu-Gi-Oh! sets and " +
|
||||
std::to_string(packCount) + " set checklists.",
|
||||
"Sets updated", wxOK | wxICON_INFORMATION);
|
||||
return "Yu-Gi-Oh! sets updated.";
|
||||
}
|
||||
|
||||
void YuGiOhGameView::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(kYgoFilterHint);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void YuGiOhGameView::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);
|
||||
if (filterInput_ != nullptr) {
|
||||
filterInput_->SetBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetForegroundColour(palette.inputText);
|
||||
filterInput_->SetOwnBackgroundColour(palette.inputBg);
|
||||
filterInput_->SetOwnForegroundColour(palette.inputText);
|
||||
filterInput_->Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ccm::ui
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
#include "ccm/ui/YuGiOhSetCompletionPanel.hpp"
|
||||
|
||||
#include "ccm/services/YuGiOhSetCompletion.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
|
||||
|
||||
YuGiOhSetCompletionPanel::YuGiOhSetCompletionPanel(wxWindow* parent,
|
||||
YuGiOhSetCatalogService& 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, &YuGiOhSetCompletionPanel::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 YuGiOhSetCompletionPanel::setCollection(std::vector<YuGiOhCard> cards) {
|
||||
collection_ = std::move(cards);
|
||||
refreshLanguageChoice();
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::reloadFromStore() {
|
||||
catalogLoaded_ = false;
|
||||
catalog_ = {};
|
||||
if (catalogStore_.exists()) {
|
||||
if (auto loaded = catalogStore_.load()) {
|
||||
catalog_ = std::move(loaded).value();
|
||||
catalogLoaded_ = true;
|
||||
}
|
||||
}
|
||||
showGridPage();
|
||||
rebuildGrid();
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::applyTheme(const ThemePalette& palette) {
|
||||
palette_ = palette;
|
||||
applyThemeToWindowTree(this, palette, inferThemeFromWindow(this));
|
||||
rebuildCurrentView();
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::showGridPage() {
|
||||
detailSetId_.clear();
|
||||
detailSetName_.clear();
|
||||
book_->SetSelection(0);
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::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 YuGiOhSetCompletionPanel::displaySetName(const std::string& setName) const {
|
||||
if (!languageFilter_.has_value()) return setName;
|
||||
return setName + " (" + std::string(to_string(*languageFilter_)) + ")";
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::refreshLanguageChoice() {
|
||||
const auto previous = languageFilter_;
|
||||
const auto present = yuGiOhLanguagesInCollection(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 YuGiOhSetCompletionPanel::onLanguageChoice(wxCommandEvent& /*event*/) {
|
||||
const int sel = languageChoice_->GetSelection();
|
||||
if (sel <= 0) {
|
||||
languageFilter_ = std::nullopt;
|
||||
} else {
|
||||
const auto present = yuGiOhLanguagesInCollection(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 YuGiOhSetCompletionPanel::rebuildCurrentView() {
|
||||
if (book_->GetSelection() == 1 && !detailSetId_.empty()) {
|
||||
const auto rows =
|
||||
computeYuGiOhSetCompletion(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 YuGiOhSetCompletionPanel::setEmptyMessage(const wxString& message) {
|
||||
clearGridTiles();
|
||||
emptyLabel_->SetLabelText(message);
|
||||
emptyLabel_->Wrap(480);
|
||||
emptyLabel_->Show();
|
||||
scroll_->Hide();
|
||||
gridPage_->Layout();
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::clearGridTiles() {
|
||||
if (gridSizer_ == nullptr) return;
|
||||
gridSizer_->Clear(true);
|
||||
}
|
||||
|
||||
void YuGiOhSetCompletionPanel::rebuildGrid() {
|
||||
if (!catalogLoaded_) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"Set checklists are not downloaded yet.\n"
|
||||
"Run Sets → Update Yu-Gi-Oh! to enable Set Completion."));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rows = computeYuGiOhSetCompletion(collection_, catalog_, languageFilter_);
|
||||
if (rows.empty()) {
|
||||
setEmptyMessage(wxString::FromUTF8(
|
||||
"No Yu-Gi-Oh! sets in progress yet.\n"
|
||||
"Add cards on the Single Cards tab to track set completion here."));
|
||||
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 YuGiOhSetCompletionPanel::rebuildChecklist(const std::string& setId) {
|
||||
checklist_->DeleteAllItems();
|
||||
const auto entries =
|
||||
yuGiOhChecklistForSet(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.
|
||||
const std::string line =
|
||||
(entry.owned ? "✓ " : " ") + entry.setNo + " — " + entry.name;
|
||||
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
|
||||
Reference in New Issue
Block a user