diff --git a/docs/HLSD/design-tab.md b/docs/HLSD/design-tab.md
index d916f3bf2f..49a4e7256b 100644
--- a/docs/HLSD/design-tab.md
+++ b/docs/HLSD/design-tab.md
@@ -157,6 +157,11 @@ button (to terminate a chain, say) declines to also open a menu, through a read-
Past either budget the event is navigation, and navigation does not transition the state
machine.
+Navigation itself is Prepare's: the camera reads the drag actions set in Preferences > Control
+for each button. The left button is shared with picking, so a whole body is swept with a
+rectangle on plain left-drag only while no camera action is assigned to it, and with
+Shift+left-drag otherwise — Prepare's own rectangle selection.
+
Entering a sketch changes three things at once so the mode is legible: a banner above the
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
`wxGLCanvas` is a native window and does not reliably stack over GL), the printer bed muted so
@@ -182,6 +187,24 @@ degenerate edges are left out (`GeometryEngine::display_edges`), and the polylin
once per shape, keyed by its `TShape`, because a recompute that leaves a body unchanged is the
common case.
+## Following the app
+
+The tab is a page of Orca's main window and answers to the same settings as Prepare.
+
+- **Theme.** Its chrome is coloured from a table of light/dark token pairs. A theme switch
+ reaches `DesignPanel::on_sys_color_changed` from `MainFrame`, which moves every colour that is
+ one theme's token onto the other theme's and then runs the app's own dark pass; the icons are
+ 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).
+- **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.
+- **Undo.** The tab keeps its own history (the recipe is not part of Prepare's snapshots), but
+ it has no Undo/Redo of its own: the top bar, `Ctrl+Z` and Edit drive it while the tab is
+ shown, greyed to what an undo would actually do.
+
## The offer is generated, not hand-written
Right-clicking geometry opens the *offer*: eight families in a fixed order, each verb at a
diff --git a/resources/images/design_arc3pt.svg b/resources/images/design_arc3pt.svg
index 1b40569b4f..951db22615 100644
--- a/resources/images/design_arc3pt.svg
+++ b/resources/images/design_arc3pt.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_arc_center.svg b/resources/images/design_arc_center.svg
index e4c85febfc..05bb4f5a56 100644
--- a/resources/images/design_arc_center.svg
+++ b/resources/images/design_arc_center.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_array.svg b/resources/images/design_array.svg
index d29da47bd7..a22343e9eb 100644
--- a/resources/images/design_array.svg
+++ b/resources/images/design_array.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_boolean.svg b/resources/images/design_boolean.svg
index 97813cf2eb..7eace7fc2b 100644
--- a/resources/images/design_boolean.svg
+++ b/resources/images/design_boolean.svg
@@ -1,4 +1,4 @@
-
+
diff --git a/resources/images/design_bspline.svg b/resources/images/design_bspline.svg
index 16a5dc9b31..180f3fc24a 100644
--- a/resources/images/design_bspline.svg
+++ b/resources/images/design_bspline.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_angle.svg b/resources/images/design_c_angle.svg
index 2359b23b6a..5fe8683f68 100644
--- a/resources/images/design_c_angle.svg
+++ b/resources/images/design_c_angle.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_coincident.svg b/resources/images/design_c_coincident.svg
index 83ab602eca..8a72107cd3 100644
--- a/resources/images/design_c_coincident.svg
+++ b/resources/images/design_c_coincident.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_collinear.svg b/resources/images/design_c_collinear.svg
index 28b8b6a8cd..ddc46acde3 100644
--- a/resources/images/design_c_collinear.svg
+++ b/resources/images/design_c_collinear.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_concentric.svg b/resources/images/design_c_concentric.svg
index 9861381b58..4e3123b6e5 100644
--- a/resources/images/design_c_concentric.svg
+++ b/resources/images/design_c_concentric.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_diameter.svg b/resources/images/design_c_diameter.svg
index 8e3766c4dc..76366179a1 100644
--- a/resources/images/design_c_diameter.svg
+++ b/resources/images/design_c_diameter.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_dist_x.svg b/resources/images/design_c_dist_x.svg
index e811a58df6..7455e3629a 100644
--- a/resources/images/design_c_dist_x.svg
+++ b/resources/images/design_c_dist_x.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_dist_y.svg b/resources/images/design_c_dist_y.svg
index 5aefec85ee..ecfb38a08f 100644
--- a/resources/images/design_c_dist_y.svg
+++ b/resources/images/design_c_dist_y.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_equal.svg b/resources/images/design_c_equal.svg
index f0657b74b0..4c3e2ebd3a 100644
--- a/resources/images/design_c_equal.svg
+++ b/resources/images/design_c_equal.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_equal_radius.svg b/resources/images/design_c_equal_radius.svg
index 4d7ccf2c5c..7ed9b3ca45 100644
--- a/resources/images/design_c_equal_radius.svg
+++ b/resources/images/design_c_equal_radius.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_fix.svg b/resources/images/design_c_fix.svg
index b64024286e..38d0ec5279 100644
--- a/resources/images/design_c_fix.svg
+++ b/resources/images/design_c_fix.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_horizontal.svg b/resources/images/design_c_horizontal.svg
index 9804a8ad82..9dbc1a4462 100644
--- a/resources/images/design_c_horizontal.svg
+++ b/resources/images/design_c_horizontal.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_midpoint.svg b/resources/images/design_c_midpoint.svg
index 6646a6cc1e..cae9ce48d4 100644
--- a/resources/images/design_c_midpoint.svg
+++ b/resources/images/design_c_midpoint.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_parallel.svg b/resources/images/design_c_parallel.svg
index cc78f9c452..7622362d2f 100644
--- a/resources/images/design_c_parallel.svg
+++ b/resources/images/design_c_parallel.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_perpendicular.svg b/resources/images/design_c_perpendicular.svg
index 9a67cebb2b..1072b1f673 100644
--- a/resources/images/design_c_perpendicular.svg
+++ b/resources/images/design_c_perpendicular.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_radius.svg b/resources/images/design_c_radius.svg
index 5e1f1cdadf..906268d3d7 100644
--- a/resources/images/design_c_radius.svg
+++ b/resources/images/design_c_radius.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_sym_h.svg b/resources/images/design_c_sym_h.svg
index 4e152787b1..6743e8a42d 100644
--- a/resources/images/design_c_sym_h.svg
+++ b/resources/images/design_c_sym_h.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_sym_v.svg b/resources/images/design_c_sym_v.svg
index f4cd99371a..5019a82fcc 100644
--- a/resources/images/design_c_sym_v.svg
+++ b/resources/images/design_c_sym_v.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_symmetric.svg b/resources/images/design_c_symmetric.svg
index 7a539be9a3..1f9b07e361 100644
--- a/resources/images/design_c_symmetric.svg
+++ b/resources/images/design_c_symmetric.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_tangent.svg b/resources/images/design_c_tangent.svg
index e82d23240d..66b320a14c 100644
--- a/resources/images/design_c_tangent.svg
+++ b/resources/images/design_c_tangent.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_c_vertical.svg b/resources/images/design_c_vertical.svg
index 98526e93d7..1399c97012 100644
--- a/resources/images/design_c_vertical.svg
+++ b/resources/images/design_c_vertical.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_chamfer.svg b/resources/images/design_chamfer.svg
index 861f7b819a..f70bd8c6f8 100644
--- a/resources/images/design_chamfer.svg
+++ b/resources/images/design_chamfer.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_check.svg b/resources/images/design_check.svg
index 82d5edd48e..d00643de54 100644
--- a/resources/images/design_check.svg
+++ b/resources/images/design_check.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_circle.svg b/resources/images/design_circle.svg
index edc74b13ea..7ace8bdab5 100644
--- a/resources/images/design_circle.svg
+++ b/resources/images/design_circle.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_circle2pt.svg b/resources/images/design_circle2pt.svg
index 844b0797eb..7d2716eb10 100644
--- a/resources/images/design_circle2pt.svg
+++ b/resources/images/design_circle2pt.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_circle3pt.svg b/resources/images/design_circle3pt.svg
index 6fdbab2474..3d82ae3788 100644
--- a/resources/images/design_circle3pt.svg
+++ b/resources/images/design_circle3pt.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_constrain.svg b/resources/images/design_constrain.svg
index cc9be6ca9b..b8ce387f81 100644
--- a/resources/images/design_constrain.svg
+++ b/resources/images/design_constrain.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_construction.svg b/resources/images/design_construction.svg
index 89d10f424d..22c4494181 100644
--- a/resources/images/design_construction.svg
+++ b/resources/images/design_construction.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_crect.svg b/resources/images/design_crect.svg
index 9e2ece0c24..e76ff04c8d 100644
--- a/resources/images/design_crect.svg
+++ b/resources/images/design_crect.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_cut.svg b/resources/images/design_cut.svg
index 3f68a7002e..0f130768bf 100644
--- a/resources/images/design_cut.svg
+++ b/resources/images/design_cut.svg
@@ -1,4 +1,4 @@
-
+
diff --git a/resources/images/design_delete.svg b/resources/images/design_delete.svg
index f5d5c6a1c7..4920eb5667 100644
--- a/resources/images/design_delete.svg
+++ b/resources/images/design_delete.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_dimension.svg b/resources/images/design_dimension.svg
index f456abb6fc..87fb20c95e 100644
--- a/resources/images/design_dimension.svg
+++ b/resources/images/design_dimension.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_draft.svg b/resources/images/design_draft.svg
index 2957584cb8..6e972c5505 100644
--- a/resources/images/design_draft.svg
+++ b/resources/images/design_draft.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_dressup.svg b/resources/images/design_dressup.svg
index cc2a09250e..cd3cbfc78f 100644
--- a/resources/images/design_dressup.svg
+++ b/resources/images/design_dressup.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_edit.svg b/resources/images/design_edit.svg
index 4de84fc390..b1dc0fabaa 100644
--- a/resources/images/design_edit.svg
+++ b/resources/images/design_edit.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_ellipse.svg b/resources/images/design_ellipse.svg
index 1f946eb5fa..22e486ad15 100644
--- a/resources/images/design_ellipse.svg
+++ b/resources/images/design_ellipse.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_ellipse_arc.svg b/resources/images/design_ellipse_arc.svg
index e9cd4aa66a..b5dea944a8 100644
--- a/resources/images/design_ellipse_arc.svg
+++ b/resources/images/design_ellipse_arc.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_extend.svg b/resources/images/design_extend.svg
index 9d4934fada..de0e9e98b3 100644
--- a/resources/images/design_extend.svg
+++ b/resources/images/design_extend.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_extrude.svg b/resources/images/design_extrude.svg
index 8137336fb2..84c8bd3c7c 100644
--- a/resources/images/design_extrude.svg
+++ b/resources/images/design_extrude.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_eye.svg b/resources/images/design_eye.svg
index 97296c7465..8499d684a6 100644
--- a/resources/images/design_eye.svg
+++ b/resources/images/design_eye.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_filletedge.svg b/resources/images/design_filletedge.svg
index 435d7c2cb9..622a2fc306 100644
--- a/resources/images/design_filletedge.svg
+++ b/resources/images/design_filletedge.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_hole.svg b/resources/images/design_hole.svg
index 35128ef51e..885462ed53 100644
--- a/resources/images/design_hole.svg
+++ b/resources/images/design_hole.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_line.svg b/resources/images/design_line.svg
index afb644ba3f..c7ead6b7f8 100644
--- a/resources/images/design_line.svg
+++ b/resources/images/design_line.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_loft.svg b/resources/images/design_loft.svg
index 4529b52554..574262ae18 100644
--- a/resources/images/design_loft.svg
+++ b/resources/images/design_loft.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_mirror.svg b/resources/images/design_mirror.svg
index 0a9642f8f3..66616a7636 100644
--- a/resources/images/design_mirror.svg
+++ b/resources/images/design_mirror.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_move.svg b/resources/images/design_move.svg
index dba4f4f4c2..3b4aaf1582 100644
--- a/resources/images/design_move.svg
+++ b/resources/images/design_move.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_movedown.svg b/resources/images/design_movedown.svg
index 89c442dc6e..35b8c97852 100644
--- a/resources/images/design_movedown.svg
+++ b/resources/images/design_movedown.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_moveup.svg b/resources/images/design_moveup.svg
index b2fe5ec127..eada91ed5f 100644
--- a/resources/images/design_moveup.svg
+++ b/resources/images/design_moveup.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_offset.svg b/resources/images/design_offset.svg
index 74758ad0ba..e71c86a5e9 100644
--- a/resources/images/design_offset.svg
+++ b/resources/images/design_offset.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_pattern.svg b/resources/images/design_pattern.svg
index c07708509b..57f8a49ddc 100644
--- a/resources/images/design_pattern.svg
+++ b/resources/images/design_pattern.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_plane.svg b/resources/images/design_plane.svg
index 8a3886158f..3e7017a625 100644
--- a/resources/images/design_plane.svg
+++ b/resources/images/design_plane.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_point.svg b/resources/images/design_point.svg
index dd5d750545..31bfeefa4f 100644
--- a/resources/images/design_point.svg
+++ b/resources/images/design_point.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_polararray.svg b/resources/images/design_polararray.svg
index 5e91d49f73..5f2c0f336b 100644
--- a/resources/images/design_polararray.svg
+++ b/resources/images/design_polararray.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_polygon.svg b/resources/images/design_polygon.svg
index de5cfd6f20..22ebdcbad9 100644
--- a/resources/images/design_polygon.svg
+++ b/resources/images/design_polygon.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_polyline.svg b/resources/images/design_polyline.svg
index 96538abea7..ea18b80090 100644
--- a/resources/images/design_polyline.svg
+++ b/resources/images/design_polyline.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_rect.svg b/resources/images/design_rect.svg
index 67b70cc6a1..adeb8feab4 100644
--- a/resources/images/design_rect.svg
+++ b/resources/images/design_rect.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_rect_oblique.svg b/resources/images/design_rect_oblique.svg
index 5d252ca677..4aecab5786 100644
--- a/resources/images/design_rect_oblique.svg
+++ b/resources/images/design_rect_oblique.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_rect_rounded.svg b/resources/images/design_rect_rounded.svg
index a8c7022c87..887c684a2b 100644
--- a/resources/images/design_rect_rounded.svg
+++ b/resources/images/design_rect_rounded.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_revolve.svg b/resources/images/design_revolve.svg
index 3a0e87f6f4..84d9e45eff 100644
--- a/resources/images/design_revolve.svg
+++ b/resources/images/design_revolve.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_rib.svg b/resources/images/design_rib.svg
index b481e38d23..50e54fe16d 100644
--- a/resources/images/design_rib.svg
+++ b/resources/images/design_rib.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_rotate.svg b/resources/images/design_rotate.svg
index 5a9b48ebfe..39b71ec06a 100644
--- a/resources/images/design_rotate.svg
+++ b/resources/images/design_rotate.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_scale.svg b/resources/images/design_scale.svg
index 5a53562ba3..38b470cc8d 100644
--- a/resources/images/design_scale.svg
+++ b/resources/images/design_scale.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_select.svg b/resources/images/design_select.svg
index 6f2e4f4ae9..aabd953c02 100644
--- a/resources/images/design_select.svg
+++ b/resources/images/design_select.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_shell.svg b/resources/images/design_shell.svg
index aa11b572e6..4a78b94a33 100644
--- a/resources/images/design_shell.svg
+++ b/resources/images/design_shell.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_sketch.svg b/resources/images/design_sketch.svg
index 102a9fe493..b4177317eb 100644
--- a/resources/images/design_sketch.svg
+++ b/resources/images/design_sketch.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_slot.svg b/resources/images/design_slot.svg
index f1c72f8c54..db7c497474 100644
--- a/resources/images/design_slot.svg
+++ b/resources/images/design_slot.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_slot_arc.svg b/resources/images/design_slot_arc.svg
index f17ab89144..63be75cb8b 100644
--- a/resources/images/design_slot_arc.svg
+++ b/resources/images/design_slot_arc.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_step.svg b/resources/images/design_step.svg
index 4bdf7811ff..a4a92b7b20 100644
--- a/resources/images/design_step.svg
+++ b/resources/images/design_step.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_surface.svg b/resources/images/design_surface.svg
index 1299dd1a97..5beec5ce48 100644
--- a/resources/images/design_surface.svg
+++ b/resources/images/design_surface.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_svg.svg b/resources/images/design_svg.svg
index 34a0685153..daa5f5129a 100644
--- a/resources/images/design_svg.svg
+++ b/resources/images/design_svg.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_sweep.svg b/resources/images/design_sweep.svg
index f1cc301b1e..876f23745a 100644
--- a/resources/images/design_sweep.svg
+++ b/resources/images/design_sweep.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_tangentarc.svg b/resources/images/design_tangentarc.svg
index 9e53da5c8e..bbcdd0ed5f 100644
--- a/resources/images/design_tangentarc.svg
+++ b/resources/images/design_tangentarc.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_text.svg b/resources/images/design_text.svg
index 78da223f62..cae04fb4c2 100644
--- a/resources/images/design_text.svg
+++ b/resources/images/design_text.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_thicken.svg b/resources/images/design_thicken.svg
index bfcd356cfa..b1c78a145c 100644
--- a/resources/images/design_thicken.svg
+++ b/resources/images/design_thicken.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_thread.svg b/resources/images/design_thread.svg
index 9bff4d778b..e27125f37a 100644
--- a/resources/images/design_thread.svg
+++ b/resources/images/design_thread.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_trim.svg b/resources/images/design_trim.svg
index 405e9fe3c4..f052f1fad7 100644
--- a/resources/images/design_trim.svg
+++ b/resources/images/design_trim.svg
@@ -1 +1 @@
-
+
diff --git a/src/slic3r/GUI/BBLTopbar.cpp b/src/slic3r/GUI/BBLTopbar.cpp
index eb6f4bcb00..cc72bd507b 100644
--- a/src/slic3r/GUI/BBLTopbar.cpp
+++ b/src/slic3r/GUI/BBLTopbar.cpp
@@ -12,6 +12,9 @@
#include "wxExtensions.hpp"
#include "Plater.hpp"
#include "MainFrame.hpp"
+#ifdef SLIC3R_CAD
+#include "CAD/DesignPanel.hpp"
+#endif
#include "WebViewDialog.hpp"
#include "PartPlate.hpp"
@@ -424,6 +427,9 @@ void BBLTopbar::OnSaveProject(wxAuiToolBarEvent& event)
void BBLTopbar::OnUndo(wxAuiToolBarEvent& event)
{
MainFrame* main_frame = dynamic_cast(m_frame);
+#ifdef SLIC3R_CAD
+ if (DesignPanel* design = main_frame->shown_design_panel()) { design->menu_undo_redo(false); return; }
+#endif
Plater* plater = main_frame->plater();
plater->undo();
}
@@ -431,6 +437,9 @@ void BBLTopbar::OnUndo(wxAuiToolBarEvent& event)
void BBLTopbar::OnRedo(wxAuiToolBarEvent& event)
{
MainFrame* main_frame = dynamic_cast(m_frame);
+#ifdef SLIC3R_CAD
+ if (DesignPanel* design = main_frame->shown_design_panel()) { design->menu_undo_redo(true); return; }
+#endif
Plater* plater = main_frame->plater();
plater->redo();
}
@@ -443,6 +452,13 @@ void BBLTopbar::EnableUndoRedoItems()
Refresh();
}
+void BBLTopbar::EnableUndoRedo(bool undo, bool redo)
+{
+ this->EnableTool(m_undo_item->GetId(), undo);
+ this->EnableTool(m_redo_item->GetId(), redo);
+ Refresh();
+}
+
void BBLTopbar::DisableUndoRedoItems()
{
this->EnableTool(m_undo_item->GetId(), false);
diff --git a/src/slic3r/GUI/BBLTopbar.hpp b/src/slic3r/GUI/BBLTopbar.hpp
index 2745490d00..c403708eaa 100644
--- a/src/slic3r/GUI/BBLTopbar.hpp
+++ b/src/slic3r/GUI/BBLTopbar.hpp
@@ -69,6 +69,7 @@ public:
void EnableUndoRedoItems();
void DisableUndoRedoItems();
+ void EnableUndoRedo(bool undo, bool redo);
void SaveNormalRect();
diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp
index 6c3516b619..95dc7600d2 100644
--- a/src/slic3r/GUI/CAD/DesignCanvas.cpp
+++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp
@@ -8,6 +8,7 @@
#include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "slic3r/GUI/3DScene.hpp"
@@ -48,12 +49,6 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D);
m_canvas->set_studio_lighting(true); // see GLCanvas3D::m_studio_lighting and phong.fs
- // CAD navigation, this canvas only: left-drag sweeps a selection rubber band, so orbit
- // moves to middle-drag and pan to right-drag. Design is a different modality from
- // Prepare/Preview and every CAD the user already knows maps the mouse this way; the other
- // tabs are untouched.
- m_canvas->set_cad_navigation(true);
-
m_canvas->enable_picking(false); // viewport face/edge picking is custom (TODO)
m_canvas->enable_moving(false);
m_canvas->enable_gizmos(false);
@@ -155,76 +150,11 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
if (m_inline_editor) m_inline_editor->commit();
};
- // Bottom-right viewport HUD: a borderless, non-focusable float label showing the active
- // tool's current values. Top-level (a child widget is hidden by the GL surface, same as
- // the inline editor). Fed every frame by the tool's on_readout; empty text hides it.
- // NON-FOCUSABLE IS THE LOAD-BEARING WORD, and a wxFrame is not: see the header. The chip
- // outlives the gesture that drew it, and while it held the X input focus every sketch
- // shortcut was swallowed until the user clicked the canvas. Same window class as the status
- // chip below for the same reason. Do not "simplify" it back to a wxFrame.
- {
- wxWindow* top = wxGetTopLevelParent(m_canvas_widget);
- m_hud = new wxPopupWindow(top, wxBORDER_NONE);
- m_hud->SetBackgroundColour(wxColour(28, 30, 34));
- m_hud_label = new wxStaticText(m_hud, wxID_ANY, wxEmptyString);
- m_hud_label->SetForegroundColour(wxColour(0x46, 0xE0, 0xC8)); // teal, reads on dark bed
- wxFont f = m_hud_label->GetFont(); f.MakeBold(); m_hud_label->SetFont(f);
- auto* hs = new wxBoxSizer(wxHORIZONTAL);
- hs->Add(m_hud_label, 0, wxALL, 6);
- m_hud->SetSizerAndFit(hs);
- m_hud->Hide();
- }
- m_sketch_tool.on_readout = [this](const std::string& s) { set_readout(s); };
-
- // Bottom-LEFT twin, carrying the status line. Top-level for the same reason as the readout
- // (a child widget is hidden by the GL surface) but a wxPopupWindow rather than a wxFrame,
- // because a popup cannot take keyboard focus. The readout gets away with a frame only
- // because it appears mid-gesture and the next input is the mouse; this one is up
- // permanently and is re-raised on every status change. As a frame it took the WM's focus
- // each time and the canvas stopped receiving keys at all — every sketch shortcut silently
- // dead, which reads as a broken tool. Do not "simplify" it back to a wxFrame.
- // Its colour is set per message — the panel decides whether a line is neutral or an error.
- {
- wxWindow* top = wxGetTopLevelParent(m_canvas_widget);
- m_status_hud = new wxPopupWindow(top, wxBORDER_NONE);
- m_status_hud->SetBackgroundColour(wxColour(28, 30, 34));
- m_status_hud_label = new wxStaticText(m_status_hud, wxID_ANY, wxEmptyString);
- auto* ss = new wxBoxSizer(wxHORIZONTAL);
- // The line never wraps — there is a whole window's width down here — so the chip is
- // ONE LINE tall. Spacers rather than a wxALL border because the two axes want
- // different numbers: roomy at the sides so it reads as a label, and just enough top
- // and bottom to clear the descenders. Zero vertical clips the glyphs; 6 (what the
- // readout chip uses) makes it look like a two-line box.
- ss->AddSpacer(10);
- ss->Add(m_status_hud_label, 0, wxTOP | wxBOTTOM, 3);
- ss->AddSpacer(10);
- m_status_hud->SetSizerAndFit(ss);
- m_status_hud->Hide();
- }
- // A floating frame does not follow its parent, so the anchor has to be recomputed whenever
- // the canvas changes size (that bind is below bind_event_handlers(), for the reason given
- // there). The readout HUD gets away without this because it is transient; the status line is
- // on screen almost permanently and would visibly detach.
- // ...and it does not follow the WINDOW either. A popup is override-redirect: the window
- // manager does not own it, so minimising the app leaves the chip sitting on the bare desktop
- // (seen on the rig: whole screen black, chip still there), and it stacks above other
- // applications rather than behind them. IsShownOnScreen does not catch this — an iconised
- // frame still counts as shown — so the frame has to say so itself. Deactivating the app is
- // the same case one step weaker: the chip belongs to a viewport the user is no longer
- // looking at. Showing it back is safe because a popup cannot take focus, so neither event
- // can be re-triggered by our own Show().
- // Members rather than lambdas so unbind_canvas_event_handlers() can Unbind them: these sit on
- // a frame that OUTLIVES this canvas, and a lambda cannot be unbound.
- if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) {
- top->Bind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this);
- top->Bind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this);
- // The anchor is an ABSOLUTE SCREEN position (ClientToScreen below), so moving the window
- // moves the canvas out from under a chip that stays where it was. Dragging the frame by
- // its title bar left the chip stranded mid-viewport until the next size, status or tab
- // change happened to re-place it. Nothing on the canvas fires for a move that does not
- // also resize, so it has to come from the frame.
- top->Bind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this);
- }
+ // The two viewport chips — the active tool's values bottom-right, the status line bottom-left
+ // — are drawn by the canvas itself, in the tool's ImGui pass: they go away with the canvas,
+ // the tab and the window, and never take the keyboard.
+ m_sketch_tool.on_readout = [this](const std::string& s) { set_readout(s); };
+ m_sketch_tool.render_overlays = [this] { render_hud(); };
refresh_bed();
@@ -236,16 +166,10 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// reverse order of binding, and GLCanvas3D swallows several events without skipping them —
// wxEVT_RIGHT_UP and wxEVT_ENTER_WINDOW in on_mouse, and wxEVT_SIZE in on_size, which is
// just `m_dirty = true;`. For those, whatever is bound LAST is the only handler that runs.
- // The context menu, the focus-follows-mouse and the status-chip re-anchor all depend on
- // running first, which is only true while this call stays ahead of them.
+ // The context menu and the focus-follows-mouse depend on running first, which is only true
+ // while this call stays ahead of them.
m_canvas->bind_event_handlers();
- // The status-chip re-anchor promised above, bound AFTER the call so it runs first — ahead of
- // it the handler never ran at all, leaving a stale anchor and wrap width after any resize
- // that did not also move the frame or change the text. Its e.Skip() is load-bearing the
- // other way: it falls through to on_size, which is what still marks the canvas dirty.
- m_canvas_widget->Bind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this);
-
// The Design GL canvas only receives key events (Esc to exit/enter Select, Ctrl+Z undo)
// while it holds keyboard focus. Clicking a side-panel button steals focus, after which
// Esc/Ctrl+Z silently do nothing until the viewport is clicked again. Restore focus
@@ -268,18 +192,6 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// Both are idempotent, and neither destroys anything: the destructor still owns that.
void DesignCanvas::unbind_canvas_event_handlers()
{
- if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) {
- top->Unbind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this);
- top->Unbind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this);
- top->Unbind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this);
- }
- // Before the popups go down, or a resize still in flight re-places and re-shows the chip.
- if (m_canvas_widget)
- m_canvas_widget->Unbind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this);
- // A popup is override-redirect: it does not go down with the frame, so one left showing sits
- // on the bare desktop for however long the teardown takes.
- show_status_hud(false);
- if (m_hud) m_hud->Hide();
if (m_canvas) m_canvas->unbind_event_handlers();
}
@@ -290,7 +202,6 @@ void DesignCanvas::reset_canvas_volumes()
DesignCanvas::~DesignCanvas()
{
- if (m_hud) m_hud->Destroy();
delete m_canvas;
delete m_canvas_widget;
}
@@ -346,14 +257,22 @@ void DesignCanvas::request_repaint()
}
}
+// ImGui's display size is shared by every canvas and only refreshed when a canvas sees its own
+// size change, so the canvas taking over must re-announce its size (Plater does the same between
+// Prepare and Preview). Otherwise the overlays anchored to it, the FPS counter first, are laid
+// out for the other canvas.
void DesignCanvas::enter_viewport()
{
if (!m_camera_swapped) swap_camera();
+ if (m_canvas) { m_canvas->reset_old_size(); m_canvas->set_as_dirty(); }
}
void DesignCanvas::leave_viewport()
{
if (m_camera_swapped) swap_camera();
+ if (Plater* plater = wxGetApp().plater())
+ if (GLCanvas3D* editor = plater->get_current_canvas3D())
+ editor->reset_old_size();
}
void DesignCanvas::swap_camera()
@@ -1188,124 +1107,59 @@ double DesignCanvas::model_mid_z() const
void DesignCanvas::set_readout(const std::string& text)
{
- if (!m_hud || !m_hud_label || !m_canvas_widget) return;
- if (text == m_hud_last) return; // only touch the WM on a real change
+ if (text == m_hud_last) return;
m_hud_last = text;
- if (text.empty()) { m_hud->Hide(); return; }
- m_hud_label->SetLabel(wxString::FromUTF8(text));
- place_readout_hud();
-}
-
-void DesignCanvas::place_readout_hud()
-{
- if (!m_hud || !m_hud_label || !m_canvas_widget) return;
- if (m_hud_last.empty() || !m_canvas_widget->IsShownOnScreen()) { m_hud->Hide(); return; }
- m_hud->Fit();
- // Anchor to the canvas's bottom-right corner with a small margin (screen coords).
- const wxSize cs = m_canvas_widget->GetClientSize();
- const wxSize hs = m_hud->GetSize();
- const wxPoint br = m_canvas_widget->ClientToScreen(
- wxPoint(cs.GetWidth() - hs.GetWidth() - 12, cs.GetHeight() - hs.GetHeight() - 12));
- if (!m_hud->IsShown()) m_hud->Show(); // Show before Move (GTK ignores pre-map Move)
- m_hud->Move(br);
- m_hud->Raise();
-}
-
-// A popup is override-redirect: the window manager does not own it, so an iconised or
-// deactivated app would leave the chip sitting on the bare desktop. The status chip already
-// had to answer this; now that the readout is a popup too, it answers it the same way.
-void DesignCanvas::show_readout_hud(bool on)
-{
- if (!m_hud) return;
- if (on) place_readout_hud();
- else m_hud->Hide();
+ if (m_canvas) m_canvas->set_as_dirty(); // drawn by the next frame (the tool feeds this from one)
}
// Clear of the view cube and the two round view buttons, which own the bottom-left corner.
-// Shared by the placement and by the wrap width, which have to agree or the chip wraps to a
-// width it is then not given.
-static constexpr int kStatusHudLeftInsetDip = 190;
+static constexpr float kStatusHudLeftInset = 190.f;
void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
{
- if (!m_status_hud || !m_status_hud_label || !m_canvas_widget) return;
if (text == m_status_hud_last && colour == m_status_hud_colour) return;
m_status_hud_last = text;
m_status_hud_colour = colour;
- if (text.IsEmpty()) { m_status_hud->Hide(); return; }
- m_status_hud_label->SetForegroundColour(colour);
- apply_status_label();
- place_status_hud();
+ request_repaint();
}
-// SetLabel + Wrap + Fit, in that order and always together. Moving the status out of the panel
-// removed the clipping of 8cc but not the underlying problem: the chip is a top-level
-// popup that Fit()s to its text, so a long sentence simply grew past the right edge of the canvas
-// and hung over the window. Wrapping to the room actually available is what makes the earlier
-// promise — "a sentence can be a sentence" — true at every window width, including the charter's
-// 1366 reach. Wrap() rewrites the label it is given, so it must follow a fresh SetLabel every
-// time; that is the whole reason this is one function instead of three call sites.
-void DesignCanvas::apply_status_label()
+void DesignCanvas::render_hud()
{
- if (!m_status_hud || !m_status_hud_label || !m_canvas_widget) return;
- m_status_hud_label->SetLabel(m_status_hud_last);
- const int avail = m_canvas_widget->GetClientSize().GetWidth()
- - m_canvas_widget->FromDIP(kStatusHudLeftInsetDip)
- - m_canvas_widget->FromDIP(24);
- if (avail > m_canvas_widget->FromDIP(120)) // a uselessly narrow canvas: leave it unwrapped
- m_status_hud_label->Wrap(avail);
- m_status_hud->Fit();
-}
-
-void DesignCanvas::place_status_hud()
-{
- if (!m_status_hud || !m_canvas_widget || m_status_hud_last.IsEmpty()) return;
- // The canvas has a client size even while its page is hidden, and it is not the size the
- // page will have when shown — anchoring against it put the chip up on the tab bar, where it
- // then stayed until the next status change moved it. Nothing to anchor to: stay down.
- if (!m_canvas_widget->IsShownOnScreen()) { m_status_hud->Hide(); return; }
- const wxSize cs = m_canvas_widget->GetClientSize();
- // Re-wrap first: this also runs on resize, and a chip wrapped for the old width either
- // overhangs a narrowed canvas or wastes a widened one.
- apply_status_label();
- const wxSize hs = m_status_hud->GetSize();
- const int kLeftInset = m_canvas_widget->FromDIP(kStatusHudLeftInsetDip);
- const wxPoint bl = m_canvas_widget->ClientToScreen(
- wxPoint(kLeftInset, cs.GetHeight() - hs.GetHeight() - 12));
- // No Raise() and no focus juggling: a popup neither takes focus nor falls behind. This was
- // caught with ORCA_CAD_KEYTRACE — shift+S logged a line, the following R logged nothing, and
- // the only thing between them was the first status update showing this window.
- if (!m_status_hud->IsShown()) m_status_hud->Show(); // Show before Move (GTK ignores pre-map Move)
- m_status_hud->Move(bl);
-}
-
-void DesignCanvas::on_frame_iconize(wxIconizeEvent& e)
-{
- show_status_hud(!e.IsIconized());
- show_readout_hud(!e.IsIconized());
- e.Skip();
-}
-
-void DesignCanvas::on_frame_activate(wxActivateEvent& e)
-{
- show_status_hud(e.GetActive());
- show_readout_hud(e.GetActive());
- e.Skip();
-}
-
-// wxEvent& so one handler serves both events that invalidate the anchor: the frame moving out
-// from under the chip, and the canvas resizing under it.
-void DesignCanvas::on_status_hud_reanchor(wxEvent& e)
-{
- place_status_hud();
- e.Skip();
-}
-
-void DesignCanvas::show_status_hud(bool on)
-{
- if (!m_status_hud) return;
- if (on) place_status_hud(); // re-anchors first: the page may have been resized while away
- else m_status_hud->Hide();
+ if (m_hud_last.empty() && m_status_hud_last.IsEmpty()) return;
+ ImGuiWrapper& imgui = *wxGetApp().imgui();
+ const ImVec2 ds = ImGui::GetIO().DisplaySize;
+ const float em = imgui.get_style_scaling(); // follows the font, so the DPI
+ const float margin = 12.f * em;
+ const int flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
+ ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
+ ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav;
+ auto chip = [&](const char* id, const std::string& text, float x, float pivot_x, float wrap,
+ const ImVec4* colour) {
+ ImGuiWrapper::push_common_window_style(m_canvas->get_scale());
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f * em, 4.f * em));
+ imgui.set_next_window_pos(x, ds.y - margin, ImGuiCond_Always, pivot_x, 1.f);
+ imgui.begin(std::string(id), flags);
+ if (wrap > 0.f) ImGui::PushTextWrapPos(wrap);
+ if (colour) ImGui::PushStyleColor(ImGuiCol_Text, *colour);
+ ImGui::TextUnformatted(text.c_str());
+ if (colour) ImGui::PopStyleColor();
+ if (wrap > 0.f) ImGui::PopTextWrapPos();
+ imgui.end();
+ ImGui::PopStyleVar();
+ ImGuiWrapper::pop_common_window_style();
+ };
+ if (!m_status_hud_last.IsEmpty()) {
+ // A sentence can be a sentence: it wraps to the room left of the readout chip.
+ const float left = kStatusHudLeftInset * em;
+ const ImVec4 col = m_status_hud_colour.IsOk()
+ ? ImVec4(m_status_hud_colour.Red() / 255.f, m_status_hud_colour.Green() / 255.f,
+ m_status_hud_colour.Blue() / 255.f, 1.f)
+ : ImVec4();
+ chip("##design_status", m_status_hud_last.ToUTF8().data(), left, 0.f,
+ std::max(ds.x * 0.6f - left, 120.f * em), m_status_hud_colour.IsOk() ? &col : nullptr);
+ }
+ if (!m_hud_last.empty())
+ chip("##design_readout", m_hud_last, ds.x - margin, 1.f, 0.f, &ImGuiWrapper::COL_ORCA);
}
void DesignCanvas::set_body_highlight(bool on)
diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp
index 50b2cab46c..3f4c53c54a 100644
--- a/src/slic3r/GUI/CAD/DesignCanvas.hpp
+++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp
@@ -2,7 +2,6 @@
#define slic3r_DesignCanvas_hpp_
#include
-#include
#include
#include
@@ -227,10 +226,6 @@ public:
// the panel clips it at ~73 characters with no warning (8cc), the viewport's
// bottom margin has the whole window width to spare. Empty text hides it.
void set_status_text(const wxString& text, const wxColour& colour);
- // Take the status line down / bring it back when the Design page leaves and re-enters view.
- // A popup is a TOP-LEVEL window: hiding the page it belongs to does not hide it. Keeps the
- // text, so coming back needs no re-selection.
- void show_status_hud(bool on);
void set_operand_bodies(int target_body, int tool_body); // -1,-1 clears
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
void set_xray_focus(int body); // >=0: fade+lock out every other body (CoordSys picking)
@@ -398,36 +393,15 @@ private:
bool m_section_on{false};
std::unique_ptr m_inline_editor; // floating in-canvas value editor
- // Bottom-right viewport HUD: a borderless float label over the GL canvas showing the
- // active tool's current values (fed by the tool's on_readout). Empty text hides it.
- // A wxPopupWindow for the SAME reason as the status chip below, and it was a wxFrame until
- // the reason was measured rather than assumed: "it appears mid-gesture and the next input is
- // the mouse" is false. The chip keeps the last value on screen AFTER the gesture ends, and a
- // frame holds the X input focus once it has it — so the next keystroke went to a 119x31
- // window that has no use for it. Measured on :10: focus on the chip, `r` produced no
- // CHAR_HOOK line at all; one bare canvas click moved focus back and the same key armed the
- // tool. That is every sketch shortcut dead after every dimensioned entity.
- wxPopupWindow* m_hud{nullptr};
- wxStaticText* m_hud_label{nullptr};
+ // The viewport chips, drawn in the tool's ImGui pass (render_hud): bottom-right the active
+ // tool's current values (fed by the tool's on_readout), bottom-left the status line written by
+ // DesignPanel. Empty text draws nothing. They were top-level popups once; a popup does not
+ // follow its frame, so it floated over other applications and outlived the tab.
std::string m_hud_last;
- void set_readout(const std::string& text);
- void place_readout_hud(); // anchor + show, using m_hud_last
- void show_readout_hud(bool on); // iconise/deactivate: a popup would float on the desktop
-
- // Bottom-LEFT viewport HUD: the selection / tool status line, written by DesignPanel.
- // A wxPopupWindow, NOT the wxFrame the readout HUD uses: a frame accepts keyboard focus,
- // and this one is on screen permanently and re-raised on every status change, so it stole
- // the keyboard from the canvas and killed every sketch shortcut in the tab.
- wxPopupWindow* m_status_hud{nullptr};
- wxStaticText* m_status_hud_label{nullptr};
wxString m_status_hud_last;
- wxColour m_status_hud_colour;
- void place_status_hud(); // re-anchors to the canvas corner (also on resize)
- void apply_status_label(); // SetLabel + Wrap to the canvas width + Fit, always together
- // On the top-level frame, which outlives this canvas — members so they can be unbound.
- void on_frame_iconize(wxIconizeEvent& e);
- void on_frame_activate(wxActivateEvent& e);
- void on_status_hud_reanchor(wxEvent& e); // frame wxEVT_MOVE and canvas wxEVT_SIZE
+ wxColour m_status_hud_colour; // wxNullColour: the overlay's own text colour
+ void set_readout(const std::string& text);
+ void render_hud();
std::function m_on_sketch_commit;
std::function&,
const std::vector&,
diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp
index 77cd696c5c..cbe5d7b3a5 100644
--- a/src/slic3r/GUI/CAD/DesignPanel.cpp
+++ b/src/slic3r/GUI/CAD/DesignPanel.cpp
@@ -11,6 +11,7 @@
#include
#include
#include // the offer/atlas join check reports on the log
+#include
#include
#include
@@ -37,7 +38,6 @@
#include
#include
#include // wxWindowDisabler, wxMilliSleep
-#include // wxMessageBox
#include
#include
@@ -53,6 +53,9 @@
#include "slic3r/GUI/Widgets/Button.hpp" // Orca-styled Button (ButtonStyle/ButtonType) — same look as Prepare
#include "slic3r/GUI/Widgets/CheckBox.hpp" // Orca teal check (label lives in the row's left column)
#include "slic3r/GUI/Widgets/ComboBox.hpp" // Orca dropdown — replaces wxChoice in every Design card
+#include "slic3r/GUI/Widgets/TextInput.hpp"
+#include "slic3r/GUI/Widgets/DialogButtons.hpp"
+#include "slic3r/GUI/MsgDialog.hpp"
#include "slic3r/GUI/Widgets/StaticBox.hpp" // Prepare's rounded white card frame around each tool dialog
#include "libslic3r/CAD/SketchImport.hpp" // text_to_regions / svg_to_regions
#include "libslic3r/CAD/ThreadStandards.hpp" // ISO metric / Unified imperial thread tables
@@ -149,21 +152,42 @@ static bool en_parse(const wxString& text, double& out)
return t.ToCDouble(&out);
}
-// Design-tab chrome tokens. The dark branch returns the EXACT legacy values so the
-// (correct) dark theme stays byte-identical; the light branch maps each onto Orca's
-// light surface so the ribbon/sidebar follow the app theme instead of staying black.
+// Design-tab chrome tokens, one {light, dark} pair each. Controls are coloured from these at
+// construction; on a theme switch on_sys_color_changed() walks the panel and moves every colour
+// that is one theme's token onto the other theme's, so the tab follows the app theme live.
+struct DpToken { wxColour light, dark; };
+enum DpTok { TokRibbonBg, TokRibbonHover, TokPanelBg, TokSecText, TokCtlText, TokItemText, TokItemDim,
+ TokBorder, TokCount };
+static const DpToken kDpTokens[TokCount] = {
+ { wxColour(0xEC,0xEC,0xEE), wxColour(0x36,0x36,0x3C) }, // ribbon
+ { wxColour(0xD7,0xD7,0xDB), wxColour(0x4D,0x4D,0x54) }, // ribbon hover
+ { wxColour(0xFB,0xFB,0xFD), wxColour(0x2D,0x2D,0x30) }, // sidebar / lists
+ { wxColour(0x66,0x66,0x68), wxColour(0x81,0x81,0x83) }, // secondary text
+ { wxColour(0x35,0x35,0x37), wxColour(0xC8,0xC8,0xC8) }, // control text
+ { wxColour(0x2C,0x2C,0x2E), wxColour(0xE0,0xE0,0xE0) }, // list item text
+ { wxColour(0xA0,0xA0,0xA2), wxColour(0x80,0x80,0x80) }, // dimmed list item
+ // Prepare's control-outline grey, sampled from its sidebar: #DBDBDB on light, #4A4A51 on the
+ // #2D2D31 dark panel. Every framed thing in Design uses this so the tab matches.
+ { wxColour(0xDB,0xDB,0xDB), wxColour(0x4A,0x4A,0x51) },
+};
static bool dp_dark() { return wxGetApp().dark_mode(); }
-static wxColour dp_ribbon_bg() { return dp_dark() ? wxColour(0x36,0x36,0x3C) : wxColour(0xEC,0xEC,0xEE); }
-static wxColour dp_ribbon_hover() { return dp_dark() ? wxColour(0x4D,0x4D,0x54) : wxColour(0xD7,0xD7,0xDB); }
-static wxColour dp_panel_bg() { return dp_dark() ? wxColour(0x2D,0x2D,0x30) : wxColour(0xFB,0xFB,0xFD); }
-static wxColour dp_sec_text() { return dp_dark() ? wxColour(0x81,0x81,0x83) : wxColour(0x66,0x66,0x68); }
-static wxColour dp_ctl_text() { return dp_dark() ? wxColour(0xC8,0xC8,0xC8) : wxColour(0x35,0x35,0x37); }
-static wxColour dp_item_text() { return dp_dark() ? wxColour(0xE0,0xE0,0xE0) : wxColour(0x2C,0x2C,0x2E); }
-static wxColour dp_item_dim() { return dp_dark() ? wxColour(0x80,0x80,0x80) : wxColour(0xA0,0xA0,0xA2); }
+static wxColour dp_tok(DpTok t) { return dp_dark() ? kDpTokens[t].dark : kDpTokens[t].light; }
+static wxColour dp_ribbon_bg() { return dp_tok(TokRibbonBg); }
+static wxColour dp_ribbon_hover() { return dp_tok(TokRibbonHover); }
+static wxColour dp_panel_bg() { return dp_tok(TokPanelBg); }
+static wxColour dp_sec_text() { return dp_tok(TokSecText); }
+static wxColour dp_ctl_text() { return dp_tok(TokCtlText); }
+static wxColour dp_item_text() { return dp_tok(TokItemText); }
+static wxColour dp_item_dim() { return dp_tok(TokItemDim); }
-// Prepare's control-outline grey, sampled from its sidebar: #4A4A51 on the #2D2D31 dark
-// panel, #DBDBDB on light. Every framed thing in Design uses this so the tab matches.
-static wxColour dp_border_col() { return dp_dark() ? wxColour(0x4A,0x4A,0x51) : wxColour(0xDB,0xDB,0xDB); }
+// The other theme's value of a token colour, or `c` itself when it is no token.
+static wxColour dp_retheme(const wxColour& c, bool to_dark)
+{
+ for (const DpToken& t : kDpTokens)
+ if (c == (to_dark ? t.light : t.dark))
+ return to_dark ? t.dark : t.light;
+ return c;
+}
// A tool card: Prepare's rounded white-bordered panel (Plater.cpp's panel_printer_preset
// idiom — radius 8, #EEEEEE border, green on hover). Every card's controls are parented
@@ -172,7 +196,9 @@ static StaticBox* make_card(wxWindow* parent)
{
auto* c = new StaticBox(parent);
c->SetCornerRadius(8);
- c->SetBorderColorNormal(dp_border_col()); // no hover accent: the frame is not clickable
+ // The light literal in a StateColor is mapped to the dark one at paint time, so the frame
+ // follows a theme switch by itself. No hover accent: the frame is not clickable.
+ c->SetBorderColor(StateColor(kDpTokens[TokBorder].light));
return c;
}
@@ -186,8 +212,8 @@ static wxSpinCtrlDouble* make_spin(wxWindow* parent, double val,
{
auto* box = new StaticBox(parent);
box->SetCornerRadius(4);
- box->SetBorderColorNormal(dp_border_col());
- auto* s = new wxSpinCtrlDouble(box, wxID_ANY, "", wxDefaultPosition, wxSize(90, -1),
+ box->SetBorderColor(StateColor(kDpTokens[TokBorder].light));
+ auto* s = new wxSpinCtrlDouble(box, wxID_ANY, "", wxDefaultPosition, parent->FromDIP(wxSize(90, -1)),
wxSP_ARROW_KEYS | wxBORDER_NONE);
s->SetRange(mn, mx);
s->SetDigits(2);
@@ -280,17 +306,23 @@ static SketchPlane face_plane_inward(const TopoDS_Face& face)
DesignPanel::DesignPanel(wxWindow* parent)
: wxPanel(parent, wxID_ANY)
{
+ // The tab is built on its first show; where that time goes is logged, per phase, because it
+ // varies by an order of magnitude between machines.
+ wxStopWatch build_clock;
+ long build_mark = 0;
+ auto build_phase = [&build_clock, &build_mark](const char* phase) {
+ const long now = build_clock.Time();
+ BOOST_LOG_TRIVIAL(info) << "Design tab build: " << phase << " " << now - build_mark << " ms";
+ build_mark = now;
+ };
// Left column: a slim feature-tree + docked tool-dialog column. All form
// controls are parented to m_form so it can scroll independently of the
// live GL viewport. The tool buttons live in the top toolbar (built below).
m_form = new wxScrolledWindow(this, wxID_ANY);
- // The sidebar/panel never carried an explicit background, so in light theme it
- // inherited the dark window colour and stayed black. Paint it on the light surface;
- // dark is left untouched (it already reads correctly via inheritance).
- if (!dp_dark()) {
- SetBackgroundColour(dp_panel_bg());
- m_form->SetBackgroundColour(dp_panel_bg());
- }
+ // Explicit token background in both themes, so a theme switch can move it (an inherited
+ // colour stays whatever the theme was when the panel was built).
+ SetBackgroundColour(dp_panel_bg());
+ m_form->SetBackgroundColour(dp_panel_bg());
auto* root = new wxBoxSizer(wxVERTICAL);
{
@@ -313,22 +345,20 @@ DesignPanel::DesignPanel(wxWindow* parent)
// #4D4D54); light maps onto the app's light chrome so the strip follows the theme.
m_toolbar->SetBackgroundColour(dp_ribbon_bg());
- const wxColour tb_bg = dp_ribbon_bg();
- const wxColour tb_hover = dp_ribbon_hover();
- auto icon_btn = [this, tb_bg, tb_hover](const char* icon, const wxString& tip) {
+ auto icon_btn = [this](const char* icon, const wxString& tip) {
// Prepare's main toolbar: 40 px icon cell, 4 px gap (GLToolbar::Default_Icons_Size
// and set_gap_size(4)) -> 44 px pitch. Match it exactly.
- auto* b = new ScalableButton(m_toolbar, wxID_ANY, icon, "", wxSize(40, 40),
+ auto* b = new ScalableButton(m_toolbar, wxID_ANY, icon, "", FromDIP(wxSize(40, 40)),
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 34);
b->SetToolTip(tip);
- b->SetBackgroundColour(tb_bg);
+ b->SetBackgroundColour(dp_ribbon_bg());
m_tool_btns.push_back(b);
// Hover affordance, honouring the active-tool teal state.
- b->Bind(wxEVT_ENTER_WINDOW, [this, b, tb_hover](wxMouseEvent& e) {
- b->SetBackgroundColour(b == m_active_tool_btn ? wxColour(0x52, 0xC7, 0xB8) : tb_hover);
+ b->Bind(wxEVT_ENTER_WINDOW, [this, b](wxMouseEvent& e) {
+ b->SetBackgroundColour(b == m_active_tool_btn ? wxColour(0x52, 0xC7, 0xB8) : dp_ribbon_hover());
b->Refresh(); e.Skip(); });
- b->Bind(wxEVT_LEAVE_WINDOW, [this, b, tb_bg](wxMouseEvent& e) {
- b->SetBackgroundColour(b == m_active_tool_btn ? wxColour(0x00, 0x96, 0x88) : tb_bg);
+ b->Bind(wxEVT_LEAVE_WINDOW, [this, b](wxMouseEvent& e) {
+ b->SetBackgroundColour(b == m_active_tool_btn ? wxColour(0x00, 0x96, 0x88) : dp_ribbon_bg());
b->Refresh(); e.Skip(); });
// Mark this tool active (teal) on press — a separate event from the
// button's command handler, so it never swallows the click action.
@@ -346,7 +376,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
};
auto add_sep = [this](wxSizer* row) {
row->AddSpacer(5);
- row->Add(new wxStaticLine(m_toolbar, wxID_ANY, wxDefaultPosition, wxSize(1, 22), wxLI_VERTICAL),
+ row->Add(new wxStaticLine(m_toolbar, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(1, 22)), wxLI_VERTICAL),
0, wxALIGN_CENTER_VERTICAL);
row->AddSpacer(5);
};
@@ -497,24 +527,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(show ? _L("Bed shown") : _L("Bed hidden"));
};
- // Shared flyout glyph tint (used by BOTH the feature and sketch toolbars). Re-tint each
- // design_* glyph to the DropDown's resolved TEXT colour so it reads on the popup in either
- // theme: text_color is 0x363636, which darkModeColorFor() maps to a light tone in dark mode
- // (the popup bg is darkModeColorFor(white) = dark) and leaves dark in light mode. The alpha
- // (the glyph shape) is preserved; only RGB is replaced.
- // ponytail: wxBitmap(img) drops the HiDPI scale factor (no scale ctor before wx 3.1.6); the
- // deploy target runs at scale 1.0, so this is exact there.
- const wxColour drop_icon_col = StateColor::darkModeColorFor(wxColour(0x36, 0x36, 0x36));
- auto tint = [](wxBitmap bmp, const wxColour& c) -> wxBitmap {
- if (!bmp.IsOk()) return bmp;
- wxImage img = bmp.ConvertToImage();
- if (!img.HasAlpha()) img.InitAlpha();
- const int w = img.GetWidth(), h = img.GetHeight();
- for (int y = 0; y < h; ++y)
- for (int x = 0; x < w; ++x)
- img.SetRGB(x, y, c.Red(), c.Green(), c.Blue());
- return wxBitmap(img);
- };
+ // 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.
// --- Feature group: Sketch / Extrude / Fillet-Chamfer / Hole / Thread / Constrain
m_tb_feature = new wxBoxSizer(wxHORIZONTAL);
@@ -572,13 +586,18 @@ DesignPanel::DesignPanel(wxWindow* parent)
DropDown::Item it;
it.text = v.tip;
it.tip = v.hint;
- it.icon = tint(create_scaled_bitmap(v.icon, m_form, 18), drop_icon_col);
+ it.icon = create_scaled_bitmap(v.icon, m_form, 18);
fo->items.push_back(it);
fo->actions.push_back(std::move(v.action));
fo->icon_names.emplace_back(v.icon);
}
fo->btn = b;
fo->drop.Create(b);
+ m_icon_refresh.push_back([this, fp = fo.get()] {
+ for (size_t i = 0; i < fp->items.size(); ++i)
+ fp->items[i].icon = create_scaled_bitmap(fp->icon_names[i], m_form, 18);
+ fp->drop.Invalidate(true);
+ });
fo->drop.SetUseContentWidth(true, false);
fo->drop.Invalidate(true);
FeatFlyout* fp = fo.get();
@@ -597,7 +616,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
fp->drop.SetUseContentWidth(false, false);
fp->drop.SetUseContentWidth(true, false);
wxPoint pos = b->ClientToScreen(wxPoint(0, -6));
- fp->drop.Position(pos, wxSize(0, b->GetSize().y + 12));
+ fp->drop.Position(pos, wxSize(0, b->GetSize().y + b->FromDIP(12)));
fp->drop.Popup();
});
m_flyout_keepalive.push_back(fo);
@@ -1224,7 +1243,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
DropDown::Item it;
it.text = v.tip;
it.tip = v.hint;
- it.icon = tint(create_scaled_bitmap(v.icon, m_form, 18), drop_icon_col);
+ it.icon = create_scaled_bitmap(v.icon, m_form, 18);
fo->items.push_back(it);
fo->modes.push_back(v.mode);
fo->hints.push_back(v.hint);
@@ -1232,6 +1251,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
fo->btn = b;
fo->drop.Create(b);
+ m_icon_refresh.push_back([this, fp = fo.get()] {
+ for (size_t i = 0; i < fp->items.size(); ++i)
+ fp->items[i].icon = create_scaled_bitmap(fp->icon_names[i], m_form, 18);
+ fp->drop.Invalidate(true);
+ });
fo->drop.SetUseContentWidth(true, false);
fo->drop.Invalidate(true);
ToolFlyout* fp = fo.get();
@@ -1253,7 +1277,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
fp->drop.SetUseContentWidth(false, false);
fp->drop.SetUseContentWidth(true, false);
wxPoint pos = b->ClientToScreen(wxPoint(0, -6));
- fp->drop.Position(pos, wxSize(0, b->GetSize().y + 12));
+ fp->drop.Position(pos, wxSize(0, b->GetSize().y + b->FromDIP(12)));
fp->drop.Popup();
});
m_flyout_keepalive.push_back(fo);
@@ -1496,67 +1520,37 @@ DesignPanel::DesignPanel(wxWindow* parent)
// active (update_action_bar). Replaces the 13 per-card buttons + sketch Finish + Done.
m_tb_action = new wxBoxSizer(wxHORIZONTAL);
{
- auto* ok = new wxButton(m_toolbar, wxID_ANY, _L("✓ Confirm"));
- ok->SetForegroundColour(*wxWHITE);
- ok->SetBackgroundColour(wxColour(0x00, 0x96, 0x88)); // Orca teal accent
+ auto* ok = new ::Button(m_toolbar, _L("Confirm"));
+ ok->SetStyle(ButtonStyle::Confirm, ButtonType::Window);
ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { tool_confirm(); });
m_confirm_btns.push_back(ok); // refresh_preview greys this on an invalid candidate
- auto* no = new wxButton(m_toolbar, wxID_ANY, _L("✗ Cancel"));
+ auto* no = new ::Button(m_toolbar, _L("Cancel"));
+ no->SetStyle(ButtonStyle::Regular, ButtonType::Window);
no->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { tool_cancel(); });
m_tb_action->Add(ok, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
m_tb_action->Add(no, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 10);
}
- // Persistent Undo/Redo group: always visible (not mode-gated like the tool groups), so
- // history is reachable from Feature, Sketch and Constrain alike. These are momentary
- // actions, so — unlike icon_btn — they are NOT registered in m_tool_btns and never take
- // the teal active-tool highlight. They route to the SAME do_undo_redo as the keyboard
- // Ctrl+Z / Ctrl+Shift+Z path, and are greyed by update_undo_redo_buttons().
- m_tb_history = new wxBoxSizer(wxHORIZONTAL);
- {
- auto hist_btn = [this, tb_bg, tb_hover](const char* icon, const wxString& tip) {
- auto* b = new ScalableButton(m_toolbar, wxID_ANY, icon, "", wxSize(40, 40),
- wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 34);
- b->SetToolTip(tip);
- b->SetBackgroundColour(tb_bg);
- b->Bind(wxEVT_ENTER_WINDOW, [b, tb_hover](wxMouseEvent& e) {
- if (b->IsEnabled()) { b->SetBackgroundColour(tb_hover); b->Refresh(); } e.Skip(); });
- b->Bind(wxEVT_LEAVE_WINDOW, [b, tb_bg](wxMouseEvent& e) {
- b->SetBackgroundColour(tb_bg); b->Refresh(); e.Skip(); });
- return b;
- };
- m_btn_undo = hist_btn("menu_undo", _L("Undo (Ctrl+Z)"));
- m_btn_undo->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { do_undo_redo(false); });
- m_btn_redo = hist_btn("menu_redo", _L("Redo (Ctrl+Shift+Z)"));
- m_btn_redo->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { do_undo_redo(true); });
- m_btn_undo->Enable(false); // nothing to undo/redo on a fresh document
- m_btn_redo->Enable(false);
- m_tb_history->Add(m_btn_undo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
- m_tb_history->Add(m_btn_redo, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
- }
-
- // Document actions, left of Undo/Redo and always visible (not mode-gated like the tools).
- // Same styling as the history pair: momentary actions, never the teal active-tool state.
+ // Document actions, always visible (not mode-gated like the tools): momentary actions,
+ // never the teal active-tool state.
m_tb_doc = new wxBoxSizer(wxHORIZONTAL);
- const wxColour tb_glyph_col = StateColor::darkModeColorFor(wxColour(0x36, 0x36, 0x36));
- const wxColour tb_commit_col(0x00, 0x96, 0x88); // Orca Confirm accent
{
- // Some Orca glyphs (toolbar_add_plate, toolbar_flatten) are drawn for a light toolbar and
- // come out the same tone as this dark one — Commit was effectively invisible. Re-tint
- // those: Commit in the teal accent it carries as the tab's primary action, the rest in
- // the same grey the other toolbar glyphs resolve to.
- auto doc_btn = [this, tb_bg, tb_hover, &tint](const char* icon, const wxString& tip,
- const wxColour* glyph = nullptr) {
- auto* b = new ScalableButton(m_toolbar, wxID_ANY, icon, "", wxSize(40, 40),
+ // Some Orca glyphs (toolbar_flatten) are drawn for Prepare's light GL toolbar and come out
+ // the same tone as a dark ribbon. Those ship a "_dark" twin, picked per theme here (and
+ // again on a theme switch) the way GLToolbar picks it.
+ auto doc_btn = [this](const char* icon, const wxString& tip, bool has_dark_twin = false) {
+ const std::string name(icon);
+ auto themed = [name, has_dark_twin] { return has_dark_twin && dp_dark() ? name + "_dark" : name; };
+ auto* b = new ScalableButton(m_toolbar, wxID_ANY, themed(), "", FromDIP(wxSize(40, 40)),
wxDefaultPosition, wxBU_EXACTFIT | wxBORDER_NONE, false, 34);
- if (glyph != nullptr)
- b->SetBitmap(tint(create_scaled_bitmap(icon, m_toolbar, 42), *glyph));
+ if (has_dark_twin)
+ m_icon_refresh.push_back([b, themed] { b->SetBitmap_(themed()); });
b->SetToolTip(tip);
- b->SetBackgroundColour(tb_bg);
- b->Bind(wxEVT_ENTER_WINDOW, [b, tb_hover](wxMouseEvent& e) {
- if (b->IsEnabled()) { b->SetBackgroundColour(tb_hover); b->Refresh(); } e.Skip(); });
- b->Bind(wxEVT_LEAVE_WINDOW, [b, tb_bg](wxMouseEvent& e) {
- b->SetBackgroundColour(tb_bg); b->Refresh(); e.Skip(); });
+ b->SetBackgroundColour(dp_ribbon_bg());
+ b->Bind(wxEVT_ENTER_WINDOW, [b](wxMouseEvent& e) {
+ if (b->IsEnabled()) { b->SetBackgroundColour(dp_ribbon_hover()); b->Refresh(); } e.Skip(); });
+ b->Bind(wxEVT_LEAVE_WINDOW, [b](wxMouseEvent& e) {
+ b->SetBackgroundColour(dp_ribbon_bg()); b->Refresh(); e.Skip(); });
return b;
};
auto add_doc = [this](ScalableButton* b) {
@@ -1591,12 +1585,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
// These act on bodies / the view, so they ride in the feature group, in the slots
// the user assigned them (9, 11bis, 16).
auto* b_place = doc_btn("toolbar_flatten", _L("Place on Face (F) — lay the picked face on the bed"),
- &tb_glyph_col);
+ true);
b_place->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { place_on_face(); });
tb_slot["place"].push_back(b_place);
auto* b_section = doc_btn("split_parts", _L("Section View — hide part of the model to see inside. "
- "PageUp/PageDown move the plane; Delete removes it."));
+ "PageUp/PageDown move the plane; Delete removes it."),
+ true);
b_section->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { toggle_section_view(); });
tb_slot["section"].push_back(b_section);
@@ -1608,7 +1603,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Commit is the tab's primary action and sits far right, next to Confirm/Cancel.
m_tb_commit = new wxBoxSizer(wxHORIZONTAL);
auto* b_commit = doc_btn("toolbar_add_plate", _L("Commit to Plate — send the solid to Prepare"),
- &tb_commit_col);
+ true);
b_commit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_commit(); });
m_tb_commit->Add(b_commit, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
}
@@ -1637,8 +1632,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
tbrow->AddSpacer(8);
tbrow->Add(m_tb_doc, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
add_sep(tbrow);
- tbrow->Add(m_tb_history, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
- add_sep(tbrow);
tbrow->Add(m_tb_feature, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
tbrow->Add(m_tb_sketch, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
tbrow->Add(m_tb_relations, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
@@ -1647,15 +1640,18 @@ DesignPanel::DesignPanel(wxWindow* parent)
tbrow->Add(m_tb_action, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
tbrow->AddSpacer(8);
m_toolbar->SetSizer(tbrow);
+ build_phase("toolbar");
// Apply the body gates once now: an empty document is exactly the state the bug was reported
// in, and feed_bodies() has not run yet on a fresh tab.
update_body_gates();
// Onshape-style dialog-card header: feature icon + bold title. out receives
// the title control so open_tool() can retitle it per feature.
- auto card_header = [](wxWindow* card, const char* icon, const wxString& title, wxStaticText*& out) -> wxSizer* {
+ auto card_header = [this](wxWindow* card, const char* icon, const wxString& title, wxStaticText*& out) -> wxSizer* {
auto* h = new wxBoxSizer(wxHORIZONTAL);
auto* ic = new wxStaticBitmap(card, wxID_ANY, create_scaled_bitmap(icon, card, 18));
+ m_icon_refresh.push_back([ic, card, name = std::string(icon)] {
+ ic->SetBitmap(create_scaled_bitmap(name, card, 18)); });
out = new wxStaticText(card, wxID_ANY, title);
out->SetFont(Label::Head_14); // Orca shared HarmonyOS card-title font
h->Add(ic, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
@@ -2191,8 +2187,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
plform->Add(m_plane_tilt_axis, 0, wxEXPAND);
// Contextual reference picks: arm a target, then click a solid face/edge in the canvas.
- auto pick_row = [&](const wxString& label, wxButton*& btn, wxStaticText*& lbl, PlanePick target) {
- btn = new wxButton(m_cards, wxID_ANY, label);
+ auto pick_row = [&](const wxString& label, ::Button*& btn, wxStaticText*& lbl, PlanePick target) {
+ btn = new ::Button(m_cards, label);
+ btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
lbl = new wxStaticText(m_cards, wxID_ANY, _L("(none)"));
btn->Bind(wxEVT_BUTTON, [this, target](wxCommandEvent&) { arm_plane_pick(target); });
plform->Add(btn);
@@ -2220,7 +2217,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_box_loft->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
m_box_loft->Add(new wxStaticText(m_cards, wxID_ANY, _L("Profiles (check 2+, in order):")),
0, wxLEFT | wxRIGHT | wxTOP, 12);
- m_loft_list = new wxCheckListBox(m_cards, wxID_ANY, wxDefaultPosition, wxSize(-1, 120));
+ m_loft_list = new wxCheckListBox(m_cards, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(-1, 120)));
m_loft_list->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent&) { refresh_preview(); });
m_box_loft->Add(m_loft_list, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
{
@@ -2292,7 +2289,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_box_surf_loft->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
m_box_surf_loft->Add(new wxStaticText(m_cards, wxID_ANY, _L("Profiles (check 2+, in order):")),
0, wxLEFT | wxRIGHT | wxTOP, 12);
- m_surf_loft_list = new wxCheckListBox(m_cards, wxID_ANY, wxDefaultPosition, wxSize(-1, 120));
+ m_surf_loft_list = new wxCheckListBox(m_cards, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(-1, 120)));
m_surf_loft_list->Bind(wxEVT_CHECKLISTBOX, [this](wxCommandEvent&) { refresh_preview(); });
m_box_surf_loft->Add(m_surf_loft_list, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
m_surf_loft_ruled = new CheckBox(m_cards);
@@ -2532,7 +2529,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_rib_sketch->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Sketch")), 0, wxALIGN_CENTER_VERTICAL);
rform->Add(m_rib_sketch, 0, wxEXPAND);
- m_rib_entity = new wxSpinCtrl(m_cards, wxID_ANY, "", wxDefaultPosition, wxSize(90, -1),
+ m_rib_entity = new wxSpinCtrl(m_cards, wxID_ANY, "", wxDefaultPosition, FromDIP(wxSize(90, -1)),
wxSP_ARROW_KEYS | wxBORDER_SIMPLE);
m_rib_entity->SetRange(0, 999);
m_rib_entity->SetValue(0);
@@ -2589,7 +2586,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_del_face_body->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
dform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Body")), 0, wxALIGN_CENTER_VERTICAL);
dform->Add(m_del_face_body, 0, wxEXPAND);
- m_del_face_add_btn = new wxButton(m_cards, wxID_ANY, _L("Add picked face"));
+ m_del_face_add_btn = new ::Button(m_cards, _L("Add picked face"));
+ m_del_face_add_btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
m_del_face_add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
// Say why nothing happened. Clicking with no face picked used to be a silent no-op,
// which is indistinguishable from the button being broken.
@@ -2686,8 +2684,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
auto* axform = two_col_form();
- auto ax_pick = [&](const wxString& label, wxButton*& btn, wxStaticText*& lbl, AxisPick target) {
- btn = new wxButton(m_cards, wxID_ANY, label);
+ auto ax_pick = [&](const wxString& label, ::Button*& btn, wxStaticText*& lbl, AxisPick target) {
+ btn = new ::Button(m_cards, label);
+ btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
lbl = new wxStaticText(m_cards, wxID_ANY, _L("(none)"));
btn->Bind(wxEVT_BUTTON, [this, target](wxCommandEvent&) { arm_axis_pick(target); });
axform->Add(btn);
@@ -2766,8 +2765,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
csform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Z")), 0, wxALIGN_CENTER_VERTICAL);
csform->Add(spin_frame(m_cs_z), 0, wxEXPAND);
- auto cs_pick = [&](const wxString& label, wxButton*& btn, wxStaticText*& lbl, CoordSysPick target) {
- btn = new wxButton(m_cards, wxID_ANY, label);
+ auto cs_pick = [&](const wxString& label, ::Button*& btn, wxStaticText*& lbl, CoordSysPick target) {
+ btn = new ::Button(m_cards, label);
+ btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
lbl = new wxStaticText(m_cards, wxID_ANY, _L("(none)"));
btn->Bind(wxEVT_BUTTON, [this, target](wxCommandEvent&) { arm_coordsys_pick(target); });
csform->Add(btn);
@@ -2776,7 +2776,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
cs_pick(_L("Pick Face"), m_cs_pick_face, m_cs_face_lbl, CoordSysPick::Face);
// ponytail: edge pick for CoordSys with rotation-direction hint
- m_cs_pick_edge = new wxButton(m_cards, wxID_ANY, _L("Edge (sets in-plane direction)"));
+ m_cs_pick_edge = new ::Button(m_cards, _L("Edge (sets in-plane direction)"));
+ m_cs_pick_edge->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
m_cs_edge_lbl = new wxStaticText(m_cards, wxID_ANY, _L("(none)"));
m_cs_pick_edge->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { arm_coordsys_pick(CoordSysPick::Edge); });
csform->Add(m_cs_pick_edge);
@@ -2920,8 +2921,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_box_expr->Add(eform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
auto* brow = new wxBoxSizer(wxHORIZONTAL);
- m_expr_set_btn = new wxButton(m_cards, wxID_ANY, _L("Set"), wxDefaultPosition, wxSize(50, 24));
- m_expr_clear_btn = new wxButton(m_cards, wxID_ANY, _L("Clear"), wxDefaultPosition, wxSize(50, 24));
+ m_expr_set_btn = new ::Button(m_cards, _L("Set"));
+ m_expr_clear_btn = new ::Button(m_cards, _L("Clear"));
+ m_expr_set_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Parameter);
+ m_expr_clear_btn->SetStyle(ButtonStyle::Regular, ButtonType::Parameter);
m_expr_set_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_set_expr(); });
m_expr_clear_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_clear_expr(); });
brow->Add(m_expr_set_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
@@ -2948,7 +2951,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// formats per the user locale (comma) with no clean override, so we own the
// formatting here to guarantee international '.' decimals.
m_value_input = new wxTextCtrl(m_cards, wxID_ANY, "", wxDefaultPosition,
- wxSize(90, -1), wxTE_PROCESS_ENTER);
+ FromDIP(wxSize(90, -1)), wxTE_PROCESS_ENTER);
m_value_input->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { confirm_value(); });
vrow->Add(new wxStaticText(m_cards, wxID_ANY, _L("Value")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
vrow->Add(m_value_input, 0, wxALIGN_CENTER_VERTICAL);
@@ -3016,19 +3019,27 @@ 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, wxSize(-1, 64),
+ 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);
- if (!dp_dark()) m_tree->SetBackgroundColour(dp_panel_bg());
+ m_tree->SetBackgroundColour(dp_panel_bg());
// Per-feature-type icons (indices match tree_icon_for): sketch/extrude/dressup/hole/thread.
- m_tree_images = new wxImageList(16, 16);
- m_tree_images->Add(create_scaled_bitmap("design_sketch", nullptr, 16)); // 0 Sketch
- m_tree_images->Add(create_scaled_bitmap("design_extrude", nullptr, 16)); // 1 Extrude
- m_tree_images->Add(create_scaled_bitmap("design_dressup", nullptr, 16)); // 2 Fillet/Chamfer
- m_tree_images->Add(create_scaled_bitmap("design_hole", nullptr, 16)); // 3 Hole
- m_tree_images->Add(create_scaled_bitmap("design_thread", nullptr, 16)); // 4 Thread
- m_tree_images->Add(create_scaled_bitmap("design_shell", nullptr, 16)); // 5 Shell
- m_tree->AssignImageList(m_tree_images);
+ // 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 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
@@ -3164,7 +3175,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
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, "", wxSize(24, 24),
+ 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;
@@ -3202,7 +3213,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
trow->Add(down, 0, wxALIGN_CENTER_VERTICAL);
// Interference check sits after a rule: it reports, it does not edit the recipe.
trow->AddSpacer(8);
- trow->Add(new wxStaticLine(m_tree_box, wxID_ANY, wxDefaultPosition, wxSize(1, 22), wxLI_VERTICAL),
+ trow->Add(new wxStaticLine(m_tree_box, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(1, 22)), wxLI_VERTICAL),
0, wxALIGN_CENTER_VERTICAL);
trow->AddSpacer(8);
trow->Add(m_btn_interfere, 0, wxALIGN_CENTER_VERTICAL);
@@ -3230,7 +3241,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// policy — the card only gives them a home next to the rows they act on.
{
auto body_btn = [this](const char* icon, const wxString& tip) {
- auto* b = new ScalableButton(m_parts_box, wxID_ANY, icon, "", wxSize(24, 24),
+ 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;
@@ -3268,10 +3279,10 @@ 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, wxSize(-1, 48),
+ 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);
- if (!dp_dark()) m_parts->SetBackgroundColour(dp_panel_bg());
+ 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
// tree rebuild — until then an empty box would sit under the header.
@@ -3358,9 +3369,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
var_hdr->Add(card_header(m_var_box, "design_constrain", _L("Variables"), var_hdr_title), 0,
wxALIGN_CENTER_VERTICAL);
var_hdr->AddStretchSpacer();
- m_btn_add_var = new wxButton(m_var_box, wxID_ANY, _L("+"), wxDefaultPosition, wxSize(30, 24));
- m_btn_edit_var = new wxButton(m_var_box, wxID_ANY, _L("Edit"), wxDefaultPosition, wxSize(50, 24));
- m_btn_del_var = new wxButton(m_var_box, wxID_ANY, _L("Del"), wxDefaultPosition, wxSize(42, 24));
+ // 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->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(); });
@@ -3375,7 +3393,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_var_list = new wxListCtrl(m_var_box, wxID_ANY, wxDefaultPosition,
wxSize(-1, FromDIP(64)), wxLC_REPORT | wxLC_SINGLE_SEL);
- if (!dp_dark()) m_var_list->SetBackgroundColour(dp_panel_bg());
+ m_var_list->SetBackgroundColour(dp_panel_bg());
m_var_list->AppendColumn(_L("Name"), wxLIST_FORMAT_LEFT, FromDIP(90));
m_var_list->AppendColumn(_L("Expression"), wxLIST_FORMAT_LEFT, FromDIP(120));
var_inner->Add(m_var_list, 0, wxEXPAND | wxALL, FromDIP(SidebarProps::ContentMargin()));
@@ -3388,7 +3406,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_status = new wxStaticText(m_form, wxID_ANY, "");
m_status->Hide(); // storage only — the line is drawn over the viewport, see set_status()
- m_status_default_fg = m_status->GetForegroundColour(); // capture BEFORE any caller writes
root->Add(m_status, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12);
// DoF / constraint-state readout (P3). Dedicated line so it never clobbers the
@@ -3460,11 +3477,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_form->FitInside();
m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out
- m_form->SetMinSize(wxSize(264, -1));
+ m_form->SetMinSize(FromDIP(wxSize(264, -1)));
+ build_phase("sidebar and tool cards");
// Right column: a small view toolbar over the live 3D viewport that mirrors
// the CadDocument body.
m_viewport = new DesignCanvas(this);
+ build_phase("3D canvas");
m_viewport->set_on_sketch_commit([this](const SketchProfile& prof, const SketchPlane& plane) {
m_doc.checkpoint(); // undo boundary: committing a sketch
@@ -4346,6 +4365,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
set_ui_mode(UiMode::Feature);
+ build_phase("wiring");
+ BOOST_LOG_TRIVIAL(info) << "Design tab build: total " << build_clock.Time() << " ms";
}
void DesignPanel::set_active_tool_btn(ScalableButton* b)
@@ -4928,7 +4949,8 @@ void DesignPanel::on_import_mesh()
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?"),
int(mesh.its.indices.size()));
- if (wxMessageBox(q, _L("Large mesh"), wxYES_NO | wxICON_WARNING, this) != wxYES)
+ MessageDialog dlg(this, q, _L("Large mesh"), wxYES_NO | wxICON_WARNING);
+ if (dlg.ShowModal() != wxID_YES)
return;
}
@@ -6288,14 +6310,11 @@ void DesignPanel::set_status(StatusKind kind, const wxString& body)
// 8cc), which silently length-limited every hint in the tab. The viewport's bottom
// margin has the whole window width, so a sentence can be a sentence.
if (m_viewport != nullptr) {
- // wxNullColour means "no opinion", and the dark default text colour is nearly invisible
- // on the dark HUD; only a colour a caller actually chose (the error red, the plane-pick
- // green) is carried over. Compared against the colour the label was CREATED with —
- // comparing against the parent's foreground instead reported "chosen" for every line,
- // and the neutral text came out the panel's grey.
- const wxColour fg = m_status->GetForegroundColour();
- m_viewport->set_status_text(text, fg != m_status_default_fg ? fg
- : wxColour(0xDD, 0xE1, 0xE6));
+ // Only a colour a caller actually chose (the error red, the plane-pick green) is carried
+ // over; a neutral line (wxNullColour above) takes the viewport overlay's own text colour,
+ // which follows the theme.
+ m_viewport->set_status_text(text, m_status->UseForegroundColour() ? m_status->GetForegroundColour()
+ : wxNullColour);
}
}
@@ -7155,30 +7174,80 @@ wxString DesignPanel::idle_hint() const
// Nothing else in the panel needs to know: the popup keeps its text and comes straight back.
void DesignPanel::on_tab_hidden()
{
- if (m_viewport) {
- m_viewport->show_status_hud(false);
+ if (m_viewport)
m_viewport->leave_viewport(); // hand the shared camera back to the editor tabs
- }
}
void DesignPanel::on_tab_shown()
{
- if (m_viewport) {
- m_viewport->show_status_hud(true); // ...and back on the way in
+ if (m_viewport)
m_viewport->enter_viewport(); // borrow the shared camera; on_tab_hidden gives it back
- }
if (m_active == Tool::None && m_doc.display_mesh.its.indices.empty())
set_status(idle_hint()); // first paint: the tab has never been edited
+ wxStopWatch show_clock;
if (m_viewport) m_viewport->refresh_bed();
hydrate_from_model();
update_reference_planes(); // entering the Design tab: show the XY/XZ/YZ planes if no object yet
+ if (show_clock.Time() > 100) // a slow first show is what users report; the usual one is not news
+ BOOST_LOG_TRIVIAL(info) << "Design tab shown: bed, project recipe and planes in " << show_clock.Time() << " ms";
sync_sidebar_width(); // keep the panel as wide as Prepare's so the canvas edge doesn't jump
if (m_viewport) m_viewport->force_repaint(); // the page was just re-shown: paint it for real
}
+void DesignPanel::refresh_icons()
+{
+ // Plain ScalableButtons re-read their icon for the current scale and theme, and the Orca
+ // widgets re-measure themselves...
+ std::function walk = [&walk](wxWindow* w) {
+ if (auto* b = dynamic_cast(w))
+ b->msw_rescale();
+ else if (auto* b = dynamic_cast<::Button*>(w))
+ b->Rescale();
+ else if (auto* c = dynamic_cast<::CheckBox*>(w))
+ c->Rescale();
+ else if (auto* c = dynamic_cast<::ComboBox*>(w))
+ c->Rescale();
+ for (wxWindow* child : w->GetChildren())
+ walk(child);
+ };
+ walk(this);
+ // ...then the icons that are not a button face (and the buttons that swap to a "_dark" twin).
+ for (auto& refresh : m_icon_refresh)
+ refresh();
+}
+
+void DesignPanel::msw_rescale()
+{
+ refresh_icons();
+ if (m_viewport) m_viewport->Refresh();
+ Layout();
+}
+
+void DesignPanel::on_sys_color_changed()
+{
+ // Every chrome colour here was set from a DpToken in the theme that was current at the time.
+ // Move each one, background and text, onto the same token in the new theme; any other colour
+ // (the teal accents, the status colours) is the same in both and stays.
+ const bool to_dark = dp_dark();
+ std::function walk = [&walk, to_dark](wxWindow* w) {
+ if (w->UseBackgroundColour())
+ w->SetBackgroundColour(dp_retheme(w->GetBackgroundColour(), to_dark));
+ if (w->UseForegroundColour())
+ w->SetForegroundColour(dp_retheme(w->GetForegroundColour(), to_dark));
+ for (wxWindow* child : w->GetChildren())
+ walk(child);
+ };
+ walk(this);
+ // The native controls (trees, lists, spins) take the app's own dark pass.
+ wxGetApp().UpdateDarkUIWin(this);
+ refresh_icons();
+ refresh_tree(); // the rows carry their own text colours
+ Refresh();
+}
+
// Rehydrate the parametric model from a freshly loaded project (the 3MF carried the recipe in
// Metadata/orca_cad.bin). Only into an EMPTY document, so an active design is never clobbered
// when the user toggles back to the tab. Called on tab show and by the control socket — which
@@ -7671,7 +7740,7 @@ void DesignPanel::after_tree_edit(bool ok)
void DesignPanel::on_new_design()
{
if (m_doc.features.empty() && m_doc.bodies.empty()) { set_status_ok(); return; }
- wxMessageDialog dlg(this,
+ MessageDialog dlg(this,
_L("Erase all features and bodies and start a new design? This cannot be undone."),
_L("New Design"), wxYES_NO | wxICON_EXCLAMATION);
if (dlg.ShowModal() != wxID_YES) return;
@@ -8188,14 +8257,14 @@ void DesignPanel::rebuild_constraint_list()
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 wxButton(m_cards, wxID_ANY, wxString::FromUTF8("✗"),
- wxDefaultPosition, wxSize(26, -1));
+ 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); });
// Clickable label: selecting it highlights the referenced entities.
- auto* lbl = new wxButton(m_cards, wxID_ANY, constraint_label(cons[i]),
- wxDefaultPosition, wxDefaultSize, wxBU_LEFT | wxBORDER_NONE);
- lbl->Bind(wxEVT_BUTTON, [this, i](wxCommandEvent&) { highlight_constraint_entities(i); });
+ auto* lbl = new wxStaticText(m_cards, wxID_ANY, constraint_label(cons[i]));
+ lbl->SetCursor(wxCursor(wxCURSOR_HAND));
+ lbl->Bind(wxEVT_LEFT_UP, [this, i](wxMouseEvent&) { highlight_constraint_entities(i); });
row->Add(del, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 6);
row->Add(lbl, 1, wxALIGN_CENTER_VERTICAL);
m_constraint_rows->Add(row, 0, wxEXPAND | wxTOP, 2);
@@ -10792,7 +10861,7 @@ void DesignPanel::refresh_preview()
default: ready = _L("Sketch ready"); break;
}
set_status(StatusKind::Ok, ready);
- for (wxButton* b : m_confirm_btns) if (b) b->Enable(true);
+ for (::Button* b : m_confirm_btns) if (b) b->Enable(true);
m_status->Refresh();
update_datum_gizmo(); // Plane card: show/refresh the in-canvas resize handles
update_helix_gizmo(); // Helix card: draw the live curve + drag handles (no solid ghost)
@@ -10835,7 +10904,7 @@ void DesignPanel::refresh_preview()
set_status(StatusKind::Error, wxString::Format(_L("Invalid: %s"), kernel_error_text(err)));
}
}
- for (wxButton* b : m_confirm_btns) if (b) b->Enable(ok);
+ for (::Button* b : m_confirm_btns) if (b) b->Enable(ok);
m_status->Refresh();
return;
}
@@ -10881,7 +10950,7 @@ void DesignPanel::refresh_preview()
}
// Onshape parity: a broken candidate cannot be committed. Grey the active dialog's
// Confirm so the user sees the gate before clicking; the red status says why.
- for (wxButton* b : m_confirm_btns)
+ for (::Button* b : m_confirm_btns)
if (b != nullptr) b->Enable(ok);
// Fillet/Chamfer/Draft: once the target edge/face yields a valid result, show ONLY the
// preview (hide the base bodies) so the user sees the finished shape, not the old solid
@@ -11450,7 +11519,7 @@ void DesignPanel::tool_cancel()
// the user to press the very button they had just pressed: a sketch could be kept but
// never discarded.
if (m_viewport && m_viewport->live_sketch_has_work()) {
- wxMessageDialog dlg(this,
+ RichMessageDialog dlg(this,
_L("Discard this sketch and everything drawn in it?"),
_L("Discard sketch"),
wxYES_NO | wxNO_DEFAULT | wxICON_EXCLAMATION);
@@ -11481,9 +11550,9 @@ bool DesignPanel::confirm_enabled() const
return (m_viewport && m_viewport->moving_body())
|| m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain;
if (m_active == Tool::Insert) return true;
- for (wxButton* b : m_confirm_btns)
+ for (::Button* b : m_confirm_btns)
if (b != nullptr && b->IsShownOnScreen()) return b->IsEnabled();
- for (wxButton* b : m_confirm_btns)
+ for (::Button* b : m_confirm_btns)
if (b != nullptr) return b->IsEnabled();
return true;
}
@@ -11582,18 +11651,10 @@ bool DesignPanel::menu_can_undo_redo(bool redo) const
void DesignPanel::update_undo_redo_buttons()
{
- // Grey Undo/Redo to mirror exactly what do_undo_redo will do: it acts only in Feature
- // mode with no tool/dialog open (otherwise Esc is the way out), so reflect that gate here
- // as well as the document's available history.
- if (m_btn_undo == nullptr || m_btn_redo == nullptr) return;
- if (m_ui_mode == UiMode::Sketch && m_viewport && m_viewport->is_sketching()) {
- m_btn_undo->Enable(m_viewport->can_undo_sketch_entity());
- m_btn_redo->Enable(m_viewport->can_redo_sketch_entity());
- return;
- }
- const bool gated = (m_ui_mode != UiMode::Feature) || (m_active != Tool::None);
- m_btn_undo->Enable(!gated && m_doc.can_undo());
- m_btn_redo->Enable(!gated && m_doc.can_redo());
+ // The tab has no Undo/Redo of its own: the app's (the top bar, Ctrl+Z, Edit) drive this
+ // history while the tab is shown, greyed to exactly what do_undo_redo will do.
+ if (MainFrame* frame = wxGetApp().mainframe; frame != nullptr && IsShownOnScreen())
+ frame->set_undo_redo_enabled(menu_can_undo_redo(false), menu_can_undo_redo(true));
}
void DesignPanel::update_action_bar()
@@ -11674,19 +11735,78 @@ void DesignPanel::refresh_variables()
}
}
+// A design variable's name and expression, in Orca's dialog style: one dialog for both fields
+// rather than two bare text prompts. Editing an existing variable keeps its name fixed.
+class DesignVariableDialog : public DPIDialog
+{
+public:
+ DesignVariableDialog(wxWindow* parent, const wxString& title, const wxString& name,
+ const wxString& expr, bool name_editable)
+ : DPIDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
+ {
+ SetBackgroundColour(*wxWHITE);
+ SetFont(Label::Body_14);
+ auto* grid = new wxFlexGridSizer(2, FromDIP(8), FromDIP(12));
+ grid->AddGrowableCol(1);
+ auto field = [this, grid](const wxString& label, const wxString& value) {
+ grid->Add(new wxStaticText(this, wxID_ANY, label), 0, wxALIGN_CENTER_VERTICAL);
+ auto* in = new ::TextInput(this, value, "", "", wxDefaultPosition, wxSize(FromDIP(240), -1),
+ wxTE_PROCESS_ENTER);
+ in->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { EndModal(wxID_OK); });
+ grid->Add(in, 1, wxEXPAND);
+ return in;
+ };
+ m_name = field(_L("Name"), name);
+ m_expr = field(_L("Expression"), expr);
+ m_name->Enable(name_editable);
+
+ m_buttons = new DialogButtons(this, {"OK", "Cancel"});
+ m_buttons->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); });
+ m_buttons->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
+
+ auto* sizer = new wxBoxSizer(wxVERTICAL);
+ sizer->Add(grid, 1, wxEXPAND | wxALL, FromDIP(16));
+ sizer->Add(m_buttons, 0, wxEXPAND);
+ SetSizerAndFit(sizer);
+ CenterOnParent();
+ (name_editable ? m_name : m_expr)->GetTextCtrl()->SetFocus();
+ wxGetApp().UpdateDlgDarkUI(this);
+ }
+
+ wxString name() const { return trimmed(m_name); }
+ wxString expression() const { return trimmed(m_expr); }
+
+protected:
+ void on_dpi_changed(const wxRect&) override
+ {
+ m_name->Rescale();
+ m_expr->Rescale();
+ GetSizer()->SetSizeHints(this);
+ Refresh();
+ }
+
+private:
+ static wxString trimmed(const ::TextInput* in)
+ {
+ wxString v = in->GetTextCtrl()->GetValue();
+ return v.Trim(true).Trim(false);
+ }
+ ::TextInput* m_name{nullptr};
+ ::TextInput* m_expr{nullptr};
+ DialogButtons* m_buttons{nullptr};
+};
+
void DesignPanel::on_add_variable()
{
- wxString name = ::wxGetTextFromUser(_L("Variable name:"), _L("Add Variable"), "", this);
- if (name.IsEmpty()) return;
- name.Trim(true).Trim(false);
+ DesignVariableDialog dlg(this, _L("Add Variable"), "", "0", true);
+ if (dlg.ShowModal() != wxID_OK) return;
+ const wxString name = dlg.name();
+ const wxString expr = dlg.expression();
+ if (name.IsEmpty() || expr.IsEmpty()) return;
if (name.Contains(' ')) {
set_status(StatusKind::Error, _L("Variable name must not contain spaces"));
return;
}
- wxString expr = ::wxGetTextFromUser(
- wxString::Format(_L("Expression for '%s':"), name),
- _L("Add Variable"), "0", this);
- if (expr.IsEmpty()) return;
const std::string name_str = name.ToUTF8().data();
const std::string expr_str = expr.ToUTF8().data();
@@ -11712,9 +11832,10 @@ void DesignPanel::on_edit_variable()
}
const std::string name_str = m_var_list->GetItemText(sel, 0).ToUTF8().data();
const std::string old_expr = m_var_list->GetItemText(sel, 1).ToUTF8().data();
- wxString expr = ::wxGetTextFromUser(
- wxString::Format(_L("Expression for '%s':"), m_var_list->GetItemText(sel, 0)),
- _L("Edit Variable"), wxString::FromUTF8(old_expr), this);
+ DesignVariableDialog dlg(this, _L("Edit Variable"), m_var_list->GetItemText(sel, 0),
+ wxString::FromUTF8(old_expr), false);
+ if (dlg.ShowModal() != wxID_OK) return;
+ const wxString expr = dlg.expression();
if (expr.IsEmpty()) return;
const std::string expr_str = expr.ToUTF8().data();
diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp
index b52942ff73..32ac394465 100644
--- a/src/slic3r/GUI/CAD/DesignPanel.hpp
+++ b/src/slic3r/GUI/CAD/DesignPanel.hpp
@@ -36,7 +36,7 @@ class wxSizer;
class wxBoxSizer;
class wxTextCtrl;
class wxListCtrl;
-class wxButton;
+class Button;
class wxPanel;
class ScalableButton;
@@ -55,6 +55,10 @@ public:
explicit DesignPanel(wxWindow* parent);
void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated
void on_tab_hidden(); // another tab took over: take the viewport status line down with us
+ // From MainFrame, like the other pages: re-rasterise the icons at the new scale, and on a theme
+ // switch also move every token colour onto the new theme.
+ void msw_rescale();
+ void on_sys_color_changed();
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void clear_document(); // New Project / Open Project: drop the document with the project
@@ -296,6 +300,8 @@ public:
// and the toolbar buttons.
void menu_undo_redo(bool redo) { do_undo_redo(redo); }
bool menu_can_undo_redo(bool redo) const;
+ // Grey the app's Undo/Redo to this tab's history and gate (shown tab only).
+ void update_undo_redo_buttons();
private:
// The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown.
SketchPlane hole_plane() const;
@@ -500,19 +506,16 @@ private:
// Unified Confirm/Cancel action bar (right end of the ribbon). Shown whenever any
// tool or mode is active; the single confirm/cancel surface for the whole tab.
wxSizer* m_tb_action{nullptr};
- // Persistent Undo/Redo group at the left of the ribbon — always visible, independent
- // of the mode-gated tool groups. The buttons are greyed per the document history and
- // the do_undo_redo gate (see update_undo_redo_buttons).
- wxSizer* m_tb_history{nullptr};
- ScalableButton* m_btn_undo{nullptr};
- ScalableButton* m_btn_redo{nullptr};
- void update_undo_redo_buttons(); // enable/disable Undo/Redo from can_undo/can_redo + gate
// All tool buttons, for the active-tool teal highlight (Onshape-style).
std::vector m_tool_btns;
ScalableButton* m_active_tool_btn{nullptr};
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> 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.
+ std::vector> m_icon_refresh;
+ void refresh_icons();
wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle
wxSpinCtrlDouble* m_move_dx{nullptr}; // Move/Rotate card: world translation
wxSpinCtrlDouble* m_move_dy{nullptr};
@@ -646,7 +649,7 @@ private:
// Delete Face controls (remove faces, heal the solid).
ComboBox* m_del_face_body{nullptr}; // target body
- wxButton* m_del_face_add_btn{nullptr}; // "Add picked face" button
+ ::Button* m_del_face_add_btn{nullptr}; // "Add picked face" button
wxStaticText* m_del_face_list{nullptr}; // shows the accumulated face ids
std::vector m_del_faces; // accumulated face list
@@ -671,8 +674,8 @@ private:
// Expression binding (per-feature, visible during edit only)
ComboBox* m_expr_field{nullptr}; // field-name picker (editable)
wxTextCtrl* m_expr_text{nullptr}; // expression string
- wxButton* m_expr_set_btn{nullptr}; // Apply / bind
- wxButton* m_expr_clear_btn{nullptr}; // Remove binding
+ ::Button* m_expr_set_btn{nullptr}; // Apply / bind
+ ::Button* m_expr_clear_btn{nullptr}; // Remove binding
wxStaticText* m_expr_status{nullptr}; // shows current bindings for the edited feature
void populate_expr_fields(Tool t); // fill m_expr_field from feature-type fields
void on_set_expr(); // checkpoint + write -> recompute -> undo on fail
@@ -681,9 +684,9 @@ private:
// Document variables panel (below the feature tree / parts)
StaticBox* m_var_box{nullptr};
wxListCtrl* m_var_list{nullptr};
- wxButton* m_btn_add_var{nullptr};
- wxButton* m_btn_edit_var{nullptr};
- wxButton* m_btn_del_var{nullptr};
+ ScalableButton* m_btn_add_var{nullptr};
+ ScalableButton* m_btn_edit_var{nullptr};
+ ScalableButton* m_btn_del_var{nullptr};
void refresh_variables(); // rebuild m_var_list from m_doc.variables
void on_add_variable();
void on_edit_variable();
@@ -721,10 +724,10 @@ private:
ComboBox* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
// Plane construction method + contextual face/edge reference picks (Onshape/Fusion parity).
ComboBox* m_plane_type{nullptr}; // PlaneType: Offset/Angle/Midplane/Tangent/TwoEdges/Coincident
- wxButton* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
- wxButton* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
- wxButton* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
- wxButton* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
+ ::Button* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
+ ::Button* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
+ ::Button* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
+ ::Button* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
wxSpinCtrlDouble* m_plane_usize{nullptr}; // datum rectangle extent u (mm) — also driven by drag handles
wxSpinCtrlDouble* m_plane_vsize{nullptr}; // datum rectangle extent v (mm)
// Captured references for the candidate datum (body index + face/edge index, -1 = none).
@@ -847,8 +850,8 @@ private:
// Axis controls (datum axis: line through two points or derived from geometry).
ComboBox* m_axis_type{nullptr}; // AxisType: TwoPoints/FaceNormal/CylinderCenterline/PlaneIntersection/AlongEdge
- wxButton* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
- wxButton* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
+ ::Button* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
+ ::Button* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
ComboBox* m_axis_plane_a{nullptr};
ComboBox* m_axis_plane_b{nullptr};
wxSpinCtrlDouble* m_axis_p1x{nullptr}; wxSpinCtrlDouble* m_axis_p1y{nullptr}; wxSpinCtrlDouble* m_axis_p1z{nullptr};
@@ -861,8 +864,8 @@ private:
ComboBox* m_coordsys_type{nullptr}; // CoordSysType: PointWorld/FaceAndDirection
ComboBox* m_cs_body{nullptr}; // body-focus chooser: restrict picking to one body
wxSpinCtrlDouble* m_cs_x{nullptr}; wxSpinCtrlDouble* m_cs_y{nullptr}; wxSpinCtrlDouble* m_cs_z{nullptr};
- wxButton* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
- wxButton* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
+ ::Button* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
+ ::Button* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
wxSpinCtrlDouble* m_cs_hx{nullptr}; wxSpinCtrlDouble* m_cs_hy{nullptr}; wxSpinCtrlDouble* m_cs_hz{nullptr};
int m_cs_face_body{-1}, m_cs_face{-1};
int m_cs_edge_body{-1}, m_cs_edge{-1};
@@ -934,7 +937,6 @@ private:
// m_status's foreground as created, captured before any caller touches it. Callers signal
// "no opinion" by setting wxNullColour, which restores exactly this — so it is the only
// reliable way to tell a chosen colour (the error red) from the default. See set_status().
- wxColour m_status_default_fg;
// The guidance sentence for the step the armed sketch tool is on, kept so a transient
// readout (the live length/angle while a segment is being dragged) can be appended to it
// instead of replacing it — the guidance used to vanish on the first mouse move after a
@@ -950,7 +952,7 @@ private:
bool m_dof_last_has{false};
int m_feature_counter{0};
- std::vector m_confirm_btns;
+ std::vector<::Button*> m_confirm_btns;
// Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit
// (Sketch or Extrude independently) uses only m_edit_index as the row to replace.
diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp
index 20edb1a145..15a43b09b6 100644
--- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp
+++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp
@@ -8988,6 +8988,8 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
// not there — there is no window to fall back to any more.
if (inline_editor != nullptr)
inline_editor->render(*wxGetApp().imgui(), m_render_scale);
+ if (render_overlays)
+ render_overlays();
(void)canvas;
if (!has_display()) {
if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD
@@ -10510,9 +10512,9 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
// Left-drag rubber band -> whole body. Past the click budget the press becomes a sweep:
// the rectangle is anchored at the ORIGINAL press point (not at the frame where the
// threshold was crossed, which would lose the first few pixels) and the events are
- // consumed from here on. Left-drag no longer orbits in this canvas — DesignCanvas puts
- // orbit on middle-drag and pan on right-drag, the CAD convention — so nothing downstream
- // is being starved of a gesture it used to own.
+ // consumed from here on. The camera follows Preferences > Control as in Prepare, so while
+ // left-drag is given to Rotate or Pan the band takes Shift+left-drag, Prepare's own
+ // rectangle selection.
// HOVER PRE-HIGHLIGHT (9xw part 3): say what a click would take, before it is
// taken. Plain motion only — no button down, no band running — because during a drag the
// pointer is doing something else and a promise about clicking would be a lie. Returns
@@ -10522,7 +10524,9 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
if (update_solid_hover(canvas, evt)) canvas.set_as_dirty();
return false;
}
- if (evt.Dragging() && evt.LeftIsDown() && m_pick_pending) {
+ const bool left_drag_sweeps =
+ evt.ShiftDown() || std::atoi(wxGetApp().app_config->get("left_mouse_drag_action").c_str()) == 0;
+ if (evt.Dragging() && evt.LeftIsDown() && m_pick_pending && (left_drag_sweeps || m_rubber.is_dragging())) {
if (!m_rubber.is_dragging()) {
if (std::max(std::abs(evt.GetX() - m_pick_press_x),
std::abs(evt.GetY() - m_pick_press_y)) <= 8)
diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp
index 56c1ac1a35..b5c1403347 100644
--- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp
+++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp
@@ -154,6 +154,8 @@ public:
// The in-canvas value field, drawn by render() before any early return. Owned by
// DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp.
class SketchInlineEditor* inline_editor{nullptr};
+ // The canvas's own overlays (its status and readout chips), drawn in this tool's ImGui pass.
+ std::function render_overlays;
// Persistent committed sketches to draw even when no session is active (e.g. an
// un-consumed sketch left visible after its extrude is removed). Each carries its
@@ -1244,9 +1246,8 @@ private:
// change) read honestly — the change has to be predictable before the click, not only after.
SolidPick m_pre; // what the pointer is currently over (kind None = nothing)
bool update_solid_hover(GLCanvas3D& canvas, const wxMouseEvent& evt); // true when it changed
- // Left-drag rubber band: sweep a rectangle over the plate to take a whole body. Orbit
- // moves to middle-drag in this canvas (DesignCanvas::set_cad_navigation) so the left
- // button is free for it, which is the CAD convention (Onshape/SolidWorks).
+ // Left-drag rubber band: sweep a rectangle over the plate to take a whole body. While
+ // Preferences give left-drag to the camera it takes Shift+left-drag, as in Prepare.
GLSelectionRectangle m_rubber;
void pick_bodies_in_rectangle(); // resolve the swept rectangle -> whole-body selection
bool on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas); // the body; on_mouse wraps it
diff --git a/src/slic3r/GUI/CAD/DesignTextDialog.cpp b/src/slic3r/GUI/CAD/DesignTextDialog.cpp
index 38e1db91e3..37e12053e1 100644
--- a/src/slic3r/GUI/CAD/DesignTextDialog.cpp
+++ b/src/slic3r/GUI/CAD/DesignTextDialog.cpp
@@ -3,17 +3,18 @@
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Widgets/DialogButtons.hpp"
+#include "slic3r/GUI/Widgets/TextInput.hpp"
+#include "slic3r/GUI/Widgets/ComboBox.hpp"
+#include "slic3r/GUI/Widgets/CheckBox.hpp"
+#include "slic3r/GUI/Widgets/Label.hpp"
#include "slic3r/Utils/WxFontUtils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Emboss.hpp"
-#include
-#include
#include
#include
#include
#include
-#include
#include
#include
@@ -25,11 +26,31 @@ namespace Slic3r { namespace GUI {
static const char* kFontKey = "cad_text_font"; // WxFontUtils::store_wxFont descriptor
static const char* kHeightKey = "cad_text_height"; // mm
-DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial)
- : DPIDialog(parent, wxID_ANY, _L("Text"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
+// Installed scalable faces, sorted, enumerated once per session: listing them takes long enough on
+// a machine with many fonts to be felt each time the dialog opened.
+static const wxArrayString& font_faces()
{
- SetFont(wxGetApp().normal_font());
- SetBackgroundColour(wxGetApp().dark_mode() ? wxColour(0x2d, 0x2d, 0x31) : *wxWHITE);
+ static wxArrayString faces = [] {
+ // '@' faces are the vertical-writing aliases GTK/MSW list twice.
+ wxArrayString f = wxFontEnumerator::GetFacenames(wxFONTENCODING_SYSTEM, false);
+ f.erase(std::remove_if(f.begin(), f.end(), [](const wxString& n) { return n.StartsWith("@"); }), f.end());
+ f.Sort();
+ return f;
+ }();
+ return faces;
+}
+
+static bool parse_mm(wxString t, double& out)
+{
+ t.Replace(",", ".");
+ return t.Trim(true).Trim(false).ToCDouble(&out) && out >= 0.5 && out <= 500.0;
+}
+
+DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial)
+ : DPIDialog(parent, wxID_ANY, _L("Text"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
+{
+ SetFont(Label::Body_14);
+ SetBackgroundColour(*wxWHITE); // light palette colour, dark-mapped by UpdateDlgDarkUI
const int em = em_unit();
// The text being edited, else the last used font and height, else the GUI font at 10 mm.
@@ -50,41 +71,47 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial)
try { height = std::clamp(std::stod(h), 0.5, 500.0); } catch (...) {}
}
}
+ m_last_height = height;
auto* form = new wxFlexGridSizer(2, em / 2, em);
form->AddGrowableCol(1, 1);
+ auto label = [this, form](const wxString& text) {
+ form->Add(new wxStaticText(this, wxID_ANY, text), 0, wxALIGN_CENTER_VERTICAL);
+ };
- m_text = new wxTextCtrl(this, wxID_ANY, initial ? initial->text : wxString(), wxDefaultPosition,
- wxSize(30 * em, -1), wxTE_PROCESS_ENTER);
- m_text->SetHint(_L("Type the text to insert"));
- form->Add(new wxStaticText(this, wxID_ANY, _L("Text")), 0, wxALIGN_CENTER_VERTICAL);
+ m_text = new ::TextInput(this, initial ? initial->text : wxString(), "", "", wxDefaultPosition,
+ wxSize(30 * em, -1), wxTE_PROCESS_ENTER);
+ m_text->GetTextCtrl()->SetHint(_L("Type the text to insert"));
+ label(_L("Text"));
form->Add(m_text, 1, wxEXPAND);
- // Scalable faces only ('@' faces are the vertical-writing aliases GTK/MSW list twice).
- wxArrayString faces = wxFontEnumerator::GetFacenames(wxFONTENCODING_SYSTEM, false);
- faces.erase(std::remove_if(faces.begin(), faces.end(), [](const wxString& f) { return f.StartsWith("@"); }),
- faces.end());
- faces.Sort();
- m_face = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, faces);
- int sel = m_face->FindString(font.GetFaceName());
+ m_face = new ::ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(30 * em, -1), 0, nullptr,
+ wxCB_READONLY);
+ const wxArrayString& faces = font_faces();
+ for (const wxString& f : faces)
+ m_face->Append(f);
+ const int sel = faces.Index(font.GetFaceName());
m_face->SetSelection(sel != wxNOT_FOUND ? sel : 0);
- form->Add(new wxStaticText(this, wxID_ANY, _L("Font")), 0, wxALIGN_CENTER_VERTICAL);
+ label(_L("Font"));
form->Add(m_face, 1, wxEXPAND);
+ // Orca's CheckBox carries no label: each one sits beside its own text.
auto* style = new wxBoxSizer(wxHORIZONTAL);
- m_bold = new wxCheckBox(this, wxID_ANY, _L("Bold"));
- m_italic = new wxCheckBox(this, wxID_ANY, _L("Italic"));
- m_bold->SetValue(WxFontUtils::is_bold(font));
- m_italic->SetValue(WxFontUtils::is_italic(font));
- style->Add(m_bold, 0, wxRIGHT, em);
- style->Add(m_italic, 0);
+ auto check = [this, style, em](const wxString& text, bool value) {
+ auto* c = new ::CheckBox(this);
+ c->SetValue(value);
+ style->Add(c, 0, wxALIGN_CENTER_VERTICAL);
+ style->Add(new wxStaticText(this, wxID_ANY, text), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, em / 2);
+ return c;
+ };
+ m_bold = check(_L("Bold"), WxFontUtils::is_bold(font));
+ m_italic = check(_L("Italic"), WxFontUtils::is_italic(font));
form->AddSpacer(0);
form->Add(style, 0);
- m_height = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(10 * em, -1),
- wxSP_ARROW_KEYS, 0.5, 500.0, height, 0.5);
- m_height->SetDigits(1);
- form->Add(new wxStaticText(this, wxID_ANY, _L("Height (mm)")), 0, wxALIGN_CENTER_VERTICAL);
+ m_height = new ::TextInput(this, wxString::FromCDouble(height, 1), _L("mm"), "", wxDefaultPosition,
+ wxSize(10 * em, -1), wxTE_PROCESS_ENTER);
+ label(_L("Height"));
form->Add(m_height, 0);
// The outline of what will be inserted, fitted to the box, with its real size under it.
@@ -104,26 +131,21 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial)
top->Add(m_size, 0, wxLEFT | wxRIGHT | wxTOP, em);
top->Add(buttons, 0, wxEXPAND);
- m_text->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update_preview(); });
+ m_text->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { update_preview(); e.Skip(); });
m_text->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { accept(); }); // Enter = OK, as everywhere in the tab
- auto refont = [this](wxCommandEvent&) { load_font(); update_preview(); };
- m_face->Bind(wxEVT_CHOICE, refont);
- m_bold->Bind(wxEVT_CHECKBOX, refont);
- m_italic->Bind(wxEVT_CHECKBOX, refont);
- m_height->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { update_preview(); });
- m_height->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { update_preview(); });
- Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
- if (e.GetId() == wxID_OK) accept();
- else if (e.GetId() == wxID_CANCEL) cancel();
- else e.Skip();
- });
+ m_face->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { load_font(); update_preview(); e.Skip(); });
+ for (::CheckBox* c : {m_bold, m_italic})
+ c->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); CallAfter([this] { load_font(); update_preview(); }); });
+ m_height->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { update_preview(); e.Skip(); });
+ m_height->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { accept(); });
+ buttons->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { accept(); });
+ buttons->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { cancel(); });
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { cancel(); });
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_ESCAPE) cancel();
else e.Skip();
});
- wxGetApp().UpdateDlgDarkUI(this);
SetSizerAndFit(top);
// Out of the middle of the window, where the text is being placed: top right of the parent.
if (parent != nullptr) {
@@ -132,17 +154,26 @@ DesignTextDialog::DesignTextDialog(wxWindow* parent, const Spec* initial)
} else {
CenterOnParent();
}
+ wxGetApp().UpdateDlgDarkUI(this);
load_font();
update_preview();
- m_text->SetFocus();
+ m_text->GetTextCtrl()->SetFocus();
}
-wxString DesignTextDialog::text() const { return m_text->GetValue(); }
+double DesignTextDialog::height_mm() const
+{
+ double h = 0.0;
+ if (parse_mm(m_height->GetTextCtrl()->GetValue(), h))
+ const_cast(this)->m_last_height = h;
+ return m_last_height;
+}
+
+wxString DesignTextDialog::text() const { return m_text->GetTextCtrl()->GetValue(); }
DesignTextDialog::Spec DesignTextDialog::spec() const
{
- return { m_text->GetValue(), WxFontUtils::store_wxFont(current_font()), m_height->GetValue() };
+ return { text(), WxFontUtils::store_wxFont(current_font()), height_mm() };
}
void DesignTextDialog::accept()
@@ -151,7 +182,7 @@ void DesignTextDialog::accept()
m_done = true;
if (AppConfig* cfg = wxGetApp().app_config) {
cfg->set(kFontKey, WxFontUtils::store_wxFont(current_font()));
- cfg->set(kHeightKey, std::to_string(m_height->GetValue()));
+ cfg->set(kHeightKey, std::to_string(height_mm()));
}
if (on_accept) on_accept();
}
@@ -167,7 +198,7 @@ wxFont DesignTextDialog::current_font() const
{
wxFontInfo info(12);
if (m_face->GetSelection() != wxNOT_FOUND)
- info.FaceName(m_face->GetStringSelection());
+ info.FaceName(m_face->GetString(m_face->GetSelection()));
info.Bold(m_bold->GetValue()).Italic(m_italic->GetValue());
return wxFont(info);
}
@@ -182,10 +213,10 @@ void DesignTextDialog::load_font()
void DesignTextDialog::update_preview()
{
- const std::string utf8(m_text->GetValue().ToUTF8().data());
+ const std::string utf8(text().ToUTF8().data());
m_regions.clear();
if (m_font_file && !utf8.empty())
- m_regions = text_to_regions(utf8, m_height->GetValue(), m_font_file);
+ m_regions = text_to_regions(utf8, height_mm(), m_font_file);
wxString line;
if (!m_font_file)
@@ -250,7 +281,20 @@ void DesignTextDialog::draw_preview(wxWindow* canvas)
void DesignTextDialog::on_dpi_changed(const wxRect&)
{
- Fit();
+ m_text->Rescale();
+ m_face->Rescale();
+ m_height->Rescale();
+ m_bold->Rescale();
+ m_italic->Rescale();
+ GetSizer()->SetSizeHints(this);
+ Refresh();
+}
+
+void DesignTextDialog::on_sys_color_changed()
+{
+ SetBackgroundColour(*wxWHITE);
+ wxGetApp().UpdateDlgDarkUI(this);
+ m_preview->Refresh(); // its colours are read at paint time
Refresh();
}
diff --git a/src/slic3r/GUI/CAD/DesignTextDialog.hpp b/src/slic3r/GUI/CAD/DesignTextDialog.hpp
index 3200301880..9b39db4651 100644
--- a/src/slic3r/GUI/CAD/DesignTextDialog.hpp
+++ b/src/slic3r/GUI/CAD/DesignTextDialog.hpp
@@ -10,11 +10,10 @@
#include
#include
-class wxTextCtrl;
-class wxChoice;
-class wxCheckBox;
-class wxSpinCtrlDouble;
class wxStaticText;
+class TextInput;
+class ComboBox;
+class CheckBox;
namespace Slic3r {
namespace Emboss { struct FontFile; }
@@ -48,6 +47,7 @@ public:
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
+ void on_sys_color_changed() override;
private:
void load_font(); // m_face/m_bold/m_italic -> m_font_file
@@ -58,11 +58,14 @@ private:
void cancel();
bool m_done{false}; // accept/cancel fire once
- wxTextCtrl* m_text{nullptr};
- wxChoice* m_face{nullptr};
- wxCheckBox* m_bold{nullptr};
- wxCheckBox* m_italic{nullptr};
- wxSpinCtrlDouble* m_height{nullptr};
+ double height_mm() const; // the typed height, or the last valid one while it is being typed
+
+ ::TextInput* m_text{nullptr};
+ ::ComboBox* m_face{nullptr};
+ ::CheckBox* m_bold{nullptr};
+ ::CheckBox* m_italic{nullptr};
+ ::TextInput* m_height{nullptr}; // mm; Orca's SpinInput is integer-only
+ double m_last_height{10.0};
wxWindow* m_preview{nullptr};
wxStaticText* m_size{nullptr};
wxWindow* m_ok{nullptr};
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index ca6e037eb7..b4efaa6b8b 100644
--- a/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -4846,8 +4846,6 @@ bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const std::map m_old_size{ 0, 0 };
bool m_is_touchpad_navigation{ false };
- // CAD navigation (Design tab only): left-drag is a selection rubber band, so orbit moves
- // to middle-drag and pan to right-drag — the Onshape/SolidWorks mapping. Off everywhere
- // else, so Prepare/Preview keep the mouse the user already learned.
- bool m_cad_navigation{ false };
-
// Screen is only refreshed from the OnIdle handler if it is dirty.
bool m_dirty;
// A frame is needed, and only for the overlay.
@@ -1144,7 +1139,6 @@ public:
bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map& mappings) const;
bool is_camera_rotate(const wxMouseEvent& evt, const std::map& mappings) const;
bool is_camera_pan(const wxMouseEvent& evt, const std::map& mappings) const;
- void set_cad_navigation(bool b) { m_cad_navigation = b; }
Size get_canvas_size() const;
Vec2d get_local_mouse_position() const;
diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp
index 54393c70c1..e0db1cda00 100644
--- a/src/slic3r/GUI/MainFrame.cpp
+++ b/src/slic3r/GUI/MainFrame.cpp
@@ -1320,6 +1320,15 @@ void MainFrame::show_option(bool show)
}
}
+void MainFrame::set_undo_redo_enabled(bool undo, bool redo)
+{
+#ifndef __APPLE__
+ m_topbar->EnableUndoRedo(undo, redo);
+#else
+ (void) undo; (void) redo; // macOS has no top bar; Edit asks the tab when it opens
+#endif
+}
+
#ifdef SLIC3R_CAD
DesignPanel* MainFrame::shown_design_panel() const
{
@@ -1393,6 +1402,11 @@ void MainFrame::init_tabpanel() {
m_topbar->DisableUndoRedoItems();
}
#endif
+#ifdef SLIC3R_CAD
+ // Design keeps its own history, and the top bar's Undo/Redo drive it while it is shown.
+ if (m_design_page != nullptr && panel == m_design_page)
+ DesignPanel::ensure()->update_undo_redo_buttons();
+#endif
if (panel)
panel->SetFocus();
@@ -2612,6 +2626,9 @@ void MainFrame::on_dpi_changed(const wxRect& suggested_rect)
MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.msw_rescale(); });
MultiMachinePage::when_built([](MultiMachinePage& multi_machine) { multi_machine.msw_rescale(); });
CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.msw_rescale(); });
+#ifdef SLIC3R_CAD
+ DesignPanel::when_built([](DesignPanel& design) { design.msw_rescale(); });
+#endif
// BBS
#if 0
@@ -2676,6 +2693,9 @@ void MainFrame::on_sys_color_changed()
wxGetApp().plater()->sys_color_changed();
MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.on_sys_color_changed(); });
CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.on_sys_color_changed(); });
+#ifdef SLIC3R_CAD
+ DesignPanel::when_built([](DesignPanel& design) { design.on_sys_color_changed(); });
+#endif
// update Tabs
for (auto tab : wxGetApp().tabs_list)
tab->sys_color_changed();
diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp
index 798ddad274..13e546508c 100644
--- a/src/slic3r/GUI/MainFrame.hpp
+++ b/src/slic3r/GUI/MainFrame.hpp
@@ -499,6 +499,8 @@ public:
// the tab that is shown: its own history when that is Design, the plater's otherwise.
DesignPanel* shown_design_panel() const;
#endif
+ // The top bar's Undo/Redo, for a tab that keeps its own history (Design).
+ void set_undo_redo_enabled(bool undo, bool redo);
//BBS: GUI refactor
LazyPage* m_monitor_page{ nullptr };