mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 22:42:37 +00:00
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.
This commit is contained in:
@@ -3,8 +3,12 @@
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <slic3r/GUI/PluginDockPanel.hpp>
|
||||
#include <slic3r/GUI/Widgets/AuiPaneLayout.hpp>
|
||||
#include <slic3r/GUI/Widgets/PluginWebContent.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
#include "python_test_support.hpp"
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
@@ -141,6 +145,8 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
||||
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
||||
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
||||
CHECK(has_attr(ui, "UiWindow"));
|
||||
CHECK(has_attr(ui, "create_dock_panel"));
|
||||
CHECK(has_attr(ui, "UiDockPanel"));
|
||||
|
||||
// With no wx application the UI calls marshal to a main thread that does not
|
||||
// exist here; they must fail cleanly with a clear error, not crash.
|
||||
@@ -151,6 +157,75 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
|
||||
try {
|
||||
ui.attr("create_dock_panel")("<p>panel</p>");
|
||||
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
|
||||
// Positional arguments follow create_window(): width and height come straight after the title.
|
||||
try {
|
||||
ui.attr("create_dock_panel")("<p>panel</p>", "Panel", 400, 300);
|
||||
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
}
|
||||
|
||||
// An unknown dock position is rejected before the application is needed.
|
||||
try {
|
||||
ui.attr("create_dock_panel")("<p>panel</p>", py::arg("dock") = "top");
|
||||
FAIL("orca.host.ui.create_dock_panel accepted an unknown dock position");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin pane names identify the plugin and title without layout delimiters", "[PluginHost]")
|
||||
{
|
||||
using Slic3r::GUI::plugin_pane_name;
|
||||
|
||||
CHECK(plugin_pane_name("dock_demo", "Scene") == "plugin:dock_demo:Scene");
|
||||
CHECK(plugin_pane_name("key", "a|b;c=d\\e").find_first_of("|;=\\") == std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("A pane's saved layout entry is found by pane name", "[PluginHost]")
|
||||
{
|
||||
using Slic3r::GUI::aui_pane_layout_entry;
|
||||
|
||||
const std::string sidebar = "name=sidebar;caption=;state=2099196;dir=4;layer=0;row=0;pos=0;bestw=390;besth=900";
|
||||
// The caption holds an escaped '|', which must not end the entry.
|
||||
const std::string plugin = "name=plugin:demo:Scene;caption=Scene \\| stats;state=2099198;dir=2;layer=0;row=1;pos=0;bestw=320;besth=480";
|
||||
const std::string layout = "layout3|" + sidebar + "|" + plugin + "|dock_size(4,0,0)=392|";
|
||||
|
||||
CHECK(aui_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
||||
CHECK(aui_pane_layout_entry(layout, "sidebar") == sidebar);
|
||||
CHECK(aui_pane_layout_entry(layout, "plugin:demo").empty());
|
||||
CHECK(aui_pane_layout_entry("", "plugin:demo:Scene").empty());
|
||||
}
|
||||
|
||||
TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside", "[PluginHost]")
|
||||
{
|
||||
using namespace Slic3r::GUI::plugin_web;
|
||||
|
||||
// A resources path holding a space, which the web view reports escaped.
|
||||
const Slic3r::ScopedResourcesDir resources("web content check");
|
||||
|
||||
// What a plugin page reports after the host swapped it in, and after the page moved to an
|
||||
// anchor within it and the user reloaded.
|
||||
CHECK(is_content_url(content_base_url()));
|
||||
CHECK(is_content_url(content_base_url() + "#tab2"));
|
||||
wxString escaped = content_base_url();
|
||||
escaped.Replace(" ", "%20");
|
||||
REQUIRE(escaped != content_base_url());
|
||||
CHECK(is_content_url(escaped));
|
||||
CHECK(is_content_url(escaped + "#tab2"));
|
||||
// A page the plugin linked to keeps its own URL and must be left alone.
|
||||
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
||||
CHECK_FALSE(is_content_url("https://example.com/"));
|
||||
CHECK_FALSE(is_content_url(""));
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||
|
||||
Reference in New Issue
Block a user