Files
OrcaSlicer/src/slic3r/GUI/CAD/SketchInlineEditor.hpp
T
Claude 829124982d Design tab: one rule for mouse, Enter and Esc; honest status messages
Keyboard and mouse
- Esc drops what is pending (picks, a dimension's first point, an edit-op or transform) and
  never applies it; Enter applies a ready edit-op/transform, ends a polyline/spline chain,
  ends an armed tool, and confirms a feature card exactly when its ✓ is enabled.
- Right-click only abandons the gesture in progress; with nothing pending it opens the offer
  in every tool (Trim, edit-ops, transforms, Dimension, Constrain, TransformArt, move gizmo).
  Clicking empty space no longer commits. The offer needs no timing, only a still press.
- Delete removes only an explicit selection. Undo/redo inside a sketch go through the same
  route as the buttons (whole shapes, with redo); Edit > Undo follows the shown tab.
- The canvas no longer handles Delete/Esc/Ctrl+Z itself (Backspace in a value field deleted
  the geometry it measured); F is in the panel's key map.
- Value fields: a refused value keeps the field open with the reason; click outside and Tab
  commit; an untouched field commits the exact value; any decimal separator is accepted;
  lengths are always mm; validation is the same for every editor.
- Snapping: the marker shows only where the click will actually snap; pick tolerances are
  one set of pixel budgets (Constrain picks within reach; no mm floor on labels).

Messages and consistency
- set_status(kind, text) gives every status line its own colour and glyph; kernel errors are
  translated into sentences and formatted, not concatenated; sketch refusals go to the status
  line instead of the per-frame HUD that erased them.
- Hints describe the gestures that now work; Dimension shows its second step; Constrain uses
  the sketch palette (red means conflict only); the straight slot's value is its width.
- Hole/Thread/Project keep the user's pick or refuse up front; circular pattern opens with its
  own preview; thread fields use the nominal diameter and the ISO internal depth.

Integration
- MCP loads the project's recipe before touching the document, refuses to mutate it while the
  tab is busy, never runs a request that already timed out, only replaces a socket at its
  path, caps line length and removes the socket at exit; not started in the G-code viewer.
- Hiding a feature keeps later body references on their bodies; a design keeps its modeling
  origin across printer changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:25 +00:00

110 lines
5.7 KiB
C++

#ifndef slic3r_SketchInlineEditor_hpp_
#define slic3r_SketchInlineEditor_hpp_
#include <functional>
#include <string>
#include <wx/gdicmn.h>
namespace Slic3r {
namespace GUI {
class ImGuiWrapper;
// Onshape-style in-canvas value editor.
//
// IT IS NOT A WINDOW. It used to be a borderless top-level wxFrame holding a wxTextCtrl, and
// that is the whole history of this file: a separate top-level window can only receive typing
// if the window manager grants it focus, and whether it does is not ours to decide. openbox
// grants it; mutter's focus-stealing prevention refuses it, so on a GNOME desktop the field
// appeared, showed its value selected, and silently ignored every keystroke — Enter then
// committed the number it opened with. Seven workarounds were tried against that (a real X11
// server timestamp for gtk_window_present, re-asserted SetFocus, dropping the _UTILITY hint,
// keeping the frame mapped between two queued fields, forwarding keys from the panel's
// CHAR_HOOK), one of them caused a macOS regression, and the test harness ended up clicking the
// field before typing — which is the workaround a user cannot be asked to perform, and is
// exactly the "label value not editable" report.
//
// So the field stops asking. It is now drawn INSIDE the GL canvas as an ImGui overlay, at the
// same screen point as before, and its keys arrive through the canvas's own key events, which
// GLCanvas3D already feeds to ImGui (see GLCanvas3D::on_key / on_char -> update_key_data). The
// canvas is part of the main window and already has focus, so there is no second window, no
// second focus, and no window manager in the path. The dimension labels next to it are already
// ImGui overlays (DesignSketchTool::draw_dim_label), so this is the same vocabulary, not a new
// one.
//
// Ownership: DesignCanvas owns it; DesignSketchTool::render() calls render() once per frame.
class SketchInlineEditor
{
public:
SketchInlineEditor() = default;
// Open the field anchored at `canvas_px` (canvas DEVICE pixels, the coordinate space the
// sketch tool works in), pre-filled with `value` and pre-selected. on_commit(parsed) fires
// on Enter with a valid number; on_cancel() on Esc.
void open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel);
void close(); // drop it with neither callback
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
// Called from inside a commit callback that cannot accept the value (a negative length, a
// count of 1): the field comes back with the typed text and `why` in its title line, instead
// of closing as if the value had been taken.
bool refuse(const std::string& why); // true if the field came back
bool is_open() const { return m_open; }
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
bool render(ImGuiWrapper& imgui, float scale);
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
// that only a per-frame trace shows:
//
// [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet
// [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now
// (nothing further) <- the canvas has nothing to redraw, so it stops
//
// This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a
// frame, from the active item, and GLCanvas3D::on_char only calls render() when
// update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no
// render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field
// looks exactly as deaf as the window it replaced. One repaint per frame while it is open
// breaks the circle.
std::function<void()> request_frame;
bool is_mapped() const { return m_open; }
// Does the ImGui text widget own the keyboard? False once a click elsewhere deactivated it;
// the panel then forwards Enter/Tab to commit() itself, since ImGui no longer sees them.
bool has_focus() const { return m_open && (m_active || m_focus_pending); }
void dismiss() { close(); }
private:
void do_commit();
void do_cancel();
std::function<void(double)> m_commit;
std::function<void()> m_cancel;
bool m_open{false};
bool m_focus_pending{false}; // one frame of SetKeyboardFocusHere after opening
wxPoint m_anchor{0, 0}; // canvas device px
std::string m_title;
std::string m_err; // why the last value was refused, shown in the title line
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
double m_prefill_value{0.0}; // the exact value the field opened with...
std::string m_prefill_text; // ...and how it was shown: an untouched field commits the former
bool m_active{false}; // the InputText was the active item last frame
// What the last commit closed, so refuse() can bring the same field back.
struct Closed {
wxPoint anchor; std::string title; std::string buf;
std::function<void(double)> commit; std::function<void()> cancel;
double prefill_value{0.0}; std::string prefill_text;
};
Closed m_last;
bool m_in_commit{false};
};
}} // namespace Slic3r::GUI
#endif // slic3r_SketchInlineEditor_hpp_