From d742b10c50e06341bb49aafc9bff6dd4a32823c6 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Wed, 3 Jun 2026 03:19:29 +0800 Subject: [PATCH] feat(automation): WxUiBackend marshaller + dump_tree + app_state --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/Automation/WxUiBackend.cpp | 172 ++++++++++++++++++++++ src/slic3r/GUI/Automation/WxUiBackend.hpp | 29 ++++ 3 files changed, 203 insertions(+) create mode 100644 src/slic3r/GUI/Automation/WxUiBackend.cpp create mode 100644 src/slic3r/GUI/Automation/WxUiBackend.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 847ba6b962..187c33bb74 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -243,6 +243,8 @@ set(SLIC3R_GUI_SOURCES GUI/Automation/AutomationRegistry.hpp GUI/Automation/ImGuiItemTable.cpp GUI/Automation/ImGuiItemTable.hpp + GUI/Automation/WxUiBackend.cpp + GUI/Automation/WxUiBackend.hpp GUI/I18N.cpp GUI/I18N.hpp GUI/DragDropPanel.cpp diff --git a/src/slic3r/GUI/Automation/WxUiBackend.cpp b/src/slic3r/GUI/Automation/WxUiBackend.cpp new file mode 100644 index 0000000000..059bad6253 --- /dev/null +++ b/src/slic3r/GUI/Automation/WxUiBackend.cpp @@ -0,0 +1,172 @@ +#include "WxUiBackend.hpp" +#include "AutomationRegistry.hpp" +#include "ImGuiItemTable.hpp" +#include "JsonRpcDispatcher.hpp" // for kErrGuiBusy and friends +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" +#include "slic3r/GUI/Plater.hpp" +#include "slic3r/GUI/GLCanvas3D.hpp" +#include "libslic3r/Model.hpp" + +#include +#include +#include // wxDialog::IsModal +#include // wxGLCanvas -> wxWindow* conversion +#include // wxTextEntry +#include +#include + +#include +#include +#include +#include +#include + +namespace Slic3r { namespace GUI { namespace Automation { + +// Run `fn` on the GUI thread, block until it returns or the timeout elapses. +// Throws AutomationError(kErrGuiBusy) on timeout. std::promise is move-only, so we +// hold it via shared_ptr to satisfy CallAfter's copyable-functor requirement. +template +static auto run_on_gui(int timeout_ms, Fn&& fn) -> decltype(fn()) { + using R = decltype(fn()); + auto prom = std::make_shared>(); + auto fut = prom->get_future(); + wxGetApp().CallAfter([prom, fn = std::forward(fn)]() mutable { + try { + if constexpr (std::is_void_v) { fn(); prom->set_value(); } + else { prom->set_value(fn()); } + } catch (...) { prom->set_exception(std::current_exception()); } + }); + if (fut.wait_for(std::chrono::milliseconds(timeout_ms)) != std::future_status::ready) + throw AutomationError(kErrGuiBusy, "GUI thread timed out"); + return fut.get(); +} + +namespace { +std::string wx_class_name(const wxWindow* w) { + const wxClassInfo* ci = w->GetClassInfo(); + std::string name = ci ? std::string(wxString(ci->GetClassName()).ToUTF8()) : "wxWindow"; + if (name.rfind("wx", 0) == 0 && name.size() > 2) name = name.substr(2); + return name; +} + +std::string wx_value_of(wxWindow* w, bool& has_value) { + has_value = false; + if (auto* tc = dynamic_cast(w)) { has_value = true; return std::string(tc->GetValue().ToUTF8()); } + if (auto* ch = dynamic_cast(w)) { has_value = true; return std::string(ch->GetStringSelection().ToUTF8()); } + if (auto* cb = dynamic_cast(w)) { has_value = true; return cb->GetValue() ? "true" : "false"; } + return {}; +} + +void build_node(wxWindow* w, UiNode& node, const std::string& parent_path, + int sibling_index, const DumpOptions& opts, int depth) { + node.backend = BackendKind::Wx; + node.klass = wx_class_name(w); + node.id = automation_id_of(w); + node.path = parent_path.empty() + ? node.klass + : parent_path + "/" + node.klass + "[" + std::to_string(sibling_index) + "]"; + node.label = std::string(w->GetLabel().ToUTF8()); + node.enabled = w->IsEnabled(); + node.visible = w->IsShownOnScreen(); + node.value = wx_value_of(w, node.has_value); + node.handle = reinterpret_cast(w); + const wxRect r = w->GetScreenRect(); + node.rect = { r.x, r.y, r.width, r.height }; + + if (opts.max_depth >= 0 && depth >= opts.max_depth) return; + int idx = 0; + for (wxWindow* child : w->GetChildren()) { + if (opts.visible_only && !child->IsShownOnScreen()) { ++idx; continue; } + UiNode cn; + build_node(child, cn, node.path, idx, opts, depth + 1); + node.children.push_back(std::move(cn)); + ++idx; + } +} + +// Map recorded ImGui items (display coords) to screen coords using the 3D canvas +// client origin + DPI scale, then append them as flat children under the root. +void append_imgui_nodes(UiNode& root) { + Plater* plater = wxGetApp().plater(); + if (plater == nullptr) return; + GLCanvas3D* canvas3d = plater->get_current_canvas3D(); + if (canvas3d == nullptr) return; + wxWindow* canvas = canvas3d->get_wxglcanvas(); + if (canvas == nullptr) return; + const wxPoint origin = canvas->ClientToScreen(wxPoint(0, 0)); + double scale = canvas->GetContentScaleFactor(); + if (scale <= 0.0) scale = 1.0; + const auto frame = ImGuiItemTable::instance().snapshot(); + for (const auto& it : frame.items) { + UiNode n; + n.backend = BackendKind::ImGui; + n.klass = it.type; + n.label = it.label; + n.path = "ImGui/" + it.window_name + "/" + it.label; + n.id = n.path; // imgui items use their path as id in v1 + n.enabled = it.enabled; + n.visible = true; + n.has_value = it.has_value; + n.value = it.value; + n.rect = { origin.x + int(it.x / scale), origin.y + int(it.y / scale), + int(it.w / scale), int(it.h / scale) }; + root.children.push_back(std::move(n)); + } +} +} // namespace + +void WxUiBackend::refresh_ui() { + run_on_gui(m_gui_timeout_ms, [] { + // Force a fresh ImGui frame so transient items are recorded, then flush + // pending events so the latest frame is the one we read. + if (Plater* p = wxGetApp().plater()) { + if (GLCanvas3D* canvas = p->get_current_canvas3D()) { + canvas->set_as_dirty(); + canvas->render(); + } + } + wxGetApp().Yield(); + }); +} + +UiNode WxUiBackend::dump_tree(const DumpOptions& opts) { + return run_on_gui(m_gui_timeout_ms, [&opts]() -> UiNode { + wxWindow* root_win = nullptr; + if (opts.root) root_win = window_for_automation_id(*opts.root); + if (root_win == nullptr) + root_win = static_cast(wxGetApp().mainframe); + UiNode root; + if (root_win) build_node(root_win, root, {}, 0, opts, 0); + if (opts.include_imgui) append_imgui_nodes(root); + return root; + }); +} + +AppState WxUiBackend::app_state() { + return run_on_gui(m_gui_timeout_ms, []() -> AppState { + AppState s; + MainFrame* mf = wxGetApp().mainframe; + Plater* p = wxGetApp().plater(); + if (mf) { + // best-effort: MainFrame has no public getter for the selected top tab + // (m_tabpanel is private), so report the frame title for now. + s.active_tab = std::string(mf->GetTitle().ToUTF8()); + s.foreground = mf->IsActive(); + } + if (p) { + s.project_loaded = !p->model().objects.empty(); + s.slicing = p->is_background_process_slicing(); + } + if (wxWindow* top = wxGetActiveWindow()) + if (auto* dlg = dynamic_cast(top)) + if (dlg != static_cast(mf) && dlg->IsModal()) + s.modal_dialog = std::string(dlg->GetTitle().ToUTF8()); + return s; + }); +} + +// click/type/keys/screenshots implemented in Task 16. + +}}} // namespace Slic3r::GUI::Automation diff --git a/src/slic3r/GUI/Automation/WxUiBackend.hpp b/src/slic3r/GUI/Automation/WxUiBackend.hpp new file mode 100644 index 0000000000..3dc6aa056e --- /dev/null +++ b/src/slic3r/GUI/Automation/WxUiBackend.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "IUiBackend.hpp" + +namespace Slic3r { namespace GUI { namespace Automation { + +// Real backend. Every public method marshals its work onto the GUI thread via +// wxGetApp().CallAfter + a std::future with a per-call timeout (error kErrGuiBusy on +// timeout). Walks the wxWindow tree, reads the ImGui item table, drives +// wxUIActionSimulator, captures screenshots. +class WxUiBackend : public IUiBackend { +public: + explicit WxUiBackend(int gui_timeout_ms = 5000) : m_gui_timeout_ms(gui_timeout_ms) {} + + void refresh_ui() override; + UiNode dump_tree(const DumpOptions& opts) override; + AppState app_state() override; + bool click(const UiNode& node, MouseButton button, bool dbl, + const std::vector& modifiers) override; + bool type_text(const std::string& text) override; + bool send_keys(const std::vector& chords) override; + PngImage screenshot_window(const UiNode* target) override; + PngImage screenshot_viewport3d(std::optional plate, std::optional width, + std::optional height) override; + +private: + int m_gui_timeout_ms; +}; + +}}} // namespace Slic3r::GUI::Automation