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
+17 -10
View File
@@ -193,16 +193,22 @@ The reference planes — XY, XZ and YZ through the modeling origin, with their h
drawn on demand, because three translucent squares over every model are noise once they are not
the thing being picked. Sketch brings them up while it waits for a plane, which is exactly when
they are picked, and the session the pick opens takes them away; a live session draws none. The
Feature tree's Origin row, pinned above the features and never removable, keeps them up outside a
sketch. Its state is a view preference in AppConfig rather than part of the recipe, so it costs
the project format nothing. The Plane tool keeps its own rule: the planes and the datums as Offset
bases, and nothing for the other methods, where a click on a plane would rewrite the datum's
references. The `P` and `A` keys are a separate, unpickable view helper and do not follow the
Origin row.
Feature tree's Origin row keeps them up outside a sketch. Its state is a view preference in
AppConfig rather than part of the recipe, so it costs the project format nothing. The Plane tool
keeps its own rule: the planes and the datums as Offset bases, and nothing for the other methods,
where a click on a plane would rewrite the datum's references. The `P` and `A` keys are a
separate, unpickable view helper and do not follow the Origin row.
The Bed row, pinned under the Origin row, is the printer bed's switch in the same way: it draws or
hides the bed and its plate grid in every mode, and `Ctrl+Shift+B` flips it from the keyboard. It
is a view preference in AppConfig too, and the bed is shown until it is turned off.
The Bed row, under the Origin row, is the printer bed's switch in the same way: it draws or hides
the bed and its plate grid in every mode. It is a view preference in AppConfig too, and the bed is
shown until it is turned off.
The two rows are view switches, not history, and the tree says so: they sit unframed on the Feature
tree's card, above the features' own framed list, and stay put while the features scroll. A click
never selects either row, since a selected Origin or Bed would have nothing to edit, move or
delete; the eye and the right-click menu are the only targets, and a row's label dims while its
thing is hidden, as a hidden body's does. Because the block never takes the focus, `Ctrl+Shift+O`
and `Ctrl+Shift+B` flip the Origin and the Bed from the keyboard.
## Rendering the bodies
@@ -277,7 +283,8 @@ The tab is a page of Orca's main window and answers to the same settings as Prep
hover background on macOS. The Feature tree and Bodies lists are a custom-drawn
`DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Edit,
Show/hide and Delete on a feature, Move, Show/hide and Delete on a body, and only Show/hide on
the Origin and Bed rows above the features — and the eye shows whether that row is hidden.
the Origin and Bed rows, a separate non-selectable list above the features — and the eye shows
whether that row is hidden.
- **Plates.** This is the one thing the tab does not follow. The canvas has a bed of its own at
the printer bed's home position, whichever plate Prepare has current, and a new document's
modeling origin is that bed's centre. A bed that followed the current plate would slide out
+7 -4
View File
@@ -238,9 +238,11 @@ DESIGN_TAB = (128, 29 + CHROME_DY)
RIBBON_DX = -56
# Feature-tree rows, measured on the rig at 1920x1080: the list's first row is centred at y=215,
# then 23 px apart. The first two rows are the Origin and the Bed, pinned above the features and
# never deleted, so the first FEATURE row is the third one. Delete and double-click do nothing on
# a pinned row, and reset_document and the reopening double-click both need a feature.
# then 23 px apart. The first two rows are the Origin and the Bed, unframed on the card above the
# features' own framed list, so the first FEATURE row sits two rows and TREE_GAP further down;
# reset_document and the reopening double-click both need a feature. TREE_GAP is the list's
# 12 px top margin plus its 1 px frame, derived rather than measured -- re-measure on the rig, like
# RIBBON_DX.
# x=300, not the label: a second click ON the label opens the inline rename, and Delete then
# edits the text instead of removing the feature.
#
@@ -248,7 +250,8 @@ RIBBON_DX = -56
# unshifted click lands 26 px BELOW the first row -- just past its 23 px height -- so the row is
# never selected, Delete does nothing, and reset_document spends 40 rounds on it before dying
# with "could not empty the feature tree". That names the feature tree, which is not the fault.
TREE_ROW0 = (300, 215 + 2 * 23 + CHROME_DY)
TREE_GAP = 12 + 1
TREE_ROW0 = (300, 215 + 2 * 23 + TREE_GAP + CHROME_DY)
def go_design():
+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