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 };