The row menu carries the whole row, and Move body goes where bodies live

Two decisions from the user, 2026-08-23, after the first pass at making rename
reachable.

ONE SURFACE CARRIES THE VOCABULARY, and it is the row's own menu: Rename (F2),
Edit, then Move up, Move down, Show / hide, then Delete. Offered as a choice
between completing the menu or completing the header icons; the menu won because
the element you click answering with what applies to it is this fork's charter,
and because a menu grows without spending an icon nobody recognises. The header
icons stay exactly as they are — a quick bar for the common three — so nothing
that worked yesterday moved.

The menu is grouped rather than listed: what the row IS (name, contents), where
it SITS (order, visibility), and what removes it. Right-click SELECTS what it
points at before opening, so the menu can never act on a row other than the one
under the cursor.

MOVE BODY LEAVES THE FEATURE-TREE HEADER. It never belonged there: that header
sits over the FEATURE tree, and the button had to guess its subject from
whatever happened to be selected — a feature row got answered with "Select a
body to move it", an instruction about a different kind of object in a list that
does not contain one. A body now has its own action row on the Bodies card:
Move, Show / hide, Delete, Colour. The last three are deliberate COPIES of
feature-tree actions, not a reorganisation: a body row is a different subject,
and someone working in the Bodies list should not travel to another card to hide
or recolour what they have just selected. Each handler already resolves the body
row itself (on_toggle_visibility, on_delete_body, on_set_body_color), so the
card gives them a home and decides nothing.

The offer already carried Move for a selected body (verb `transform`, accepts
body_solid), so that half of the requirement was in place and is untouched.

One piece of the old button was function, not clutter: it also scaled imported
Text/SVG artwork, which IS a feature-row action. That moved to the row's own
menu as "Scale artwork", shown only on a row that has imported regions — so the
capability survives the split instead of disappearing with the button.

Verified on the rig, by the gestures themselves: right-click a row -> the six
items in their three groups; choosing Move down turned ['Sketch1', 'Sketch2']
into ['Sketch2', 'Sketch1']; and with a body present the Bodies card shows its
four actions while the feature header no longer shows Move.

Gate green: ALL LADDERS HELD — offer table OK, kernel 188 cases / 2532
assertions, engine rungs 1-8, 977-sheet corpus + the heaviest sheets, gesture
ladder 98/98, offer ladder 108/108.

