patch: Feature/ygo set selection (#16)

This commit is contained in:
Sebastian Dine
2026-05-13 21:16:41 +02:00
committed by GitHub
parent 8a50e8daba
commit 42926f2fb5
32 changed files with 827 additions and 52 deletions
+3
View File
@@ -2,6 +2,8 @@
// declared in the corresponding base headers (BaseCardListPanel.hpp,
// BaseSelectedCardPanel.hpp) and defined exactly once here, so that template
// instantiations (Magic, Pokemon, ...) all use the same event type tag.
// EVT_CARD_ACTIVATED is declared alongside EVT_CARD_SELECTED in
// BaseCardListPanel.hpp.
#include "ccm/ui/BaseCardListPanel.hpp"
#include "ccm/ui/BaseSelectedCardPanel.hpp"
@@ -9,6 +11,7 @@
namespace ccm::ui {
wxDEFINE_EVENT(EVT_CARD_SELECTED, wxCommandEvent);
wxDEFINE_EVENT(EVT_CARD_ACTIVATED, wxCommandEvent);
wxDEFINE_EVENT(EVT_PREVIEW_STATUS, wxCommandEvent);
} // namespace ccm::ui
+18
View File
@@ -1,11 +1,13 @@
#include "ccm/ui/MagicGameView.hpp"
#include "ccm/ui/CardEditModalGuard.hpp"
#include "ccm/ui/MagicCardEditDialog.hpp"
#include "ccm/ui/MagicCardListPanel.hpp"
#include "ccm/ui/MagicSelectedCardPanel.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/msgdlg.h>
#include <wx/window.h>
#include <optional>
#include <string>
@@ -54,6 +56,10 @@ wxPanel* MagicGameView::listPanel(wxWindow* parent) {
selectedPanel_->setCard(listPanel_->selected());
}
});
listPanel_->Bind(EVT_CARD_ACTIVATED, [this](wxCommandEvent&) {
wxWindow* owner = wxGetTopLevelParent(listPanel_);
onEditCard(owner != nullptr ? owner : static_cast<wxWindow*>(listPanel_));
});
}
return listPanel_;
}
@@ -88,6 +94,11 @@ const std::vector<Set>& MagicGameView::setsForDialog() {
}
void MagicGameView::onAddCard(wxWindow* parentWindow) {
if (cardEditModalIsActive()) {
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
wxString::FromUTF8("Add card"), wxOK | wxICON_INFORMATION);
return;
}
MagicCard fresh;
fresh.amount = 1;
fresh.language = Language::English;
@@ -96,6 +107,7 @@ void MagicGameView::onAddCard(wxWindow* parentWindow) {
MagicCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Create, fresh,
&setsForDialog());
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
auto added = collection_.add(Game::Magic, dlg.card());
@@ -132,9 +144,15 @@ void MagicGameView::onEditCard(wxWindow* parentWindow) {
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit", wxOK | wxICON_INFORMATION);
return;
}
if (cardEditModalIsActive()) {
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
wxString::FromUTF8("Edit"), wxOK | wxICON_INFORMATION);
return;
}
MagicCardEditDialog dlg(parentWindow, images_, sets_, EditMode::Edit, *sel,
&setsForDialog());
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
auto updated = collection_.update(Game::Magic, dlg.card());
if (!updated) {
+18
View File
@@ -1,11 +1,13 @@
#include "ccm/ui/PokemonGameView.hpp"
#include "ccm/ui/CardEditModalGuard.hpp"
#include "ccm/ui/PokemonCardEditDialog.hpp"
#include "ccm/ui/PokemonCardListPanel.hpp"
#include "ccm/ui/PokemonSelectedCardPanel.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/msgdlg.h>
#include <wx/window.h>
#include <optional>
#include <string>
@@ -51,6 +53,10 @@ wxPanel* PokemonGameView::listPanel(wxWindow* parent) {
selectedPanel_->setCard(listPanel_->selected());
}
});
listPanel_->Bind(EVT_CARD_ACTIVATED, [this](wxCommandEvent&) {
wxWindow* owner = wxGetTopLevelParent(listPanel_);
onEditCard(owner != nullptr ? owner : static_cast<wxWindow*>(listPanel_));
});
}
return listPanel_;
}
@@ -85,6 +91,11 @@ const std::vector<Set>& PokemonGameView::setsForDialog() {
}
void PokemonGameView::onAddCard(wxWindow* parentWindow) {
if (cardEditModalIsActive()) {
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
wxString::FromUTF8("Add card"), wxOK | wxICON_INFORMATION);
return;
}
PokemonCard fresh;
fresh.amount = 1;
fresh.language = Language::English;
@@ -93,6 +104,7 @@ void PokemonGameView::onAddCard(wxWindow* parentWindow) {
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create, fresh,
&setsForDialog());
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
auto added = collection_.add(Game::Pokemon, dlg.card());
@@ -129,9 +141,15 @@ void PokemonGameView::onEditCard(wxWindow* parentWindow) {
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit", wxOK | wxICON_INFORMATION);
return;
}
if (cardEditModalIsActive()) {
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
wxString::FromUTF8("Edit"), wxOK | wxICON_INFORMATION);
return;
}
PokemonCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit, *sel,
&setsForDialog());
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
auto updated = collection_.update(Game::Pokemon, dlg.card());
if (!updated) {
+135
View File
@@ -0,0 +1,135 @@
#include "ccm/ui/SwitchCtrl.hpp"
#include "ccm/domain/Enums.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/dcbuffer.h>
#include <wx/dcclient.h>
#include <algorithm>
namespace ccm::ui {
wxDEFINE_EVENT(EVT_CCM_SWITCH, wxCommandEvent);
namespace {
wxColour liftRgb(const wxColour& c, int delta) {
auto lift = [delta](unsigned char ch) -> unsigned char {
const int v = static_cast<int>(ch) + delta;
return static_cast<unsigned char>(v > 255 ? 255 : (v < 0 ? 0 : v));
};
return wxColour(lift(c.Red()), lift(c.Green()), lift(c.Blue()));
}
} // namespace
SwitchCtrl::SwitchCtrl(wxWindow* parent, wxWindowID id, bool initialOn)
: wxWindow(parent, id, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE, wxString()),
on_(initialOn) {
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetCursor(wxCURSOR_HAND);
const wxSize sz = FromDIP(wxSize(40, 20));
SetMinSize(sz);
SetMaxSize(sz);
SetInitialSize(sz);
Bind(wxEVT_PAINT, &SwitchCtrl::onPaint, this);
Bind(wxEVT_LEFT_DOWN, &SwitchCtrl::onLeftDown, this);
Bind(wxEVT_ENTER_WINDOW, &SwitchCtrl::onEnter, this);
Bind(wxEVT_LEAVE_WINDOW, &SwitchCtrl::onLeave, this);
Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
}
void SwitchCtrl::SetValue(bool on, bool notify) {
if (on_ == on) return;
on_ = on;
Refresh();
if (notify) {
wxCommandEvent e(EVT_CCM_SWITCH, GetId());
e.SetEventObject(this);
e.SetInt(on_ ? 1 : 0);
ProcessEvent(e);
}
}
bool SwitchCtrl::Enable(bool enable) {
const bool ok = wxWindow::Enable(enable);
SetCursor(enable ? wxCURSOR_HAND : wxCURSOR_ARROW);
Refresh();
return ok;
}
void SwitchCtrl::onEnter(wxMouseEvent& ev) {
hovered_ = true;
Refresh();
ev.Skip();
}
void SwitchCtrl::onLeave(wxMouseEvent& ev) {
hovered_ = false;
Refresh();
ev.Skip();
}
void SwitchCtrl::onLeftDown(wxMouseEvent& ev) {
if (!IsEnabled()) {
ev.Skip();
return;
}
on_ = !on_;
Refresh();
wxCommandEvent e(EVT_CCM_SWITCH, GetId());
e.SetEventObject(this);
e.SetInt(on_ ? 1 : 0);
ProcessEvent(e);
ev.Skip(false);
}
void SwitchCtrl::onPaint(wxPaintEvent&) {
wxAutoBufferedPaintDC dc(this);
const wxRect rect = GetClientRect();
if (rect.width <= 0 || rect.height <= 0) return;
const Theme theme = inferThemeFromWindow(this);
const ThemePalette p = paletteForTheme(theme);
const bool dark = theme == Theme::Dark;
wxColour trackOff = p.inputBg;
wxColour trackOn = p.buttonBg;
wxColour thumb = dark ? wxColour(240, 240, 240) : wxColour(252, 252, 252);
wxColour border = dark ? wxColour(72, 72, 72) : wxColour(158, 158, 158);
wxColour track = on_ ? trackOn : trackOff;
if (hovered_ && IsEnabled()) {
track = liftRgb(track, dark ? 14 : 10);
}
if (!IsEnabled()) {
track = liftRgb(track, dark ? -22 : -25);
thumb = liftRgb(thumb, dark ? -55 : -35);
border = liftRgb(border, dark ? -15 : 10);
}
// Fill the full client rect first so rounded-track corners do not show
// undrawn pixels (often black) against the parent panel.
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(p.panelBg));
dc.DrawRectangle(rect);
dc.SetPen(wxPen(border));
dc.SetBrush(wxBrush(track));
const int radius = rect.height / 2;
dc.DrawRoundedRectangle(rect, radius);
const int pad = FromDIP(2);
const int thumbD = std::max(4, rect.height - 2 * pad);
const int travel = std::max(0, rect.width - 2 * pad - thumbD);
const int thumbX = pad + (on_ ? travel : 0);
const int thumbY = rect.y + (rect.height - thumbD) / 2;
wxColour thumbBorder = liftRgb(border, dark ? 18 : -12);
dc.SetPen(wxPen(thumbBorder));
dc.SetBrush(wxBrush(thumb));
dc.DrawEllipse(thumbX, thumbY, thumbD, thumbD);
}
} // namespace ccm::ui
+33 -5
View File
@@ -2,6 +2,7 @@
#include <wx/button.h>
#include <wx/bmpbuttn.h>
#include <wx/tglbtn.h>
#include <wx/choice.h>
#include <wx/dcbuffer.h>
#include <wx/frame.h>
@@ -444,8 +445,11 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
#endif
}
// `wxToggleButton` is not a `wxButton` on MSW; without this branch it keeps
// native visual styles (e.g. light hover flashes) under dark palette dialogs.
if (dynamic_cast<wxButton*>(root) != nullptr ||
dynamic_cast<wxBitmapButton*>(root) != nullptr) {
dynamic_cast<wxBitmapButton*>(root) != nullptr ||
dynamic_cast<wxToggleButton*>(root) != nullptr) {
const bool darkLike = isDarkLikeTheme(theme);
root->SetThemeEnabled(!darkLike);
root->SetBackgroundColour(palette.buttonBg);
@@ -485,7 +489,11 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
return;
}
it->second.hovered = false;
const wxColour bg = it->second.focused ? it->second.hoverBg : it->second.normalBg;
const bool toggleOn =
dynamic_cast<wxToggleButton*>(root) != nullptr &&
static_cast<wxToggleButton*>(root)->GetValue();
const wxColour bg =
(it->second.focused || toggleOn) ? it->second.hoverBg : it->second.normalBg;
root->SetBackgroundColour(bg);
root->SetForegroundColour(it->second.text);
root->Refresh();
@@ -513,7 +521,11 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
const wxPoint localPos = root->ScreenToClient(mousePos);
const bool inside = root->GetClientRect().Contains(localPos);
it->second.hovered = inside;
const wxColour bg = (inside || it->second.focused) ? it->second.hoverBg : it->second.normalBg;
const bool toggleOn =
dynamic_cast<wxToggleButton*>(root) != nullptr &&
static_cast<wxToggleButton*>(root)->GetValue();
const wxColour bg =
(inside || it->second.focused || toggleOn) ? it->second.hoverBg : it->second.normalBg;
root->SetBackgroundColour(bg);
root->SetForegroundColour(it->second.text);
root->Refresh();
@@ -539,12 +551,25 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
}
it->second.focused = false;
it->second.pressed = false;
const wxColour bg = it->second.hovered ? it->second.hoverBg : it->second.normalBg;
const bool toggleOn =
dynamic_cast<wxToggleButton*>(root) != nullptr &&
static_cast<wxToggleButton*>(root)->GetValue();
const wxColour bg =
(it->second.hovered || toggleOn) ? it->second.hoverBg : it->second.normalBg;
root->SetBackgroundColour(bg);
root->SetForegroundColour(it->second.text);
root->Refresh();
event.Skip();
});
if (auto* toggle = dynamic_cast<wxToggleButton*>(root)) {
toggle->Bind(wxEVT_TOGGLEBUTTON, [root](wxCommandEvent& event) {
auto it = gButtonVisualStates.find(root);
if (it != gButtonVisualStates.end() && it->second.darkLike) {
root->Refresh();
}
event.Skip();
});
}
root->SetBackgroundStyle(wxBG_STYLE_PAINT);
root->Bind(wxEVT_ERASE_BACKGROUND, [](wxEraseEvent&) {});
root->Bind(wxEVT_PAINT, [root](wxPaintEvent& event) {
@@ -555,10 +580,13 @@ void applyThemeToWindowTree(wxWindow* root, const ThemePalette& palette, Theme t
}
wxAutoBufferedPaintDC dc(root);
const wxRect rect = root->GetClientRect();
const bool toggleOn =
dynamic_cast<wxToggleButton*>(root) != nullptr &&
static_cast<wxToggleButton*>(root)->GetValue();
wxColour bg = it->second.normalBg;
if (it->second.pressed) {
bg = it->second.pressedBg;
} else if (it->second.hovered || it->second.focused) {
} else if (it->second.hovered || it->second.focused || toggleOn) {
bg = it->second.hoverBg;
}
const wxColour fg = it->second.text;
+98 -1
View File
@@ -1,6 +1,8 @@
#include "ccm/ui/YuGiOhCardEditDialog.hpp"
#include "ccm/ui/SwitchCtrl.hpp"
#include "ccm/domain/Enums.hpp"
#include "ccm/util/YuGiOhPrintingSlot.hpp"
#include "ccm/util/YuGiOhSetLookup.hpp"
#include <wx/app.h>
#include <wx/panel.h>
#include <algorithm>
@@ -57,6 +59,33 @@ void YuGiOhCardEditDialog::buildFlagsRow(wxBoxSizer* flagsBox) {
flagsBox->Add(alteredCheck_, 0, wxRIGHT, 12);
}
void YuGiOhCardEditDialog::customizeSetPickerRow(wxBoxSizer& row, wxComboBox* combo) {
wxWindow* const host = combo->GetParent();
setCodeRowPanel_ = new wxPanel(host, wxID_ANY);
auto* inner = new wxBoxSizer(wxHORIZONTAL);
setCodeText_ = new wxTextCtrl(setCodeRowPanel_, wxID_ANY);
setCodeAutoBtn_ = new wxButton(setCodeRowPanel_, wxID_ANY, "Auto detect");
inner->Add(setCodeText_, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
inner->Add(setCodeAutoBtn_, 0, wxALIGN_CENTER_VERTICAL);
setCodeRowPanel_->SetSizer(inner);
setCodeRowPanel_->Show(false);
setModeHint_ = new wxStaticText(host, wxID_ANY, wxString());
setPickerSwitch_ = new SwitchCtrl(host, wxID_ANY, false);
setPickerSwitch_->Bind(EVT_CCM_SWITCH, &YuGiOhCardEditDialog::onSetRowSwitch, this);
setCodeAutoBtn_->Bind(wxEVT_BUTTON, &YuGiOhCardEditDialog::onSetCodeAutoDetect, this);
row.Add(combo, 1, wxEXPAND);
row.Add(setCodeRowPanel_, 1, wxEXPAND);
row.Add(setModeHint_, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 5);
row.Add(setPickerSwitch_, 0, wxALIGN_CENTER_VERTICAL);
if (availableSets().empty()) {
setPickerSwitch_->Enable(false);
}
syncSetModeHint();
}
void YuGiOhCardEditDialog::appendExtraRows(wxFlexGridSizer* grid) {
auto* setNoPanel = new wxPanel(this, wxID_ANY);
setNoCtrl_ = new wxTextCtrl(setNoPanel, wxID_ANY);
@@ -340,10 +369,78 @@ void YuGiOhCardEditDialog::onSetNoTextChanged(wxCommandEvent&) {
}
void YuGiOhCardEditDialog::onSetSelectionChanged(wxCommandEvent& ev) {
handleSetSelectionChanged();
ev.Skip();
}
void YuGiOhCardEditDialog::onSetSelectionApplied() {
handleSetSelectionChanged();
}
void YuGiOhCardEditDialog::handleSetSelectionChanged() {
clearCachedPrintVariants();
refreshSetNoFullPreview();
scheduleDeferredVariantPrefetch();
ev.Skip();
}
void YuGiOhCardEditDialog::syncSetModeHint() {
if (!setModeHint_ || !setPickerSwitch_) return;
// Switch on = set-code entry; hint tells user how to return to the name list.
setModeHint_->SetLabel(setPickerSwitch_->GetValue() ? wxString::FromUTF8("Set name")
: wxString::FromUTF8("Set code"));
}
void YuGiOhCardEditDialog::onSetRowSwitch(wxCommandEvent&) {
if (!setPickerSwitch_ || !setComboControl() || !setCodeRowPanel_) return;
syncSetModeHint();
const bool codeMode = setPickerSwitch_->GetValue();
setComboControl()->Show(!codeMode);
setCodeRowPanel_->Show(codeMode);
wxWindow* host = setComboControl()->GetParent();
if (host) {
host->Layout();
}
Layout();
}
void YuGiOhCardEditDialog::onSetCodeAutoDetect(wxCommandEvent&) {
if (!setCodeText_ || !setPickerSwitch_) return;
const auto& sets = availableSets();
if (sets.empty()) {
showThemedMessageDialog(this,
"No sets are cached. Use Sets > Update Yu-Gi-Oh! first.",
"Set code", wxOK | wxICON_INFORMATION);
return;
}
const std::string raw = setCodeText_->GetValue().ToStdString(wxConvUTF8);
const auto r = lookupYuGiOhSetByShorthand(raw, sets);
using Kind = YuGiOhSetShorthandLookup::Kind;
if (r.kind == Kind::NotFound) {
showThemedMessageDialog(
this,
"No set matches that code. Check the code spelling or use Sets > Update Yu-Gi-Oh! to refresh the list.",
"Set code", wxOK | wxICON_INFORMATION);
return;
}
if (r.kind == Kind::Ambiguous) {
showThemedMessageDialog(this,
"Multiple cached sets match that code. Refresh the set list or pick the set from the list.",
"Set code", wxOK | wxICON_INFORMATION);
return;
}
applySetSelectionByIndex(r.index);
setPickerSwitch_->SetValue(false, false);
syncSetModeHint();
setComboControl()->Show(true);
setCodeRowPanel_->Show(false);
wxWindow* host = setComboControl()->GetParent();
if (host) {
host->Layout();
}
Layout();
}
std::string YuGiOhCardEditDialog::extractSetNoNumeric(std::string_view fullSetNo) const {
+18
View File
@@ -1,11 +1,13 @@
#include "ccm/ui/YuGiOhGameView.hpp"
#include "ccm/ui/CardEditModalGuard.hpp"
#include "ccm/ui/YuGiOhCardEditDialog.hpp"
#include "ccm/ui/YuGiOhCardListPanel.hpp"
#include "ccm/ui/YuGiOhSelectedCardPanel.hpp"
#include "ccm/ui/Theme.hpp"
#include <wx/msgdlg.h>
#include <wx/window.h>
#include <optional>
#include <algorithm>
@@ -56,6 +58,10 @@ wxPanel* YuGiOhGameView::listPanel(wxWindow* parent) {
selectedPanel_->setCard(listPanel_->selected());
}
});
listPanel_->Bind(EVT_CARD_ACTIVATED, [this](wxCommandEvent&) {
wxWindow* owner = wxGetTopLevelParent(listPanel_);
onEditCard(owner != nullptr ? owner : static_cast<wxWindow*>(listPanel_));
});
}
return listPanel_;
}
@@ -94,6 +100,11 @@ const std::vector<Set>& YuGiOhGameView::setsForDialog() {
}
void YuGiOhGameView::onAddCard(wxWindow* parentWindow) {
if (cardEditModalIsActive()) {
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
wxString::FromUTF8("Add card"), wxOK | wxICON_INFORMATION);
return;
}
YuGiOhCard fresh;
fresh.amount = 1;
fresh.language = Language::English;
@@ -102,6 +113,7 @@ void YuGiOhGameView::onAddCard(wxWindow* parentWindow) {
YuGiOhCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Create, fresh,
&setsForDialog());
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
auto added = collection_.add(Game::YuGiOh, dlg.card());
@@ -141,9 +153,15 @@ void YuGiOhGameView::onEditCard(wxWindow* parentWindow) {
showThemedMessageDialog(parentWindow, "Select a card first.", "Edit", wxOK | wxICON_INFORMATION);
return;
}
if (cardEditModalIsActive()) {
showThemedMessageDialog(parentWindow, wxString::FromUTF8(kCardEditModalBlockedUtf8),
wxString::FromUTF8("Edit"), wxOK | wxICON_INFORMATION);
return;
}
YuGiOhCardEditDialog dlg(parentWindow, images_, sets_, cardPreview_, EditMode::Edit, *sel,
&setsForDialog());
themeModalDialog(&dlg, config_.current().theme);
CardEditModalGuard modalGuard;
if (dlg.ShowModal() != wxID_OK) return;
auto updated = collection_.update(Game::YuGiOh, dlg.card());
if (!updated) {