From 7c287018942f8a549425013f03c66f5608ac01d8 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Tue, 15 Sep 2026 18:52:58 +0800 Subject: [PATCH] Add a Dockable HTML Panel API for Plugins orca.host.ui.create_dock_panel() hosts plugin HTML in a pane of the Plater's dock manager, next to the sidebar, and returns a UiDockPanel handle (post/show/hide/close/is_open). The panel reuses the window.orca bridge of plugin windows, restores its position and size from the saved window layout, hides with the Plater off the Prepare and Preview tabs when floating, and is closed with its plugin. Includes a sample plugin (sandboxes/orca_dock_panel_plugin_any.py) and binding and layout-helper tests in slic3rutils. --- sandboxes/orca_dock_panel_plugin_any.py | 146 ++++++++++++++++ src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/Plater.cpp | 127 ++++++++++++++ src/slic3r/GUI/Plater.hpp | 8 + src/slic3r/GUI/PluginDockPanel.cpp | 170 +++++++++++++++++++ src/slic3r/GUI/PluginDockPanel.hpp | 63 +++++++ src/slic3r/GUI/PluginWebDialog.cpp | 6 +- src/slic3r/GUI/PluginWebDialog.hpp | 6 + src/slic3r/GUI/Widgets/WebViewHostDialog.cpp | 2 + src/slic3r/GUI/Widgets/WebViewHostDialog.hpp | 2 + src/slic3r/plugin/host/PluginHostUi.cpp | 144 +++++++++++++--- tests/slic3rutils/test_plugin_host_api.cpp | 42 +++++ 12 files changed, 697 insertions(+), 21 deletions(-) create mode 100644 sandboxes/orca_dock_panel_plugin_any.py create mode 100644 src/slic3r/GUI/PluginDockPanel.cpp create mode 100644 src/slic3r/GUI/PluginDockPanel.hpp diff --git a/sandboxes/orca_dock_panel_plugin_any.py b/sandboxes/orca_dock_panel_plugin_any.py new file mode 100644 index 0000000000..bb6a8143b4 --- /dev/null +++ b/sandboxes/orca_dock_panel_plugin_any.py @@ -0,0 +1,146 @@ +# /// script +# requires-python = ">=3.12" +# +# [tool.orcaslicer.plugin] +# name = "Dock Panel Demo" +# description = "Opens a dockable panel beside the 3D view that lists the objects on the plate." +# author = "OrcaSlicer" +# version = "0.0.1" +# /// +"""Dock Panel Demo -- orca.host.ui.create_dock_panel(). + +Run it from the Plugins dialog. It opens an HTML panel docked on the right of the 3D view, in the +same dock area as the sidebar. Drag its caption to dock it on another side (or float it, where the +platform allows), hide it from the page and run the plugin again to bring it back, or close it with +its close button or from the page. + + page --orca.postMessage({command: 'refresh'})--> plugin.on_message() + page --orca.postMessage({command: 'hide'})--> plugin.on_message() -> panel.hide() + page --orca.close()--> panel closes, plugin.on_close() + plugin --panel.post({command: 'objects', ...})--> page (orca.onMessage) +""" +import orca + +PAGE = """ + + +

Objects on the plate

+

Docked beside the 3D view. Drag the caption to move it.

+ +
+ + + +
+ + + + +
NamePartsCopies
+

Waiting for the plugin...

