Files
OrcaSlicer/src/slic3r/GUI/PluginWebDialog.cpp
T
HanifKoh 8b63628cf9 Restore Plugin HTML After a WebKit Reload (#15737)
Plugin dialog content is injected with SetPage, so on the WebKit backends a
reload (context menu, keyboard shortcut or location.reload()) re-fetches the
SetPage base URL instead of the injected document, and the plugin UI is gone
for good: load_plugin_content() returned early once m_content_loaded was set.

Re-inject the plugin HTML when a main-frame load after the initial swap is
neither that swap nor a page the plugin linked to. m_own_page_load marks the
load our own SetPage caused, and the URL test recognises the reload: the
injected document and the directory a reload re-fetches both report the base
URL, so a load of any other URL is left alone. The test ignores a fragment the
page navigated to, and undoes the escaping the web view applies to what the
resources path holds.

A load reaching the base URL is not enough on its own, because WebKitGTK reports
a navigation that never committed against the document that stayed and then
finishes that document again: a link to a missing file therefore arrives as a
load of the base URL and reads exactly like a reload. So the re-injection also
requires a navigation to the base URL to have committed, which a reload always
does and a failure never does.

A bootstrap page that cannot be loaded is still not recovered from: WebKitGTK
substitutes a stock error page for it, and that load supersedes the swap
whichever way the swap is ordered around it. The file ships, so this is a
broken-install path; nothing here makes it worse than it already was.

No separate MSW path is needed: wxWebViewEdge ignores the SetPage base URL, so
its documents report about:blank and the test never matches there, and WebView2
reloads NavigateToString content from its own history entry anyway.
2026-09-21 17:09:05 +08:00

274 lines
8.8 KiB
C++

#include "PluginWebDialog.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <wx/event.h>
#include <wx/uri.h>
#include <utility>
namespace Slic3r { namespace GUI {
namespace {
// Injected into the top-level page at document start (before the plugin's own
// scripts). Defines window.orca as the only host surface the page may use. It
// references window.wx lazily (at call time) so it never races the backend's
// deferred registration of the "wx" message handler. Guarded against
// double-injection so it is harmless if also prepended.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.top !== window.self) return;
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
window.__orcaDispatch = function (payload) {
var data = payload ? payload.data : null;
for (var i = 0; i < handlers.length; i++) {
try { handlers[i](data); } catch (e) {}
}
};
})();
)JS";
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
// relative URLs resolve against the bundled web resources directory.
wxString web_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
// Whether a loaded document is the plugin HTML's own base URL. The web view reports the URL it
// parsed, so any fragment the page navigated to is ignored and the escaping it applies to what the
// resources path holds (a space, a non-ASCII character) is undone first.
bool is_content_url(const wxString& url)
{
return wxURI::Unescape(url.BeforeFirst('#')) == web_base_url();
}
} // namespace
PluginWebDialog::PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
SubmitHandler on_submit,
CloseHandler on_close,
CloseHandler on_destroyed,
long wx_style)
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
, m_html(html)
, m_on_message(std::move(on_message))
, m_on_submit(std::move(on_submit))
, m_on_close(std::move(on_close))
, m_on_destroyed(std::move(on_destroyed))
{
// 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));
// Paint the window/webview in the themed background so there is no white
// flash before the (transparent) bootstrap page and plugin HTML render.
SetBackgroundColour(wxGetApp().get_window_default_clr());
if (wxWebView* wv = browser()) {
wv->SetBackgroundColour(wxGetApp().get_window_default_clr());
// Theme contract + plugin defaults + bridge are registered by the base
// create_webview() via add_user_scripts(); nothing to add here.
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_NAVIGATED, &PluginWebDialog::on_navigated, this, wv->GetId());
}
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
}
void PluginWebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
wv->AddUserScript(wxString::FromUTF8(WebViewHostDialog::plugin_defaults_user_script()));
wv->AddUserScript(ORCA_BRIDGE_JS);
}
}
PluginWebDialog::~PluginWebDialog()
{
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
// that event is sent from the base ~wxDialog(), after this subclass's members
// are already destroyed. Here the members are still alive, and the callback
// only touches the host-side registry (no Python), so this is safe.
if (m_on_destroyed)
m_on_destroyed();
}
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
{
if (dialog != nullptr && dialog->is_open())
dialog->push_message(data);
}
void PluginWebDialog::request_close(PluginWebDialog* dialog)
{
if (dialog != nullptr)
dialog->Close();
}
void PluginWebDialog::destroy_for_plugin(PluginWebDialog* dialog)
{
if (dialog == nullptr)
return;
// Forced plugin teardown must not invoke Python close callbacks. End a modal
// loop first, otherwise destroying the window can leave ShowModal() running.
if (dialog->IsModal()) {
dialog->m_open = false;
dialog->EndModal(wxID_CANCEL);
}
dialog->Destroy();
}
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
{
const bool loaded = event.GetEventType() == wxEVT_WEBVIEW_LOADED;
// The first bootstrap load (or its error) triggers the swap to plugin HTML.
if (!m_content_loaded)
load_plugin_content();
// WebKit reloads the SetPage base URL, so a committed load of it that we did not start is a reload.
// A failed navigation is reported against the page that stayed but never commits. Edge ignores the
// base URL and restores SetPage content itself, so nothing matches there.
else if (is_content_url(event.GetURL())) {
if (m_own_page_load)
m_own_page_load = false;
else if (loaded && m_content_navigated)
load_plugin_content();
}
if (loaded)
m_content_navigated = false;
event.Skip();
}
void PluginWebDialog::on_navigated(wxWebViewEvent& event)
{
m_content_navigated = is_content_url(event.GetURL());
event.Skip();
}
void PluginWebDialog::load_plugin_content()
{
m_content_loaded = true;
if (wxWebView* wv = browser()) {
m_own_page_load = true;
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
}
}
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
{
if (payload.value("channel", std::string()) == "orca") {
const std::string kind = payload.value("kind", std::string());
const nlohmann::json data = payload.contains("data") ? payload["data"] : nlohmann::json();
if (kind == "message") {
if (m_on_message)
m_on_message(data);
} else if (kind == "submit") {
finish(true, data);
} else if (kind == "close") {
finish(false, nlohmann::json());
}
return;
}
// Fall back to the shared shell commands (e.g. "close_page").
handle_common_script_command(payload);
}
void PluginWebDialog::push_message(const nlohmann::json& data)
{
if (!m_open)
return;
nlohmann::json envelope;
envelope["data"] = data;
call_web_handler(envelope, wxT("__orcaDispatch"));
}
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
{
if (!m_open)
return;
m_open = false;
if (submitted) {
m_result = data;
fire_submit(data);
} else {
m_result.reset();
fire_close();
}
if (IsModal())
EndModal(submitted ? wxID_OK : wxID_CANCEL);
else
Close();
}
void PluginWebDialog::on_close_window(wxCloseEvent&)
{
if (!m_open) {
// finish() already dispatched submit/close and requested the close.
// Modeless windows still need to be destroyed after that request.
if (!IsModal())
Destroy();
return;
}
m_open = false;
m_result.reset();
fire_close();
if (IsModal()) {
EndModal(wxID_CANCEL);
return;
}
Destroy();
}
void PluginWebDialog::fire_submit(const nlohmann::json& data)
{
if (m_on_submit) {
SubmitHandler cb = std::move(m_on_submit);
cb(data);
}
}
void PluginWebDialog::fire_close()
{
if (m_close_fired)
return;
m_close_fired = true;
if (m_on_close) {
CloseHandler cb = m_on_close;
m_on_close = nullptr;
cb();
}
}
}} // namespace Slic3r::GUI