mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
Show feature and body actions on their rows in the Design sidebar
Each Feature tree row now carries Edit, Show/hide and Delete, and each Bodies row carries Move, Show/hide and Delete. These act on that row instead of on the selection. The eye shows a closed eye when the item is hidden. Every clickable icon in the sidebar now highlights on hover. Rename and Color now come first in the right-click menu, as items of their own instead of inside Modify. This applies wherever they appear, whether opened from the viewport or from a Bodies row. Clicking an already-selected row no longer starts a rename; use F2, the row's right-click menu, or the Rename command.
This commit is contained in:
+14
-2
@@ -197,7 +197,14 @@ The tab is a page of Orca's main window and answers to the same settings as Prep
|
||||
Orca's sidebar grey, which the icon cache maps per theme, so they are re-rasterised rather
|
||||
than re-tinted.
|
||||
- **Scale.** Sizes are in DIP, and a DPI change reaches `DesignPanel::msw_rescale`, which
|
||||
re-rasterises every icon (button faces, flyout rows, card headers, the tree's image list).
|
||||
re-rasterises every icon (button faces, flyout rows, card headers, the feature and body lists'
|
||||
row icons).
|
||||
- **Sidebar icons.** Every clickable icon in the sidebar shows a hover chip. The card-header and
|
||||
constraint-row buttons are Orca's self-painted `Button`, because a native button cannot take a
|
||||
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 the eye shows
|
||||
whether that row is hidden.
|
||||
- **Viewport text.** The status line and the active tool's values are drawn by the canvas in
|
||||
its ImGui pass, so they go with the canvas: a top-level window over GL does not follow its
|
||||
frame and was left floating over other applications.
|
||||
@@ -219,7 +226,11 @@ Right-clicking geometry opens the *offer*: eight families in a fixed order, each
|
||||
permanent row index, verbs that do not apply shown disabled **in place with their reason**
|
||||
rather than removed. The invariant is that a verb's row index is identical in every selection
|
||||
where it appears and that adding a verb never moves an existing one — the hand learns the
|
||||
position, so the menu is never re-sorted, compacted or adaptively ordered.
|
||||
position, so the menu is never re-sorted, compacted or adaptively ordered. Above the families
|
||||
sits one *flat* row, holding Rename and Color — what a selection is opened for most: its verbs are
|
||||
items of their own at the top of the menu rather than a family's submenu. It is appended after
|
||||
the eight, so it moved no existing index, and it reads the same from the viewport and from a row
|
||||
of the Bodies list.
|
||||
|
||||
An invariant across 92 verbs and 20 selection kinds does not survive by review, so the map
|
||||
exists once, as data: `scripts/CAD/tool_atlas.json` carries every verb with its row, key, icon,
|
||||
@@ -250,6 +261,7 @@ scripted action and a clicked one cannot diverge. It is off unless the variable
|
||||
| `src/libslic3r/CAD/SketchSolver.*` | constraint solving, over the vendored solver |
|
||||
| `src/libslic3r/slvs/` | vendored 2D constraint solver (GPLv3) |
|
||||
| `src/slic3r/GUI/CAD/DesignPanel.*` | the tab: toolbar, feature cards, tree, key maps |
|
||||
| `src/slic3r/GUI/CAD/DesignRowList.*` | the Feature tree and Bodies lists, with per-row actions |
|
||||
| `src/slic3r/GUI/CAD/DesignCanvas.*` | viewport integration |
|
||||
| `src/slic3r/GUI/CAD/DesignSketchTool.*` | in-canvas sketching |
|
||||
| `src/slic3r/GUI/CAD/DesignInteraction.hpp` | the Esc level contract |
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#949494" stroke-width="0.85" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8.5C5 14.5 19 14.5 22 8.5"/><path d="M12 13v3M5.97 11.9l-1.4 2.5M18.03 11.9l1.4 2.5M3.2 10.1l-1.8 1.8M20.8 10.1l1.8 1.8"/></svg>
|
||||
|
After Width: | Height: | Size: 320 B |
@@ -114,7 +114,7 @@ def main():
|
||||
"struct OfferVerb {",
|
||||
" const char* id;",
|
||||
" const char* name; // drawing-office word (L10); marked L(); translated at use",
|
||||
" int row; // 0..7, the ratified index — NEVER reorder",
|
||||
" int row; // index into kOfferRowNames, the ratified address — NEVER reorder",
|
||||
" const char* key; // shortcut shown in the row, or nullptr",
|
||||
" const char* action;",
|
||||
" const char* refusal; // why this row is greyed, in the product's own words",
|
||||
@@ -135,11 +135,20 @@ def main():
|
||||
"// Row labels, in ratified order.",
|
||||
"static const char* const kOfferRowNames[] = {",
|
||||
]
|
||||
# A flat row's label is never shown, so it is not marked for translation.
|
||||
for s in A["slots"]:
|
||||
lines.append(f' {tstr(s["label"])},')
|
||||
lines.append(f' {cstr(s["label"]) if s.get("flat") else tstr(s["label"])},')
|
||||
lines += [
|
||||
"};",
|
||||
f"static const int kOfferRowCount = {len(slots)};",
|
||||
"// A flat row is not a family: its verbs come first, at the top level of the offer, each",
|
||||
"// an item of its own.",
|
||||
"static const bool kOfferRowFlat[] = {",
|
||||
]
|
||||
for s in A["slots"]:
|
||||
lines.append(f' {"true" if s.get("flat") else "false"},')
|
||||
lines += [
|
||||
"};",
|
||||
"",
|
||||
"static const OfferVerb kOfferVerbs[] = {",
|
||||
]
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
"angle": 225,
|
||||
"label": "Modify",
|
||||
"why": "edits or removes what is already there"
|
||||
},
|
||||
{
|
||||
"id": "top",
|
||||
"label": "Top",
|
||||
"flat": true,
|
||||
"why": "the verbs a selection is opened for most. A flat slot is not a family: its verbs sit first, at the top level of the offer, each an item of its own, and its label is never shown. Appended last, so no ratified index moved."
|
||||
}
|
||||
],
|
||||
"selections": [
|
||||
@@ -1044,7 +1050,7 @@
|
||||
{
|
||||
"id": "rename",
|
||||
"name": "Rename…",
|
||||
"slot": "modify",
|
||||
"slot": "top",
|
||||
"key": "F2",
|
||||
"feature": "Tree",
|
||||
"mcp": null,
|
||||
@@ -1084,7 +1090,7 @@
|
||||
{
|
||||
"id": "colour",
|
||||
"name": "Color",
|
||||
"slot": "modify",
|
||||
"slot": "top",
|
||||
"key": null,
|
||||
"feature": null,
|
||||
"mcp": null,
|
||||
|
||||
@@ -848,6 +848,8 @@ if (SLIC3R_CAD)
|
||||
list(APPEND SLIC3R_GUI_SOURCES
|
||||
GUI/CAD/DesignPanel.cpp
|
||||
GUI/CAD/DesignPanel.hpp
|
||||
GUI/CAD/DesignRowList.cpp
|
||||
GUI/CAD/DesignRowList.hpp
|
||||
GUI/CAD/DesignTextDialog.cpp
|
||||
GUI/CAD/DesignTextDialog.hpp
|
||||
GUI/CAD/DesignCanvas.cpp
|
||||
|
||||
@@ -53,7 +53,7 @@ inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
|
||||
struct OfferVerb {
|
||||
const char* id;
|
||||
const char* name; // drawing-office word (L10); marked L(); translated at use
|
||||
int row; // 0..7, the ratified index — NEVER reorder
|
||||
int row; // index into kOfferRowNames, the ratified address — NEVER reorder
|
||||
const char* key; // shortcut shown in the row, or nullptr
|
||||
const char* action;
|
||||
const char* refusal; // why this row is greyed, in the product's own words
|
||||
@@ -81,8 +81,22 @@ static const char* const kOfferRowNames[] = {
|
||||
L("Transform"),
|
||||
L("Reference"),
|
||||
L("Modify"),
|
||||
"Top",
|
||||
};
|
||||
static const int kOfferRowCount = 9;
|
||||
// A flat row is not a family: its verbs come first, at the top level of the offer, each
|
||||
// an item of its own.
|
||||
static const bool kOfferRowFlat[] = {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
};
|
||||
static const int kOfferRowCount = 8;
|
||||
|
||||
static const OfferVerb kOfferVerbs[] = {
|
||||
{"sketch", L("Sketch"), 0, "Shift+S", "key:S+S", L("Click a face or a reference plane in the viewport, then a sketch tool"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Click a face or a reference plane, then pick a drawing tool")},
|
||||
@@ -125,9 +139,9 @@ static const OfferVerb kOfferVerbs[] = {
|
||||
{"mass_props", L("Volume and area"), 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", L("Report the volume and surface area of the selected body")},
|
||||
{"interference", L("Interference"), 6, nullptr, "btn:interference", L("Interference needs at least two bodies"), 0x00000280u, 2, 0, false, false, nullptr, nullptr, L("Check whether two bodies overlap — reports, changes nothing")},
|
||||
{"edit_feature", L("Edit"), 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", L("Reopen the selected feature to change what it was made from")},
|
||||
{"rename", L("Rename…"), 7, "F2", "btn:rename", L("Select a feature, or a body, to rename it"), 0x00004080u, 0, 0, false, false, nullptr, nullptr, L("Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)")},
|
||||
{"rename", L("Rename…"), 8, "F2", "btn:rename", L("Select a feature, or a body, to rename it"), 0x00004080u, 0, 0, false, false, nullptr, nullptr, L("Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)")},
|
||||
{"delete_face", L("Delete Face"), 7, nullptr, "fly:dressup#3", L("Delete Face needs a body — add or import one first"), 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", L("Remove faces from a body and heal the solid")},
|
||||
{"colour", L("Color"), 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", L("Set the selected body's display color")},
|
||||
{"colour", L("Color"), 8, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", L("Set the selected body's display color")},
|
||||
{"delete", L("Delete"), 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", L("Delete what is selected")},
|
||||
{"delete_body", L("Delete Body"), 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", L("Delete this whole body — removes the feature it was made from")},
|
||||
{"sk_line_t", L("Line"), 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_line", L("Line — click start, then end")},
|
||||
|
||||
+268
-337
@@ -3,6 +3,7 @@
|
||||
#include "slic3r/GUI/CAD/DesignCanvas.hpp"
|
||||
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
|
||||
#include "slic3r/GUI/CAD/DesignTextDialog.hpp" // Text: font, height, live outline
|
||||
#include "slic3r/GUI/CAD/DesignRowList.hpp" // Feature tree and Bodies rows with their own actions
|
||||
#include "slic3r/GUI/CAD/DesignOffer.hpp" // generated offer table — see scripts/CAD/tool_atlas.json
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor
|
||||
#include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set
|
||||
@@ -58,12 +59,9 @@
|
||||
#include <wx/spinctrl.h>
|
||||
#include <wx/listctrl.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/treebase.h>
|
||||
#include <wx/tglbtn.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/translation.h>
|
||||
#include <wx/treectrl.h>
|
||||
#include <wx/imaglist.h>
|
||||
#include <wx/statline.h>
|
||||
#include <wx/statbmp.h>
|
||||
#include <wx/image.h>
|
||||
@@ -242,6 +240,43 @@ static StaticBox* make_card(wxWindow* parent)
|
||||
return c;
|
||||
}
|
||||
|
||||
// The sidebar's icon-only buttons (card headers, constraint rows) highlight under the pointer with
|
||||
// a rounded chip. Orca's ::Button rather than a ScalableButton: a native button cannot take that
|
||||
// hover colour (macOS ignores a button background, MSW turns the button owner-drawn), while
|
||||
// ::Button paints itself the same on every platform. Its sizes are in pixels, and the panel
|
||||
// background is a Design token the dark map does not know, so refresh_icons() re-applies this
|
||||
// after a DPI or theme change to every button named "design_icon_btn".
|
||||
static void style_sidebar_icon_btn(::Button* b)
|
||||
{
|
||||
b->SetPaddingSize(b->FromDIP(wxSize(2, 2)));
|
||||
b->SetMinSize(b->FromDIP(wxSize(24, 24))); // square, whatever the icon size
|
||||
b->SetCornerRadius(b->FromDIP(4));
|
||||
b->SetBackgroundColor(StateColor(
|
||||
std::pair<wxColour, int>(wxColour(DesignRowList::hover_chip), StateColor::Hovered), // dark-mapped at paint
|
||||
std::pair<wxColour, int>(dp_panel_bg(), StateColor::Normal))); // must stay last
|
||||
}
|
||||
|
||||
static ::Button* sidebar_icon_btn(wxWindow* parent, const char* icon, const wxString& tip, int px = 20)
|
||||
{
|
||||
auto* b = new ::Button(parent, "", icon, wxBORDER_NONE, px);
|
||||
b->SetName("design_icon_btn");
|
||||
b->SetCanFocus(false);
|
||||
b->SetIconSpacing(0); // off macOS an empty label still reserves the icon-text gap
|
||||
b->SetBorderColor(StateColor());
|
||||
b->SetToolTip(tip);
|
||||
style_sidebar_icon_btn(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
// The icons on each Feature tree and Bodies row (DesignRowList::Action::id).
|
||||
enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete };
|
||||
|
||||
// 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)
|
||||
{
|
||||
return { RowVisibility, shown ? "design_eye" : "design_eye_off", shown ? _L("Hide") : _L("Show") };
|
||||
}
|
||||
|
||||
// Prepare outlines every numeric field (rounded, #4A4A51 on dark). wxSpinCtrlDouble is a
|
||||
// native GTK control that cannot draw that, and Orca's own SpinInput is int-only — it would
|
||||
// silently truncate a 2.5 mm radius. So the double spin keeps its arrows and validation and
|
||||
@@ -1040,29 +1075,20 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
fadd("color", b_color);
|
||||
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
|
||||
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
|
||||
// Rename exists as a slow double-click on the row too, but the offer is this tab's only
|
||||
// tool vocabulary — a rename that only a double-click reveals is not discoverable, and
|
||||
// the row IS the object, so it belongs in the menu (and on F2) as well as on the row.
|
||||
// A row's double-click is Edit, so renaming needs a door of its own. The offer is this
|
||||
// tab's only tool vocabulary and the row IS the object, so rename sits in the offer, in
|
||||
// the feature row's right-click menu and on F2, and opens the editor on the row itself.
|
||||
auto rename_feature = [this] {
|
||||
// A BODY renames ITSELF. The earlier version resolved the body to
|
||||
// CadBody::source_feature and renamed that feature, which is the wrong object: a
|
||||
// body accumulates many features and the first one is not its name. The body row
|
||||
// is editable now (CadBody::user_name), so the verb opens the editor there.
|
||||
const int b = tree_body_selection();
|
||||
if (b >= 0 && b < int(m_tree_body_items.size())) {
|
||||
const wxTreeItemId row = m_tree_body_items[b];
|
||||
// After the menu, not inside it: an editor opened from within PopupMenu's
|
||||
// nested loop never appears.
|
||||
CallAfter([this, row] { m_parts->SetFocus(); m_parts->EditLabel(row); });
|
||||
return;
|
||||
}
|
||||
const int sel = tree_selection();
|
||||
if (sel != wxNOT_FOUND && sel < int(m_tree_items.size())) {
|
||||
const wxTreeItemId row = m_tree_items[sel];
|
||||
CallAfter([this, row] { m_tree->SetFocus(); m_tree->EditLabel(row); });
|
||||
} else {
|
||||
if (const int b = tree_body_selection(); b >= 0)
|
||||
m_parts->begin_rename(b);
|
||||
else if (const int sel = tree_selection(); sel != wxNOT_FOUND)
|
||||
m_tree->begin_rename(sel);
|
||||
else
|
||||
set_status(_L("Select a feature, or a body, first — then rename it")); // not an error
|
||||
}
|
||||
};
|
||||
m_verb_actions["btn:rename"] = rename_feature;
|
||||
m_keys_feature[WXK_F2] = rename_feature; // a function key, so no letter space spent
|
||||
@@ -3047,8 +3073,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
t->Wrap(240);
|
||||
|
||||
// Feature tree card. Same idiom as Prepare's sections (icon + Head_14 title + rule) via the
|
||||
// shared card_header helper, instead of the bare micro-label this used to be; the row-edit
|
||||
// actions live in the header, as Prepare puts its section actions.
|
||||
// shared card_header helper, instead of the bare micro-label this used to be. What acts on one
|
||||
// feature sits on that feature's row; the header keeps reordering and the interference check.
|
||||
m_tree_box = make_card(m_form);
|
||||
auto* tree_inner = new wxBoxSizer(wxVERTICAL);
|
||||
m_tree_box->SetSizer(tree_inner);
|
||||
@@ -3062,43 +3088,21 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
FromDIP(SidebarProps::ContentMargin()));
|
||||
tree_inner->Add(new wxStaticLine(m_tree_box), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP,
|
||||
FromDIP(SidebarProps::TitlebarMargin()));
|
||||
m_tree = new wxTreeCtrl(m_tree_box, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(-1, 64)),
|
||||
wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES |
|
||||
wxTR_FULL_ROW_HIGHLIGHT | wxBORDER_SIMPLE | wxTR_EDIT_LABELS);
|
||||
// Sized to its rows, so a short history wastes no block, and scrolling past 9.
|
||||
m_tree = new DesignRowList(m_tree_box, 9);
|
||||
m_tree->SetBackgroundColour(dp_panel_bg());
|
||||
// Per-feature-type icons (indices match tree_icon_for): sketch/extrude/dressup/hole/thread.
|
||||
// The list is sized from the bitmaps themselves (image-list sizes are physical and must match
|
||||
// them), and rebuilt with the other icons on a DPI or theme change.
|
||||
auto tree_images = [this] {
|
||||
static const char* const kIcons[] = { "design_sketch", "design_extrude", "design_dressup",
|
||||
"design_hole", "design_thread", "design_shell" };
|
||||
std::vector<wxBitmap> bmps;
|
||||
for (const char* name : kIcons)
|
||||
bmps.push_back(create_scaled_bitmap(name, this, 16));
|
||||
const wxSize sz = bmps.front().GetSize();
|
||||
m_tree_images = new wxImageList(sz.x, sz.y);
|
||||
for (const wxBitmap& b : bmps)
|
||||
m_tree_images->Add(b);
|
||||
m_tree->AssignImageList(m_tree_images); // takes ownership; frees the previous list
|
||||
};
|
||||
tree_images();
|
||||
m_icon_refresh.push_back(tree_images);
|
||||
tree_inner->Add(m_tree, 0, wxEXPAND | wxALL, 12);
|
||||
|
||||
// Selecting a body-producing feature (Extrude/Fillet/Chamfer/Hole/Thread) in the
|
||||
// tree highlights the solid in the viewport; a Sketch row clears the highlight
|
||||
// (its face is already shown via the persistent sketch overlay).
|
||||
m_tree->Bind(wxEVT_TREE_SEL_CHANGED, [this](wxTreeEvent&) {
|
||||
m_tree->on_select = [this] {
|
||||
if (!m_viewport) return;
|
||||
// Bodies live in the Parts list now; picking a feature here drops any body selection
|
||||
// so the two lists can't both claim to be "the target".
|
||||
// ONLY when this tree actually has a selection. These two lists clear each other's
|
||||
// selection so that "the target" is never ambiguous, and that was harmless while both
|
||||
// calls were UnselectAll() — a no-op on a wxTR_SINGLE tree. Now that Unselect() really
|
||||
// clears, the pair became a loop: clicking a body row runs apply_body_row, which calls
|
||||
// m_tree->Unselect(), which fires THIS handler, which cleared the body row the user had
|
||||
// just clicked. The guard keeps the mutual-exclusion and drops the echo.
|
||||
if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->Unselect();
|
||||
// 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.
|
||||
if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->unselect();
|
||||
const int sel = tree_selection();
|
||||
const bool body = (sel >= 0 && sel < int(m_doc.features.size()) &&
|
||||
m_doc.features[sel].type != CadFeatureType::Sketch &&
|
||||
@@ -3116,27 +3120,31 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
_L("%s selected — F2 or right-click renames it, double-click edits it"),
|
||||
wxString::FromUTF8(m_doc.features[sel].name)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Double-click a row = Edit, the same gesture that re-opens a committed sketch on the canvas.
|
||||
// Without it the row only highlights and the feature looks dead until the user finds the
|
||||
// Edit button in the section header.
|
||||
m_tree->Bind(wxEVT_TREE_ITEM_ACTIVATED, [this](wxTreeEvent&) { on_edit_feature(); });
|
||||
m_tree->on_activate = [this] { on_edit_feature(); };
|
||||
|
||||
// Right-click a row: the three things a row can do. Renaming had no discoverable route at
|
||||
// all — the header pencil is Edit, a double-click ACTIVATES the row and is also Edit (the
|
||||
// "slow double-click renames" the old comment promised does not survive wxGTK, which fires
|
||||
// ITEM_ACTIVATED first), none of the seven header icons renames, and F2 is a function key
|
||||
// nothing announces. A user who wants to name a sketch tries the row, and now the row
|
||||
// answers. rename.
|
||||
m_tree->Bind(wxEVT_TREE_ITEM_RIGHT_CLICK, [this](wxTreeEvent& e) {
|
||||
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.
|
||||
// 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.
|
||||
m_tree->on_action = [this](int, int id) {
|
||||
if (m_parts) m_parts->unselect();
|
||||
switch (id) {
|
||||
case RowEdit: on_edit_feature(); break;
|
||||
case RowVisibility: on_toggle_visibility(); break;
|
||||
case RowDelete: on_delete_feature(); break;
|
||||
}
|
||||
};
|
||||
|
||||
// Right-click a row: everything a row can do. Renaming had no discoverable route at all — a
|
||||
// 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) {
|
||||
// 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();
|
||||
@@ -3150,8 +3158,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 = sel < int(m_doc.features.size()) &&
|
||||
!m_doc.features[sel].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"));
|
||||
@@ -3159,99 +3167,43 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
menu.Append(id_vis, _L("Show / hide"));
|
||||
menu.AppendSeparator();
|
||||
menu.Append(id_del, _L("Delete"));
|
||||
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) {
|
||||
const int row = tree_selection();
|
||||
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&) { m_verb_actions["btn:rename"](); }, id_rename);
|
||||
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);
|
||||
});
|
||||
|
||||
// In-place rename of a feature row (slow double-click, the offer's Rename verb, or F2).
|
||||
// The name is what makes a tree of eight sketches readable, and the tree row IS the object —
|
||||
// so renaming belongs on the row, not in a side-panel field. Bodies are computed results,
|
||||
// not named features, so a body row must never open an editor (it cannot, they live in the
|
||||
// Parts list, but the guard keeps a future change from slipping a body into this tree).
|
||||
auto item_index = [this](const wxTreeItemId& it) -> int {
|
||||
for (size_t i = 0; i < m_tree_items.size(); ++i)
|
||||
if (m_tree_items[i] == it) return int(i);
|
||||
return wxNOT_FOUND;
|
||||
menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { on_transform_imported(row); }, id_art);
|
||||
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
|
||||
};
|
||||
m_tree->Bind(wxEVT_TREE_BEGIN_LABEL_EDIT, [this, item_index](wxTreeEvent& e) {
|
||||
// The event's item is the authority for WHAT is being edited; tree_selection() is not,
|
||||
// because the editor can open on a row that is not the current selection. A row that is
|
||||
// not a feature (a body, or a stray id) gets the edit vetoed before it can take a name.
|
||||
if (tree_body_selection() >= 0 || item_index(e.GetItem()) == wxNOT_FOUND) { e.Veto(); return; }
|
||||
e.Skip();
|
||||
});
|
||||
m_tree->Bind(wxEVT_TREE_END_LABEL_EDIT, [this, item_index](wxTreeEvent& e) {
|
||||
if (e.IsEditCancelled()) return;
|
||||
const int idx = item_index(e.GetItem());
|
||||
if (idx == wxNOT_FOUND) { e.Veto(); return; }
|
||||
wxString label = e.GetLabel();
|
||||
label.Trim(true).Trim(false);
|
||||
if (label.empty()) { e.Veto(); return; } // a nameless row is worse than a badly named one
|
||||
m_doc.features[idx].name = std::string(label.ToUTF8().data());
|
||||
sync_recipe_to_model(); // the name is part of the recipe, so the save path persists it
|
||||
e.Skip(); // let wx finish applying the label to the item it is holding
|
||||
// REBUILD LATER, NOT NOW. refresh_tree() deletes and re-creates every wxTreeItemId, and
|
||||
// we are inside wx's own END_LABEL_EDIT dispatch for one of them — destroying it here
|
||||
// frees the item the caller is still using and takes the process down. Measured: typing
|
||||
// a name and pressing Enter killed the app outright, with the keystrokes traced and no
|
||||
// trace for the Return. Deferring to the next event-loop turn lets wx finish first.
|
||||
CallAfter([this] { refresh_tree(); });
|
||||
});
|
||||
|
||||
// Feature-tree edit actions: act on the selected feature (delete / reorder). These sit in the
|
||||
// card header (Prepare puts its section actions there too) rather than on a loose row below.
|
||||
// In-place rename of a feature row (F2, the offer's Rename verb, or the row's menu). The name
|
||||
// is what makes a tree of eight sketches readable, and the tree row IS the object — so
|
||||
// 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) {
|
||||
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
|
||||
};
|
||||
|
||||
// The header keeps what is not one row's own action: reordering, which moves the selected
|
||||
// feature among the others, and the interference check, which reports on every body.
|
||||
{
|
||||
wxBoxSizer* trow = m_hdr_tree_row;
|
||||
auto edit_btn = [this](const char* icon, const wxString& tip) {
|
||||
// Header-sized: reads as a section action, not a primary control.
|
||||
auto* b = new ScalableButton(m_tree_box, wxID_ANY, icon, "", FromDIP(wxSize(24, 24)),
|
||||
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 20);
|
||||
b->SetToolTip(tip);
|
||||
return b;
|
||||
};
|
||||
auto* edit = edit_btn("design_edit", _L("Edit"));
|
||||
edit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_edit_feature(); });
|
||||
// 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.
|
||||
// body lives: on each body's row in the Bodies list, and in 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"));
|
||||
del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
|
||||
// A body row deletes the feature that made it. on_delete_body() already resolves
|
||||
// CadBody::source_feature and asks for confirmation by name; it was reachable only
|
||||
// from the right-click offer, so this button answered a selected body row with
|
||||
// "select the FEATURE that created this body" — an instruction the user cannot act
|
||||
// on, since the tree does not say which feature that is. It does now.
|
||||
if (tree_body_selection() >= 0) on_delete_body();
|
||||
else on_delete_feature();
|
||||
});
|
||||
auto* up = edit_btn("design_moveup", _L("Move up"));
|
||||
auto* up = sidebar_icon_btn(m_tree_box, "design_moveup", _L("Move up"));
|
||||
up->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_move_feature(-1); });
|
||||
auto* down = edit_btn("design_movedown", _L("Move down"));
|
||||
auto* down = sidebar_icon_btn(m_tree_box, "design_movedown", _L("Move down"));
|
||||
down->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_move_feature(+1); });
|
||||
m_btn_interfere = edit_btn("color_palette", _L("Check interference — find overlapping bodies"));
|
||||
m_btn_interfere = sidebar_icon_btn(m_tree_box, "color_palette", _L("Check interference — find overlapping bodies"));
|
||||
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(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);
|
||||
trow->Add(down, 0, wxALIGN_CENTER_VERTICAL);
|
||||
// Interference check sits after a rule: it reports, it does not edit the recipe.
|
||||
@@ -3274,45 +3226,20 @@ 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. Boolean is not a copy — it is the one
|
||||
// body-body operation, gated through on_boolean_tool. 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.
|
||||
// The bodies card header carries the body actions that are not one row's own. Boolean is the
|
||||
// one body-body operation, gated through on_boolean_tool, and Colour recolours the selected
|
||||
// body, resolving it itself (on_set_body_color), so nothing here decides policy. Move,
|
||||
// Show/hide and Delete act on one body, so they sit on its row, as a feature's actions do.
|
||||
{
|
||||
auto body_btn = [this](const char* icon, const wxString& tip) {
|
||||
auto* b = new ScalableButton(m_parts_box, wxID_ANY, icon, "", FromDIP(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 {
|
||||
set_status(StatusKind::Info, _L("Select a body row first, then move it"));
|
||||
}
|
||||
});
|
||||
// Boolean lives here as well as on the toolbar: combining two bodies is a body action,
|
||||
// and a user working in the body list should not have to leave it to find this.
|
||||
auto* bbool = body_btn("design_boolean", _L("Boolean — join, subtract or intersect with another body"));
|
||||
auto* bbool = sidebar_icon_btn(m_parts_box, "design_boolean", _L("Boolean — join, subtract or intersect with another body"));
|
||||
bbool->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_boolean_tool(); });
|
||||
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("Color"));
|
||||
auto* bcol = sidebar_icon_btn(m_parts_box, "color_palette", _L("Color"));
|
||||
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(bbool, 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,
|
||||
@@ -3322,9 +3249,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
FromDIP(SidebarProps::TitlebarMargin()));
|
||||
m_parts_hdr->ShowItems(false); // no bodies yet on a fresh document
|
||||
m_parts_rule->Hide();
|
||||
m_parts = new wxTreeCtrl(m_parts_box, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(-1, 48)),
|
||||
wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES |
|
||||
wxTR_FULL_ROW_HIGHLIGHT | wxBORDER_SIMPLE | wxTR_EDIT_LABELS);
|
||||
m_parts = new DesignRowList(m_parts_box, 6); // scrolls past 6
|
||||
m_parts->SetBackgroundColour(dp_panel_bg());
|
||||
parts_inner->Add(m_parts, 0, wxEXPAND | wxALL, 12);
|
||||
// Start hidden: a fresh document has no bodies, and refresh_parts() only runs on the first
|
||||
@@ -3333,16 +3258,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_parts_label->Hide();
|
||||
// Taking a body from the list means the SAME state change however it was asked for, so the
|
||||
// normalisation lives here and not inside a selection handler. That distinction is not
|
||||
// pedantry: SelectItem() on a row that is ALREADY selected fires no SEL_CHANGED at all, so a
|
||||
// version of this that only ran on selection left a stale vertex/edge from an earlier
|
||||
// viewport pick in place — and offer_selection_kind() tests vertex FIRST, so right-clicking
|
||||
// the body row served the VERTEX offer (Fillet greyed, Mirror in place of Repeat) while the
|
||||
// row sat highlighted. Measured on the rig 2026-08-02; it is invisible from the code alone.
|
||||
// pedantry: selecting a row that is ALREADY selected notifies nobody, so a version of this
|
||||
// that only ran on selection left a stale vertex/edge from an earlier viewport pick in place
|
||||
// — and offer_selection_kind() tests vertex FIRST, so right-clicking the body row served the
|
||||
// VERTEX offer (Fillet greyed, Mirror in place of Repeat) while the row sat highlighted.
|
||||
// Measured on the rig 2026-08-02; it is invisible from the code alone.
|
||||
auto apply_body_row = [this](int b) {
|
||||
if (!m_viewport || b < 0) return;
|
||||
// One selection at a time: a body row and a feature row mean different things to the
|
||||
// op bar, so clear the feature tree's highlight when a body takes over.
|
||||
if (m_tree) m_tree->Unselect(); // wxTR_SINGLE: UnselectAll() does nothing here
|
||||
if (m_tree) m_tree->unselect();
|
||||
m_viewport->set_body_highlight(false); // the per-body overlay does the tint
|
||||
m_viewport->select_body(b); // also drops the vertex/edge marker
|
||||
m_sel_solid_body = b;
|
||||
@@ -3351,53 +3276,41 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
m_pick_face = m_pick_face_body = -1; // chosen from the list, no face was pointed at
|
||||
set_status(StatusKind::Info, wxString::Format(_L("Body %d selected — right-click for what applies to it"), b + 1));
|
||||
};
|
||||
m_parts->Bind(wxEVT_TREE_SEL_CHANGED, [this, apply_body_row](wxTreeEvent&) {
|
||||
apply_body_row(tree_body_selection());
|
||||
});
|
||||
m_parts->on_select = [this, apply_body_row] { apply_body_row(tree_body_selection()); };
|
||||
// The row's own Move / Show-hide / Delete. apply_body_row runs unconditionally, as for the
|
||||
// menu below: a face picked in the viewport since the row was selected has moved
|
||||
// m_sel_solid_body, which is the body on_move_body and on_delete_body act on.
|
||||
m_parts->on_action = [this, apply_body_row](int row, int id) {
|
||||
apply_body_row(row);
|
||||
switch (id) {
|
||||
case RowMove: on_move_body(); break;
|
||||
case RowVisibility: on_toggle_visibility(); break;
|
||||
case RowDelete: on_delete_body(); break;
|
||||
}
|
||||
};
|
||||
// The third door onto the offer, after the viewport right-click and the Menu key. A body ROW
|
||||
// is an unambiguous body, so the offer reports BodySolid and the body verbs act on the row you
|
||||
// can see highlighted. That is the confirmation a face pick cannot give: pointing at a face
|
||||
// lights the face, never the body the verb will actually change. The status line above has
|
||||
// been promising this right-click since before it existed.
|
||||
m_parts->on_menu = [this, apply_body_row](int row, const wxPoint& screen) {
|
||||
apply_body_row(row); // unconditional — see above
|
||||
// Let the modal menu take the loop after this handler returns — same CallAfter as the
|
||||
// sketch path, which learned it the hard way.
|
||||
CallAfter([this, screen] { show_offer_menu(screen); });
|
||||
};
|
||||
// Renaming a BODY names the body itself. It does NOT rename the feature that created it:
|
||||
// an Extrude, a Cut and a Fillet all land on one body, so source_feature is one operation in
|
||||
// its history and renaming that is renaming the wrong object — reported, correctly, as "you
|
||||
// consider the extrusion = the body". CadBody::user_name is carried across recompute() by
|
||||
// index and written into the recipe, so the name outlives both the rebuild and the save.
|
||||
m_parts->Bind(wxEVT_TREE_BEGIN_LABEL_EDIT, [this](wxTreeEvent& e) {
|
||||
if (tree_body_selection() < 0) { e.Veto(); return; }
|
||||
e.Skip();
|
||||
});
|
||||
m_parts->Bind(wxEVT_TREE_END_LABEL_EDIT, [this](wxTreeEvent& e) {
|
||||
if (e.IsEditCancelled()) return;
|
||||
const int b = tree_body_selection();
|
||||
if (b < 0 || b >= int(m_doc.bodies.size())) { e.Veto(); return; }
|
||||
wxString label = e.GetLabel();
|
||||
label.Trim(true).Trim(false);
|
||||
if (label.empty()) { e.Veto(); return; } // a nameless row is worse than a bad name
|
||||
m_parts->on_rename = [this](int b, const wxString& name) {
|
||||
if (b < 0 || b >= int(m_doc.bodies.size())) return;
|
||||
m_doc.bodies[b].has_user_name = true;
|
||||
m_doc.bodies[b].user_name = std::string(label.ToUTF8().data());
|
||||
m_doc.bodies[b].user_name = std::string(name.ToUTF8().data());
|
||||
sync_recipe_to_model(); // the name is part of what gets saved
|
||||
e.Skip();
|
||||
// Rebuild on the NEXT event-loop turn: refresh_parts() destroys every wxTreeItemId and
|
||||
// we are inside wx's own END_LABEL_EDIT dispatch for one of them. The feature tree
|
||||
// learned this the hard way — doing it here took the process down.
|
||||
CallAfter([this] { refresh_parts(); });
|
||||
});
|
||||
|
||||
m_parts->Bind(wxEVT_TREE_ITEM_MENU, [this, apply_body_row](wxTreeEvent& e) {
|
||||
if (e.GetItem().IsOk())
|
||||
m_parts->SelectItem(e.GetItem()); // the row under the cursor, never a stale one
|
||||
apply_body_row(tree_body_selection()); // unconditional — see above, SelectItem on an
|
||||
// already-selected row raises no event
|
||||
// GetPoint() is tree-client; it is (-1,-1) when the KEYBOARD menu key raised this, so fall
|
||||
// back to the shared anchor rather than popping the menu at a garbage coordinate.
|
||||
const wxPoint p = e.GetPoint();
|
||||
const wxPoint screen = (p.x >= 0 && p.y >= 0) ? m_parts->ClientToScreen(p) : offer_anchor();
|
||||
// Let the modal menu take the loop after this handler returns — same CallAfter as the
|
||||
// sketch path, which learned it the hard way.
|
||||
CallAfter([this, screen] { show_offer_menu(screen); });
|
||||
});
|
||||
refresh_parts();
|
||||
};
|
||||
|
||||
// --- Variables (document-scope named expressions) ---
|
||||
// Below the feature tree + parts, always visible. wxListCtrl in report mode with two
|
||||
@@ -3413,15 +3326,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
|
||||
wxALIGN_CENTER_VERTICAL);
|
||||
var_hdr->AddStretchSpacer();
|
||||
// Icon actions in the card header, as the Feature tree and Bodies cards have them.
|
||||
auto var_btn = [this](const char* icon, const wxString& tip) {
|
||||
auto* b = new ScalableButton(m_var_box, wxID_ANY, icon, "", FromDIP(wxSize(24, 24)),
|
||||
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 20);
|
||||
b->SetToolTip(tip);
|
||||
return b;
|
||||
};
|
||||
m_btn_add_var = var_btn("add", _L("Add variable"));
|
||||
m_btn_edit_var = var_btn("design_edit", _L("Edit variable"));
|
||||
m_btn_del_var = var_btn("design_delete", _L("Delete variable"));
|
||||
m_btn_add_var = sidebar_icon_btn(m_var_box, "add", _L("Add variable"));
|
||||
m_btn_edit_var = sidebar_icon_btn(m_var_box, "design_edit", _L("Edit variable"));
|
||||
m_btn_del_var = sidebar_icon_btn(m_var_box, "design_delete", _L("Delete variable"));
|
||||
m_btn_add_var->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_variable(); });
|
||||
m_btn_edit_var->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_edit_variable(); });
|
||||
m_btn_del_var->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_remove_variable(); });
|
||||
@@ -6687,7 +6594,15 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
}
|
||||
}
|
||||
|
||||
for (int row = 0; row < kOfferRowCount; ++row) {
|
||||
// Flat rows first, then the families, each group in ratified order. A flat row's verbs sit at
|
||||
// the top level, each an item of its own: they are what a selection is opened for most.
|
||||
std::vector<int> rows;
|
||||
for (const bool flat : {true, false})
|
||||
for (int row = 0; row < kOfferRowCount; ++row)
|
||||
if (kOfferRowFlat[row] == flat) rows.push_back(row);
|
||||
bool flat_items = false;
|
||||
|
||||
for (const int row : rows) {
|
||||
std::vector<const OfferVerb*> live, family;
|
||||
for (int i = 0; i < kOfferVerbCount; ++i) {
|
||||
const OfferVerb& v = kOfferVerbs[i];
|
||||
@@ -6697,6 +6612,34 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
|
||||
}
|
||||
if (family.empty())
|
||||
continue; // no verb of this family in this mode
|
||||
|
||||
if (kOfferRowFlat[row]) {
|
||||
// Only the verbs about this selection; one blocked by the document stays, greyed,
|
||||
// with its reason, as it would inside a family.
|
||||
for (const OfferVerb* v : family) {
|
||||
if (!(v->accepts & bit)) continue;
|
||||
const int id = base + int(bound.size());
|
||||
if (applies(*v)) {
|
||||
offer_trace("row=%d %s -> %s%s", row, kOfferRowNames[row], v->id,
|
||||
v->action ? "" : " (no GUI route)");
|
||||
append_offer_item(&menu, id, label(*v), *v)->Enable(v->action != nullptr);
|
||||
bound.push_back(v);
|
||||
} else {
|
||||
offer_trace("row=%d %s DISABLED (%s)", row, kOfferRowNames[row],
|
||||
v->refusal ? v->refusal : "blocked");
|
||||
wxString s = tr(v->name);
|
||||
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
|
||||
append_offer_item(&menu, id, s, *v)->Enable(false);
|
||||
bound.push_back(nullptr);
|
||||
}
|
||||
flat_items = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (flat_items) {
|
||||
menu.AppendSeparator(); // between the flat items and the families
|
||||
flat_items = false;
|
||||
}
|
||||
const wxString fam = tr(kOfferRowNames[row]);
|
||||
|
||||
if (live.empty()) {
|
||||
@@ -7170,43 +7113,44 @@ void DesignPanel::on_add_draft()
|
||||
refresh_tree();
|
||||
}
|
||||
|
||||
int DesignPanel::tree_icon_for(CadFeatureType t)
|
||||
// The Feature tree's per-type row icon, by family.
|
||||
const char* DesignPanel::tree_icon_for(CadFeatureType t)
|
||||
{
|
||||
switch (t) {
|
||||
case CadFeatureType::Sketch: return 0;
|
||||
case CadFeatureType::Extrude: return 1;
|
||||
case CadFeatureType::Sketch:
|
||||
case CadFeatureType::Plane:
|
||||
case CadFeatureType::Axis:
|
||||
case CadFeatureType::CoordSys:
|
||||
case CadFeatureType::Project: return "design_sketch"; // sketches, datums, projections
|
||||
case CadFeatureType::Fillet:
|
||||
case CadFeatureType::Chamfer: return 2;
|
||||
case CadFeatureType::Hole: return 3;
|
||||
case CadFeatureType::Thread: return 4;
|
||||
case CadFeatureType::Shell: return 5;
|
||||
case CadFeatureType::Revolve: return 1;
|
||||
case CadFeatureType::Sweep: return 1;
|
||||
case CadFeatureType::Pattern: return 1;
|
||||
case CadFeatureType::Plane: return 0; // datum plane: sketch-family icon
|
||||
case CadFeatureType::Loft: return 1;
|
||||
case CadFeatureType::Draft: return 5; // dressup-family icon
|
||||
case CadFeatureType::Import: return 1; // imported solid: solid-family icon
|
||||
case CadFeatureType::Boolean: return 1; // body-body combine: solid-family icon
|
||||
case CadFeatureType::Cut: return 1; // plane split: solid-family icon
|
||||
case CadFeatureType::Axis: return 0; // datum axis: sketch-family icon
|
||||
case CadFeatureType::CoordSys: return 0; // datum coord sys: sketch-family icon
|
||||
case CadFeatureType::SurfaceExtrude: return 1;
|
||||
case CadFeatureType::SurfaceRevolve: return 1;
|
||||
case CadFeatureType::SurfaceLoft: return 1;
|
||||
case CadFeatureType::SurfaceFill: return 1;
|
||||
case CadFeatureType::ThickenSurface: return 1;
|
||||
case CadFeatureType::SurfaceOffset: return 1;
|
||||
case CadFeatureType::Transform: return 1; // solid-family icon
|
||||
case CadFeatureType::Mirror: return 1; // solid-family icon
|
||||
case CadFeatureType::Thicken: return 1; // solid-family icon
|
||||
case CadFeatureType::Rib: return 1; // solid-family icon
|
||||
case CadFeatureType::Project: return 0; // sketch-family icon (produces sketch)
|
||||
case CadFeatureType::DeleteFace: return 5; // dressup-family icon
|
||||
case CadFeatureType::Helix: return 4; // thread-family icon (curve)
|
||||
case CadFeatureType::Mate: return 2; // dressup-family icon (assembly)
|
||||
case CadFeatureType::Chamfer:
|
||||
case CadFeatureType::Mate: return "design_dressup";
|
||||
case CadFeatureType::Hole: return "design_hole";
|
||||
case CadFeatureType::Thread:
|
||||
case CadFeatureType::Helix: return "design_thread";
|
||||
case CadFeatureType::Shell:
|
||||
case CadFeatureType::Draft:
|
||||
case CadFeatureType::DeleteFace: return "design_shell";
|
||||
case CadFeatureType::Extrude:
|
||||
case CadFeatureType::Revolve:
|
||||
case CadFeatureType::Sweep:
|
||||
case CadFeatureType::Loft:
|
||||
case CadFeatureType::Pattern:
|
||||
case CadFeatureType::Import:
|
||||
case CadFeatureType::Boolean:
|
||||
case CadFeatureType::Cut:
|
||||
case CadFeatureType::Transform:
|
||||
case CadFeatureType::Mirror:
|
||||
case CadFeatureType::Thicken:
|
||||
case CadFeatureType::Rib:
|
||||
case CadFeatureType::SurfaceExtrude:
|
||||
case CadFeatureType::SurfaceRevolve:
|
||||
case CadFeatureType::SurfaceLoft:
|
||||
case CadFeatureType::SurfaceFill:
|
||||
case CadFeatureType::ThickenSurface:
|
||||
case CadFeatureType::SurfaceOffset: return "design_extrude"; // solids and surfaces
|
||||
}
|
||||
return 0;
|
||||
return "design_sketch";
|
||||
}
|
||||
|
||||
// The reason detect_mate_conflicts() recorded for this feature, or nullptr. A linear scan: an
|
||||
@@ -7269,8 +7213,12 @@ void DesignPanel::refresh_icons()
|
||||
std::function<void(wxWindow*)> walk = [&walk](wxWindow* w) {
|
||||
if (auto* b = dynamic_cast<ScalableButton*>(w))
|
||||
b->msw_rescale();
|
||||
else if (auto* b = dynamic_cast<::Button*>(w))
|
||||
else if (auto* b = dynamic_cast<::Button*>(w)) {
|
||||
b->Rescale();
|
||||
if (b->GetName() == "design_icon_btn")
|
||||
style_sidebar_icon_btn(b); // pixel sizes and the panel colour, for the new scale and theme
|
||||
} else if (auto* l = dynamic_cast<DesignRowList*>(w))
|
||||
l->Rescale();
|
||||
else if (auto* c = dynamic_cast<::CheckBox*>(w))
|
||||
c->Rescale();
|
||||
else if (auto* c = dynamic_cast<::ComboBox*>(w))
|
||||
@@ -7307,7 +7255,7 @@ void DesignPanel::on_sys_color_changed()
|
||||
walk(child);
|
||||
};
|
||||
walk(this);
|
||||
// The native controls (trees, lists, spins) take the app's own dark pass.
|
||||
// The native controls (lists, spins) take the app's own dark pass.
|
||||
wxGetApp().UpdateDarkUIWin(this);
|
||||
refresh_icons();
|
||||
refresh_tree(); // the rows carry their own text colours
|
||||
@@ -7475,23 +7423,22 @@ void DesignPanel::refresh_tree()
|
||||
// last feature still clears it, through the tree-edit call site that always did.
|
||||
if (!m_doc.features.empty()) sync_recipe_to_model();
|
||||
|
||||
// Preserve the selected row across the rebuild — wxTreeCtrl::DeleteAllItems
|
||||
// drops the selection, which made every edit/add feel like it "lost" the
|
||||
// selection (and broke Edit/Move/Delete on the just-touched feature).
|
||||
// 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).
|
||||
const int keep = tree_selection();
|
||||
|
||||
m_tree->DeleteAllItems();
|
||||
m_tree_items.clear();
|
||||
m_tree_body_items.clear();
|
||||
wxTreeItemId root = m_tree->AddRoot("root");
|
||||
// Datum/reference planes carry no solid; feed them to the viewport so they render as
|
||||
// translucent rectangles (otherwise a Plane feature is invisible in the canvas).
|
||||
refresh_datum_planes();
|
||||
update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes
|
||||
std::vector<DesignRowList::Row> rows;
|
||||
rows.reserve(m_doc.features.size());
|
||||
for (size_t fi = 0; fi < m_doc.features.size(); ++fi) {
|
||||
const CadFeature& f = m_doc.features[fi];
|
||||
const int img = tree_icon_for(f.type);
|
||||
wxTreeItemId id = m_tree->AppendItem(root, wxString::FromUTF8(f.name), img, img);
|
||||
DesignRowList::Row row;
|
||||
row.icon = tree_icon_for(f.type);
|
||||
row.label = wxString::FromUTF8(f.name);
|
||||
// Three states, in this order of precedence:
|
||||
// disabled -> dim. A SUPPRESSED mate is the user's answer to a conflict, so it must
|
||||
// read as suppressed rather than keep shouting about the conflict.
|
||||
@@ -7502,23 +7449,19 @@ void DesignPanel::refresh_tree()
|
||||
// A conflict is NOT a document error — the document still evaluates — so the row is
|
||||
// marked and never hidden, and the reason goes to the status line on selection rather
|
||||
// than into a modal that interrupts without offering an action.
|
||||
m_tree->SetItemTextColour(id, !f.enabled ? dp_item_dim()
|
||||
: mate_conflict_reason(int(fi)) != nullptr ? wxColour(235, 110, 110)
|
||||
: dp_item_text());
|
||||
m_tree_items.push_back(id);
|
||||
row.colour = !f.enabled ? dp_item_dim()
|
||||
: mate_conflict_reason(int(fi)) != nullptr ? wxColour(235, 110, 110)
|
||||
: dp_item_text();
|
||||
row.actions = {
|
||||
{ RowEdit, "design_edit", _L("Edit") },
|
||||
eye_action(f.enabled),
|
||||
{ RowDelete, "design_delete", _L("Delete") },
|
||||
};
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
m_tree->set_rows(std::move(rows));
|
||||
refresh_parts(); // bodies live in their own list below the tree, never clipped by history
|
||||
if (keep >= 0 && keep < int(m_tree_items.size()))
|
||||
m_tree->SelectItem(m_tree_items[keep]);
|
||||
|
||||
// Size the tree to its content (clamped) so it doesn't waste a fixed-height block when
|
||||
// there are few features, and scrolls internally past ~9 rows instead of growing forever.
|
||||
const int rows = int(m_tree_items.size()); // bodies are in their own list now
|
||||
const int rowH = std::max(m_tree->GetCharHeight() + 8, 20);
|
||||
const int shown = std::min(std::max(rows, 1), 9);
|
||||
const wxSize ts(-1, shown * rowH + 8);
|
||||
m_tree->SetMinSize(ts);
|
||||
m_tree->SetMaxSize(ts);
|
||||
m_tree->select(keep);
|
||||
if (m_form && m_form->GetSizer()) { update_cards_frame(); m_form->Layout(); m_form->FitInside(); }
|
||||
}
|
||||
|
||||
@@ -7529,11 +7472,9 @@ void DesignPanel::refresh_parts()
|
||||
if (m_parts == nullptr) return;
|
||||
const int keep = tree_body_selection();
|
||||
|
||||
m_parts->DeleteAllItems();
|
||||
m_tree_body_items.clear();
|
||||
wxTreeItemId proot = m_parts->AddRoot("root");
|
||||
|
||||
sync_body_visible(); // keep flags parallel before reading them for the row colour
|
||||
std::vector<DesignRowList::Row> rows;
|
||||
rows.reserve(m_doc.bodies.size());
|
||||
for (size_t b = 0; b < m_doc.bodies.size(); ++b) {
|
||||
// "Body N" keeps the positional identity every status line and message uses ("Body 2
|
||||
// selected", the interference report), and the NAME follows it because that is the part
|
||||
@@ -7547,17 +7488,24 @@ void DesignPanel::refresh_parts()
|
||||
const wxString bname = m_doc.bodies[b].has_user_name
|
||||
? wxString::FromUTF8(m_doc.bodies[b].user_name)
|
||||
: wxString::FromUTF8(m_doc.bodies[b].name);
|
||||
wxTreeItemId id = m_parts->AppendItem(proot,
|
||||
bname.IsEmpty() ? wxString::Format(_L("Body %zu"), b + 1)
|
||||
: wxString::Format(_L("Body %zu — %s"), b + 1, bname));
|
||||
// Hidden bodies are greyed so the show/hide state reads at a glance (eye toggle).
|
||||
m_parts->SetItemTextColour(id, vis ? dp_item_text() : dp_item_dim());
|
||||
m_tree_body_items.push_back(id);
|
||||
DesignRowList::Row row;
|
||||
row.label = bname.IsEmpty() ? wxString::Format(_L("Body %zu"), b + 1)
|
||||
: wxString::Format(_L("Body %zu — %s"), b + 1, bname);
|
||||
row.edit_text = bname; // the name is the body's to edit, not its number
|
||||
// Hidden bodies are greyed and their eye is closed, so the state reads at a glance.
|
||||
row.colour = vis ? dp_item_text() : dp_item_dim();
|
||||
row.actions = {
|
||||
{ RowMove, "design_move", _L("Move") },
|
||||
eye_action(vis),
|
||||
{ RowDelete, "design_delete", _L("Delete") },
|
||||
};
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
m_parts->set_rows(std::move(rows));
|
||||
|
||||
// Hide the whole block until there is something to list, so an empty document doesn't
|
||||
// show a stray empty box.
|
||||
const bool any = !m_tree_body_items.empty();
|
||||
const bool any = m_parts->GetItemCount() > 0;
|
||||
m_parts->Show(any);
|
||||
if (m_parts_label) m_parts_label->Show(any);
|
||||
if (m_parts_hdr) m_parts_hdr->ShowItems(any); // icon + title live in this sizer
|
||||
@@ -7565,26 +7513,14 @@ void DesignPanel::refresh_parts()
|
||||
// ...and the frame with it, or an empty bordered box floats there.
|
||||
if (m_parts_box && m_form && m_form->GetSizer()) m_form->GetSizer()->Show(m_parts_box, any, false);
|
||||
|
||||
if (any) {
|
||||
const int rowH = std::max(m_parts->GetCharHeight() + 8, 20);
|
||||
const int shown = std::min(int(m_tree_body_items.size()), 6); // scrolls past 6
|
||||
const wxSize ps(-1, shown * rowH + 8);
|
||||
m_parts->SetMinSize(ps);
|
||||
m_parts->SetMaxSize(ps);
|
||||
if (keep >= 0 && keep < int(m_tree_body_items.size()))
|
||||
m_parts->SelectItem(m_tree_body_items[keep]);
|
||||
}
|
||||
if (any)
|
||||
m_parts->select(keep);
|
||||
if (m_form && m_form->GetSizer()) { update_cards_frame(); m_form->Layout(); m_form->FitInside(); }
|
||||
}
|
||||
|
||||
int DesignPanel::tree_body_selection() const
|
||||
{
|
||||
if (m_parts == nullptr) return -1;
|
||||
const wxTreeItemId sel = m_parts->GetSelection();
|
||||
if (!sel.IsOk()) return -1;
|
||||
for (size_t i = 0; i < m_tree_body_items.size(); ++i)
|
||||
if (m_tree_body_items[i] == sel) return int(i);
|
||||
return -1;
|
||||
return m_parts == nullptr ? -1 : m_parts->selection();
|
||||
}
|
||||
|
||||
void DesignPanel::update_section_flip_btn()
|
||||
@@ -7824,17 +7760,13 @@ bool DesignPanel::place_on_face()
|
||||
|
||||
int DesignPanel::tree_selection() const
|
||||
{
|
||||
const wxTreeItemId sel = m_tree->GetSelection();
|
||||
if (!sel.IsOk()) return wxNOT_FOUND;
|
||||
for (size_t i = 0; i < m_tree_items.size(); ++i)
|
||||
if (m_tree_items[i] == sel) return int(i);
|
||||
return wxNOT_FOUND;
|
||||
return m_tree->selection();
|
||||
}
|
||||
|
||||
void DesignPanel::set_tree_selection(int row)
|
||||
{
|
||||
if (row >= 0 && row < int(m_tree_items.size()))
|
||||
m_tree->SelectItem(m_tree_items[row]);
|
||||
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
|
||||
@@ -8004,12 +7936,11 @@ void DesignPanel::on_toggle_visibility()
|
||||
&m_body_visible, &m_body_xform);
|
||||
}
|
||||
refresh_tree();
|
||||
// Keep the row selected for repeat toggles. m_parts, NOT m_tree: these ids belong
|
||||
// to the Bodies list, and handing a foreign item to the feature tree left the row
|
||||
// unselected — so the second press of the eye found tree_body_selection() == -1 and
|
||||
// fell through to the FEATURE-level branch below instead of un-hiding the body.
|
||||
if (m_parts != nullptr && bsel < int(m_tree_body_items.size()))
|
||||
m_parts->SelectItem(m_tree_body_items[bsel]);
|
||||
// Keep the row selected for repeat toggles. m_parts, NOT m_tree: the row is a body,
|
||||
// and selecting it in the feature tree left the body row unselected — so the second
|
||||
// press of the eye found tree_body_selection() == -1 and fell through to the
|
||||
// FEATURE-level branch below instead of un-hiding the body.
|
||||
if (m_parts != nullptr) m_parts->select(bsel);
|
||||
set_status(StatusKind::Info, wxString::Format(now_visible ? _L("Body %d shown")
|
||||
: _L("Body %d hidden"), bsel + 1));
|
||||
}
|
||||
@@ -8412,11 +8343,11 @@ void DesignPanel::rebuild_constraint_list()
|
||||
for (int i = 0; i < int(cons.size()); ++i) {
|
||||
auto* row = new wxBoxSizer(wxHORIZONTAL);
|
||||
// Delete button first (fixed left position, always visible — long labels can
|
||||
// horizontally scroll but ✗ stays put and clickable). BMP-safe ✗ glyph.
|
||||
auto* del = new ScalableButton(m_cards, wxID_ANY, "design_delete", "", FromDIP(wxSize(24, 24)),
|
||||
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 16);
|
||||
del->SetToolTip(_L("Delete constraint"));
|
||||
del->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent&) { delete_constraint(i); });
|
||||
// horizontally scroll but ✗ stays put and clickable).
|
||||
auto* del = sidebar_icon_btn(m_cards, "design_delete", _L("Delete constraint"), 16);
|
||||
// After the click: deleting rebuilds these rows, and with them this button, which must
|
||||
// not be destroyed while its own click is still being dispatched.
|
||||
del->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent&) { CallAfter([this, i] { delete_constraint(i); }); });
|
||||
// Clickable label: selecting it highlights the referenced entities.
|
||||
auto* lbl = new wxStaticText(m_cards, wxID_ANY, constraint_label(cons[i]));
|
||||
lbl->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
@@ -8467,8 +8398,8 @@ void DesignPanel::delete_constraint(int idx)
|
||||
{
|
||||
// Live session: the constraint lives in the sketch tool, not in any feature. Removing it
|
||||
// re-solves and fires on_constraints_changed, which rebuilds these rows — so this branch
|
||||
// deliberately does NOT call rebuild_constraint_list() itself (it would run twice, and the
|
||||
// second run would delete the wxButton whose click handler is still on the stack).
|
||||
// deliberately does NOT call rebuild_constraint_list() itself (it would run twice). A row's ✗
|
||||
// calls this after its click has finished, so either rebuild may destroy that button.
|
||||
if (live_constraint_scope()) {
|
||||
if (!m_viewport->remove_sketch_constraint(idx))
|
||||
return;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <wx/panel.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/treebase.h> // wxTreeItemId
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
@@ -30,8 +29,6 @@ class wxCheckBox;
|
||||
class wxCheckListBox;
|
||||
class wxSpinCtrl;
|
||||
class wxSpinCtrlDouble;
|
||||
class wxTreeCtrl;
|
||||
class wxImageList;
|
||||
class wxStaticText;
|
||||
class wxStaticLine;
|
||||
class Button; // Orca-styled button (Widgets/Button.hpp)
|
||||
@@ -53,6 +50,7 @@ class ScalableButton;
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class DesignTextDialog;
|
||||
class DesignRowList;
|
||||
|
||||
class DesignCanvas;
|
||||
|
||||
@@ -539,8 +537,8 @@ private:
|
||||
void set_active_tool_btn(ScalableButton* b); // nullptr clears the highlight
|
||||
// Owns the themed DropDown flyouts (and the item vectors they hold by ref).
|
||||
std::vector<std::shared_ptr<void>> m_flyout_keepalive;
|
||||
// Icons that are not a plain ScalableButton face (flyout rows, card headers, the tree's image
|
||||
// list, theme-twinned buttons): each re-creates its bitmaps for the current scale and theme.
|
||||
// Icons that are not a plain ScalableButton face (flyout rows, card headers, theme-twinned
|
||||
// buttons): each re-creates its bitmaps for the current scale and theme.
|
||||
std::vector<std::function<void()>> m_icon_refresh;
|
||||
void refresh_icons();
|
||||
wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle
|
||||
@@ -711,16 +709,16 @@ private:
|
||||
// Document variables panel (below the feature tree / parts)
|
||||
StaticBox* m_var_box{nullptr};
|
||||
wxListCtrl* m_var_list{nullptr};
|
||||
ScalableButton* m_btn_add_var{nullptr};
|
||||
ScalableButton* m_btn_edit_var{nullptr};
|
||||
ScalableButton* m_btn_del_var{nullptr};
|
||||
::Button* m_btn_add_var{nullptr};
|
||||
::Button* m_btn_edit_var{nullptr};
|
||||
::Button* m_btn_del_var{nullptr};
|
||||
void refresh_variables(); // rebuild m_var_list from m_doc.variables
|
||||
void on_add_variable();
|
||||
void on_edit_variable();
|
||||
void on_remove_variable();
|
||||
|
||||
// Feature-tree button
|
||||
ScalableButton* m_btn_interfere{nullptr};
|
||||
::Button* m_btn_interfere{nullptr};
|
||||
|
||||
// Pattern controls (replicate the target body: linear or circular).
|
||||
ComboBox* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular
|
||||
@@ -903,21 +901,18 @@ 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: a wxTreeCtrl with per-feature-type icons. Callers keep using
|
||||
// integer row indices via tree_selection()/set_tree_selection(); m_tree_items
|
||||
// maps feature order -> tree node, rebuilt by refresh_tree().
|
||||
wxTreeCtrl* m_tree{nullptr};
|
||||
wxTreeCtrl* m_parts{nullptr}; // Bodies list under the feature tree
|
||||
// Feature tree: 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_tree{nullptr};
|
||||
// Bodies list under the feature tree: one row per body (parallel to m_doc.bodies). Selecting
|
||||
// one highlights that body and makes it the target for the next op.
|
||||
DesignRowList* m_parts{nullptr};
|
||||
wxStaticText* m_parts_label{nullptr}; // its "Bodies" caption (hidden when empty)
|
||||
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title)
|
||||
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title + body actions)
|
||||
wxStaticLine* m_parts_rule{nullptr}; // rule under that header
|
||||
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + row actions
|
||||
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + list actions
|
||||
wxStaticText* m_hdr_tree{nullptr}; // its title label
|
||||
wxImageList* m_tree_images{nullptr};
|
||||
std::vector<wxTreeItemId> m_tree_items;
|
||||
// Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one
|
||||
// highlights that body and makes it the target for the next op.
|
||||
std::vector<wxTreeItemId> m_tree_body_items;
|
||||
|
||||
// Section views (non-destructive): named "Section View N" entries listed in the tree, each a
|
||||
// horizontal clip height. View-only — NOT bodies/features, never serialized. Key X adds one;
|
||||
@@ -953,7 +948,7 @@ private:
|
||||
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 row);
|
||||
static int tree_icon_for(CadFeatureType t);
|
||||
static const char* tree_icon_for(CadFeatureType t);
|
||||
|
||||
wxStaticText* m_status{nullptr};
|
||||
// m_status's foreground as created, captured before any caller touches it. Callers signal
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
#include "slic3r/GUI/CAD/DesignRowList.hpp"
|
||||
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
|
||||
#include <wx/control.h>
|
||||
#include <wx/dc.h>
|
||||
#include <wx/graphics.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/textctrl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Row and icon cell geometry, in DIP.
|
||||
static constexpr int kIconPx = 16; // type and action icons
|
||||
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)
|
||||
, m_max_visible(std::max(max_visible, 1))
|
||||
{
|
||||
Bind(wxEVT_LISTBOX, [this](wxCommandEvent&) { if (on_select) on_select(); });
|
||||
Bind(wxEVT_LISTBOX_DCLICK, [this](wxCommandEvent&) { if (on_activate) on_activate(); });
|
||||
|
||||
Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
|
||||
set_hover(hit_test(e.GetPosition()));
|
||||
e.Skip();
|
||||
});
|
||||
Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) {
|
||||
set_hover(Hit{});
|
||||
m_pressed = Hit{};
|
||||
e.Skip();
|
||||
});
|
||||
// A press on an action cell is remembered and skipped, so the list still selects the row.
|
||||
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
|
||||
const Hit h = hit_test(e.GetPosition());
|
||||
m_pressed = h.cell >= 0 ? h : Hit{};
|
||||
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; }
|
||||
m_pressed = h;
|
||||
});
|
||||
Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
|
||||
e.Skip();
|
||||
const Hit pressed = m_pressed;
|
||||
m_pressed = Hit{};
|
||||
if (pressed.cell < 0 || hit_test(e.GetPosition()) != pressed)
|
||||
return;
|
||||
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.
|
||||
CallAfter([this, row = pressed.row, id] {
|
||||
if (row < int(GetItemCount()) && row == GetSelection() && on_action)
|
||||
on_action(row, id);
|
||||
});
|
||||
});
|
||||
// Right-click targets the row under the pointer. Skipped, because on MSW a handled right
|
||||
// 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);
|
||||
e.Skip();
|
||||
});
|
||||
Bind(wxEVT_CONTEXT_MENU, [this](wxContextMenuEvent& e) {
|
||||
wxPoint screen = e.GetPosition(); // screen coordinates; wxDefaultPosition from the keyboard
|
||||
int row = GetSelection();
|
||||
if (screen == wxDefaultPosition) {
|
||||
if (row == wxNOT_FOUND) return;
|
||||
screen = ClientToScreen(GetItemRect(row).GetBottomLeft());
|
||||
} else {
|
||||
row = VirtualHitTest(ScreenToClient(screen).y);
|
||||
}
|
||||
if (row == wxNOT_FOUND || row >= int(m_rows.size()))
|
||||
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 (on_menu) on_menu(row, screen);
|
||||
});
|
||||
// wxVListBox takes every wheel event, even with nothing to scroll, so a short list would stop
|
||||
// the sidebar it sits in from scrolling under the pointer. Hand it on instead. Not on GTK,
|
||||
// where the sidebar scrolls natively and passes on an unhandled wheel by itself.
|
||||
Bind(wxEVT_MOUSEWHEEL, [this](wxMouseEvent& e) {
|
||||
end_rename(true);
|
||||
#ifndef __WXGTK__
|
||||
if (GetVisibleRowsBegin() == 0 && GetVisibleRowsEnd() >= GetItemCount()) {
|
||||
for (wxWindow* w = GetParent(); w != nullptr; w = w->GetParent())
|
||||
if (auto* sw = dynamic_cast<wxScrolledWindow*>(w)) {
|
||||
wxMouseEvent fwd(e);
|
||||
fwd.SetEventObject(sw);
|
||||
sw->GetEventHandler()->ProcessEvent(fwd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
e.Skip();
|
||||
});
|
||||
Bind(wxEVT_SIZE, [this](wxSizeEvent& e) {
|
||||
end_rename(true); // the editor sits on a row position the new size may have moved
|
||||
e.Skip();
|
||||
});
|
||||
measure_row();
|
||||
fit_rows();
|
||||
}
|
||||
|
||||
// Every row is one height. Measured once per font and scale rather than on each OnMeasureItem,
|
||||
// which wxVListBox calls for every row it walks on every paint and hit test.
|
||||
void DesignRowList::measure_row()
|
||||
{
|
||||
m_row_h = std::max(GetCharHeight() + 8, FromDIP(kCellDip + 2));
|
||||
}
|
||||
|
||||
void DesignRowList::set_rows(std::vector<Row> rows)
|
||||
{
|
||||
end_rename(false); // the row it names may no longer be that row
|
||||
SetSelection(wxNOT_FOUND);
|
||||
m_rows = std::move(rows);
|
||||
m_hover = Hit{};
|
||||
m_pressed = Hit{};
|
||||
UnsetToolTip();
|
||||
load_icons();
|
||||
SetItemCount(m_rows.size());
|
||||
fit_rows();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void DesignRowList::select(int row)
|
||||
{
|
||||
if (row < 0 || row >= int(GetItemCount()))
|
||||
row = wxNOT_FOUND;
|
||||
if (row == GetSelection())
|
||||
return;
|
||||
SetSelection(row);
|
||||
if (on_select) on_select();
|
||||
}
|
||||
|
||||
void DesignRowList::fit_rows()
|
||||
{
|
||||
const int shown = std::clamp(int(GetItemCount()), 1, m_max_visible);
|
||||
// Plus the frame, so the last row is not clipped by it.
|
||||
const int h = shown * m_row_h + (GetSize().y - GetClientSize().y);
|
||||
SetMinSize(wxSize(-1, h));
|
||||
SetMaxSize(wxSize(-1, h));
|
||||
}
|
||||
|
||||
void DesignRowList::Rescale()
|
||||
{
|
||||
m_icons.clear();
|
||||
load_icons();
|
||||
measure_row();
|
||||
// The scrollbar's total is only recomputed when the item count is set; RefreshAll() alone
|
||||
// would leave it at the old scale.
|
||||
SetItemCount(GetItemCount());
|
||||
fit_rows();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void DesignRowList::begin_rename(int row)
|
||||
{
|
||||
CallAfter([this, row] { open_editor(row); });
|
||||
}
|
||||
|
||||
void DesignRowList::open_editor(int row)
|
||||
{
|
||||
if (row < 0 || row >= int(m_rows.size()))
|
||||
return;
|
||||
end_rename(false);
|
||||
if (m_editor == nullptr) {
|
||||
// A raw wxTextCtrl, as wxTreeCtrl's own label editor is: it is a transient overlay on one
|
||||
// row, not a form field, and ::TextInput's frame would not fit inside the row.
|
||||
m_editor = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
|
||||
wxTE_PROCESS_ENTER | wxBORDER_SIMPLE);
|
||||
m_editor->Hide();
|
||||
m_editor->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) {
|
||||
end_rename(true);
|
||||
SetFocus();
|
||||
});
|
||||
// Esc must stop here. CHAR_HOOK reaches the focused editor before its ancestors, and the
|
||||
// Design panel's own hook answers Esc whatever holds focus, so a skipped Esc would close
|
||||
// the open tool as well as the editor.
|
||||
m_editor->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
|
||||
if (e.GetKeyCode() != WXK_ESCAPE) { e.Skip(); return; }
|
||||
end_rename(false);
|
||||
SetFocus();
|
||||
});
|
||||
m_editor->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) {
|
||||
e.Skip();
|
||||
end_rename(true);
|
||||
});
|
||||
}
|
||||
if (!IsRowVisible(row))
|
||||
ScrollToRow(row);
|
||||
const Row& r = m_rows[row];
|
||||
const wxRect rc = label_rect(GetItemRect(row), r);
|
||||
m_editor->SetFont(GetFont());
|
||||
m_editor->SetBackgroundColour(GetBackgroundColour());
|
||||
m_editor->SetForegroundColour(r.colour.IsOk() ? r.colour : GetForegroundColour());
|
||||
m_editor->SetValue(r.edit_text.empty() ? r.label : r.edit_text);
|
||||
const int h = std::max(rc.height, m_editor->GetBestSize().y);
|
||||
const int y = std::clamp(rc.y + (rc.height - h) / 2, 0, std::max(0, GetClientSize().y - h));
|
||||
m_editor->SetSize(rc.x - FromDIP(2), y, rc.width + FromDIP(2), h);
|
||||
m_edit_row = row;
|
||||
m_editor->Show();
|
||||
m_editor->SetFocus();
|
||||
m_editor->SelectAll();
|
||||
}
|
||||
|
||||
void DesignRowList::end_rename(bool commit)
|
||||
{
|
||||
if (m_edit_row == wxNOT_FOUND)
|
||||
return;
|
||||
// Cleared first: hiding the focused editor moves the focus, whose kill-focus comes back here.
|
||||
const int row = m_edit_row;
|
||||
wxString text = m_editor->GetValue();
|
||||
m_edit_row = wxNOT_FOUND;
|
||||
m_editor->Hide();
|
||||
text.Trim(true).Trim(false);
|
||||
if (commit && on_rename && !text.empty()) // a nameless row is worse than a badly named one
|
||||
CallAfter([this, row, text] {
|
||||
if (on_rename && row < int(GetItemCount()))
|
||||
on_rename(row, text);
|
||||
});
|
||||
}
|
||||
|
||||
wxCoord DesignRowList::OnMeasureItem(size_t) const
|
||||
{
|
||||
return m_row_h;
|
||||
}
|
||||
|
||||
void DesignRowList::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const
|
||||
{
|
||||
// ObjectList's selection teal, and the dropdowns' lighter hover teal; both are dark-mapped.
|
||||
wxColour bg;
|
||||
if (IsSelected(n))
|
||||
bg = StateColor::darkModeColorFor(wxColour("#BFE1DE"));
|
||||
else if (int(n) == m_hover.row)
|
||||
bg = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
|
||||
if (!bg.IsOk())
|
||||
return; // the list already cleared to its background colour
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(bg));
|
||||
dc.DrawRectangle(rect);
|
||||
}
|
||||
|
||||
void DesignRowList::OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const
|
||||
{
|
||||
if (n >= m_rows.size())
|
||||
return;
|
||||
const Row& r = m_rows[n];
|
||||
|
||||
if (const ScalableBitmap* bmp = icon(r.icon)) {
|
||||
const wxSize sz = bmp->GetBmpSize();
|
||||
dc.DrawBitmap(bmp->bmp(), rect.x + FromDIP(kPadDip), rect.y + (rect.height - sz.y) / 2, true);
|
||||
}
|
||||
|
||||
const wxRect lr = label_rect(rect, r);
|
||||
dc.SetFont(GetFont());
|
||||
dc.SetTextForeground(r.colour.IsOk() ? r.colour : GetForegroundColour());
|
||||
const wxString text = wxControl::Ellipsize(r.label, dc, wxELLIPSIZE_END, std::max(lr.width, 0));
|
||||
dc.DrawText(text, lr.x, lr.y + (lr.height - dc.GetCharHeight()) / 2);
|
||||
|
||||
for (size_t i = 0; i < r.actions.size(); ++i) {
|
||||
const wxRect cell = cell_rect(rect, r.actions.size(), i);
|
||||
if (m_hover.row == int(n) && m_hover.cell == int(i)) {
|
||||
// Drawn through a graphics context so the rounded corners are anti-aliased on MSW too,
|
||||
// and released before the icon goes on top.
|
||||
const wxColour chip = StateColor::darkModeColorFor(wxColour(hover_chip));
|
||||
const double rad = FromDIP(4);
|
||||
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::CreateFromUnknownDC(dc));
|
||||
if (gc) {
|
||||
gc->SetPen(*wxTRANSPARENT_PEN);
|
||||
gc->SetBrush(wxBrush(chip));
|
||||
gc->DrawRoundedRectangle(cell.x, cell.y, cell.width, cell.height, rad);
|
||||
} else {
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(chip));
|
||||
dc.DrawRoundedRectangle(cell, rad);
|
||||
}
|
||||
}
|
||||
if (const ScalableBitmap* bmp = icon(r.actions[i].icon)) {
|
||||
const wxSize sz = bmp->GetBmpSize();
|
||||
dc.DrawBitmap(bmp->bmp(), cell.x + (cell.width - sz.x) / 2, cell.y + (cell.height - sz.y) / 2, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DesignRowList::Hit DesignRowList::hit_test(const wxPoint& pt) const
|
||||
{
|
||||
Hit h;
|
||||
h.row = VirtualHitTest(pt.y);
|
||||
if (h.row == wxNOT_FOUND || h.row >= int(m_rows.size()))
|
||||
return Hit{};
|
||||
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)) {
|
||||
h.cell = int(i);
|
||||
break;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
wxRect DesignRowList::cell_rect(const wxRect& row, size_t count, size_t i) const
|
||||
{
|
||||
const int side = FromDIP(kCellDip);
|
||||
const int x = row.GetRight() + 1 - FromDIP(kPadDip) - int(count - i) * side;
|
||||
return wxRect(x, row.y + (row.height - side) / 2, side, side);
|
||||
}
|
||||
|
||||
wxRect DesignRowList::label_rect(const wxRect& row, const Row& r) const
|
||||
{
|
||||
int left = row.x + FromDIP(kPadDip);
|
||||
if (const ScalableBitmap* bmp = icon(r.icon))
|
||||
left += bmp->GetBmpSize().x + FromDIP(kGapDip);
|
||||
const int right = r.actions.empty() ? row.GetRight() + 1 - FromDIP(kPadDip)
|
||||
: cell_rect(row, r.actions.size(), 0).x - FromDIP(kPadDip);
|
||||
return wxRect(left, row.y, std::max(right - left, 0), row.height);
|
||||
}
|
||||
|
||||
// nullptr for no icon: load_icons() never stores an empty name.
|
||||
const ScalableBitmap* DesignRowList::icon(const std::string& name) const
|
||||
{
|
||||
const auto it = m_icons.find(name);
|
||||
return it == m_icons.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
void DesignRowList::load_icons()
|
||||
{
|
||||
auto load = [this](const std::string& name) {
|
||||
if (!name.empty() && m_icons.find(name) == m_icons.end())
|
||||
m_icons.emplace(name, ScalableBitmap(this, name, kIconPx));
|
||||
};
|
||||
for (const Row& r : m_rows) {
|
||||
load(r.icon);
|
||||
for (const Action& a : r.actions)
|
||||
load(a.icon);
|
||||
}
|
||||
}
|
||||
|
||||
void DesignRowList::set_hover(const Hit& h)
|
||||
{
|
||||
if (h == m_hover)
|
||||
return;
|
||||
const int old = m_hover.row;
|
||||
m_hover = h;
|
||||
if (old != wxNOT_FOUND && old < int(GetItemCount()))
|
||||
RefreshRow(old);
|
||||
if (h.row != wxNOT_FOUND && h.row != old)
|
||||
RefreshRow(h.row);
|
||||
|
||||
// The tip names the icon under the pointer, or the whole label where the row cuts it short.
|
||||
wxString tip;
|
||||
if (h.row != wxNOT_FOUND) {
|
||||
const Row& r = m_rows[h.row];
|
||||
if (h.cell >= 0)
|
||||
tip = r.actions[h.cell].tip;
|
||||
else if (GetTextExtent(r.label).x > label_rect(GetItemRect(h.row), r).width)
|
||||
tip = r.label;
|
||||
}
|
||||
if (tip.empty())
|
||||
UnsetToolTip();
|
||||
else if (tip != GetToolTipText())
|
||||
SetToolTip(tip);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,108 @@
|
||||
#ifndef slic3r_DesignRowList_hpp_
|
||||
#define slic3r_DesignRowList_hpp_
|
||||
|
||||
#include <wx/vlbox.h>
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "slic3r/GUI/wxExtensions.hpp" // ScalableBitmap
|
||||
|
||||
class wxTextCtrl;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// The Design tab's Feature tree and Bodies lists: a single-selection list whose rows carry their
|
||||
// own action icons. A wxTreeCtrl cannot host a clickable icon per row (it is native on MSW), so
|
||||
// this list draws its own: each row draws its actions at its right end, highlights the one under
|
||||
// the pointer, and takes its icons from the owner, so the eye can show whether the row is hidden.
|
||||
//
|
||||
// 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.
|
||||
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"
|
||||
wxString tip;
|
||||
};
|
||||
struct Row {
|
||||
std::string icon; // type icon at the left; empty for none
|
||||
wxString label;
|
||||
wxString edit_text; // what the rename editor opens with; empty for the label
|
||||
wxColour colour; // label colour
|
||||
std::vector<Action> actions; // icon cells at the right end, in display order
|
||||
};
|
||||
|
||||
// The hover chip behind an action icon, a light colour dark-mapped at paint. The sidebar's
|
||||
// icon buttons hover in it too.
|
||||
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);
|
||||
|
||||
// Replace every row. Clears the selection without notifying and cancels a rename in progress;
|
||||
// the owner re-selects the row it keeps, which notifies.
|
||||
void set_rows(std::vector<Row> rows);
|
||||
int selection() const { return GetSelection(); }
|
||||
// Select `row` (wxNOT_FOUND clears) and call on_select if that changed the selection.
|
||||
void select(int row);
|
||||
void unselect() { select(wxNOT_FOUND); }
|
||||
// Open the in-place editor on a row, once the current event has finished: opened from inside
|
||||
// a popup menu's nested loop it would never appear. Enter or clicking away commits, Esc cancels.
|
||||
void begin_rename(int row);
|
||||
// Re-rasterise the icons and re-measure the rows after a DPI or theme change.
|
||||
void Rescale();
|
||||
|
||||
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.
|
||||
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.
|
||||
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;
|
||||
|
||||
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;
|
||||
wxCoord OnMeasureItem(size_t n) const override;
|
||||
|
||||
private:
|
||||
struct Hit {
|
||||
int row{wxNOT_FOUND};
|
||||
int cell{-1}; // index into the row's actions, -1 for the rest of the row
|
||||
bool operator==(const Hit& o) const { return row == o.row && cell == o.cell; }
|
||||
bool operator!=(const Hit& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
Hit hit_test(const wxPoint& pt) const;
|
||||
wxRect cell_rect(const wxRect& row, size_t count, size_t i) const;
|
||||
wxRect label_rect(const wxRect& row, const Row& r) const;
|
||||
const ScalableBitmap* icon(const std::string& name) const;
|
||||
void load_icons();
|
||||
void set_hover(const Hit& h);
|
||||
void open_editor(int row);
|
||||
void end_rename(bool commit);
|
||||
void measure_row();
|
||||
void fit_rows();
|
||||
|
||||
std::vector<Row> m_rows;
|
||||
std::map<std::string, ScalableBitmap> m_icons;
|
||||
int m_max_visible;
|
||||
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
|
||||
wxTextCtrl* m_editor{nullptr}; // created on the first rename, then reused
|
||||
int m_edit_row{wxNOT_FOUND};
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_DesignRowList_hpp_
|
||||
Reference in New Issue
Block a user