RIG DISCIPLINE, learned the expensive way in this session: ladder-all.sh does
NOT relaunch the app, so hand-driving the rig immediately before it leaves state
the ladder's reset_document() does not clear — here the first rung drew nothing
at all and reported "sides []", which reads exactly like a regression in the
rectangle tool. Relaunch the app before a gate, and re-run before believing a
failure that appears in rung one.
This commit is contained in:
Tommaso Bianchi
2026-08-23 16:53:43 +02:00
parent 3d3324d663
commit b0657fb2c9
+70 -17
View File
@@ -3104,12 +3104,30 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_tree->SelectItem(e.GetItem()); // right-click targets what it points at
const int sel = tree_selection();
if (sel == wxNOT_FOUND) return;
// EVERYTHING A ROW CAN DO, in one place. The header icons stay as a 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();
const int id_down = wxWindow::NewControlId();
const int id_vis = wxWindow::NewControlId();
const int id_art = wxWindow::NewControlId();
const int id_del = wxWindow::NewControlId();
menu.Append(id_rename, _L("Rename\tF2"));
menu.Append(id_edit, _L("Edit"));
// 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 = sel < int(m_doc.features.size()) &&
!m_doc.features[sel].imported_regions.empty();
if (art) menu.Append(id_art, _L("Scale artwork"));
menu.AppendSeparator();
menu.Append(id_up, _L("Move up"));
menu.Append(id_down, _L("Move down"));
menu.Append(id_vis, _L("Show / hide"));
menu.AppendSeparator();
menu.Append(id_del, _L("Delete"));
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) {
@@ -3117,8 +3135,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
if (row != wxNOT_FOUND && row < int(m_tree_items.size()))
m_tree->EditLabel(m_tree_items[row]);
}, id_rename);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_edit_feature(); }, id_edit);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_delete_feature(); }, id_del);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_edit_feature(); }, id_edit);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_move_feature(-1); }, id_up);
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_move_feature(+1); }, id_down);
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](wxCommandEvent&) {
const int row = tree_selection();
if (row != wxNOT_FOUND) on_transform_imported(row);
}, id_art);
m_tree->PopupMenu(&menu);
});
@@ -3170,20 +3196,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
};
auto* edit = edit_btn("design_edit", _L("Edit"));
edit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_edit_feature(); });
auto* move = edit_btn("design_move", _L("Move body / Scale imported Text-SVG"));
move->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
const int sel = tree_selection();
if (sel != wxNOT_FOUND && sel < int(m_doc.features.size()) &&
!m_doc.features[sel].imported_regions.empty()) {
on_transform_imported(sel);
} else if (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) {
on_move_body(); // translate the selected body with the 3-axis gizmo
} else {
m_status->SetForegroundColour(wxColour(235, 110, 110));
set_status(_L("Select a body to move it, or an imported Text/SVG to scale"));
m_status->Refresh();
}
});
// NO Move here. Moving a body is not a feature-row action — this header sits over the
// FEATURE tree, and the button had to guess its subject from whatever happened to be
// selected, answering a feature row with an instruction about bodies. It lives where a
// body lives: the Bodies card's own action row, and the offer for a selected body.
// Scaling imported artwork, which shared this button, moved to the row's own menu.
auto* vis = edit_btn("design_eye", _L("Show / hide"));
vis->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_toggle_visibility(); });
auto* del = edit_btn("design_delete", _L("Delete"));
@@ -3204,7 +3221,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_btn_interfere->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_check_interference(); });
const int gap = FromDIP(SidebarProps::ElementSpacing());
trow->Add(edit, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
trow->Add(move, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
trow->Add(vis, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
trow->Add(del, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
trow->Add(up, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
@@ -3229,6 +3245,43 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_parts_hdr = new wxBoxSizer(wxHORIZONTAL);
m_parts_hdr->Add(card_header(m_parts_box, "design_extrude", _L("Bodies"), m_parts_label), 0,
wxALIGN_CENTER_VERTICAL);
// The bodies card carries the actions that act on a BODY. Move came from the feature-tree
// header, where it had to guess whether its subject was a body or a feature; Show/hide,
// Delete and Colour are deliberate COPIES of feature-tree actions, because a body row is a
// different subject and a user working in this list should not have to travel to another
// card to hide or recolour what they have selected. Each one already resolves the body row
// itself (on_toggle_visibility, on_delete_body, on_set_body_color), so nothing here decides
// policy — the card only gives them a home next to the rows they act on.
{
auto body_btn = [this](const char* icon, const wxString& tip) {
auto* b = new ScalableButton(m_parts_box, wxID_ANY, icon, "", wxSize(24, 24),
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 20);
b->SetToolTip(tip);
return b;
};
auto* bmove = body_btn("design_move", _L("Move body"));
bmove->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
if (m_sel_solid_body >= 0 && m_sel_solid_body < int(m_doc.bodies.size())) {
on_move_body();
} else {
m_status->SetForegroundColour(wxNullColour);
set_status(_L("Select a body row first, then move it"));
m_status->Refresh();
}
});
auto* bvis = body_btn("design_eye", _L("Show / hide"));
bvis->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_toggle_visibility(); });
auto* bdel = body_btn("design_delete", _L("Delete"));
bdel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_delete_body(); });
auto* bcol = body_btn("color_palette", _L("Colour"));
bcol->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_set_body_color(); });
const int bgap = FromDIP(SidebarProps::ElementSpacing());
m_parts_hdr->AddStretchSpacer(1);
m_parts_hdr->Add(bmove, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, bgap);
m_parts_hdr->Add(bvis, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, bgap);
m_parts_hdr->Add(bdel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, bgap);
m_parts_hdr->Add(bcol, 0, wxALIGN_CENTER_VERTICAL);
}
parts_inner->Add(m_parts_hdr, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP,
FromDIP(SidebarProps::ContentMargin()));
m_parts_rule = new wxStaticLine(m_parts_box);