mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
Sketch value fields: content-based key arbiter + the gate that can judge it
The reported defect: sketch dimension labels are "not editable" — you draw a
rectangle, its Width field opens, you type, and the as-drawn number is committed
instead. It affects every sketch tool, not just the rounded rectangle.
WHAT THIS ADDS
1. The arbiter (DesignPanel CHAR_HOOK -> DesignCanvas::inline_type_char ->
SketchInlineEditor::type_char). Routes a key by what it IS, not by who the
window manager focused: digits, sign, decimal separator and Backspace/Delete
go to the open value field, Enter/Tab commit, letters stay tool shortcuts.
This is FreeCAD Sketcher's rule (DrawSketchKeyboardManager::
detectKeyboardEventHandlingMode), and the reason its sketcher behaves the same
on every desktop: it never asks who has focus.
2. The [UX] trace (SNAPORCA_UXTRACE) in SketchInlineEditor: open/commit/refused/
cancel, with the prefill and what the control actually held at Enter. It did
not exist — the ladder below was written against a surface no build emitted,
so it could only ever report "nothing opened". typed == prefill on a commit is
the defect's signature and nothing else makes it visible.
3. A draw-then-edit trace in DesignSketchTool: four early returns can swallow the
value-field chain and from outside they are indistinguishable.
4. scripts/CAD/check-gui-click-edit.py — types WITHOUT clicking the field, as a
person does, across Line/Rectangle/Circle/Slot/Polygon/Ellipse/Arc plus label
click-to-edit, and asserts committed == typed != prefill.
5. scripts/CAD/focus-loop.sh — sync/build/assert on behemoth. NOT the orcacad-gui
rig: its image pins deps 216 non-CAD files behind cad-mainline, so today's CAD
sources cannot build there without a deps rebuild.
WHAT IS PROVEN, AND WHAT IS NOT
Green under openbox: 28 checks, every tool, committed == typed != prefill.
But openbox CANNOT adjudicate this bug and the ladder says so in place. There the
field always wins the keyboard, so the same ladder also passes against a binary
with the arbiter compiled out — measured twice. Two ways of removing the keyboard
were tried and both are recorded as dead ends: XSetInputFocus loses to the field's
own re-focus CallAfter, and XSendEvent (xdotool --window) is dropped by GTK, which
made every run red regardless of the code.
Under metacity — same focus-stealing-prevention lineage as the user's mutter — the
mechanism appears in the WM's own log:
Buggy client sent a _NET_ACTIVE_WINDOW message with a timestamp of 0
That is the activation being refused, which is exactly the reported symptom.
present_toplevel() already asks for a server timestamp, so a path is still falling
through to frame->Raise(), which sends time 0. That is the next thing to fix, and
it is tracked; the arbiter alone does not close it. metacity also aborts on this
window (frames.c:1239), so the gate needs a WM that survives before it can return
a verdict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
This commit is contained in:
co-authored by
Claude Opus 5
parent
af4bbe0217
commit
9134299233
@@ -1339,6 +1339,11 @@ 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();
|
||||
|
||||
@@ -244,6 +244,8 @@ 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
|
||||
|
||||
@@ -4205,6 +4205,25 @@ 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;
|
||||
// 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.
|
||||
}
|
||||
|
||||
@@ -1383,10 +1383,22 @@ std::string DesignSketchTool::dimtype_title(DimType k) const {
|
||||
// Draw-then-edit dispatcher: mirror the Select-mode quote-click logic, but target the
|
||||
// freshly-drawn selection's PRIMARY value and use the tentative (clean-cancel) path for
|
||||
// scalar quotes. Runs after render_live_quotes, so the live-quote state is populated.
|
||||
// Why a draw-then-edit chain did not start. Four early returns can swallow it, and from outside
|
||||
// they are indistinguishable: the shape appears, no field opens, and nothing says which guard
|
||||
// fired. check-gui-click-edit.py reports that as "a value field opened (nothing did)" for every
|
||||
// tool at once, which reads like a total product failure and is not necessarily one.
|
||||
static void trace_autoedit(const char* why, size_t n)
|
||||
{
|
||||
if (!std::getenv("SNAPORCA_UXTRACE")) return;
|
||||
fprintf(stderr, "[UX] autoedit %s steps=%zu\n", why, n);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
void DesignSketchTool::open_primary_autoedit()
|
||||
{
|
||||
if (!on_inline_edit || m_awaiting_length) return; // no host, or a field is already open
|
||||
if (!m_active) return; // session ended before the deferred tick
|
||||
if (!on_inline_edit) { trace_autoedit("skip: no on_inline_edit host", 0); return; }
|
||||
if (m_awaiting_length) { trace_autoedit("skip: a field is already open", 0); return; }
|
||||
if (!m_active) { trace_autoedit("skip: session ended before the deferred tick", 0); return; }
|
||||
|
||||
// Build ONE ordered list of edit steps covering EVERY characteristic dimension of the
|
||||
// freshly-drawn shape — scalar quotes (constraint-based) AND geometric editors — so every
|
||||
@@ -1494,6 +1506,8 @@ void DesignSketchTool::open_primary_autoedit()
|
||||
[this, fi](double v){ set_rect_angle(fi, v); }, span(fi), "Angle" });
|
||||
}
|
||||
|
||||
trace_autoedit(m_autoedit_dims.empty() ? "built NO steps (no live quote matched)" : "opening",
|
||||
m_autoedit_dims.size());
|
||||
if (!m_autoedit_dims.empty()) {
|
||||
m_autoedit_dim_idx = 0;
|
||||
open_next_autoedit_dim();
|
||||
@@ -8857,6 +8871,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas)
|
||||
// next render_live_quotes(), so the deferred open still sees this frame's values.
|
||||
if (m_autoedit_pending) {
|
||||
m_autoedit_pending = false;
|
||||
trace_autoedit("pending -> deferring open", 0);
|
||||
wxGetApp().CallAfter([this] { open_primary_autoedit(); });
|
||||
}
|
||||
if (is_edit_op_mode())
|
||||
|
||||
@@ -90,6 +90,23 @@ constexpr bool keep_mapped_between_fields =
|
||||
false;
|
||||
#endif
|
||||
|
||||
// The click-edit contract, made observable. scripts/CAD/check-gui-click-edit.py grades a build
|
||||
// on these four lines and nothing else, because they are the only place the distinction it cares
|
||||
// about is visible: a field that is on screen but deaf commits its PREFILL, and every other
|
||||
// signal — the field drew, a constraint appeared, the solve succeeded — looks perfectly healthy
|
||||
// either way. `typed` is what the control actually held when Enter arrived; `prefill` is what
|
||||
// open() put there. typed == prefill on a commit means the keyboard never reached the field.
|
||||
//
|
||||
// stderr, one line, no buffering, only under SNAPORCA_UXTRACE: this is a test surface, not
|
||||
// logging, and it must cost nothing in a normal run.
|
||||
void trace_ux(const char* event, const std::string& title, const std::string& kv)
|
||||
{
|
||||
if (!std::getenv("SNAPORCA_UXTRACE")) return;
|
||||
fprintf(stderr, "[UX] %s title=%s%s%s\n", event, title.c_str(),
|
||||
kv.empty() ? "" : " ", kv.c_str());
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
void trace_inline_focus(wxFrame* frame, const std::string& title)
|
||||
{
|
||||
if (!std::getenv("SNAPORCA_KEYTRACE")) return;
|
||||
@@ -214,6 +231,8 @@ void SketchInlineEditor::open(const wxPoint& screen_px, double value,
|
||||
m_ctrl->SetFocus();
|
||||
m_ctrl->SelectAll();
|
||||
m_open = true;
|
||||
m_prefill = m_ctrl->GetValue();
|
||||
trace_ux("open", title, "prefill=" + std::string(m_prefill.utf8_str()));
|
||||
trace_inline_focus(m_frame, title);
|
||||
// Re-assert on the next tick too: the GL canvas can reclaim focus while it finishes
|
||||
// handling the click/render that opened us, so a single immediate SetFocus may be stolen.
|
||||
@@ -235,6 +254,8 @@ void SketchInlineEditor::do_commit()
|
||||
// Silence here read as a freeze: Enter did nothing, the text re-selected itself, and
|
||||
// nothing on screen said the value had been refused or what would be accepted. Every
|
||||
// other CAD names the problem in place; so do we.
|
||||
trace_ux("refused", std::string(m_title_text.utf8_str()),
|
||||
"typed=" + std::string(m_ctrl->GetValue().utf8_str()));
|
||||
flag_invalid(m_ctrl->GetValue().Strip(wxString::both).IsEmpty()
|
||||
? _L("Enter a number")
|
||||
: _L("Not a number"));
|
||||
@@ -242,6 +263,17 @@ void SketchInlineEditor::do_commit()
|
||||
m_ctrl->SelectAll();
|
||||
return;
|
||||
}
|
||||
{
|
||||
// LOCALE-INVARIANT on purpose. printf honours the app's locale, which on an Italian
|
||||
// desktop makes this "61,0000" — and the ladder that reads it does float(), which raises
|
||||
// on a comma and takes the whole run down one check after the first success. A machine
|
||||
// surface must not change shape with the user's regional settings.
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.4f", v);
|
||||
for (char* c = buf; *c; ++c) if (*c == ',') *c = '.';
|
||||
trace_ux("commit", std::string(m_title_text.utf8_str()),
|
||||
"typed=" + std::string(m_ctrl->GetValue().utf8_str()) + " value=" + buf);
|
||||
}
|
||||
auto cb = m_commit;
|
||||
m_open = false; // logically closed; whether it stays MAPPED is per-toolkit
|
||||
m_commit = nullptr;
|
||||
@@ -262,6 +294,53 @@ void SketchInlineEditor::do_commit()
|
||||
});
|
||||
}
|
||||
|
||||
// Deliver one character into the field without the window manager's permission.
|
||||
//
|
||||
// This is the whole content-based-routing idea in one function: the caller has already decided,
|
||||
// from the KEY ITSELF, that this keystroke belongs to a number field, so the field takes it —
|
||||
// whether or not any window manager saw fit to give it focus. FreeCAD's sketcher works exactly
|
||||
// this way and never asks who is focused.
|
||||
bool SketchInlineEditor::type_char(int key)
|
||||
{
|
||||
if (!m_open || m_ctrl == nullptr) return false;
|
||||
|
||||
if (key == WXK_BACK || key == WXK_DELETE) {
|
||||
long from = 0, to = 0;
|
||||
m_ctrl->GetSelection(&from, &to);
|
||||
if (from != to) {
|
||||
m_ctrl->Remove(from, to);
|
||||
} else {
|
||||
const long ip = m_ctrl->GetInsertionPoint();
|
||||
if (key == WXK_BACK) { if (ip > 0) m_ctrl->Remove(ip - 1, ip); }
|
||||
else { if (ip < m_ctrl->GetLastPosition()) m_ctrl->Remove(ip, ip + 1); }
|
||||
}
|
||||
clear_invalid();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The numeric keypad reports its own key codes, and a keypad is exactly what someone typing
|
||||
// dimensions all day uses.
|
||||
int ch = key;
|
||||
if (key >= WXK_NUMPAD0 && key <= WXK_NUMPAD9) ch = '0' + (key - WXK_NUMPAD0);
|
||||
else if (key == WXK_NUMPAD_DECIMAL) ch = '.';
|
||||
else if (key == WXK_NUMPAD_SUBTRACT) ch = '-';
|
||||
|
||||
const bool numeric = (ch >= '0' && ch <= '9') || ch == '-' || ch == '+' || ch == '.' || ch == ',';
|
||||
if (!numeric) return false;
|
||||
|
||||
// A decimal COMMA is normalised to a point on the way in: this field feeds a CAD kernel and
|
||||
// the rest of the file already promises a point whatever the locale (see fmt_value).
|
||||
if (ch == ',') ch = '.';
|
||||
|
||||
// WriteText replaces the current selection — and open() left the whole prefill selected, so
|
||||
// the FIRST character typed replaces the as-drawn value and the rest append. That is the
|
||||
// behaviour a person expects from a pre-selected field, obtained for free rather than
|
||||
// reimplemented.
|
||||
m_ctrl->WriteText(wxString(wxUniChar(ch)));
|
||||
clear_invalid();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SketchInlineEditor::cancel()
|
||||
{
|
||||
if (m_open) do_cancel();
|
||||
@@ -345,6 +424,7 @@ void SketchInlineEditor::clear_invalid()
|
||||
void SketchInlineEditor::do_cancel()
|
||||
{
|
||||
if (!m_open) return;
|
||||
trace_ux("cancel", std::string(m_title_text.utf8_str()), "");
|
||||
auto cb = m_cancel;
|
||||
close();
|
||||
if (cb) cb();
|
||||
|
||||
@@ -49,8 +49,23 @@ private:
|
||||
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.
|
||||
//
|
||||
// NOTE what this is NOT for any more: deciding whether the field may receive a character.
|
||||
// Whether a borderless top-level window is granted focus is the window manager's call and
|
||||
// differs per desktop — openbox grants it, mutter refuses it — so a routing rule built on
|
||||
// this question gives a different product on every machine. Routing is now by CONTENT
|
||||
// (DesignPanel's arbiter); this stays only to avoid forwarding a key the field is already
|
||||
// going to get for itself, which would type it twice.
|
||||
bool has_focus() const { return m_ctrl != nullptr && wxWindow::FindFocus() == m_ctrl; }
|
||||
|
||||
// Deliver one character into the field programmatically, bypassing focus entirely.
|
||||
// `key` is a wx key code: a printable character is inserted, WXK_BACK/WXK_DELETE edit.
|
||||
// Returns true if the field consumed it. Modelled on FreeCAD, whose sketcher decides where a
|
||||
// key belongs from the key itself and never queries focus:
|
||||
// DrawSketchKeyboardManager::detectKeyboardEventHandlingMode routes digits, '-', '.', ','
|
||||
// and Backspace/Delete to the on-view parameter and everything else to the view.
|
||||
bool type_char(int key);
|
||||
|
||||
private:
|
||||
void do_commit();
|
||||
void do_cancel();
|
||||
@@ -70,6 +85,7 @@ private:
|
||||
bool m_open{false};
|
||||
bool m_closing{false};
|
||||
wxString m_title_text; // the real title, restored after an error message
|
||||
wxString m_prefill; // what open() put in the field; see trace_ux
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
Reference in New Issue
Block a user