mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Plugin dialog content is injected with SetPage, so the webview's current URL stays the base URL and the injected document has none of its own. Reloading the page (context menu or keyboard shortcut) therefore re-fetches the base URL and the plugin UI disappears, permanently: load_plugin_content() returned early once m_content_loaded was set. Re-inject the plugin HTML whenever a main-frame load arrives after the initial swap. m_own_page_load marks the load caused by our own SetPage so it is not mistaken for a reload, and in-document navigation is ignored: the MSW backend synthesises a wxEVT_WEBVIEW_LOADED when a page changes location.hash, which would otherwise wipe the page under the user. A post-load error is left alone too, as it only ever means a failed subresource.
269 lines
8.8 KiB
C++
269 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 <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) + "/";
|
|
}
|
|
|
|
|
|
// True when two consecutive main-frame URLs name the same document, i.e. they differ
|
|
// only in the fragment. The MSW backend synthesises a wxEVT_WEBVIEW_LOADED for such
|
|
// in-document navigation (a page setting location.hash), which must not be mistaken
|
|
// for a reload; an identical URL is a genuine reload and is not fragment navigation.
|
|
bool is_fragment_navigation(const wxString& from, const wxString& to)
|
|
{
|
|
return from != to && from.BeforeFirst('#') == to.BeforeFirst('#');
|
|
}
|
|
|
|
} // 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_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)
|
|
{
|
|
// The first bootstrap load (or its error) triggers the swap to plugin HTML. Once that
|
|
// page has settled, a further main-frame load is a browser reload (context menu or
|
|
// keyboard shortcut): the injected page has no URL of its own, so the reload fetches
|
|
// the base URL instead and the plugin HTML has to be put back. In-document navigation
|
|
// is not a reload and must be left alone, as is a post-load error, which only ever
|
|
// means a failed subresource.
|
|
if (!m_content_loaded)
|
|
load_plugin_content();
|
|
else if (event.GetEventType() == wxEVT_WEBVIEW_LOADED) {
|
|
const wxString previous_url = m_last_url;
|
|
m_last_url = event.GetURL();
|
|
if (m_own_page_load)
|
|
m_own_page_load = false;
|
|
else if (!is_fragment_navigation(previous_url, m_last_url))
|
|
load_plugin_content();
|
|
}
|
|
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
|