Files
OrcaSlicer/src/slic3r/GUI/PluginWebPanel.cpp
T
Hanif Koh b9fb7ca820 Add a Dockable HTML Panel API for Plugins
orca.host.ui.create_dock_panel(html, title, width, height, on_message,
on_close, dock) 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 arguments follow create_window(). The
panel uses 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; plugin panes
are removed in MainFrame::shutdown().

The web view hosting moves out of PluginPage into a shared PluginWebPanel
base: bootstrap page and swap to the plugin HTML, theme, element-default and
bridge scripts, window.orca message parsing, delivery to the page, and live
re-theming, also re-applied on every load after the swap. Pages tabs and
docked panels both derive from it. Pages tabs now re-theme in place on a
theme change instead of being reloaded, and a window.orca call a host does
not support is logged.

What the hosts share no longer lives in one of them: the bootstrap page, the
base URL and the plugin-window bridge move to Widgets/PluginWebContent, used
by PluginWebDialog and PluginWebPanel alike, and the saved-layout parser
moves to Widgets/AuiPaneLayout, so the Plater does not depend on a plugin
widget.

Includes a sample plugin (sandboxes/orca_dock_panel_plugin_any.py) and
binding and layout-helper tests in slic3rutils.
2026-09-19 16:00:26 +08:00

122 lines
5.0 KiB
C++

#include "PluginWebPanel.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "Widgets/PluginWebContent.hpp"
#include "Widgets/WebView.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <wx/sizer.h>
namespace Slic3r { namespace GUI {
PluginWebPanel::PluginWebPanel(wxWindow* parent, const char* bridge_script)
: wxPanel(parent, wxID_ANY)
{
SetBackgroundColour(wxGetApp().get_window_default_clr());
auto* sizer = new wxBoxSizer(wxVERTICAL);
SetSizer(sizer);
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
const std::string bootstrap = (boost::filesystem::path(resources_dir()) / plugin_web::BOOTSTRAP_PAGE).make_preferred().string();
m_browser = WebView::CreateWebView(this, wxString("file://") + from_u8(bootstrap));
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(wxString::FromUTF8(bridge_script));
m_browser->Bind(wxEVT_WEBVIEW_LOADED, &PluginWebPanel::on_load_event, this);
m_browser->Bind(wxEVT_WEBVIEW_ERROR, &PluginWebPanel::on_load_event, this);
m_browser->Bind(wxEVT_WEBVIEW_NAVIGATED, &PluginWebPanel::on_navigated, this);
m_browser->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &PluginWebPanel::on_script_message, this);
m_browser->Bind(EVT_WEBVIEW_RECREATED, &PluginWebPanel::on_webview_recreated, this);
sizer->Add(m_browser, 1, wxEXPAND);
}
void PluginWebPanel::on_load_event(wxWebViewEvent& event)
{
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
if (!m_content_loaded) {
// The first bootstrap load (or its error) triggers the swap to the plugin HTML.
m_content_loaded = true;
load_page_html();
} else if (!plugin_web::is_content_url(event.GetURL())) {
// Every document but ours — a page a link loaded, an error page a backend substituted —
// reports its own URL. Not ours to reload, but the app theme still applies to it.
if (loaded)
apply_theme();
} else if (m_own_page_load) {
m_own_page_load = false;
// The document-start theme script keeps the theme the web view was created with, so bring
// the swapped-in plugin page onto the app theme.
if (loaded)
apply_theme();
} else if (loaded && m_content_navigated) {
// WebKit reloads the SetPage base URL rather than the injected page, and the injected
// document reports that same URL, so a load of it that is not the swap we started is a
// browser reload and the plugin HTML has to be put back. A navigation that fails is reported
// against the document that stayed, base URL and all, so the reload also has to have
// committed; nothing commits for a failure. The Edge backend ignores the base URL, so nothing
// matches there.
load_page_html();
}
if (loaded)
m_content_navigated = false;
event.Skip();
}
void PluginWebPanel::on_navigated(wxWebViewEvent& event)
{
m_content_navigated = plugin_web::is_content_url(event.GetURL());
event.Skip();
}
void PluginWebPanel::load_page_html()
{
if (const std::optional<std::string> html = page_html()) {
m_own_page_load = true;
m_browser->SetPage(wxString::FromUTF8(*html), plugin_web::content_base_url());
}
}
void PluginWebPanel::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 (!on_page_message(kind, payload.contains("data") ? payload["data"] : nlohmann::json()))
BOOST_LOG_TRIVIAL(warning) << "Plugin web panel ignored a window.orca '" << kind << "' call; this host does not support it";
}
void PluginWebPanel::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.
apply_theme();
}
void PluginWebPanel::apply_theme()
{
WebView::RunScript(m_browser, wxString::FromUTF8(WebViewHostDialog::theme_apply_script()));
}
void PluginWebPanel::post_to_page(const std::string& json)
{
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"
"})({data: %s}, 0);",
wxString::FromUTF8(json)));
}
}} // namespace Slic3r::GUI