The value field stops being a window, and now takes what is typed

Rebases the in-canvas work onto cad-mainline and finishes it. The field is drawn
by ImGui inside the GL canvas instead of being a borderless top-level wxFrame.

WHY THE FLOATING FRAME COULD NOT BE FIXED. Whether a borderless top-level may hold
the keyboard is the window manager's decision, and it differs per desktop: openbox
grants it, mutter refuses it, macOS denies key status outright. Seven workarounds
fought that and one cost a macOS regression. Drawn inside the canvas there is no
second top-level for anyone to refuse, so the question is never asked. The field is
fed exactly like every other ImGui widget in the app — GLCanvas3D::on_char ->
ImGuiWrapper::update_key_data -> io.AddInputCharacter.

MEASURED, on behemoth: the click-edit ladder holds 28 checks — Line, Rectangle,
Circle, Slot, Polygon, Ellipse, Arc, and click-to-edit on a placed dimension label
— typing with NO click into the field first, committed == typed != prefill every
time, and 27 [UX] imgui_char lines showing the characters arriving.

WHAT WAS ACTUALLY WRONG. Not the field. The belief that "characters never reach the
ImGui InputText" came from the harness: the ladder was delivering keys with
`xdotool type --window` (XSendEvent), which GTK discards, so no build of any kind
could have received them. The new probe in ImGuiWrapper::update_key_data — the one
place ImGui is ever handed a character — is what separated that from a real defect,
and it stays, because a canvas-side probe provably cannot answer the question:
GLCanvas3D::on_char is bound later than any constructor-time probe, wx runs handlers
in reverse bind order, and on_char returns without Skip(), so such a probe is silent
whether or not the key arrived. A day was lost reading that silence as evidence.

Also drops DesignPanel's content-based forwarder and DesignCanvas::inline_type_char.
They were the right rule for a field that could not be focused; with the field
inside the canvas there is nothing to forward, and keeping them would have masked
whether the normal path works.

STILL UNVERIFIED: behaviour under mutter itself. Neither focus-stealing-prevention
WM available here survives long enough to judge — metacity SEGVs ~20s in and xfwm4
dies with BadWindow on SetInputFocus, both before the sketch opens and both
unrelated to this field. The design's claim is structural rather than measured: no
second top-level means no focus to refuse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
This commit is contained in:
Tommaso Bianchi
2026-09-06 11:10:02 +02:00
co-authored by Claude Opus 5
parent f6c551540e
commit e5659e0f0f
5 changed files with 44 additions and 65 deletions
-5
View File
@@ -1373,11 +1373,6 @@ bool DesignCanvas::inline_has_focus() const
return m_inline_editor && m_inline_editor->has_focus();
}
bool DesignCanvas::inline_type_char(int key)
{
return m_inline_editor && m_inline_editor->type_char(key);
}
void DesignCanvas::inline_commit()
{
if (m_inline_editor) m_inline_editor->commit();
-2
View File
@@ -244,8 +244,6 @@ public:
void delete_selected_sketch_entities();
bool inline_busy() const; // a sketch value field is open (guard keys)
bool inline_has_focus() const; // the field itself holds keyboard focus
// Hand one character to the open value field, bypassing focus. See DesignPanel's arbiter.
bool inline_type_char(int key);
void inline_commit(); // accept the typed value (Enter/Tab)
void inline_cancel(); // discard the typed value (Esc)
// The layered Esc: abandon the points of the gesture in progress, else drop the armed tool
+5 -19
View File
@@ -4212,25 +4212,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_viewport->inline_commit();
return;
}
// THE ARBITER. Route by what the key IS, not by who the window manager focused.
//
// This is FreeCAD's rule, from Sketcher's DrawSketchKeyboardManager::
// detectKeyboardEventHandlingMode: a digit, a sign, a decimal separator or a
// Backspace/Delete is unambiguously meant for the number the user is entering; a
// letter is unambiguously a tool shortcut; Enter/Tab hand control back to the view.
// FreeCAD never queries focus anywhere in that decision, and that is precisely why
// its sketcher behaves the same on every desktop.
//
// Ours asked "who has focus?" instead — a question whose answer is the window
// manager's opinion. openbox grants this borderless top-level the keyboard, mutter
// refuses it, so the same binary took typed values on one machine and silently
// committed the pre-filled as-drawn number on another. Seven workarounds fought that
// and one of them cost a macOS regression. The question was wrong, not the answers.
//
// The has_focus() guard above keeps this from double-typing where the toolkit DID
// give the field the keyboard: there the field's own binding will get the key too.
if (!ctrl && m_viewport->inline_type_char(key))
return;
// NO forwarding here any more. The field is drawn INSIDE the GL canvas now, so it
// is fed the way every other ImGui widget in this app is fed: GLCanvas3D::on_char ->
// ImGuiWrapper::update_key_data -> io.AddInputCharacter. Re-adding a panel-side
// forwarder would also mask whether that path works, which is exactly what is being
// measured.
// Esc is NOT special-cased here any more: escape() routes it, and the open field is
// exactly what CadLevel::Transient means, so it closes the field and stops there.
}
+17
View File
@@ -505,6 +505,23 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
if (evt.GetEventType() == wxEVT_CHAR) {
// Char event
const auto key = evt.GetUnicodeKey();
// THE MEASUREMENT THAT CANNOT LIE. This is the ONLY place in the application where ImGui
// is ever handed a character, so an ImGui text field that stays empty while reporting
// itself active has exactly two possible causes, and this line separates them: no output
// at all means the wxEVT_CHAR never reached the GL canvas (a focus problem, upstream of
// ImGui entirely), while output with unicode=0 means the character arrived empty and is
// being dropped right here.
//
// It lives here rather than on the canvas because a probe bound on the canvas CANNOT
// answer this: GLCanvas3D::on_char is bound later than any constructor-time probe, wx
// runs handlers in reverse bind order, and on_char returns without Skip() whenever this
// function returns true — so such a probe stays silent whether or not the key arrived.
// A day was lost to reading that silence as evidence.
if (std::getenv("SNAPORCA_UXTRACE")) {
fprintf(stderr, "[UX] imgui_char unicode=%d keycode=%d want_text=%d\n",
(int) key, evt.GetKeyCode(), (int) io.WantTextInput);
fflush(stderr);
}
if (key != 0) {
io.AddInputCharacter(key);
}