Files
OrcaSlicer/sandboxes/orca_dock_panel_plugin_any.py
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

147 lines
4.9 KiB
Python

# /// script
# requires-python = ">=3.12"
#
# [tool.orcaslicer.plugin]
# name = "Dock Panel Demo"
# description = "Opens a dockable panel beside the 3D view that lists the objects on the plate."
# author = "OrcaSlicer"
# version = "0.0.1"
# ///
"""Dock Panel Demo -- orca.host.ui.create_dock_panel().
Run it from the Plugins dialog. It opens an HTML panel docked on the right of the 3D view, in the
same dock area as the sidebar. Drag its caption to dock it on another side (or float it, where the
platform allows), hide it from the page and run the plugin again to bring it back, or close it with
its close button or from the page.
page --orca.postMessage({command: 'refresh'})--> plugin.on_message()
page --orca.postMessage({command: 'hide'})--> plugin.on_message() -> panel.hide()
page --orca.close()--> panel closes, plugin.on_close()
plugin --panel.post({command: 'objects', ...})--> page (orca.onMessage)
"""
import orca
PAGE = """
<style>
body { margin: 0; padding: 12px; font-size: 13px; }
h3 { margin: 0 0 4px; font-size: 14px; }
.note { margin: 0 0 12px; color: var(--orca-muted); font-size: 12px; }
.actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.actions button.quiet { background: transparent; color: var(--orca-fg); border-color: var(--orca-border); }
table { width: 100%; border-collapse: collapse; }
td.count { text-align: right; font-variant-numeric: tabular-nums; }
#status { margin-top: 10px; color: var(--orca-muted); font-size: 12px; }
</style>
<h3>Objects on the plate</h3>
<p class="note">Docked beside the 3D view. Drag the caption to move it.</p>
<div class="actions">
<button type="button" id="refresh">Refresh</button>
<button type="button" id="hide" class="quiet">Hide</button>
<button type="button" id="close" class="quiet">Close</button>
</div>
<table>
<thead><tr><th>Name</th><th>Parts</th><th>Copies</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<p id="status">Waiting for the plugin...</p>
<script>
(function () {
function text(value) {
var span = document.createElement("span");
span.textContent = value;
return span.innerHTML;
}
function render(message) {
var rows = document.getElementById("rows");
var status = document.getElementById("status");
if (message.error) {
rows.innerHTML = "";
status.textContent = message.error;
return;
}
rows.innerHTML = message.objects.map(function (object) {
return "<tr><td>" + text(object.name) + "</td><td class=\\"count\\">" + object.volumes +
"</td><td class=\\"count\\">" + object.instances + "</td></tr>";
}).join("");
status.textContent = message.objects.length + " object(s), refreshed " + new Date().toLocaleTimeString();
}
orca.onMessage(function (message) {
if (message && message.command === "objects")
render(message);
});
document.getElementById("refresh").addEventListener("click", function () {
orca.postMessage({ command: "refresh" });
});
document.getElementById("hide").addEventListener("click", function () {
orca.postMessage({ command: "hide" });
});
document.getElementById("close").addEventListener("click", function () {
orca.close();
});
orca.postMessage({ command: "refresh" });
})();
</script>
"""
def plate_objects():
try:
model = orca.host.model()
except RuntimeError as error:
return {"command": "objects", "error": str(error)}
return {
"command": "objects",
"objects": [
{"name": obj.name or "(unnamed)", "volumes": obj.volume_count(), "instances": obj.instance_count()}
for obj in model.objects()
],
}
class DockPanelDemo(orca.script.ScriptPluginCapabilityBase):
panel = None
def get_name(self):
return "Dock Panel Demo"
def execute(self):
# The capability instance lives as long as the plugin, so a second run finds the open panel.
if self.panel is not None and self.panel.is_open():
self.panel.show()
return orca.ExecutionResult.success("Dock Panel Demo is already open.")
self.panel = orca.host.ui.create_dock_panel(
html=PAGE,
title="Dock Panel Demo",
width=320,
height=480,
on_message=self.on_message,
on_close=self.on_close,
dock="right",
)
return orca.ExecutionResult.success("Dock Panel Demo opened.")
# Called on the UI thread when the page posts.
def on_message(self, message):
command = (message or {}).get("command")
if command == "refresh":
self.panel.post(plate_objects())
elif command == "hide":
self.panel.hide()
def on_close(self):
self.panel = None
@orca.plugin
class DockPanelDemoPlugin(orca.base):
def register_capabilities(self):
orca.register_capability(DockPanelDemo)