From 410399fdb5437724987ede04073f79cf3766be0e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 15:38:24 +0000 Subject: [PATCH] Design tab: follow Orca's theme, scale, mouse settings, dialogs and undo Felix14-v2's review of OrcaSlicer#16019 found the tab behaving as a world of its own. - Icons: the design_* glyphs were drawn in a fixed light grey, made for the dark ribbon, and the toolbar re-tinted some of them by rebuilding the bitmap from a wxImage, which drops the HiDPI scale factor Orca sets on Windows: at 150 % the icons came out half again too large for buttons that were sized in raw pixels, overlapping and clipped. The glyphs now use Orca's sidebar icon grey (#949494), which the icon cache maps per theme, nothing is re-tinted, toolbar glyphs drawn for Prepare's light toolbar use their "_dark" twin, and every size is in DIP. - Theme: the chrome colours were read once, at construction, and nothing in the tab answered a theme switch, so switching left light surfaces and unreadable text in a dark tab and the other way round. The colours are now {light, dark} token pairs; MainFrame::on_sys_color_changed reaches DesignPanel::on_sys_color_changed, which moves every token colour onto the other theme's, runs the app's dark pass and re-rasterises the icons. Card borders are StateColors, resolved at paint time. - Scale: MainFrame::on_dpi_changed reaches DesignPanel::msw_rescale, which re-rasterises every icon (buttons, flyout rows, card headers, the tree's image list, now sized from its bitmaps) and re-measures the Orca widgets. - Mouse: the canvas no longer forces middle-drag to orbit and right-drag to pan; it reads the drag actions in Preferences > Control like Prepare. Left-drag is shared with picking, so the whole-body rubber band takes Shift+left-drag while left-drag is given to the camera. - FPS counter: ImGui's display size is shared and only refreshed when a canvas sees its own size change; the Design canvas now re-announces its size when the tab is entered (and the editor canvas when it is left), as Plater does between Prepare and Preview. - Viewport text: the status line and the tool readout were top-level popups over GL. A popup does not follow its frame, so the empty-canvas hint floated over other applications, and the readout was never taken down with the tab. Both are drawn by the canvas in the tool's ImGui pass now, with the theme's overlay style. - Dialogs: messages use MessageDialog/RichMessageDialog; Add/Edit Variable is one Orca dialog with Name and Expression fields instead of two native text prompts; the Text dialog uses TextInput, ComboBox and CheckBox (its height is a TextInput: SpinInput is integer-only), and enumerates the installed fonts once per session. The ribbon's Confirm/Cancel, the reference pick buttons and the expression buttons are Orca Buttons; the variable actions are icon buttons like the other cards'. - Undo: the tab's own Undo/Redo buttons are gone. The top bar's Undo/Redo drive the Design history while the tab is shown, greyed to what an undo would do, as Ctrl+Z and Edit already did. - The first build of the tab logs how long each phase took ("Design tab build: ..."): it is under a second here but was reported at about fifteen on Windows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9 --- docs/HLSD/design-tab.md | 23 + resources/images/design_arc3pt.svg | 2 +- resources/images/design_arc_center.svg | 2 +- resources/images/design_array.svg | 2 +- resources/images/design_boolean.svg | 2 +- resources/images/design_bspline.svg | 2 +- resources/images/design_c_angle.svg | 2 +- resources/images/design_c_coincident.svg | 2 +- resources/images/design_c_collinear.svg | 2 +- resources/images/design_c_concentric.svg | 2 +- resources/images/design_c_diameter.svg | 2 +- resources/images/design_c_dist_x.svg | 2 +- resources/images/design_c_dist_y.svg | 2 +- resources/images/design_c_equal.svg | 2 +- resources/images/design_c_equal_radius.svg | 2 +- resources/images/design_c_fix.svg | 2 +- resources/images/design_c_horizontal.svg | 2 +- resources/images/design_c_midpoint.svg | 2 +- resources/images/design_c_parallel.svg | 2 +- resources/images/design_c_perpendicular.svg | 2 +- resources/images/design_c_radius.svg | 2 +- resources/images/design_c_sym_h.svg | 2 +- resources/images/design_c_sym_v.svg | 2 +- resources/images/design_c_symmetric.svg | 2 +- resources/images/design_c_tangent.svg | 2 +- resources/images/design_c_vertical.svg | 2 +- resources/images/design_chamfer.svg | 2 +- resources/images/design_check.svg | 2 +- resources/images/design_circle.svg | 2 +- resources/images/design_circle2pt.svg | 2 +- resources/images/design_circle3pt.svg | 2 +- resources/images/design_constrain.svg | 2 +- resources/images/design_construction.svg | 2 +- resources/images/design_crect.svg | 2 +- resources/images/design_cut.svg | 2 +- resources/images/design_delete.svg | 2 +- resources/images/design_dimension.svg | 2 +- resources/images/design_draft.svg | 2 +- resources/images/design_dressup.svg | 2 +- resources/images/design_edit.svg | 2 +- resources/images/design_ellipse.svg | 2 +- resources/images/design_ellipse_arc.svg | 2 +- resources/images/design_extend.svg | 2 +- resources/images/design_extrude.svg | 2 +- resources/images/design_eye.svg | 2 +- resources/images/design_filletedge.svg | 2 +- resources/images/design_hole.svg | 2 +- resources/images/design_line.svg | 2 +- resources/images/design_loft.svg | 2 +- resources/images/design_mirror.svg | 2 +- resources/images/design_move.svg | 2 +- resources/images/design_movedown.svg | 2 +- resources/images/design_moveup.svg | 2 +- resources/images/design_offset.svg | 2 +- resources/images/design_pattern.svg | 2 +- resources/images/design_plane.svg | 2 +- resources/images/design_point.svg | 2 +- resources/images/design_polararray.svg | 2 +- resources/images/design_polygon.svg | 2 +- resources/images/design_polyline.svg | 2 +- resources/images/design_rect.svg | 2 +- resources/images/design_rect_oblique.svg | 2 +- resources/images/design_rect_rounded.svg | 2 +- resources/images/design_revolve.svg | 2 +- resources/images/design_rib.svg | 2 +- resources/images/design_rotate.svg | 2 +- resources/images/design_scale.svg | 2 +- resources/images/design_select.svg | 2 +- resources/images/design_shell.svg | 2 +- resources/images/design_sketch.svg | 2 +- resources/images/design_slot.svg | 2 +- resources/images/design_slot_arc.svg | 2 +- resources/images/design_step.svg | 2 +- resources/images/design_surface.svg | 2 +- resources/images/design_svg.svg | 2 +- resources/images/design_sweep.svg | 2 +- resources/images/design_tangentarc.svg | 2 +- resources/images/design_text.svg | 2 +- resources/images/design_thicken.svg | 2 +- resources/images/design_thread.svg | 2 +- resources/images/design_trim.svg | 2 +- src/slic3r/GUI/BBLTopbar.cpp | 16 + src/slic3r/GUI/BBLTopbar.hpp | 1 + src/slic3r/GUI/CAD/DesignCanvas.cpp | 258 +++------- src/slic3r/GUI/CAD/DesignCanvas.hpp | 40 +- src/slic3r/GUI/CAD/DesignPanel.cpp | 513 ++++++++++++-------- src/slic3r/GUI/CAD/DesignPanel.hpp | 50 +- src/slic3r/GUI/CAD/DesignSketchTool.cpp | 12 +- src/slic3r/GUI/CAD/DesignSketchTool.hpp | 7 +- src/slic3r/GUI/CAD/DesignTextDialog.cpp | 144 ++++-- src/slic3r/GUI/CAD/DesignTextDialog.hpp | 21 +- src/slic3r/GUI/GLCanvas3D.cpp | 4 - src/slic3r/GUI/GLCanvas3D.hpp | 6 - src/slic3r/GUI/MainFrame.cpp | 20 + src/slic3r/GUI/MainFrame.hpp | 2 + 95 files changed, 666 insertions(+), 611 deletions(-) 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 };