+ + +""" + + +def plate_objects(): + try: + model = orca.host.model() + except RuntimeError as error: + return {"command": "objects", "error": str(error)} + return { + "command": "objects", + "objects": [ + {"name": obj.name or "(unnamed)", "volumes": obj.volume_count(), "instances": obj.instance_count()} + for obj in model.objects() + ], + } + + +class DockPanelDemo(orca.script.ScriptPluginCapabilityBase): + panel = None + + def get_name(self): + return "Dock Panel Demo" + + def execute(self): + # The capability instance lives as long as the plugin, so a second run finds the open panel. + if self.panel is not None and self.panel.is_open(): + self.panel.show() + return orca.ExecutionResult.success("Dock Panel Demo is already open.") + self.panel = orca.host.ui.create_dock_panel( + html=PAGE, + title="Dock Panel Demo", + dock="right", + width=320, + height=480, + on_message=self.on_message, + on_close=self.on_close, + ) + return orca.ExecutionResult.success("Dock Panel Demo opened.") + + # Called on the UI thread when the page posts. + def on_message(self, message): + command = (message or {}).get("command") + if command == "refresh": + self.panel.post(plate_objects()) + elif command == "hide": + self.panel.hide() + + def on_close(self): + self.panel = None + + +@orca.plugin +class DockPanelDemoPlugin(orca.base): + def register_capabilities(self): + orca.register_capability(DockPanelDemo) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 691675a452..97e367faab 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -139,6 +139,8 @@ set(SLIC3R_GUI_SOURCES GUI/PluginProgressDialog.hpp GUI/PluginWebDialog.cpp GUI/PluginWebDialog.hpp + GUI/PluginDockPanel.cpp + GUI/PluginDockPanel.hpp GUI/DragCanvas.cpp GUI/DragCanvas.hpp GUI/EditGCodeDialog.cpp diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0fdb9dcd94..be472b198a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -86,6 +86,7 @@ #ifdef __WXGTK__ #include "LinuxDisplayBackend.hpp" #endif +#include "PluginDockPanel.hpp" #include "GUI_Utils.hpp" #include "GUI_Factories.hpp" #include "wxExtensions.hpp" @@ -6720,6 +6721,14 @@ struct Plater::priv // GUI elements AuiMgr m_aui_mgr; + // Live plugin panes. `on_close` runs when the user closes one from its close button; `shown` is + // what the plugin asked for. + struct PluginPane + { + std::function on_close; + bool shown{true}; + }; + std::map m_plugin_panes; wxString m_default_window_layout; wxPanel* current_panel{ nullptr }; std::vector panels; @@ -6891,6 +6900,11 @@ struct Plater::priv void update_sidebar(bool force_update = false); void reset_window_layout(); Sidebar::DockingState get_sidebar_docking_state(); + void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock, + const wxSize& size, std::function on_close); + void remove_plugin_pane(wxWindow* window); + void show_plugin_pane(wxWindow* window, bool show); + bool plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const; bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); } @@ -7471,6 +7485,18 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) panel_3d->SetSizer(panel_sizer); m_aui_mgr.AddPane(panel_3d, wxAuiPaneInfo().Name("main").CenterPane().PaneBorder(false)); + q->Bind(wxEVT_AUI_PANE_CLOSE, [this](wxAuiManagerEvent& evt) { + const wxAuiPaneInfo* pane = evt.GetPane(); + auto it = pane != nullptr ? m_plugin_panes.find(pane->window) : m_plugin_panes.end(); + if (it != m_plugin_panes.end()) { + const std::function on_close = std::move(it->second.on_close); + m_plugin_panes.erase(it); + if (on_close) + on_close(); + } + evt.Skip(); + }); + m_default_window_layout = m_aui_mgr.SavePerspective(); { @@ -8141,6 +8167,14 @@ void Plater::priv::update_sidebar(bool force_update) { } } + for (const auto& [window, plugin_pane] : m_plugin_panes) { + wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); + if (pane.IsOk() && pane.IsShown() != plugin_pane_visible(plugin_pane, pane)) { + pane.Show(!pane.IsShown()); + needs_update = true; + } + } + if (needs_update) { notification_manager->set_sidebar_collapsed(sidebar.IsShown()); m_aui_mgr.Update(); @@ -8150,10 +8184,95 @@ void Plater::priv::update_sidebar(bool force_update) { void Plater::priv::reset_window_layout() { m_aui_mgr.LoadPerspective(m_default_window_layout, false); + // Loading a layout hides every pane it does not list, and the default layout lists no plugin panes. + for (const auto& [window, plugin_pane] : m_plugin_panes) + if (wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); pane.IsOk()) + pane.Show(plugin_pane.shown); sidebar_layout.is_collapsed = false; update_sidebar(true); } +bool Plater::priv::plugin_pane_visible(const PluginPane& plugin_pane, const wxAuiPaneInfo& pane) const +{ + // A floating pane is a top-level window, so it does not hide with the Plater on other tabs. + return plugin_pane.shown && (!pane.IsFloating() || sidebar_layout.show); +} + +void Plater::priv::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock, + const wxSize& size, std::function on_close) +{ + const wxString base_name = wxString::FromUTF8(name); + wxString unique_name = base_name; + for (int i = 2; m_aui_mgr.GetPane(unique_name).IsOk(); ++i) + unique_name = base_name + wxString::Format("#%d", i); + + // A restored layout below already holds pixels. + const wxSize pixels = q->FromDIP(size); + wxAuiPaneInfo info; + info.Name(unique_name).Caption(caption).BestSize(pixels).FloatingSize(pixels).DestroyOnClose(true); + if (dock == "left") + info.Left(); + else if (dock == "bottom") + info.Bottom(); + else + info.Right(); + if (dock == "float") + info.Float(); + + // Put the pane back where it was the last time the window layout was saved with it open. + const std::string saved = plugin_pane_layout_entry(wxGetApp().app_config->get("window_layout"), unique_name.utf8_string()); + if (!saved.empty()) { + m_aui_mgr.LoadPaneInfo(wxString::FromUTF8(saved), info); + info.Caption(caption).DestroyOnClose(true).Show(); + } + + // Floating is disabled on Wayland. + if ((m_aui_mgr.GetFlags() & wxAUI_MGR_ALLOW_FLOATING) == 0) { + info.Dock().Floatable(false); + if (info.dock_direction == wxAUI_DOCK_NONE) + info.Right(); + } + + const PluginPane& plugin_pane = m_plugin_panes[window] = PluginPane{std::move(on_close)}; + info.Show(plugin_pane_visible(plugin_pane, info)); + m_aui_mgr.AddPane(window, info); + + // wxAUI does not record a dragged sash in best_size, so track the docked size like the sidebar + // does, for the saved layout. + window->Bind(wxEVT_IDLE, [this, window](wxIdleEvent& evt) { + wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); + if (pane.IsOk() && pane.IsShown() && pane.IsDocked() && pane.rect.GetWidth() > 0 && pane.rect.GetHeight() > 0) { + const bool horizontal = pane.dock_direction == wxAUI_DOCK_TOP || pane.dock_direction == wxAUI_DOCK_BOTTOM; + pane.BestSize(horizontal ? pane.best_size.GetWidth() : pane.rect.GetWidth(), + horizontal ? pane.rect.GetHeight() : pane.best_size.GetHeight()); + } + evt.Skip(); + }); + + m_aui_mgr.Update(); +} + +void Plater::priv::remove_plugin_pane(wxWindow* window) +{ + m_plugin_panes.erase(window); + if (m_aui_mgr.DetachPane(window)) + m_aui_mgr.Update(); + window->Destroy(); +} + +void Plater::priv::show_plugin_pane(wxWindow* window, bool show) +{ + const auto it = m_plugin_panes.find(window); + wxAuiPaneInfo& pane = m_aui_mgr.GetPane(window); + if (it == m_plugin_panes.end() || !pane.IsOk()) + return; + it->second.shown = show; + if (pane.IsShown() == plugin_pane_visible(it->second, pane)) + return; + pane.Show(!pane.IsShown()); + m_aui_mgr.Update(); +} + Sidebar::DockingState Plater::priv::get_sidebar_docking_state() { if (!sidebar_layout.is_enabled) { return Sidebar::None; @@ -17710,6 +17829,14 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_ void Plater::reset_window_layout() { p->reset_window_layout(); } +void Plater::add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock, + const wxSize& size, std::function on_close) +{ + p->add_plugin_pane(window, name, caption, dock, size, std::move(on_close)); +} +void Plater::remove_plugin_pane(wxWindow* window) { p->remove_plugin_pane(window); } +void Plater::show_plugin_pane(wxWindow* window, bool show) { p->show_plugin_pane(window, show); } + //BBS void Plater::select_curr_plate_all() { p->select_curr_plate_all(); } void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 84e64acea0..121e9c10eb 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -475,6 +475,14 @@ public: void reset_window_layout(); + // Plugin panes dock alongside the sidebar; `window` must be a child of the Plater. `dock` is + // "left", "right", "bottom" or "float", and `size` is in DIPs. A pane closed from its own close + // button is destroyed after on_close runs; remove_plugin_pane() destroys it without calling on_close. + void add_plugin_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock, + const wxSize& size, std::function on_close); + void remove_plugin_pane(wxWindow* window); + void show_plugin_pane(wxWindow* window, bool show); + // Called after the Preferences dialog is closed and the program settings are saved. // Update the UI based on the current preferences. void update_ui_from_settings(); diff --git a/src/slic3r/GUI/PluginDockPanel.cpp b/src/slic3r/GUI/PluginDockPanel.cpp new file mode 100644 index 0000000000..8fd6fe8e08 --- /dev/null +++ b/src/slic3r/GUI/PluginDockPanel.cpp @@ -0,0 +1,170 @@ +#include "PluginDockPanel.hpp" + +#include "GUI.hpp" +#include "GUI_App.hpp" +#include "Plater.hpp" +#include "PluginWebDialog.hpp" +#include "Widgets/WebView.hpp" +#include "Widgets/WebViewHostDialog.hpp" + +#include + +#include +#include + +#include + +#include +#include + +namespace Slic3r { namespace GUI { + +std::string plugin_pane_name(const std::string& plugin_key, const std::string& title) +{ + std::string name = "plugin:" + plugin_key + ":" + title; + std::replace_if(name.begin(), name.end(), [](char c) { return c == '|' || c == ';' || c == '=' || c == '\\'; }, '_'); + return name; +} + +std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name) +{ + // Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|". + const std::string prefix = "name=" + pane_name + ";"; + size_t begin = 0; + for (size_t i = 0; i <= layout.size(); ++i) { + if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\'))) + continue; + if (layout.compare(begin, prefix.size(), prefix) == 0) + return layout.substr(begin, i - begin); + begin = i + 1; + } + return {}; +} + +PluginDockPanel::PluginDockPanel(wxWindow* parent, + const std::string& html, + MessageHandler on_message, + CloseHandler on_close, + CloseHandler on_destroyed) + : wxPanel(parent, wxID_ANY) + , m_html(html) + , m_on_message(std::move(on_message)) + , m_on_close(std::move(on_close)) + , m_on_destroyed(std::move(on_destroyed)) +{ + SetBackgroundColour(wxGetApp().get_window_default_clr()); + auto* sizer = new wxBoxSizer(wxVERTICAL); + SetSizer(sizer); + + const std::string bootstrap = (boost::filesystem::path(resources_dir()) / PluginWebDialog::BOOTSTRAP_PAGE).make_preferred().string(); + m_browser = WebView::CreateWebView(this, wxString("file://") + from_u8(bootstrap)); + if (m_browser == nullptr) { + BOOST_LOG_TRIVIAL(error) << "Could not create the web view for a plugin dock panel"; + return; + } + + m_browser->SetBackgroundColour(GetBackgroundColour()); + m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script())); + m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script())); + m_browser->AddUserScript(PluginWebDialog::bridge_user_script()); + m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginDockPanel::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginDockPanel::on_bootstrap_event, this); + m_browser->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); }); + m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginDockPanel::on_script_message, this); + m_browser->Bind(EVT_WEBVIEW_RECREATED, &PluginDockPanel::on_webview_recreated, this); + sizer->Add(m_browser, 1, wxEXPAND); +} + +PluginDockPanel::~PluginDockPanel() +{ + if (m_on_destroyed) + m_on_destroyed(); +} + +void PluginDockPanel::on_bootstrap_event(wxWebViewEvent& event) +{ + if (!m_content_loaded) { + m_content_loaded = true; + m_browser->SetPage(wxString::FromUTF8(m_html), PluginWebDialog::content_base_url()); + } + event.Skip(); +} + +void PluginDockPanel::on_script_message(wxWebViewEvent& event) +{ + const nlohmann::json payload = nlohmann::json::parse(event.GetString().utf8_string(), nullptr, false); + if (!payload.is_object() || payload.value("channel", std::string()) != "orca") + return; + + const std::string kind = payload.value("kind", std::string()); + if (kind == "message") { + if (m_on_message) + m_on_message(payload.contains("data") ? payload["data"] : nlohmann::json()); + } else if (kind == "close") { + request_close(); + } +} + +void PluginDockPanel::on_webview_recreated(wxCommandEvent&) +{ + SetBackgroundColour(wxGetApp().get_window_default_clr()); + m_browser->SetBackgroundColour(GetBackgroundColour()); + Refresh(); + // Handled without Skip(), so WebView::RecreateAll() does not reload the plugin page. + WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script())); +} + +void PluginDockPanel::push_message(const nlohmann::json& data) +{ + if (m_browser == nullptr || m_closing) + return; + + nlohmann::json envelope; + envelope["data"] = data; + // The page may still be loading, so wait briefly for the bridge to define __orcaDispatch. + WebView::RunScript(m_browser, wxString::Format( + "(function dispatch(payload, attempts) {\n" + " if (typeof window.__orcaDispatch === 'function') { window.__orcaDispatch(payload); return; }\n" + " if (attempts < 100) window.setTimeout(function() { dispatch(payload, attempts + 1); }, 25);\n" + "})(%s, 0);", + wxString::FromUTF8(envelope.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)))); +} + +void PluginDockPanel::fire_close() +{ + if (m_closing) + return; + m_closing = true; + if (m_on_close) { + CloseHandler on_close = std::move(m_on_close); + m_on_close = nullptr; + on_close(); + } +} + +void PluginDockPanel::request_close() +{ + if (m_closing) + return; + fire_close(); + // A close requested by the page arrives inside the web view's own script-message callback, + // which must return before the web view is destroyed. + CallAfter([this]() { remove_pane(); }); +} + +void PluginDockPanel::destroy_for_plugin() +{ + m_closing = true; + m_on_close = nullptr; + remove_pane(); +} + +void PluginDockPanel::remove_pane() +{ + if (Plater* plater = wxGetApp().plater()) + plater->remove_plugin_pane(this); + else + Destroy(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PluginDockPanel.hpp b/src/slic3r/GUI/PluginDockPanel.hpp new file mode 100644 index 0000000000..60c553779b --- /dev/null +++ b/src/slic3r/GUI/PluginDockPanel.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include + +#include +#include + +namespace Slic3r { namespace GUI { + +// Name of a plugin's pane in the Plater's dock manager. It stays the same across sessions, so the +// saved window layout can put the pane back, and it never contains a wxAuiManager layout delimiter. +std::string plugin_pane_name(const std::string& plugin_key, const std::string& title); + +// The pane part saved for `pane_name` in a wxAuiManager layout string, in the form +// wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane. +std::string plugin_pane_layout_entry(const std::string& layout, const std::string& pane_name); + +// Plugin-supplied HTML hosted in a pane of the Plater's dock manager, bridged to the plugin through +// the same window.orca API as PluginWebDialog. Python-agnostic for the same reason: it can be +// destroyed on the main thread without the GIL, so its hooks must not capture pybind11 objects. +class PluginDockPanel : public wxPanel +{ +public: + using MessageHandler = std::function; + using CloseHandler = std::function; + + // on_close fires once, on a user or page initiated close. on_destroyed runs from the destructor + // on every path and must touch host-side state only. + PluginDockPanel(wxWindow* parent, + const std::string& html, + MessageHandler on_message, + CloseHandler on_close, + CloseHandler on_destroyed); + ~PluginDockPanel() override; + + // Main thread only. + void push_message(const nlohmann::json& data); + // Fires on_close, then removes the pane. + void request_close(); + // Removes the pane without firing on_close, for plugin unload. + void destroy_for_plugin(); + // Fires on_close at most once. The Plater calls it when the pane's own close button is used. + void fire_close(); + +private: + void on_bootstrap_event(wxWebViewEvent& event); + void on_script_message(wxWebViewEvent& event); + void on_webview_recreated(wxCommandEvent& event); + void remove_pane(); + + wxWebView* m_browser{nullptr}; + std::string m_html; + bool m_content_loaded{false}; + bool m_closing{false}; + MessageHandler m_on_message; + CloseHandler m_on_close; + CloseHandler m_on_destroyed; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PluginWebDialog.cpp b/src/slic3r/GUI/PluginWebDialog.cpp index d89aac7270..442ad7ba84 100644 --- a/src/slic3r/GUI/PluginWebDialog.cpp +++ b/src/slic3r/GUI/PluginWebDialog.cpp @@ -57,6 +57,10 @@ wxString web_base_url() } // namespace +const char* PluginWebDialog::bridge_user_script() { return ORCA_BRIDGE_JS; } + +wxString PluginWebDialog::content_base_url() { return web_base_url(); } + PluginWebDialog::PluginWebDialog(wxWindow* parent, const wxString& title, const std::string& html, @@ -75,7 +79,7 @@ PluginWebDialog::PluginWebDialog(wxWindow* parent, { // A tiny bundled bootstrap page brings the webview up; the real plugin HTML // is swapped in via SetPage once the bootstrap finishes loading. - create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240)); + create_webview(BOOTSTRAP_PAGE, title, size, wxSize(320, 240)); // Paint the window/webview in the themed background so there is no white // flash before the (transparent) bootstrap page and plugin HTML render. diff --git a/src/slic3r/GUI/PluginWebDialog.hpp b/src/slic3r/GUI/PluginWebDialog.hpp index 05f77f5148..836ace7331 100644 --- a/src/slic3r/GUI/PluginWebDialog.hpp +++ b/src/slic3r/GUI/PluginWebDialog.hpp @@ -47,6 +47,12 @@ public: static void request_close(PluginWebDialog* dialog); static void destroy_for_plugin(PluginWebDialog* dialog); + // Shared with PluginDockPanel: the bundled blank page a plugin web view loads before the plugin + // HTML is swapped in, the window.orca bridge, and the base URL the plugin HTML is loaded against. + static constexpr const char* BOOTSTRAP_PAGE = "web/dialog/PluginWebDialog/blank.html"; + static const char* bridge_user_script(); + static wxString content_base_url(); + // Push a payload to the page; delivered to handlers registered via // window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals). void push_message(const nlohmann::json& data); diff --git a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp index 044fe33cde..e78ddc035b 100644 --- a/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp +++ b/src/slic3r/GUI/Widgets/WebViewHostDialog.cpp @@ -75,6 +75,8 @@ if(document.documentElement) } // namespace +std::string WebViewHostDialog::theme_apply_script() { return host_theme_apply_js(); } + // Document-start user script: injects the contract