A field that is logically closed can still be eating every key

Measured on the running app, not deduced. After a queued dimension chain the value field's frame
is left MAPPED on purpose (mutter refuses keyboard focus to a re-mapped window), so there is a
window in which m_open is already false and the frame is still on screen holding the X input
focus. GTK meanwhile reports that window inactive and routes nothing into the text control. Every
key then lands somewhere that cannot use it and will not give it back:

  [KEYTRACE] key=27 ui_mode=1 inline_busy=0     <- the last key the panel ever sees
  === MARK press Delete ===                     <- no trace line at all
  xdotool getwindowfocus -> 0xe00404 86x60      <- the value field, still mapped

Delete, Esc and typing all read as dead, which is exactly the report. And nothing could recover
it: close(), cancel() and do_cancel() all return early on !m_open, so the one window still
receiving keystrokes was also the one window no code could dismiss.

is_mapped() asks the question the flag cannot answer, and dismiss() tears the frame down with no
m_open guard, since m_open is precisely what lies in this state. Esc inside the field falls back
to it — while the frame holds focus that handler is the only code the keyboard can still reach,
so if it refuses, nothing else gets a turn. Every close now hands focus back to the canvas
explicitly, because hiding a window does not move the X input focus off it. And inline_busy()
reports the union of "a value is pending" and "a frame is mapped", so Esc routes to the field
whenever one is on screen at all.

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-05 13:00:23 +02:00
co-authored by Claude Opus 5
parent 3f52166e32
commit cf444a6ab6
3 changed files with 64 additions and 5 deletions
+6 -1
View File
@@ -1305,7 +1305,12 @@ void DesignCanvas::delete_selected_sketch_entities()
bool DesignCanvas::inline_busy() const
{
return m_sketch_tool.inline_busy();
// The TOOL's flag says a value is pending; the FRAME being mapped says a window is on screen
// holding the keyboard. Either one means "a field is up", and only the union of the two is
// safe to route Esc by: the flag alone went false while the frame was still mapped, which is
// the orphan that swallowed every key with nothing able to close it.
return m_sketch_tool.inline_busy()
|| (m_inline_editor && m_inline_editor->is_mapped());
}
bool DesignCanvas::inline_has_focus() const
+44 -4
View File
@@ -137,6 +137,7 @@ static const wxColour kTitleErr(232, 106, 106);
SketchInlineEditor::SketchInlineEditor(wxWindow* parent_canvas)
{
m_parent = parent_canvas; // where the keyboard goes back to when this field lets go
wxWindow* top = parent_canvas ? wxGetTopLevelParent(parent_canvas) : nullptr;
// Borderless floating frame: a top-level window so the WM composites it above the
// GL canvas (a child widget would be hidden by the GL surface). Floats on its
@@ -163,7 +164,11 @@ SketchInlineEditor::SketchInlineEditor(wxWindow* parent_canvas)
// outlives the input it was about is just noise on the next attempt.
m_ctrl->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { clear_invalid(); e.Skip(); });
m_ctrl->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_ESCAPE) do_cancel();
// Esc on an ORPHAN (mapped, m_open already false) must still take the field off the
// screen. do_cancel() returns early there, and while the frame holds the X input focus
// this handler is the ONLY code the keyboard can still reach — so if it refuses, nothing
// else gets a turn and the application looks frozen.
if (e.GetKeyCode() == WXK_ESCAPE) { if (m_open) do_cancel(); else dismiss(); }
// Tab commits, exactly like Enter — the caller's on_commit is what walks to the next
// dimension. Left to wx's default handling it navigated within this one-control popup,
// i.e. back to the same field with the text re-selected: typing 60, Tab, 40 looked like
@@ -250,19 +255,53 @@ void SketchInlineEditor::do_commit()
// CallAfter, queued during cb(v), therefore BEFORE the one below) or it does not. Hiding
// here would unmap the window and mutter would refuse to focus the re-map; so hide only
// after the reopen has had its turn. Harmless on the unmapped path — already hidden.
m_frame->CallAfter([this] { if (!m_open && m_frame) m_frame->Hide(); });
m_frame->CallAfter([this] {
if (m_open || m_frame == nullptr) return;
m_frame->Hide();
return_focus(); // the chain is over; the keyboard belongs to the canvas again
});
}
void SketchInlineEditor::cancel()
{
if (m_open) do_cancel();
if (m_open) do_cancel();
else if (is_mapped()) dismiss(); // orphan: logically gone, still on screen, still eating keys
}
bool SketchInlineEditor::is_mapped() const
{
return m_frame != nullptr && m_frame->IsShown();
}
// Everything the frame can hold onto, released — with no m_open guard, because the state this
// exists for is precisely the one where m_open lies.
void SketchInlineEditor::dismiss()
{
if (m_frame == nullptr) return;
m_open = false;
m_commit = nullptr;
m_cancel = nullptr;
if (m_frame->IsShown()) m_frame->Hide();
return_focus();
}
// Hiding the frame is not enough: X keeps the input focus pointed at the window that had it, so
// an unmapped field keeps swallowing keys. The canvas has to ask for it back explicitly.
void SketchInlineEditor::return_focus()
{
if (m_parent == nullptr) return;
if (wxWindow* top = wxGetTopLevelParent(m_parent))
top->Raise();
m_parent->SetFocus();
}
// Accept what is typed and close. Leaving a tool must not silently discard the value the user
// just entered — the same rule set_tool already follows for a ready edit-op.
void SketchInlineEditor::commit()
{
if (!m_open) return;
// Same orphan case as cancel(): Enter or Tab forwarded by the panel must not be the one
// gesture that leaves the field on screen.
if (!m_open) { if (is_mapped()) dismiss(); return; }
do_commit();
// do_commit REFUSES to close on unparseable text, which is right while the user is still
// typing — but this entry point is "we are leaving", and the caller (set_tool) unfreezes
@@ -323,6 +362,7 @@ void SketchInlineEditor::close()
m_commit = nullptr;
m_cancel = nullptr;
m_closing = false;
return_focus();
}
}} // namespace Slic3r::GUI
+14
View File
@@ -34,6 +34,19 @@ public:
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
bool is_open() const { return m_open; }
// MAPPED is not the same question as OPEN, and conflating them is how the keyboard dies.
// The frame is deliberately left mapped across a queued dimension chain (mutter refuses
// focus to a re-mapped window), so there is a window in which m_open is already false and
// the frame is still on screen holding the X input focus. GTK meanwhile reports the window
// inactive, so it routes nothing to the text control — and every key the user presses lands
// in a window that cannot use it and will not give it back. Delete, Esc and typing all read
// as dead. Callers ask this to find the orphan; dismiss() is how they kill it.
bool is_mapped() const;
void dismiss(); // unconditional teardown: works on an ORPHANED frame too
private:
void return_focus(); // hand the keyboard back to the canvas, not to a hidden window
public:
// True when the field itself holds keyboard focus. Callers use this to decide whether the
// field will handle a key on its own or needs it forwarded — see DesignPanel's CHAR_HOOK.
bool has_focus() const { return m_ctrl != nullptr && wxWindow::FindFocus() == m_ctrl; }
@@ -48,6 +61,7 @@ private:
void flag_invalid(const wxString& why);
void clear_invalid();
wxWindow* m_parent{nullptr}; // the GL canvas: where focus must go back to
wxFrame* m_frame{nullptr};
wxTextCtrl* m_ctrl{nullptr};
wxStaticText* m_title{nullptr};