#include "slic3r/GUI/CAD/SketchInlineEditor.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/I18N.hpp" #include "libslic3r/Color.hpp" #include #include // BringWindowToDisplayFront / GetCurrentWindow #include #include #include #include namespace Slic3r { namespace GUI { namespace { // Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel, // and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error. // Parsing accepts either separator because a keyboard's numeric pad may only offer one. std::string fmt_value(double v, int digits = 2) { char fmt[16]; std::snprintf(fmt, sizeof(fmt), "%%.%df", digits); char buf[64]; std::snprintf(buf, sizeof(buf), fmt, v); for (char* c = buf; *c; ++c) if (*c == ',') *c = '.'; return std::string(buf); } bool parse_value(const char* text, double& out) { if (text == nullptr) return false; std::string t(text); for (char& c : t) if (c == ',') c = '.'; // strtod, not std::stod: no exceptions, and `end` tells us whether the WHOLE field was a // number. "12mm" must be refused, not silently read as 12. const char* b = t.c_str(); char* end = nullptr; const double v = std::strtod(b, &end); if (end == b) return false; while (*end == ' ' || *end == '\t') ++end; if (*end != '\0') return false; out = v; return true; } // One machine-readable line per event of the click-edit contract, for the UX check that runs // after every build (scripts/CAD/check-gui-click-edit.py). Deliberately NOT the same switch as // ORCA_CAD_KEYTRACE: that one is a debugging firehose, this one is an assertion surface and its // format is a contract the script parses. // // The pair that matters is `open` vs `commit`: the check always types a value DIFFERENT from the // prefill, so a field that is on screen but not editable commits its prefill and the two lines // disagree. A focus flag cannot show that — it read 0 even when typing worked — but the number // the user actually gets can. void ux_trace(const char* event, const std::string& title, const std::string& detail) { if (!std::getenv("ORCA_CAD_UXTRACE")) return; std::fprintf(stderr, "[UX] %s title=%s %s\n", event, title.c_str(), detail.c_str()); std::fflush(stderr); } } // namespace void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std::string& title, std::function on_commit, std::function on_cancel) { m_anchor = canvas_px; m_title = title; m_err.clear(); m_commit = std::move(on_commit); m_cancel = std::move(on_cancel); const std::string v = fmt_value(value); std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str()); m_open = true; // ImGui takes keyboard focus for one frame on request; asking on the frame the field first // appears is what makes typing land without a click. There is no window manager to consult. m_focus_pending = true; ux_trace("open", m_title, "prefill=" + v); } void SketchInlineEditor::close() { m_open = false; m_focus_pending = false; m_commit = nullptr; m_cancel = nullptr; m_err.clear(); } void SketchInlineEditor::cancel() { if (m_open) do_cancel(); } void SketchInlineEditor::commit() { if (m_open) do_commit(); } void SketchInlineEditor::do_cancel() { ux_trace("cancel", m_title, ""); auto cb = m_cancel; close(); if (cb) cb(); } void SketchInlineEditor::do_commit() { double v = 0.0; if (!parse_value(m_buf, v)) { // Refusing input in silence is indistinguishable from the app having frozen: the field // just sits there and the user has no idea what it wants. Say so in the title line and // keep editing. ux_trace("refused", m_title, std::string("typed=") + m_buf); m_err = (m_buf[0] == '\0') ? _u8L("Enter a number") : _u8L("Not a number"); m_focus_pending = true; return; } ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4)); auto cb = m_commit; close(); // AFTER close(): the callback may open the next queued dimension (a rectangle queues Width // then Height), and doing that into a field that still believes it is open would drop the // second one's prefill on the floor. if (cb) cb(v); } bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale) { if (!m_open) return false; ImGuiWrapper::push_common_window_style(scale); imgui.set_next_window_pos((float) m_anchor.x, (float) m_anchor.y, ImGuiCond_Always, 0.5f, 0.5f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f); // NoInputs is what every other sketch overlay sets and is exactly what this one must not: // it is the only overlay in the tab that the user types into. imgui.begin(std::string("##sketchvalue"), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); if (!m_title.empty() || !m_err.empty()) { if (m_err.empty()) { imgui.text(m_title); } else { ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(ColorRGBA(0.91f, 0.42f, 0.42f, 1.0f))); imgui.text(m_err); ImGui::PopStyleColor(); } } if (m_focus_pending) { ImGui::SetKeyboardFocusHere(); m_focus_pending = false; } ImGui::PushItemWidth(90.0f * scale); // EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is // replaced by the first digit typed, which is what "pre-selected" meant when this was a // wxTextCtrl and is what makes typing a value a single gesture. const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_CharsDecimal); // MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the // keyboard and whether our widget is the active one. "Typing does not arrive" has two very // different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never // processes ImGui's queued characters) versus frames that run while the input is not active — // and they are indistinguishable from outside. if (std::getenv("ORCA_CAD_UXTRACE")) { const ImGuiIO& io = ImGui::GetIO(); std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n", m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard, (int) ImGui::IsItemActive(), m_buf); std::fflush(stderr); } ImGui::PopItemWidth(); imgui.end(); ImGui::PopStyleVar(); ImGuiWrapper::pop_common_window_style(); // Keep the frames coming while the field is up — see request_frame's note in the header. if (m_open && request_frame) request_frame(); // Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that // must not happen inside this frame's window. if (entered) do_commit(); else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) do_cancel(); return true; } }} // namespace Slic3r::GUI