Set the Design tab's Origin and Bed rows apart from the feature list

They now sit above the features' framed list as fixed view switches:
a click no longer selects them, they stay put while the features scroll,
their eyes line up with the features' eyes, and their labels dim when
hidden. Ctrl+Shift+O toggles the Origin from the keyboard, as
Ctrl+Shift+B does the Bed. The test script's position for the first
feature row is calculated, not measured, and needs re-measuring on the
test setup.
This commit is contained in:
SoftFever
2026-10-06 20:46:09 +08:00
parent 6d8a13c33b
commit ce7d4815e6
6 changed files with 144 additions and 94 deletions
+74 -56
View File
@@ -285,11 +285,8 @@ static std::string commit_icon(bool bodies)
// The icons on each Feature tree and Bodies row (DesignRowList::Action::id).
enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete };
// The Feature tree's first two rows are the Origin and the Bed, pinned above the features:
// feature i is row i + kFeatureRow0.
static constexpr int kOriginRow = 0, kBedRow = 1, kFeatureRow0 = 2;
// The feature a Feature tree row shows, or wxNOT_FOUND for a pinned row (and for no row).
static int feature_of_row(int row) { return row >= kFeatureRow0 ? row - kFeatureRow0 : wxNOT_FOUND; }
// The rows of the Feature tree's pinned block, above the features.
static constexpr int kOriginRow = 0, kBedRow = 1;
// A row's eye shows the state the row is in; its tip names what a click does.
static DesignRowList::Action eye_action(bool shown)
@@ -619,15 +616,20 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(_L("Axonometric view, fitted"));
};
// Commit to Plate and the bed toggle are mouse targets — a toolbar button and the Feature
// tree's Bed row — so without an accelerator neither could be reached from the keyboard, nor
// by anything driving the keyboard. Ctrl+Shift+P is Plate, Ctrl+Shift+B is Bed; neither
// collides with Orca's own Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate).
// Commit to Plate and the bed and origin toggles are mouse targets — a toolbar button and the
// Feature tree's Bed and Origin rows, which never take the focus — so without an accelerator
// none could be reached from the keyboard, nor by anything driving the keyboard. Ctrl+Shift+P
// is Plate, Ctrl+Shift+B is Bed, Ctrl+Shift+O is Origin; none collides with Orca's own
// Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate).
m_keys_feature['P' | SC_SHIFT | SC_CTRL] = [this] { on_commit(); };
m_keys_feature['B' | SC_SHIFT | SC_CTRL] = [this] {
toggle_bed();
set_status(m_show_bed ? _L("Bed shown") : _L("Bed hidden"));
};
m_keys_feature['O' | SC_SHIFT | SC_CTRL] = [this] {
toggle_origin();
set_status(m_show_origin ? _L("Reference planes shown") : _L("Reference planes hidden"));
};
// Flyout rows show the design_* glyphs as they are: drawn in Orca's icon grey (#949494), which
// the icon cache maps per theme like every other sidebar icon, so they need no re-tint.
@@ -3130,11 +3132,34 @@ DesignPanel::DesignPanel(wxWindow* parent)
FromDIP(SidebarProps::ContentMargin()));
tree_inner->Add(new wxStaticLine(m_tree_box), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP,
FromDIP(SidebarProps::TitlebarMargin()));
// The Origin and Bed switches sit on the card itself, unframed, above the features' own framed
// list: view switches, not history. A list of their own so they stay put while the features
// scroll, and a non-selectable one because selecting them would do nothing: only their eyes
// answer. 13 = the feature list's margin plus its frame, so the two lists' icons line up.
m_pinned = new DesignRowList(m_tree_box, 2, false, wxBORDER_NONE);
m_pinned->SetBackgroundColour(dp_panel_bg());
tree_inner->Add(m_pinned, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 13);
// Sized to its rows, so a short history wastes no block, and scrolling past 9 features.
m_tree = new DesignRowList(m_tree_box, 9 + kFeatureRow0);
m_tree = new DesignRowList(m_tree_box, 9);
m_tree->SetBackgroundColour(dp_panel_bg());
tree_inner->Add(m_tree, 0, wxEXPAND | wxALL, 12);
m_pinned->on_action = [this](int row, int) {
if (row == kBedRow)
toggle_bed();
else if (row == kOriginRow)
toggle_origin();
};
// Exactly what the eye does, named.
m_pinned->on_menu = [this](int row, const wxPoint& screen) {
const bool bed = (row == kBedRow);
wxMenu menu;
const int id_toggle = wxWindow::NewControlId();
menu.Append(id_toggle, bed ? bed_toggle_text(m_show_bed) : origin_toggle_text(m_show_origin));
menu.Bind(wxEVT_MENU, [this, bed](wxCommandEvent&) { bed ? toggle_bed() : toggle_origin(); }, id_toggle);
m_pinned->PopupMenu(&menu, m_pinned->ScreenToClient(screen));
};
// Selecting a feature that leaves a body (Extrude, Fillet, Chamfer, Hole, ...) lights the
// faces it made in the viewport — the fillet's round, not the whole part it sits on.
m_tree->on_select = [this] {
@@ -3142,8 +3167,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Picking a feature drops any body selection, so the two lists never both claim to be
// "the target" — but ONLY when this tree has a selection. Each list notifies on every
// change, so clicking a body row runs apply_body_row, whose m_tree->unselect() fires
// THIS handler, which would otherwise clear the body row the user had just clicked. The
// pinned Origin and Bed rows count: they are selected rows like any other.
// THIS handler, which would otherwise clear the body row the user had just clicked.
if (m_parts && m_tree->selection() != wxNOT_FOUND) m_parts->unselect();
const int sel = tree_selection();
// Likewise a viewport pick, which would be drawn just like the feature's faces. Not while
@@ -3166,18 +3190,14 @@ DesignPanel::DesignPanel(wxWindow* parent)
};
// Double-click a row = Edit, the same gesture that re-opens a committed sketch on the canvas.
// The pinned rows have nothing to edit.
m_tree->on_activate = [this] {
if (tree_selection() != wxNOT_FOUND) on_edit_feature();
};
// The row's own Edit / Show-hide / Delete, on the row the click selected. The body list is
// cleared here too, not left to on_select, which re-clicking the selected row does not run:
// a body row still selected would be what on_toggle_visibility acts on. A pinned row's one
// icon is its eye.
m_tree->on_action = [this](int row, int id) {
if (row == kOriginRow) { toggle_origin(); return; }
if (row == kBedRow) { toggle_bed(); return; }
// a body row still selected would be what on_toggle_visibility acts on.
m_tree->on_action = [this](int, int id) {
if (m_parts) m_parts->unselect();
switch (id) {
case RowEdit: on_edit_feature(); break;
@@ -3190,22 +3210,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
// double-click is Edit, and F2 is a function key nothing announces. A user who wants to name
// a sketch tries the row, and the row answers.
m_tree->on_menu = [this](int row, const wxPoint& screen) {
wxMenu menu;
// A pinned row is no feature: it cannot be renamed, edited, moved or deleted, and its
// menu holds exactly what its eye does.
const int feat = feature_of_row(row);
if (feat == wxNOT_FOUND) {
const bool bed = (row == kBedRow);
const int id_toggle = wxWindow::NewControlId();
menu.Append(id_toggle, bed ? bed_toggle_text(m_show_bed) : origin_toggle_text(m_show_origin));
menu.Bind(wxEVT_MENU, [this, bed](wxCommandEvent&) { bed ? toggle_bed() : toggle_origin(); }, id_toggle);
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
return;
}
// EVERYTHING A ROW CAN DO, in one place. The row's icons are the quick bar, but the menu
// is the reference: the element you click answers with what applies to it, and a menu
// grows without spending an icon nobody recognises. Split into what the row IS (name,
// contents), where it SITS (order, visibility) and what removes it.
wxMenu menu;
const int id_rename = wxWindow::NewControlId();
const int id_edit = wxWindow::NewControlId();
const int id_up = wxWindow::NewControlId();
@@ -3218,8 +3227,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Scale artwork acts on THIS feature's imported outline, so it belongs to the row and
// is offered only where it means something. It used to hide inside the header's Move
// button, which otherwise moved a body — two different subjects on one icon.
const bool art = feat < int(m_doc.features.size()) &&
!m_doc.features[feat].imported_regions.empty();
const bool art = row < int(m_doc.features.size()) &&
!m_doc.features[row].imported_regions.empty();
if (art) menu.Append(id_art, _L("Scale artwork"));
menu.AppendSeparator();
menu.Append(id_up, _L("Move up"));
@@ -3234,7 +3243,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_toggle_visibility(); }, id_vis);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_delete_feature(); }, id_del);
if (art)
menu.Bind(wxEVT_MENU, [this, feat](wxCommandEvent&) { on_transform_imported(feat); }, id_art);
menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { on_transform_imported(row); }, id_art);
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
};
@@ -3243,9 +3252,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// renaming belongs on the row, not in a side-panel field. The list hands the name over after
// its editor's events have finished, so rebuilding the rows here is safe.
m_tree->on_rename = [this](int row, const wxString& name) {
const int feat = feature_of_row(row);
if (feat == wxNOT_FOUND || feat >= int(m_doc.features.size())) return;
m_doc.features[feat].name = std::string(name.ToUTF8().data());
if (row < 0 || row >= int(m_doc.features.size())) return;
m_doc.features[row].name = std::string(name.ToUTF8().data());
refresh_tree(); // which syncs the recipe, so the save path persists the name
};
@@ -4424,9 +4432,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_show_bed = wxGetApp().app_config->get_bool("design_show_bed");
if (m_viewport) m_viewport->set_show_bed(m_show_bed);
set_ui_mode(UiMode::Feature);
// The pinned rows from the first paint: nothing else builds the rows before the first edit,
// project load or theme switch.
refresh_tree();
// The Origin and Bed rows from the first paint: nothing else builds them before the first
// edit, project load or theme switch.
refresh_pinned();
build_phase("wiring");
BOOST_LOG_TRIVIAL(info) << "Design tab build: total " << build_clock.Time() << " ms";
}
@@ -7354,6 +7362,7 @@ void DesignPanel::on_sys_color_changed()
wxGetApp().UpdateDarkUIWin(this);
refresh_icons();
refresh_tree(); // the rows carry their own text colours
refresh_pinned();
Refresh();
}
@@ -7524,7 +7533,7 @@ void DesignPanel::refresh_tree()
// Preserve the selected row across the rebuild — set_rows() drops the selection, which made
// every edit/add feel like it "lost" the selection (and broke Edit/Move/Delete on the
// just-touched feature). By row, so a selected pinned row stays selected too.
// just-touched feature).
const int keep = m_tree->selection();
// Datum/reference planes carry no solid; feed them to the viewport so they render as
@@ -7532,16 +7541,7 @@ void DesignPanel::refresh_tree()
refresh_datum_planes();
update_reference_planes();
std::vector<DesignRowList::Row> rows;
rows.reserve(m_doc.features.size() + kFeatureRow0);
// Always first, in kOriginRow / kBedRow order, and never removable: the reference planes' and
// the printer bed's own switches, and nothing else.
auto add_pinned = [&rows](const char* icon, const wxString& label, bool shown, const wxString& tip) {
DesignRowList::Action eye = eye_action(shown);
eye.tip = tip;
rows.push_back({ icon, label, {}, dp_item_text(), { eye } });
};
add_pinned("design_plane", _L("Origin"), m_show_origin, origin_toggle_text(m_show_origin));
add_pinned("design_bed", _L("Bed"), m_show_bed, bed_toggle_text(m_show_bed));
rows.reserve(m_doc.features.size());
for (size_t fi = 0; fi < m_doc.features.size(); ++fi) {
const CadFeature& f = m_doc.features[fi];
DesignRowList::Row row;
@@ -7963,13 +7963,13 @@ bool DesignPanel::place_on_face()
int DesignPanel::tree_selection() const
{
return feature_of_row(m_tree->selection());
return m_tree->selection();
}
void DesignPanel::set_tree_selection(int feature)
void DesignPanel::set_tree_selection(int row)
{
if (feature >= 0 && feature + kFeatureRow0 < int(m_tree->GetItemCount()))
m_tree->select(feature + kFeatureRow0);
if (row >= 0 && row < int(m_tree->GetItemCount()))
m_tree->select(row);
}
// The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and
@@ -11122,7 +11122,8 @@ void DesignPanel::toggle_origin()
// A plane picked while they were up is not a selection once nobody can see it.
if (!m_show_origin) drop_plane_pick();
// Not an edit: a plane choice under way stays, and keeps its planes up.
refresh_tree(); // the Origin row's eye, and update_reference_planes(), which repaints
update_reference_planes(); // repaints
refresh_pinned(); // the Origin row's eye
}
void DesignPanel::toggle_bed()
@@ -11130,7 +11131,24 @@ void DesignPanel::toggle_bed()
m_show_bed = !m_show_bed;
wxGetApp().app_config->set_bool("design_show_bed", m_show_bed);
if (m_viewport) m_viewport->set_show_bed(m_show_bed); // repaints
refresh_tree(); // the Bed row's eye
refresh_pinned(); // the Bed row's eye
}
// View switches, not history: a view toggle rebuilds these two rows and leaves the features
// alone. The label dims while its thing is hidden, as a hidden body's does.
void DesignPanel::refresh_pinned()
{
std::vector<DesignRowList::Row> rows;
auto add = [&rows](const char* icon, const wxString& label, bool shown, const wxString& tip) {
DesignRowList::Action eye = eye_action(shown);
eye.tip = tip;
// A blank cell under the features' Delete, so the eye sits in the features' eye column.
rows.push_back({ icon, label, {}, shown ? dp_item_text() : dp_item_dim(), { eye, {} } });
};
// In kOriginRow / kBedRow order.
add("design_plane", _L("Origin"), m_show_origin, origin_toggle_text(m_show_origin));
add("design_bed", _L("Bed"), m_show_bed, bed_toggle_text(m_show_bed));
m_pinned->set_rows(std::move(rows));
}
void DesignPanel::start_sketch()
+9 -7
View File
@@ -384,8 +384,9 @@ private:
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, plane choice, Plane tool
void toggle_origin(); // the Origin row's eye: keep the reference planes up, or not
void toggle_origin(); // the Origin row's eye (and Ctrl+Shift+O): keep the reference planes up
void toggle_bed(); // the Bed row's eye (and Ctrl+Shift+B): draw the printer bed, or not
void refresh_pinned(); // rebuild the Origin and Bed rows (their eyes and label colours)
// Sketch: open a session on the picked face or plane, or with none picked, put the planes up
// and wait for one (m_choosing_sketch_plane).
void start_sketch();
@@ -932,10 +933,11 @@ private:
std::function<void(double)> m_value_cont; // deferred apply, run on Confirm
std::function<void()> m_value_cancel; // optional action when the card is cancelled
// Feature tree: the pinned Origin and Bed rows, then one row per feature, in feature order,
// with a per-type icon and the row's own Edit / Show-hide / Delete icons. Feature i is row
// i + 2; callers use feature indices via tree_selection()/set_tree_selection();
// refresh_tree() rebuilds the rows.
// Feature tree: the Origin and Bed rows, a fixed block of view switches that never scrolls
// and selects nothing, then, in a frame of its own below them, one row per feature, in feature
// order, with a per-type icon and the row's own Edit / Show-hide / Delete icons. Callers use
// row indices via tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
DesignRowList* m_pinned{nullptr};
DesignRowList* m_tree{nullptr};
// The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected.
// Finding them replays the history, so they are kept per row and topology generation.
@@ -990,10 +992,10 @@ private:
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature's index, or wxNOT_FOUND (none, or a pinned row)
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
void refresh_parts(); // rebuild the Bodies list under the feature tree
void set_tree_selection(int feature);
void set_tree_selection(int row);
static const char* tree_icon_for(CadFeatureType t);
wxStaticText* m_status{nullptr};
+22 -12
View File
@@ -29,11 +29,19 @@ static constexpr int kCellDip = 20; // square action cell, hover chip included
static constexpr int kPadDip = 4; // row edges, and the gap before the action cells
static constexpr int kGapDip = 6; // type icon to label
DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
// wxVListBox defaults to wxBORDER_THEME; the sidebar's lists take a simple frame.
: wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE)
DesignRowList::DesignRowList(wxWindow* parent, int max_visible, bool selectable, long style)
// wxVListBox defaults to wxBORDER_THEME; `style` defaults to the simple frame the sidebar's
// lists take, and a list meant to sit unframed on its card passes wxBORDER_NONE.
: wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, style)
, m_max_visible(std::max(max_visible, 1))
, m_selectable(selectable)
{
// A non-selectable list takes no focus: from the mouse because it consumes the presses that
// would focus it, below, and from the keyboard through AcceptsFocus(). wxGTK reads that once,
// while the base class is being built, before the override exists, so it is told here.
if (!m_selectable)
SetCanFocus(false);
Bind(wxEVT_LISTBOX, [this](wxCommandEvent&) { if (on_select) on_select(); });
Bind(wxEVT_LISTBOX_DCLICK, [this](wxCommandEvent&) { if (on_activate) on_activate(); });
@@ -46,18 +54,20 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
m_pressed = Hit{};
e.Skip();
});
// A press on an action cell is remembered and skipped, so the list still selects the row.
// A press on an action cell is remembered and skipped, so the list still selects the row. A
// non-selectable list keeps every press from wxVListBox, which would select the row and take
// the focus.
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
m_pressed = h.cell >= 0 ? h : Hit{};
e.Skip();
if (m_selectable) e.Skip();
});
// The second press of a double-click on a cell is a press too, and is not skipped: the list
// would otherwise turn it into a row double-click, so a quick double toggle of the eye would
// also open the feature for editing.
Bind(wxEVT_LEFT_DCLICK, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
if (h.cell < 0) { m_pressed = Hit{}; e.Skip(); return; }
if (h.cell < 0) { m_pressed = Hit{}; if (m_selectable) e.Skip(); return; }
m_pressed = h;
});
Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
@@ -69,9 +79,9 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
const int id = m_rows[pressed.row].actions[pressed.cell].id;
// After the click has finished dispatching: the action may rebuild these rows. The row
// must still exist and still be the selected one, or the click is dropped rather than
// applied to whatever row took its place.
// applied to whatever row took its place. A non-selectable list has no selected row.
CallAfter([this, row = pressed.row, id] {
if (row < int(GetItemCount()) && row == GetSelection() && on_action)
if (row < int(GetItemCount()) && (!m_selectable || row == GetSelection()) && on_action)
on_action(row, id);
});
});
@@ -79,7 +89,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
// press suppresses the wxEVT_CONTEXT_MENU that follows it.
Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
if (h.row != wxNOT_FOUND) select(h.row);
if (m_selectable && h.row != wxNOT_FOUND) select(h.row);
e.Skip();
});
Bind(wxEVT_CONTEXT_MENU, [this](wxContextMenuEvent& e) {
@@ -95,7 +105,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
return;
// On MSW the menu comes with the button's release, and the pointer may have moved to
// another row since the press selected one: the menu is for the row it opens over.
select(row);
if (m_selectable) select(row);
if (on_menu) on_menu(row, screen);
});
// wxVListBox takes every wheel event, even with nothing to scroll, so a short list would stop
@@ -254,7 +264,7 @@ void DesignRowList::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) con
wxColour bg;
if (IsSelected(n))
bg = StateColor::darkModeColorFor(wxColour("#BFE1DE"));
else if (int(n) == m_hover.row)
else if (m_selectable && int(n) == m_hover.row) // a non-selectable row lights only its icons
bg = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
if (!bg.IsOk())
return; // the list already cleared to its background colour
@@ -314,7 +324,7 @@ DesignRowList::Hit DesignRowList::hit_test(const wxPoint& pt) const
const wxRect rect = GetItemRect(h.row);
const auto& acts = m_rows[h.row].actions;
for (size_t i = 0; i < acts.size(); ++i)
if (cell_rect(rect, acts.size(), i).Contains(pt)) {
if (!acts[i].icon.empty() && cell_rect(rect, acts.size(), i).Contains(pt)) {
h.cell = int(i);
break;
}
+15 -5
View File
@@ -29,12 +29,16 @@ namespace Slic3r { namespace GUI {
// Selection follows wxTreeCtrl's contract, which DesignPanel's mutual exclusion between the two
// lists relies on: on_select runs on every change, whether the user or select() made it, and
// selecting the row that is already selected changes nothing and notifies nobody.
//
// A list built non-selectable is a row of switches, not of objects: a click never selects a row,
// the pointer highlights only the action icon under it, and the list never takes the focus. Its
// action icons and its context menu are the only things that answer.
class DesignRowList : public wxVListBox
{
public:
struct Action {
int id; // the owner's code for it, handed back to on_action
std::string icon; // icon name, e.g. "design_eye"
std::string icon; // icon name, e.g. "design_eye"; empty for a blank cell that only holds a column
wxString tip;
};
struct Row {
@@ -50,7 +54,8 @@ public:
static constexpr const char* hover_chip = "#D4D4D4";
// The list is as tall as its rows, at least one and at most `max_visible`; past that it scrolls.
DesignRowList(wxWindow* parent, int max_visible);
// `style` is its frame: wxBORDER_NONE for a list that sits unframed on its card.
DesignRowList(wxWindow* parent, int max_visible, bool selectable = true, long style = wxBORDER_SIMPLE);
// Replace every row. Clears the selection without notifying and cancels a rename in progress;
// the owner re-selects the row it keeps, which notifies.
@@ -67,15 +72,19 @@ public:
std::function<void()> on_select; // the selection changed
std::function<void()> on_activate; // the selected row was double-clicked
// A row's action icon was clicked. Runs after the click has finished dispatching, and only
// while that row is still the selected one; the click itself selected it.
// A row's action icon was clicked. Runs after the click has finished dispatching, while the
// row still exists and — in a selectable list, where the click itself selected it — is still
// the selected one.
std::function<void(int row, int id)> on_action;
// Context menu on a row, at a screen position; the row under the pointer is selected first.
// Context menu on a row, at a screen position; the row under the pointer is selected first,
// unless the list is non-selectable.
std::function<void(int row, const wxPoint& screen)> on_menu;
// The editor committed `name` for `row`, trimmed and never empty (an empty commit cancels).
// Runs after the editor's own events have finished, so the owner may rebuild the rows here.
std::function<void(int row, const wxString& name)> on_rename;
bool AcceptsFocus() const override { return m_selectable && wxVListBox::AcceptsFocus(); }
protected:
void OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const override;
void OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const override;
@@ -103,6 +112,7 @@ private:
std::vector<Row> m_rows;
std::map<std::string, ScalableBitmap> m_icons;
int m_max_visible;
bool m_selectable;
int m_row_h{0}; // every row's height, from measure_row()
Hit m_hover;
Hit m_pressed; // action cell under the last left press