mirror of
https://github.com/sebastiandine/Card-Collection-Manager-3.git
synced 2026-09-04 17:23:29 +00:00
convenience features (#21)
This commit is contained in:
@@ -80,16 +80,23 @@ public:
|
||||
using card_type = TCard;
|
||||
using sort_column_type = TSortColumn;
|
||||
|
||||
// Replace the displayed rows. Selection is reset (the panel will pick
|
||||
// the first row on the next idle turn — see rebuildRows()).
|
||||
void setCards(std::vector<TCard> cards) {
|
||||
// Replace the displayed rows. When preferSelectId is set, selects that
|
||||
// card if present (used after Add). Otherwise preserves the previously
|
||||
// selected card by id when still present; the first-row CallAfter path in
|
||||
// rebuildRows() runs only when there was no prior selection (startup).
|
||||
void setCards(std::vector<TCard> cards,
|
||||
std::optional<std::uint32_t> preferSelectId = std::nullopt) {
|
||||
std::optional<std::uint32_t> keepId = preferSelectId;
|
||||
if (!keepId) {
|
||||
if (auto sel = selected()) keepId = sel->id;
|
||||
}
|
||||
cards_ = std::move(cards);
|
||||
// Drop sort state when the underlying data is replaced - the indicator
|
||||
// shown in the header should match the order actually rendered, and
|
||||
// wxListCtrl keeps the indicator across DeleteAllItems().
|
||||
nextDirByCol_.clear();
|
||||
list_->RemoveSortIndicator();
|
||||
rebuildRows();
|
||||
rebuildRows(keepId);
|
||||
if (!autoSizedOnce_ && !cards_.empty()) {
|
||||
autoSizeAllColumns();
|
||||
autoSizedOnce_ = true;
|
||||
@@ -143,6 +150,30 @@ public:
|
||||
list_->SetFocus();
|
||||
}
|
||||
|
||||
// Move the selection by `delta` rows (+1 / -1). Used when Up/Down are
|
||||
// pressed while focus is on the filter box. Clamps to the visible range;
|
||||
// leaves list HWND focus alone so the caret can stay in the filter.
|
||||
void nudgeSelection(int delta) {
|
||||
if (list_ == nullptr || list_->GetItemCount() <= 0 || delta == 0) return;
|
||||
long row = list_->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
|
||||
if (row < 0) row = 0;
|
||||
const long count = list_->GetItemCount();
|
||||
long next = row + delta;
|
||||
if (next < 0) next = 0;
|
||||
if (next >= count) next = count - 1;
|
||||
if (next == row) {
|
||||
list_->EnsureVisible(next);
|
||||
return;
|
||||
}
|
||||
suppressListFocus_ = true;
|
||||
list_->SetItemState(row, 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
|
||||
list_->SetItemState(next,
|
||||
wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
|
||||
wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED);
|
||||
list_->EnsureVisible(next);
|
||||
suppressListFocus_ = false;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Column descriptor types -------------------------------------------------
|
||||
|
||||
@@ -330,6 +361,17 @@ private:
|
||||
addText(textCols_.back().label, textCols_.back().width, noteCol);
|
||||
|
||||
headerRow_->SetSizer(s);
|
||||
|
||||
// Header is mouse-only (sort / resize). Keep it out of the tab order so
|
||||
// Up/Down after a header click still drive the list, not wx focus travel.
|
||||
headerRow_->SetCanFocus(false);
|
||||
for (wxWindow* cell : headerCells_) {
|
||||
if (cell == nullptr) continue;
|
||||
cell->SetCanFocus(false);
|
||||
for (wxWindow* child : cell->GetChildren()) {
|
||||
if (child != nullptr) child->SetCanFocus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- header drag-resize / sort hit-test ---------------------------------
|
||||
@@ -486,6 +528,7 @@ private:
|
||||
|
||||
sortBy(*sortCol, ascending);
|
||||
rebuildRows(keepId);
|
||||
if (list_ != nullptr) list_->SetFocus();
|
||||
}
|
||||
|
||||
// ----- cached icon bitmaps for NM_CUSTOMDRAW -----------------------------
|
||||
@@ -644,15 +687,22 @@ private:
|
||||
// no per-row icon swap is required here.
|
||||
(void)event;
|
||||
if (inRebuild_) return;
|
||||
// Row click / native arrow keys: keep HWND focus on the list. Filter
|
||||
// nudge sets suppressListFocus_ so the caret stays in the text box.
|
||||
if (!suppressListFocus_ && list_ != nullptr) list_->SetFocus();
|
||||
notifySelectionChanged();
|
||||
}
|
||||
|
||||
void onListItemActivated(wxListEvent& event) {
|
||||
(void)event;
|
||||
if (inRebuild_) return;
|
||||
wxCommandEvent ev(EVT_CARD_ACTIVATED, GetId());
|
||||
ev.SetEventObject(this);
|
||||
ProcessWindowEvent(ev);
|
||||
// Defer so ShowModal (Edit) does not run inside the list notify path.
|
||||
CallAfter([this]() {
|
||||
if (inRebuild_) return;
|
||||
wxCommandEvent ev(EVT_CARD_ACTIVATED, GetId());
|
||||
ev.SetEventObject(this);
|
||||
ProcessWindowEvent(ev);
|
||||
});
|
||||
}
|
||||
|
||||
// ----- members ----------------------------------------------------------
|
||||
@@ -682,6 +732,8 @@ private:
|
||||
|
||||
// Rebuild guard - see ui_wx/AGENTS.md for the burst-suppression rationale.
|
||||
bool inRebuild_{false};
|
||||
// When true, onSelectionChanged skips list_->SetFocus (filter Up/Down nudge).
|
||||
bool suppressListFocus_{false};
|
||||
|
||||
std::map<TSortColumn, bool> nextDirByCol_;
|
||||
|
||||
|
||||
@@ -50,12 +50,13 @@ public:
|
||||
}
|
||||
[[nodiscard]] bool hostsOwnLayout() const noexcept override { return true; }
|
||||
|
||||
void refreshCollection() override;
|
||||
void refreshCollection(std::optional<std::uint32_t> selectId = std::nullopt) override;
|
||||
void onAddCard(wxWindow* parentWindow) override;
|
||||
void onEditCard(wxWindow* parentWindow) override;
|
||||
void onDeleteCard(wxWindow* parentWindow) override;
|
||||
std::string onUpdateSets(wxWindow* parentWindow) override;
|
||||
void setFilter(std::string_view filter) override;
|
||||
void nudgeSelection(int delta) override;
|
||||
void applyTheme(const ThemePalette& palette) override;
|
||||
[[nodiscard]] std::string updateSetsMenuLabel() const override {
|
||||
return "Update Digimon (Digi-Battle)";
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include "ccm/domain/Set.hpp"
|
||||
#include "ccm/ui/Theme.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -54,9 +56,10 @@ public:
|
||||
// list/selected panels parented onto MainFrame's shared splitter.
|
||||
[[nodiscard]] virtual bool hostsOwnLayout() const noexcept { return false; }
|
||||
|
||||
// Reload the active collection from disk and refresh the panels. The
|
||||
// selected card is preserved when possible.
|
||||
virtual void refreshCollection() = 0;
|
||||
// Reload the active collection from disk and refresh the panels. When
|
||||
// selectId is set, that card is selected if present (e.g. after Add);
|
||||
// otherwise the previously selected card is preserved when possible.
|
||||
virtual void refreshCollection(std::optional<std::uint32_t> selectId = std::nullopt) = 0;
|
||||
|
||||
// Toolbar actions. `parentWindow` is the dialog owner for any modal we
|
||||
// open (typically the `MainFrame`).
|
||||
@@ -71,6 +74,10 @@ public:
|
||||
// Forwarded by `MainFrame` whenever the filter input changes.
|
||||
virtual void setFilter(std::string_view filter) = 0;
|
||||
|
||||
// Move the card-list selection by `delta` rows (+1 / -1). Used when Up/Down
|
||||
// are pressed while the filter text box has focus.
|
||||
virtual void nudgeSelection(int delta) = 0;
|
||||
|
||||
// Apply the active palette to all panels owned by this view.
|
||||
virtual void applyTheme(const ThemePalette& palette) = 0;
|
||||
|
||||
|
||||
@@ -38,12 +38,13 @@ public:
|
||||
wxPanel* listPanel(wxWindow* parent) override;
|
||||
wxPanel* selectedPanel(wxWindow* parent) override;
|
||||
|
||||
void refreshCollection() override;
|
||||
void refreshCollection(std::optional<std::uint32_t> selectId = std::nullopt) override;
|
||||
void onAddCard(wxWindow* parentWindow) override;
|
||||
void onEditCard(wxWindow* parentWindow) override;
|
||||
void onDeleteCard(wxWindow* parentWindow) override;
|
||||
std::string onUpdateSets(wxWindow* parentWindow) override;
|
||||
void setFilter(std::string_view filter) override;
|
||||
void nudgeSelection(int delta) override;
|
||||
void applyTheme(const ThemePalette& palette) override;
|
||||
[[nodiscard]] std::string updateSetsMenuLabel() const override { return "Update Magic"; }
|
||||
|
||||
|
||||
@@ -55,12 +55,13 @@ public:
|
||||
}
|
||||
[[nodiscard]] bool hostsOwnLayout() const noexcept override { return true; }
|
||||
|
||||
void refreshCollection() override;
|
||||
void refreshCollection(std::optional<std::uint32_t> selectId = std::nullopt) override;
|
||||
void onAddCard(wxWindow* parentWindow) override;
|
||||
void onEditCard(wxWindow* parentWindow) override;
|
||||
void onDeleteCard(wxWindow* parentWindow) override;
|
||||
std::string onUpdateSets(wxWindow* parentWindow) override;
|
||||
void setFilter(std::string_view filter) override;
|
||||
void nudgeSelection(int delta) override;
|
||||
void applyTheme(const ThemePalette& palette) override;
|
||||
[[nodiscard]] std::string updateSetsMenuLabel() const override { return "Update Pokemon"; }
|
||||
|
||||
|
||||
@@ -50,12 +50,13 @@ public:
|
||||
}
|
||||
[[nodiscard]] bool hostsOwnLayout() const noexcept override { return true; }
|
||||
|
||||
void refreshCollection() override;
|
||||
void refreshCollection(std::optional<std::uint32_t> selectId = std::nullopt) override;
|
||||
void onAddCard(wxWindow* parentWindow) override;
|
||||
void onEditCard(wxWindow* parentWindow) override;
|
||||
void onDeleteCard(wxWindow* parentWindow) override;
|
||||
std::string onUpdateSets(wxWindow* parentWindow) override;
|
||||
void setFilter(std::string_view filter) override;
|
||||
void nudgeSelection(int delta) override;
|
||||
void applyTheme(const ThemePalette& palette) override;
|
||||
[[nodiscard]] std::string updateSetsMenuLabel() const override { return "Update Yu-Gi-Oh!"; }
|
||||
|
||||
|
||||
@@ -128,6 +128,14 @@ void DigiBattle99GameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer*
|
||||
if (filterInput_ == nullptr) return;
|
||||
setFilter(filterInput_->GetValue().ToStdString(wxConvUTF8));
|
||||
});
|
||||
filterInput_->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& ev) {
|
||||
const int code = ev.GetKeyCode();
|
||||
if (code == WXK_UP || code == WXK_DOWN) {
|
||||
nudgeSelection(code == WXK_UP ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
ev.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
@@ -308,7 +316,7 @@ wxPanel* DigiBattle99GameView::selectedPanel(wxWindow* parent) {
|
||||
return selectedPanel_;
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::refreshCollection() {
|
||||
void DigiBattle99GameView::refreshCollection(std::optional<std::uint32_t> selectId) {
|
||||
// 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;
|
||||
@@ -323,7 +331,7 @@ void DigiBattle99GameView::refreshCollection() {
|
||||
}
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards);
|
||||
listPanel_->setCards(cards, selectId);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
@@ -387,7 +395,7 @@ void DigiBattle99GameView::onAddCard(wxWindow* parentWindow) {
|
||||
"Card added, but image rename to ID-prefixed format failed: " + normalized.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
refreshCollection();
|
||||
refreshCollection(added.value());
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::onEditCard(wxWindow* parentWindow) {
|
||||
@@ -499,6 +507,10 @@ void DigiBattle99GameView::setFilter(std::string_view filter) {
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::nudgeSelection(int delta) {
|
||||
if (listPanel_) listPanel_->nudgeSelection(delta);
|
||||
}
|
||||
|
||||
void DigiBattle99GameView::applyTheme(const ThemePalette& palette) {
|
||||
if (contentPanel_) applyThemeToWindowTree(contentPanel_, palette, config_.current().theme);
|
||||
if (listPanel_) listPanel_->applyTheme(palette);
|
||||
|
||||
@@ -71,7 +71,7 @@ wxPanel* MagicGameView::selectedPanel(wxWindow* parent) {
|
||||
return selectedPanel_;
|
||||
}
|
||||
|
||||
void MagicGameView::refreshCollection() {
|
||||
void MagicGameView::refreshCollection(std::optional<std::uint32_t> selectId) {
|
||||
if (listPanel_ == nullptr) return;
|
||||
auto loaded = collection_.list(Game::Magic);
|
||||
if (!loaded) {
|
||||
@@ -79,7 +79,7 @@ void MagicGameView::refreshCollection() {
|
||||
"Error", wxOK | wxICON_ERROR);
|
||||
return;
|
||||
}
|
||||
listPanel_->setCards(std::move(loaded).value());
|
||||
listPanel_->setCards(std::move(loaded).value(), selectId);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
@@ -134,7 +134,7 @@ void MagicGameView::onAddCard(wxWindow* parentWindow) {
|
||||
showThemedMessageDialog(parentWindow, "Card added, but image rename to ID-prefixed format failed: " + normalized.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
refreshCollection();
|
||||
refreshCollection(added.value());
|
||||
}
|
||||
|
||||
void MagicGameView::onEditCard(wxWindow* parentWindow) {
|
||||
@@ -200,6 +200,10 @@ void MagicGameView::setFilter(std::string_view filter) {
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void MagicGameView::nudgeSelection(int delta) {
|
||||
if (listPanel_) listPanel_->nudgeSelection(delta);
|
||||
}
|
||||
|
||||
void MagicGameView::applyTheme(const ThemePalette& palette) {
|
||||
if (listPanel_) listPanel_->applyTheme(palette);
|
||||
if (selectedPanel_) selectedPanel_->applyTheme(palette);
|
||||
|
||||
@@ -185,6 +185,16 @@ void MainFrame::buildLayout() {
|
||||
view->setFilter(filterInput_->GetValue().ToStdString());
|
||||
}
|
||||
});
|
||||
filterInput_->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& ev) {
|
||||
const int code = ev.GetKeyCode();
|
||||
if (code == WXK_UP || code == WXK_DOWN) {
|
||||
if (auto* view = activeView()) {
|
||||
view->nudgeSelection(code == WXK_UP ? -1 : 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ev.Skip();
|
||||
});
|
||||
|
||||
// Selection changes are handled per-view (each IGameView binds
|
||||
// EVT_CARD_SELECTED on its own typed list panel and pushes the typed
|
||||
|
||||
@@ -136,6 +136,14 @@ void PokemonGameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* page
|
||||
if (filterInput_ == nullptr) return;
|
||||
setFilter(filterInput_->GetValue().ToStdString(wxConvUTF8));
|
||||
});
|
||||
filterInput_->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& ev) {
|
||||
const int code = ev.GetKeyCode();
|
||||
if (code == WXK_UP || code == WXK_DOWN) {
|
||||
nudgeSelection(code == WXK_UP ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
ev.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
void PokemonGameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
@@ -310,7 +318,7 @@ wxPanel* PokemonGameView::selectedPanel(wxWindow* parent) {
|
||||
return selectedPanel_;
|
||||
}
|
||||
|
||||
void PokemonGameView::refreshCollection() {
|
||||
void PokemonGameView::refreshCollection(std::optional<std::uint32_t> selectId) {
|
||||
if (contentPanel_ == nullptr && listPanel_ == nullptr) return;
|
||||
|
||||
auto loaded = collection_.list(Game::Pokemon);
|
||||
@@ -321,7 +329,7 @@ void PokemonGameView::refreshCollection() {
|
||||
}
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards);
|
||||
listPanel_->setCards(cards, selectId);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
@@ -389,7 +397,7 @@ void PokemonGameView::onAddCard(wxWindow* parentWindow) {
|
||||
showThemedMessageDialog(parentWindow, "Card added, but image rename to ID-prefixed format failed: " + normalized.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
refreshCollection();
|
||||
refreshCollection(added.value());
|
||||
}
|
||||
|
||||
void PokemonGameView::onEditCard(wxWindow* parentWindow) {
|
||||
@@ -582,6 +590,10 @@ void PokemonGameView::setFilter(std::string_view filter) {
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void PokemonGameView::nudgeSelection(int delta) {
|
||||
if (listPanel_) listPanel_->nudgeSelection(delta);
|
||||
}
|
||||
|
||||
void PokemonGameView::applyTheme(const ThemePalette& palette) {
|
||||
if (contentPanel_) applyThemeToWindowTree(contentPanel_, palette, config_.current().theme);
|
||||
if (listPanel_) listPanel_->applyTheme(palette);
|
||||
|
||||
@@ -134,6 +134,14 @@ void YuGiOhGameView::buildSingleCardsToolbar(wxWindow* parent, wxBoxSizer* pageS
|
||||
if (filterInput_ == nullptr) return;
|
||||
setFilter(filterInput_->GetValue().ToStdString(wxConvUTF8));
|
||||
});
|
||||
filterInput_->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& ev) {
|
||||
const int code = ev.GetKeyCode();
|
||||
if (code == WXK_UP || code == WXK_DOWN) {
|
||||
nudgeSelection(code == WXK_UP ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
ev.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
void YuGiOhGameView::refreshToolbarIcons(const ThemePalette& palette) {
|
||||
@@ -308,7 +316,7 @@ wxPanel* YuGiOhGameView::selectedPanel(wxWindow* parent) {
|
||||
return selectedPanel_;
|
||||
}
|
||||
|
||||
void YuGiOhGameView::refreshCollection() {
|
||||
void YuGiOhGameView::refreshCollection(std::optional<std::uint32_t> selectId) {
|
||||
if (contentPanel_ == nullptr && listPanel_ == nullptr) return;
|
||||
|
||||
auto loaded = collection_.list(Game::YuGiOh);
|
||||
@@ -319,7 +327,7 @@ void YuGiOhGameView::refreshCollection() {
|
||||
}
|
||||
auto cards = std::move(loaded).value();
|
||||
if (listPanel_ != nullptr) {
|
||||
listPanel_->setCards(cards);
|
||||
listPanel_->setCards(cards, selectId);
|
||||
listPanel_->activateSelection();
|
||||
if (selectedPanel_) selectedPanel_->setCard(listPanel_->selected());
|
||||
}
|
||||
@@ -391,7 +399,7 @@ void YuGiOhGameView::onAddCard(wxWindow* parentWindow) {
|
||||
"Card added, but image rename to ID-prefixed format failed: " + normalized.error(),
|
||||
"Warning", wxOK | wxICON_WARNING);
|
||||
}
|
||||
refreshCollection();
|
||||
refreshCollection(added.value());
|
||||
}
|
||||
|
||||
void YuGiOhGameView::onEditCard(wxWindow* parentWindow) {
|
||||
@@ -505,6 +513,10 @@ void YuGiOhGameView::setFilter(std::string_view filter) {
|
||||
if (listPanel_) listPanel_->setFilter(filter);
|
||||
}
|
||||
|
||||
void YuGiOhGameView::nudgeSelection(int delta) {
|
||||
if (listPanel_) listPanel_->nudgeSelection(delta);
|
||||
}
|
||||
|
||||
void YuGiOhGameView::applyTheme(const ThemePalette& palette) {
|
||||
if (contentPanel_) applyThemeToWindowTree(contentPanel_, palette, config_.current().theme);
|
||||
if (listPanel_) listPanel_->applyTheme(palette);
|
||||
|
||||
Reference in New Issue
Block a user