Fix plugin configuration not taking effect, and improve the plugin config UI (#14944)

# Description

Changing a slicing plugin's configuration had no effect on the sliced
result until you forced a re-slice some other way; it now applies
immediately. Print, printer and filament presets also keep their plugin
configuration separately, so configuring a plugin on one no longer wipes
out what you set on another.

A plugin's custom configuration page gets the same round of improvements
in both the Plugins dialog and the per-preset dialog: it follows the
app's light/dark theme, keeps its state while you edit instead of
resetting under the cursor, and can tell whether it is being edited
globally or for a preset, so "Restore defaults" can be labeled for what
it will actually do. The two bundled examples show this off — Twistify
now ships a custom configuration UI, and Inspector is themed, groups

# Screenshots/Recordings/Graphs



https://github.com/user-attachments/assets/02ca062a-5143-49a3-abe0-a2a040b3a928



## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
SoftFever
2026-07-25 20:00:56 +08:00
committed by GitHub
parent d6cb667b89
commit 9a72dda79a
18 changed files with 1202 additions and 408 deletions

View File

@@ -48,6 +48,7 @@
<span id="statusText" class="status-text"></span>
</footer>
</main>
<script src="../js/plugin-config-ui.js"></script>
<script src="index.js"></script>
</body>
</html>

View File

@@ -9,6 +9,12 @@ let selectedCapabilityType = "";
let selectedHasPresetOverride = false;
let selectedReadOnly = false;
// Whether the frame already holds the selected capability's custom UI (payloads are gated by
// IsCurrentCapability and every selection change clears the view). Saving re-sends the whole
// capability_config payload, and rebuilding the frame from it would reload the plugin's page under
// the user's cursor, so a loaded frame gets the new values posted in instead.
let customFrameLoaded = false;
function SafeJsonParse(text) {
try {
return JSON.parse(text);
@@ -193,6 +199,7 @@ function ClearCapabilityConfigView() {
if (custom) {
custom.hidden = true;
custom.removeAttribute("srcdoc");
customFrameLoaded = false;
}
if (text)
text.value = "";
@@ -248,8 +255,14 @@ function ApplyCapabilityConfig(payload) {
if (html) {
if (custom) {
const context = OrcaConfigContext(payload, "preset");
custom.hidden = false;
custom.srcdoc = BuildCustomConfigDocument(html, config);
if (customFrameLoaded && custom.contentWindow) {
custom.contentWindow.postMessage({ __orca: "config", config: config, context: context }, "*");
} else {
customFrameLoaded = true;
custom.srcdoc = BuildCustomConfigDocument(html, config, context);
}
}
if (editor)
editor.hidden = true;
@@ -260,6 +273,7 @@ function ApplyCapabilityConfig(payload) {
if (custom) {
custom.hidden = true;
custom.removeAttribute("srcdoc");
customFrameLoaded = false;
}
if (editor)
editor.hidden = false;
@@ -354,39 +368,6 @@ function ApplyCapabilityConfigSaved(payload) {
SetConfigValidation("");
}
// The whole host surface a custom config UI gets: read the config, save one, drop the preset's
// override, and be told when either lands. The frame is sandboxed into an opaque origin, so this
// bridge is its only channel.
function BuildCustomConfigDocument(html, config) {
// Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the
// literal stays valid JSON.
const seed = JSON.stringify(config).replace(/</g, "\\u003c");
const bridge = `<script>
(function () {
var handlers = [];
var current = ${seed};
window.orca = {
getConfig: function () { return current; },
saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); },
restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); },
onConfig: function (cb) {
if (typeof cb !== "function") return;
handlers.push(cb);
try { cb(current); } catch (e) {}
}
};
window.addEventListener("message", function (event) {
if (!event.data || event.data.__orca !== "config") return;
current = event.data.config || {};
handlers.forEach(function (handler) {
try { handler(current); } catch (e) {}
});
});
})();
<\/script>`;
return bridge + html;
}
function OnCustomConfigMessage(event) {
const custom = document.getElementById("configCustom");
// Only the frame we created, and only while it is actually showing.
@@ -432,6 +413,7 @@ document.addEventListener("DOMContentLoaded", () => {
// OnCustomConfigMessage matches on the frame's contentWindow, not the origin ("null" when
// sandboxed), and ignores anything else.
window.addEventListener("message", OnCustomConfigMessage);
OrcaWatchThemeForFrame("configCustom");
SendMessage("request_capabilities");
});

View File

@@ -15,6 +15,7 @@
<script type="text/javascript" src="../../data/text.js"></script>
<script type="text/javascript" src="../js/globalapi.js"></script>
<script type="text/javascript" src="../js/common.js"></script>
<script src="../js/plugin-config-ui.js"></script>
<script src="./index.js"></script>
<script src="./plugin-sort.js"></script>
<script src="../js/fuzzy-search.js"></script>

View File

@@ -90,6 +90,7 @@ function OnInit() {
// OnCustomConfigMessage matches on the frame's contentWindow, not the origin ("null" when
// sandboxed), and ignores anything else.
window.addEventListener("message", OnCustomConfigMessage);
OrcaWatchThemeForFrame("configCustom");
document.addEventListener("click", (event) => {
if (!event.target.closest(".ctx"))
@@ -1077,7 +1078,7 @@ function ApplyCapabilityConfig(payload) {
if (html) {
if (custom) {
custom.hidden = false;
custom.srcdoc = BuildCustomConfigDocument(html, config);
custom.srcdoc = BuildCustomConfigDocument(html, config, OrcaConfigContext(payload, "global"));
}
if (editor)
editor.hidden = true;
@@ -1178,39 +1179,6 @@ function ApplyCapabilityConfigSaved(payload) {
SetConfigValidation("");
}
// The whole host surface a custom config UI gets: read the config, save one, restore the plugin's
// defaults, and be told when either lands. The frame is sandboxed into an opaque origin, so this
// bridge is its only channel.
function BuildCustomConfigDocument(html, config) {
// Inlined into a <script>: a stored "</script>" would close the tag early, so escape "<" — the
// literal stays valid JSON.
const seed = JSON.stringify(config).replace(/</g, "\\u003c");
const bridge = `<script>
(function () {
var handlers = [];
var current = ${seed};
window.orca = {
getConfig: function () { return current; },
saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); },
restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); },
onConfig: function (cb) {
if (typeof cb !== "function") return;
handlers.push(cb);
try { cb(current); } catch (e) {}
}
};
window.addEventListener("message", function (event) {
if (!event.data || event.data.__orca !== "config") return;
current = event.data.config || {};
handlers.forEach(function (handler) {
try { handler(current); } catch (e) {}
});
});
})();
<\/script>`;
return bridge + html;
}
function OnCustomConfigMessage(event) {
const custom = document.getElementById("configCustom");
// Only the frame we created, and only while it is actually showing.

View File

@@ -0,0 +1,105 @@
// The host surface a plugin capability's custom configuration UI gets, shared by the Plugins dialog
// and by a preset's plugin configuration dialog. Both sandbox the page into an opaque origin, so this
// bridge is its only channel — and both must offer plugin authors the same one.
// The host theme (WebViewHostDialog::host_theme_vars_css) is injected as a ":root{--orca-*}" rule in
// <style id="orca-host-theme-vars">, but only on the top-level page, so a sandboxed config UI never
// sees it unless we hand it over. Relay that CSS verbatim instead of re-deriving it: C++ stays the
// only place the contract is spelled out, and the host re-themes the style in place, so reading it
// always yields the live theme.
function OrcaThemeSnapshot() {
const style = document.getElementById("orca-host-theme-vars");
return {
theme: document.documentElement.getAttribute("data-orca-theme") === "dark" ? "dark" : "light",
css: style ? style.textContent : ""
};
}
// Inlined into a <script> or a JSON payload: a stored "</script>" would close the tag early, so
// escape "<" — the literal stays valid JSON.
function OrcaInlineJson(value) {
return JSON.stringify(value).replace(/</g, "\\u003c");
}
// What a custom UI can learn about the surface it is edited on, kept to what changes the page's own
// behavior: "Restore defaults" writes the plugin's get_default_config() in the Plugins dialog but
// drops the preset's override in a preset dialog, so a page needs the scope to label its own button.
function OrcaConfigContext(payload, scope) {
return {
scope: scope,
readOnly: payload ? payload.read_only === true : false,
hasPresetOverride: payload ? payload.has_preset_override === true : false
};
}
// The whole host surface: read the config, save one, restore defaults, follow the theme, and be told
// when any of it lands.
function BuildCustomConfigDocument(html, config, context) {
const theme = OrcaThemeSnapshot();
const bridge = `<style id="orca-host-theme-vars"></style>
<script>
(function () {
var handlers = [];
var themeHandlers = [];
var current = ${OrcaInlineJson(config === undefined ? {} : config)};
var context = ${OrcaInlineJson(context || {})};
var theme = ${OrcaInlineJson(theme)};
function applyTheme(next) {
if (next && typeof next.css === "string") theme = next;
var style = document.getElementById("orca-host-theme-vars");
if (style) style.textContent = theme.css;
if (document.documentElement) document.documentElement.setAttribute("data-orca-theme", theme.theme);
themeHandlers.forEach(function (handler) {
try { handler(theme.theme); } catch (e) {}
});
}
window.orca = {
getConfig: function () { return current; },
saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); },
restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); },
getContext: function () { return context; },
onConfig: function (cb) {
if (typeof cb !== "function") return;
handlers.push(cb);
try { cb(current); } catch (e) {}
},
onTheme: function (cb) {
if (typeof cb !== "function") return;
themeHandlers.push(cb);
try { cb(theme.theme); } catch (e) {}
}
};
window.addEventListener("message", function (event) {
if (!event.data) return;
if (event.data.__orca === "theme") { applyTheme(event.data.theme); return; }
if (event.data.__orca !== "config") return;
current = event.data.config || {};
if (event.data.context) context = event.data.context;
handlers.forEach(function (handler) {
try { handler(current); } catch (e) {}
});
});
applyTheme();
})();
<\/script>`;
return bridge + html;
}
// The host re-themes an open dialog in place (WebViewHostDialog::host_theme_apply_js), which a
// sandboxed child frame never sees. Relay it so a custom UI follows a light/dark switch live.
function OrcaWatchThemeForFrame(frameId) {
const relay = () => {
const frame = document.getElementById(frameId);
if (!frame || frame.hidden || !frame.contentWindow)
return;
frame.contentWindow.postMessage({ __orca: "theme", theme: OrcaThemeSnapshot() }, "*");
};
new MutationObserver(relay).observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-orca-theme"]
});
}