diff --git a/docs/HLSD/design-tab.md b/docs/HLSD/design-tab.md index f7d48e0ee6..2d7810cd4f 100644 --- a/docs/HLSD/design-tab.md +++ b/docs/HLSD/design-tab.md @@ -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 diff --git a/scripts/CAD/check-gui-sketching.py b/scripts/CAD/check-gui-sketching.py index b3c4514499..df83803819 100644 --- a/scripts/CAD/check-gui-sketching.py +++ b/scripts/CAD/check-gui-sketching.py @@ -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(): diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 964e1f23ca..de07d4451c 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -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 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 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() diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index 013de06fc8..3ae91acb25 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -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 m_value_cont; // deferred apply, run on Confirm std::function 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}; diff --git a/src/slic3r/GUI/CAD/DesignRowList.cpp b/src/slic3r/GUI/CAD/DesignRowList.cpp index facf0aa5ed..1c39009cd5 100644 --- a/src/slic3r/GUI/CAD/DesignRowList.cpp +++ b/src/slic3r/GUI/CAD/DesignRowList.cpp @@ -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; } diff --git a/src/slic3r/GUI/CAD/DesignRowList.hpp b/src/slic3r/GUI/CAD/DesignRowList.hpp index ff1ecc482a..c100e80437 100644 --- a/src/slic3r/GUI/CAD/DesignRowList.hpp +++ b/src/slic3r/GUI/CAD/DesignRowList.hpp @@ -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 on_select; // the selection changed std::function 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 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 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 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 m_rows; std::map 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