Files
OrcaSlicer/src/slic3r/GUI/PluginDockPanel.cpp
T
Hanif Koh 2403855d93 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, and it now owns the bootstrap page, base
URL and plugin-window bridge that PluginWebDialog uses. 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.

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

112 lines
3.1 KiB
C++

#include "PluginDockPanel.hpp"
#include "GUI_App.hpp"
#include "Plater.hpp"
#include <algorithm>
#include <utility>
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)
: PluginWebPanel(parent, orca_bridge_script())
, m_html(html)
, m_on_message(std::move(on_message))
, m_on_close(std::move(on_close))
, m_on_destroyed(std::move(on_destroyed))
{
// A link asking for a new window has nowhere to open from a docked panel.
browser()->Bind(wxEVT_WEBVIEW_NEWWINDOW, [](wxWebViewEvent& event) { event.Veto(); });
}
PluginDockPanel::~PluginDockPanel()
{
if (m_on_destroyed)
m_on_destroyed();
}
bool PluginDockPanel::on_page_message(const std::string& kind, const nlohmann::json& data)
{
if (kind == "message") {
if (m_on_message)
m_on_message(data);
return true;
}
if (kind == "close") {
request_close();
return true;
}
return false;
}
void PluginDockPanel::push_message(const nlohmann::json& data)
{
if (!m_closing)
post_to_page(data.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