#include "PluginHostUi.hpp" #include "slic3r/plugin/PluginAuditManager.hpp" #include "slic3r/plugin/PythonInterpreter.hpp" // PythonGILState #include "slic3r/plugin/PluginFsUtils.hpp" // json_to_py / py_to_json #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace py = pybind11; using json = nlohmann::json; namespace Slic3r { namespace { // -------------------------------------------------------------------------- // GIL-safe holder for a Python callable. A std::function that captured a bare // py::object could be destroyed on the main thread without the GIL (a dialog // teardown), which would Py_DECREF unsafely. Wrapping the callable here means // the GIL is acquired exactly when the last reference is released, on any thread. // -------------------------------------------------------------------------- struct GilSafeCallable { py::object fn; explicit GilSafeCallable(py::object f) : fn(std::move(f)) {} ~GilSafeCallable() { if (fn) { PythonGILState gil; if (gil) fn = py::object(); else (void) fn.release(); } } }; using CallablePtr = std::shared_ptr; CallablePtr make_holder(py::object obj) { if (!obj || obj.is_none()) return nullptr; return std::make_shared(std::move(obj)); } // Adapt a Python callable to a GUI message handler that acquires the GIL and // swallows/logs exceptions (a raising handler must not escape into wx events). GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message) { CallablePtr holder = make_holder(std::move(on_message)); if (!holder) return nullptr; return [holder](const json& data) { PythonGILState gil; if (!gil) return; try { holder->fn(json_to_py(data)); } catch (py::error_already_set& e) { BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_message handler raised: " << e.what(); PyErr_Clear(); } }; } GUI::PluginWebDialog::SubmitHandler make_submit_adapter(py::object on_submit) { CallablePtr holder = make_holder(std::move(on_submit)); if (!holder) return nullptr; return [holder](const json& data) { PythonGILState gil; if (!gil) return; try { holder->fn(json_to_py(data)); } catch (py::error_already_set& e) { BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_submit handler raised: " << e.what(); PyErr_Clear(); } }; } // -------------------------------------------------------------------------- // Registry of live plugin UI resources. Keyed by an opaque id; tracks the // owning plugin so all of a plugin's UI can be torn down on unload. // -------------------------------------------------------------------------- class UiRegistry { public: static UiRegistry& instance() { static UiRegistry r; return r; } int reserve_id() { std::lock_guard lk(m_mtx); return m_next_id++; } void bind(int id, wxWindow* window, const std::string& plugin_key) { std::lock_guard lk(m_mtx); m_resources[id] = window; m_owners[id] = plugin_key; } void remove(int id) { std::lock_guard lk(m_mtx); m_resources.erase(id); m_owners.erase(id); } template T* get_as(int id) { std::lock_guard lk(m_mtx); auto it = m_resources.find(id); if (it == m_resources.end()) return nullptr; return dynamic_cast(it->second); } bool is_open(int id) { std::lock_guard lk(m_mtx); return m_resources.count(id) > 0; // presence only; no pointer deref -> thread-safe } std::vector take_for_plugin(const std::string& plugin_key) { std::lock_guard lk(m_mtx); std::vector out; for (auto it = m_owners.begin(); it != m_owners.end();) { if (it->second == plugin_key) { auto rit = m_resources.find(it->first); if (rit != m_resources.end()) { out.push_back(rit->second); m_resources.erase(rit); } it = m_owners.erase(it); } else { ++it; } } return out; } private: std::mutex m_mtx; std::unordered_map m_resources; std::unordered_map m_owners; int m_next_id{1}; }; // -------------------------------------------------------------------------- // Run a (pure C++/wx) callable on the main/UI thread, blocking the caller until // it completes, with the GIL released across the wait. If already on the main // thread, run inline (also with the GIL released so other Python threads run). // -------------------------------------------------------------------------- template auto run_on_ui_blocking(Fn&& fn) -> std::invoke_result_t { using R = std::invoke_result_t; if (wxTheApp == nullptr) throw std::runtime_error("OrcaSlicer application is not initialized"); if (wxIsMainThread()) { py::gil_scoped_release nogil; return fn(); } std::promise prom; std::future fut = prom.get_future(); py::gil_scoped_release nogil; GUI::wxGetApp().CallAfter([&prom, &fn]() { try { if constexpr (std::is_void_v) { fn(); prom.set_value(); } else { prom.set_value(fn()); } } catch (...) { prom.set_exception(std::current_exception()); } }); return fut.get(); } wxWindow* ui_parent() { return wxTheApp == nullptr ? nullptr : dynamic_cast(GUI::wxGetApp().mainframe); } // -------------------------------------------------------------------------- // orca.host.ui.message // -------------------------------------------------------------------------- long message_style(const std::string& buttons, const std::string& icon) { long style = wxOK; if (buttons == "ok_cancel") style = wxOK | wxCANCEL; else if (buttons == "yes_no") style = wxYES_NO; else if (buttons == "yes_no_cancel") style = wxYES_NO | wxCANCEL; if (icon == "warning") style |= wxICON_WARNING; else if (icon == "error") style |= wxICON_ERROR; else if (icon == "question") style |= wxICON_QUESTION; else style |= wxICON_INFORMATION; return style; } std::string button_to_string(int rc) { switch (rc) { case wxID_OK: return "ok"; case wxID_YES: return "yes"; case wxID_NO: return "no"; default: return "cancel"; } } std::string ui_message(const std::string& text, const std::string& title, const std::string& buttons, const std::string& icon) { const long style = message_style(buttons, icon); return run_on_ui_blocking([&]() -> std::string { GUI::MessageDialog dlg(nullptr, wxString::FromUTF8(text), wxString::FromUTF8(title), style); return button_to_string(dlg.ShowModal()); }); } // -------------------------------------------------------------------------- constexpr long WINDOW_MODELESS = 0L; constexpr long WINDOW_MODAL = 1L << 0; constexpr long PLUGIN_WX_STYLE = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER; // orca.host.ui.create_window + UiWindow handle // -------------------------------------------------------------------------- struct UiWindowHandle { int id{0}; }; struct UiProgressHandle { int id{0}; }; py::object ui_create_window(const std::string& html, const std::string& title, int width, int height, py::object on_message, py::object on_close, long style, py::object on_submit) { auto msg_adapter = make_message_adapter(std::move(on_message)); auto submit_adapter = make_submit_adapter(std::move(on_submit)); CallablePtr close_holder = make_holder(std::move(on_close)); const std::string plugin_key = PluginAuditManager::instance().current_plugin(); const int w = width > 0 ? width : 820; const int h = height > 0 ? height : 600; if ((style & ~WINDOW_MODAL) != 0) throw std::invalid_argument("unsupported orca.host.ui.create_window style flags"); const bool modal = (style & WINDOW_MODAL) != 0; if (wxTheApp == nullptr) throw std::runtime_error("OrcaSlicer application is not initialized"); // The whole wxWindow/webview creation is deferred to a clean main-loop iteration. // The WebKit backends deliver script messages synchronously from inside the native // callback (GTK: HandleWindowEvent in wxgtk_webview_webkit_script_message_received, // wx src/gtk/webview_webkit2.cpp; macOS: ProcessWindowEvent in the // WKScriptMessageHandler delegate, src/osx/webview_webkit.mm), so a script-triggered // create_window otherwise runs with the plugins-dialog webview's signal/delegate // frame still on the stack — creating and presenting a second webview window from // there crashed on Linux (observed at a since-removed Raise(): gtk_window_present // right after a Show() whose real map can still be deferred behind the X11 // frame-extents handshake, wx src/gtk/toplevel.cpp). WebView2 already queues script // messages (AddPendingEvent), so on Windows this deferral merely matches wx's own // delivery model. // // The id is pre-bound with a null window so the returned handle is live at once: // is_open() keys on registry presence, post()/close() are CallAfter-marshaled and // FIFO-ordered after the creation below, and a plugin teardown claims the pending // id (take_for_plugin), which the creation detects and skips — no orphan window. const int new_id = UiRegistry::instance().reserve_id(); UiRegistry::instance().bind(new_id, nullptr, plugin_key); GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, w, h, msg_adapter = std::move(msg_adapter), submit_adapter = std::move(submit_adapter), close_holder = std::move(close_holder), modal]() mutable { // Torn down (plugin unload / app shutdown) before the window materialized. if (!UiRegistry::instance().is_open(new_id)) return; // Plugin's on_close: fired only on a user/JS-initiated close (not forced // teardown), while the dialog is alive. Empty if the plugin passed None. GUI::PluginWebDialog::CloseHandler on_close; if (close_holder) { on_close = [close_holder]() { PythonGILState gil; if (!gil) return; try { close_holder->fn(); } catch (py::error_already_set& e) { BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what(); PyErr_Clear(); } }; } // Registry cleanup: GIL-free, runs from the dialog destructor on every path. auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); }; auto* dlg = new GUI::PluginWebDialog(ui_parent(), wxString::FromUTF8(title), html, wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter), std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE); UiRegistry::instance().bind(new_id, dlg, plugin_key); if (modal) { dlg->ShowModal(); // Plugin teardown may already have removed and destroyed this dialog // while its modal loop was running. if (UiRegistry::instance().is_open(new_id)) dlg->Destroy(); } else { dlg->Show(); } }); return py::cast(UiWindowHandle{new_id}); } void handle_post(int id, py::object data) { if (wxTheApp == nullptr) return; json j = py_to_json(data); // GIL held (binding body) GUI::wxGetApp().CallAfter([id, j = std::move(j)]() { auto* d = UiRegistry::instance().get_as(id); GUI::PluginWebDialog::post_message(d, j); }); } void handle_close(int id) { if (wxTheApp == nullptr) return; GUI::wxGetApp().CallAfter([id]() { auto* d = UiRegistry::instance().get_as(id); GUI::PluginWebDialog::request_close(d); }); } UiProgressHandle ui_create_progress_dialog(const std::string& title, const std::string& message, int maximum, int style) { const std::string plugin_key = PluginAuditManager::instance().current_plugin(); const int max_value = maximum > 0 ? maximum : 100; return run_on_ui_blocking([&]() -> UiProgressHandle { const int new_id = UiRegistry::instance().reserve_id(); // Registry cleanup: GIL-free, runs from the dialog destructor on every path. auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); }; auto* dlg = GUI::PluginProgressDialog::create_dialog(ui_parent(), wxString::FromUTF8(title), wxString::FromUTF8(message), max_value, style, std::move(on_destroyed)); UiRegistry::instance().bind(new_id, dlg, plugin_key); return UiProgressHandle{new_id}; }); } UiProgressHandle* new_progress_dialog(const std::string& title, const std::string& message, int maximum, int style) { return new UiProgressHandle(ui_create_progress_dialog(title, message, maximum, style)); } bool progress_is_open(int id) { return UiRegistry::instance().is_open(id); } bool progress_pulse(int id, const std::string& message) { return run_on_ui_blocking([&]() { auto* d = UiRegistry::instance().get_as(id); return GUI::PluginProgressDialog::pulse(d, wxString::FromUTF8(message)); }); } bool progress_update(int id, int value, const std::string& message) { return run_on_ui_blocking([&]() { auto* d = UiRegistry::instance().get_as(id); return GUI::PluginProgressDialog::update(d, value, wxString::FromUTF8(message)); }); } void progress_start_pulse(int id, int interval_ms, const std::string& message) { run_on_ui_blocking([&]() { auto* d = UiRegistry::instance().get_as(id); GUI::PluginProgressDialog::start_pulse(d, interval_ms, wxString::FromUTF8(message)); }); } void progress_stop_pulse(int id) { run_on_ui_blocking([&]() { auto* d = UiRegistry::instance().get_as(id); GUI::PluginProgressDialog::stop_pulse(d); }); } void progress_close(int id) { run_on_ui_blocking([&]() { auto* d = UiRegistry::instance().get_as(id); GUI::PluginProgressDialog::request_close(d); }); } } // namespace void PluginHostUi::RegisterBindings(pybind11::module_& host) { auto ui = host.def_submodule( "ui", "Host UI: native dialogs and interactive HTML windows. Calls run on the main/UI " "thread (marshaled from the plugin thread). See the plugin docs for the window.orca bridge. " "Do not call these from a slicing pipeline hook (SlicingPipeline capability): that hook runs " "on the slicing worker thread, which the UI thread can itself be blocked waiting on, so a " "marshaled UI call from there can deadlock the application."); ui.def("message", &ui_message, py::arg("text"), py::arg("title") = "OrcaSlicer", py::arg("buttons") = "ok", py::arg("icon") = "info", "Show a native modal message box; returns the clicked button id " "(\"ok\"/\"cancel\"/\"yes\"/\"no\"). buttons: \"ok\"|\"ok_cancel\"|\"yes_no\"|\"yes_no_cancel\"; " "icon: \"info\"|\"warning\"|\"error\"|\"question\"."); ui.attr("PD_APP_MODAL") = py::int_(wxPD_APP_MODAL); ui.attr("PD_AUTO_HIDE") = py::int_(wxPD_AUTO_HIDE); ui.attr("PD_CAN_ABORT") = py::int_(wxPD_CAN_ABORT); ui.attr("PD_CAN_SKIP") = py::int_(wxPD_CAN_SKIP); ui.attr("PD_ELAPSED_TIME") = py::int_(wxPD_ELAPSED_TIME); ui.attr("PD_ESTIMATED_TIME") = py::int_(wxPD_ESTIMATED_TIME); ui.attr("PD_REMAINING_TIME") = py::int_(wxPD_REMAINING_TIME); ui.attr("WINDOW_MODELESS") = py::int_(WINDOW_MODELESS); ui.attr("WINDOW_MODAL") = py::int_(WINDOW_MODAL); py::class_(ui, "UiWindow", "Handle to a plugin HTML window or modal dialog created by create_window().") .def_property_readonly("id", [](const UiWindowHandle& h) { return h.id; }) .def( "post", [](const UiWindowHandle& h, py::object data) { handle_post(h.id, std::move(data)); }, py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).") .def( "close", [](const UiWindowHandle& h) { handle_close(h.id); }, "Close the window (fires on_close).") .def( "is_open", [](const UiWindowHandle& h) { return UiRegistry::instance().is_open(h.id); }, "Return True while the window is open."); ui.def("create_window", &ui_create_window, py::arg("html"), py::arg("title") = "OrcaSlicer", py::arg("width") = 820, py::arg("height") = 600, py::arg("on_message") = py::none(), py::arg("on_close") = py::none(), py::arg("style") = WINDOW_MODELESS, py::arg("on_submit") = py::none(), "Open a persistent HTML window or modal dialog and return a UiWindow. style is WINDOW_MODELESS " "or WINDOW_MODAL. on_message(data) is called on the UI thread when the page posts; on_submit(data) " "is called when the page submits; offload heavy work to a thread and push results back with window.post()."); py::class_(ui, "ProgressDialog", "Handle to a native progress dialog.") .def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE) .def_property_readonly("id", [](const UiProgressHandle& h) { return h.id; }) .def( "pulse", [](const UiProgressHandle& h, const std::string& message) { return progress_pulse(h.id, message); }, py::arg("message") = "", "Pulse the dialog gauge; returns False if the dialog is closed or cancelled.") .def( "update", [](const UiProgressHandle& h, int value, const std::string& message) { return progress_update(h.id, value, message); }, py::arg("value"), py::arg("message") = "", "Set the dialog progress value; returns False if the dialog is closed or cancelled.") .def( "start_pulse", [](const UiProgressHandle& h, int interval_ms, const std::string& message) { progress_start_pulse(h.id, interval_ms, message); }, py::arg("interval_ms") = 100, py::arg("message") = "", "Start periodic pulsing.") .def( "stop_pulse", [](const UiProgressHandle& h) { progress_stop_pulse(h.id); }, "Stop periodic pulsing.") .def( "close", [](const UiProgressHandle& h) { progress_close(h.id); }, "Close the progress dialog.") .def( "is_open", [](const UiProgressHandle& h) { return progress_is_open(h.id); }, "Return True while this progress dialog is registered.") .def("__enter__", [](UiProgressHandle& h) -> UiProgressHandle& { return h; }, py::return_value_policy::reference_internal) .def("__exit__", [](const UiProgressHandle& h, py::object, py::object, py::object) { progress_close(h.id); return false; }); ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"), py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE, "Create a native progress dialog and return a ProgressDialog handle."); } void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key) { if (wxTheApp == nullptr) return; auto teardown = [plugin_key]() { // Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on // forced teardown (intended); the resource destructor still cleans the registry. for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) { if (auto* dialog = dynamic_cast(window)) GUI::PluginWebDialog::destroy_for_plugin(dialog); else if (window != nullptr) window->Destroy(); } }; if (wxIsMainThread()) teardown(); else GUI::wxGetApp().CallAfter(teardown); } } // namespace Slic3r