diff --git a/resources/web/dialog/SpeedDial/index.html b/resources/web/dialog/SpeedDial/index.html
new file mode 100644
index 0000000000..0a3b9f6947
--- /dev/null
+++ b/resources/web/dialog/SpeedDial/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+ Speed Dial
+
+
+
+
+
+
+
+
+
diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js
new file mode 100644
index 0000000000..60652463a5
--- /dev/null
+++ b/resources/web/dialog/SpeedDial/speeddial.js
@@ -0,0 +1,395 @@
+// Speed Dial launcher page. Static-safe module: no DOM access at load time so a
+// node vm can exercise the pure helpers (filterActions / parseId / nextSel).
+
+// ---- state (populated by the C++ bridge via window.HandleStudio) ----
+var ACTIONS = []; // [{id,title,pkg,runnable,shortcut}], already frecency-sorted by C++
+var FAVS = []; // [id...]
+var query = "";
+var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
+var lastResizeHeight = 0;
+var matchIndex = {};
+
+function FoldChar(ch) {
+ return ch.normalize("NFD").replace(/\p{Diacritic}/gu, "");
+}
+
+function NormChar(ch) {
+ return FoldChar(ch).toLowerCase();
+}
+
+// element handles, assigned in OnInit (kept null so load-time touches no DOM)
+var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null;
+
+// ---- pure helpers (no DOM; unit-tested) -------------------------------------
+function fuzzyRanges(text, query) {
+ var t = text || "";
+ var q = query || "";
+ if (!q)
+ return null;
+ var needle = Array.from(q).map(function (c) { return NormChar(c); }).join("");
+ var ranges = [];
+ var qi = 0;
+ for (var i = 0; i < t.length && qi < needle.length; i++) {
+ if (NormChar(t[i]) === needle[qi]) {
+ if (ranges.length && ranges[ranges.length - 1][1] === i)
+ ranges[ranges.length - 1][1] = i + 1;
+ else
+ ranges.push([i, i + 1]);
+ qi++;
+ }
+ }
+ return qi === needle.length ? ranges : null;
+}
+
+function filterActions(actions, query) {
+ var q = (query || "").trim();
+ var list = actions || [];
+ matchIndex = {};
+ if (!q)
+ return list.slice(0);
+
+ var out = [];
+ for (var i = 0; i < list.length; i++) {
+ var a = list[i];
+ var titleMatch = fuzzyRanges(a.title, q);
+ var pkgMatch = fuzzyRanges(a.pkg, q);
+ if (!titleMatch && !pkgMatch)
+ continue;
+ matchIndex[a.id] = { title: titleMatch, pkg: pkgMatch, useTitle: !!titleMatch };
+ out.push(a);
+ }
+ return out;
+}
+
+// why: ids are "script:::"; C++ keys off for dont-ask scoping.
+function parseId(id) {
+ var body = id.slice(id.indexOf(":") + 1); // strip "script:"
+ var sep = body.indexOf("::");
+ return { key: body.slice(0, sep), cap: body.slice(sep + 2) };
+}
+
+function visibleFavourites(favourites, actions) {
+ var seen = {};
+ (actions || []).forEach(function (a) { seen[a.id] = true; });
+ return (favourites || []).filter(function (id, i, arr) {
+ return seen[id] && arr.indexOf(id) === i;
+ });
+}
+
+function syncClearButton() {
+ if (clearEl)
+ clearEl.hidden = !query;
+}
+
+function stateFromPayload(payload) {
+ return {
+ actions: payload.actions || [],
+ favourites: payload.favourites || [],
+ query: "",
+ sel: { zone: "list", i: 0 },
+ lastResizeHeight: 0
+ };
+}
+
+function resetScrollPositions(list, doc) {
+ if (list)
+ list.scrollTop = 0;
+ if (doc && doc.scrollingElement)
+ doc.scrollingElement.scrollTop = 0;
+ if (doc && doc.documentElement)
+ doc.documentElement.scrollTop = 0;
+ if (doc && doc.body)
+ doc.body.scrollTop = 0;
+}
+
+// nextSel: pure arrow-nav transition. Down fav->list0; Down list->clamp; Up list@0->fav0;
+// Up list->i-1; Left/Right clamp within fav. Returns a fresh {zone,i}.
+function nextSel(sel, key, listLen, favLen) {
+ var zone = sel.zone, i = sel.i;
+ if (key === "ArrowDown") {
+ if (zone === "fav") return { zone: "list", i: 0 };
+ return { zone: "list", i: Math.min(i + 1, Math.max(0, listLen - 1)) };
+ }
+ if (key === "ArrowUp") {
+ if (zone === "list") {
+ if (i <= 0) return favLen ? { zone: "fav", i: 0 } : { zone: "list", i: 0 };
+ return { zone: "list", i: i - 1 };
+ }
+ return { zone: zone, i: i };
+ }
+ if (key === "ArrowLeft" && zone === "fav") return { zone: "fav", i: Math.max(0, i - 1) };
+ if (key === "ArrowRight" && zone === "fav") return { zone: "fav", i: Math.min(favLen - 1, i + 1) };
+ return { zone: zone, i: i };
+}
+
+// ---- bridge ------------------------------------------------------------------
+function SendMessage(msg) {
+ if (typeof SendWXMessage !== "function")
+ return;
+ if (typeof msg === "string") msg = { command: msg };
+ if (msg.sequence_id === undefined) msg.sequence_id = Date.now();
+ SendWXMessage(JSON.stringify(msg));
+}
+
+// C++ pushes payloads here. Only list_actions is handled; it (re)seeds all state.
+window.HandleStudio = function (payload) {
+ if (!payload) return;
+ if (typeof payload === "string") { try { payload = JSON.parse(payload); } catch (e) { return; } }
+ if (payload.command === "list_actions") {
+ var next = stateFromPayload(payload);
+ ACTIONS = next.actions;
+ FAVS = next.favourites;
+ query = next.query;
+ sel = next.sel;
+ lastResizeHeight = next.lastResizeHeight;
+ if (qEl) {
+ qEl.value = "";
+ syncClearButton();
+ }
+ render({ resize: true, resetScroll: true });
+ focusInput();
+ }
+};
+
+// ---- DOM helpers -------------------------------------------------------------
+function $(id) { return document.getElementById(id); }
+
+function byId(id) {
+ for (var i = 0; i < ACTIONS.length; i++) if (ACTIONS[i].id === id) return ACTIONS[i];
+ return null;
+}
+
+function currentVisibleFavs() { return visibleFavourites(FAVS, ACTIONS); }
+
+function hue(id) {
+ var h = 0;
+ for (var i = 0; i < id.length; i++)
+ h = (h * 31 + id.charCodeAt(i)) >>> 0;
+ return h % 360;
+}
+
+// Build a with the search-match ranges wrapped in
. Used for both the
+// title and the pkg eyebrow. Pure (only touches the document factory), so the node-vm test never
+// calls it and load-time stays DOM-free.
+function markedText(className, text, match) {
+ var node = document.createElement("div");
+ node.className = className; node.title = text;
+ if (!match || !match.length) {
+ node.textContent = text;
+ return node;
+ }
+ var last = 0;
+ for (var i = 0; i < match.length; i++) {
+ var range = match[i];
+ if (range[0] > last)
+ node.appendChild(document.createTextNode(text.slice(last, range[0])));
+ var m = document.createElement("mark");
+ m.textContent = text.slice(range[0], range[1]);
+ node.appendChild(m);
+ last = range[1];
+ }
+ if (last < text.length)
+ node.appendChild(document.createTextNode(text.slice(last)));
+ return node;
+}
+
+function starSvg(on) {
+ return '';
+}
+
+// ---- render ------------------------------------------------------------------
+function renderFav() {
+ favEl.innerHTML = "";
+ var favs = currentVisibleFavs();
+ favEl.hidden = favs.length === 0;
+ if (!favs.length && sel.zone === "fav")
+ sel = { zone: "list", i: 0 };
+ else if (sel.zone === "fav")
+ sel.i = Math.max(0, Math.min(sel.i, favs.length - 1));
+ updateFavEyebrow(favs);
+ favs.forEach(function (id, i) {
+ var a = byId(id);
+ var tile = document.createElement("button");
+ tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
+ tile.style.setProperty("--h", hue(id));
+ tile.textContent = a.title.charAt(0).toUpperCase();
+ tile.title = a.title;
+ tile.onclick = function () { sel = { zone: "fav", i: i }; run(a); };
+ favEl.appendChild(tile);
+ });
+}
+
+// Name of the selected favourite, shown above the bar; hidden unless a fav is selected.
+function updateFavEyebrow(favs) {
+ if (!eyeEl) return;
+ var a = sel.zone === "fav" && favs.length ? byId(favs[sel.i]) : null;
+ eyeEl.textContent = a ? a.title : "";
+ eyeEl.hidden = !a;
+}
+
+function renderList() {
+ var q = (query || "").trim();
+ var arr = filterActions(ACTIONS, query);
+ if (sel.zone === "list")
+ sel.i = Math.max(0, Math.min(sel.i, arr.length - 1));
+ listEl.innerHTML = "";
+ // why: search-first - the list stays blank until the user types; only a
+ // non-empty query with zero hits earns the "No actions match" message.
+ if (!q) {
+ listEl.className = "dial-list empty";
+ return;
+ }
+ listEl.className = "dial-list" + (!arr.length ? " empty" : "");
+ if (!arr.length) {
+ var empty = document.createElement("div");
+ empty.className = "dial-empty";
+ empty.textContent = "No actions match";
+ listEl.appendChild(empty);
+ return;
+ }
+ arr.forEach(function (a, i) {
+ var on = FAVS.indexOf(a.id) !== -1;
+ var row = document.createElement("div");
+ row.className = "row" + (sel.zone === "list" && sel.i === i ? " sel" : "") + (a.runnable === false ? " disabled" : "");
+
+ var tile = document.createElement("div");
+ tile.className = "tile";
+ tile.style.setProperty("--h", hue(a.id));
+ tile.textContent = a.title.charAt(0).toUpperCase();
+
+ var left = document.createElement("div");
+ left.className = "row-left";
+ var mi = matchIndex[a.id];
+ var pkg = markedText("row-eyebrow", a.pkg, mi ? mi.pkg : null);
+ var line = document.createElement("div");
+ line.className = "row-line";
+ var name = markedText("row-name", a.title, mi ? mi.title : null);
+ line.appendChild(name);
+ if (a.shortcut) {
+ var sc = document.createElement("div");
+ sc.className = "row-sc";
+ a.shortcut.split("+").forEach(function (k) {
+ var key = document.createElement("kbd");
+ key.textContent = k;
+ sc.appendChild(key);
+ });
+ line.appendChild(sc);
+ }
+ left.appendChild(pkg);
+ left.appendChild(line);
+ row.appendChild(tile);
+ row.appendChild(left);
+
+ if (a.runnable !== false) {
+ var star = document.createElement("button");
+ star.className = "star" + (on ? " on" : "");
+ star.innerHTML = starSvg(on);
+ star.title = on ? "Unpin from favourites" : "Pin to favourites";
+ star.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); };
+ // why: two quick fav/unfav clicks must not dblclick-run the row
+ star.ondblclick = function (ev) { ev.stopPropagation(); };
+ row.appendChild(star);
+ }
+
+ row.onclick = function () { sel = { zone: "list", i: i }; render(); };
+ row.ondblclick = function () { sel = { zone: "list", i: i }; run(a); };
+ listEl.appendChild(row);
+ });
+}
+
+function render(opts) {
+ renderFav();
+ renderList();
+ scrollSelectedIntoView();
+ if (opts && opts.resetScroll)
+ resetScrollPositions(listEl, document);
+ if (opts && opts.resize)
+ requestResize();
+}
+
+// Keep the selected item in view as arrows move it: the list scrolls vertically, the fav bar
+// horizontally (arrow nav "pushes" the scrollable fav row to follow the selection).
+function scrollSelectedIntoView() {
+ var el = null;
+ if (sel.zone === "list" && listEl)
+ el = listEl.querySelector(".row.sel");
+ else if (sel.zone === "fav" && favEl)
+ el = favEl.querySelector(".fav-tile.sel");
+ if (el && el.scrollIntoView)
+ el.scrollIntoView({ block: "nearest", inline: "nearest" });
+}
+
+function requestResize() {
+ if (!document.body)
+ return;
+ setTimeout(function () {
+ var launcher = document.querySelector(".launcher");
+ if (!launcher)
+ return;
+ var height = Math.ceil(launcher.getBoundingClientRect().height);
+ if (!height || height === lastResizeHeight)
+ return;
+ lastResizeHeight = height;
+ SendMessage({ command: "resize", height: height });
+ }, 0);
+}
+
+// ---- actions -----------------------------------------------------------------
+function toggleFav(id) {
+ var k = FAVS.indexOf(id);
+ var newState = k === -1;
+ if (newState) FAVS.push(id); else FAVS.splice(k, 1);
+ SendMessage({ command: "toggle_favourite", id: id, fav: newState });
+ render({ resize: true });
+}
+
+// Fire the action; C++ owns the run-confirm (native dialog) + suppression, then closes the popup + toasts.
+function run(a) {
+ if (!a || a.runnable === false) return;
+ SendMessage({ command: "run_action", id: a.id, title: a.title });
+}
+
+function runSelected() {
+ if (sel.zone === "fav") { var id = currentVisibleFavs()[sel.i]; if (id) run(byId(id)); return; }
+ var a = filterActions(ACTIONS, query)[sel.i];
+ if (a) run(a);
+}
+
+function focusInput() { setTimeout(function () { qEl.focus(); }, 0); }
+
+// ---- init --------------------------------------------------------------------
+function OnInit() {
+ qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow");
+ syncClearButton();
+
+ $("clear").onclick = function () {
+ query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; render({ resize: true, resetScroll: true }); qEl.focus();
+ syncClearButton();
+ };
+ qEl.addEventListener("input", function () {
+ query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true });
+ });
+
+ document.addEventListener("keydown", function (e) {
+ var arr = filterActions(ACTIONS, query);
+ var favs = currentVisibleFavs();
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "ArrowLeft" || e.key === "ArrowRight") {
+ e.preventDefault();
+ sel = nextSel(sel, e.key, arr.length, favs.length);
+ // why: entering/leaving the fav zone toggles the eyebrow line, changing launcher height;
+ // resize so the popup grows/shrinks instead of clipping. requestResize no-ops when unchanged.
+ render({ resize: true });
+ } else if (e.key === "Enter") {
+ e.preventDefault();
+ runSelected();
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ if (query) { query = ""; qEl.value = ""; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); }
+ else SendMessage({ command: "close_page" });
+ }
+ });
+
+ SendMessage({ command: "request_actions" });
+}
diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css
new file mode 100644
index 0000000000..5a0ba0f023
--- /dev/null
+++ b/resources/web/dialog/SpeedDial/style.css
@@ -0,0 +1,185 @@
+* { box-sizing: border-box; }
+html, body { margin: 0; }
+body {
+ font-family: var(--orca-font, "Segoe UI", sans-serif);
+ font-size: 13px;
+ color: var(--text, var(--orca-fg, #1b1c1e));
+ background: var(--bg, var(--orca-bg, #fff));
+ overflow: hidden;
+ user-select: none;
+}
+.launcher {
+ display: flex;
+ flex-direction: column;
+ background: var(--panel, var(--orca-bg, #fff));
+ border: 1px solid var(--border, var(--orca-border, #ddd));
+ overflow: hidden;
+ /* why: no height cap here - the launcher reports its true natural height to C++, which
+ sizes the popup to match (HTML is source of truth). The list's own max-height is what
+ bounds growth; capping the launcher would make the measurement circular. */
+}
+.fav-bar {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 9px 10px;
+ border-bottom: 1px solid var(--border, var(--orca-border, #ddd));
+ overflow-x: auto; /* scroll horizontally once favourites overflow the row */
+ scrollbar-width: thin;
+ /* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge,
+ which would clip its selected outline. Matches the 10px horizontal padding. */
+ scroll-padding-inline: 10px;
+}
+.fav-bar[hidden] { display: none; }
+/* Name of the selected favourite, above the bar; left-aligned to the tiles' 10px inset. */
+.fav-eyebrow { flex: 0 0 auto; padding: 8px 10px 0; }
+.fav-tile {
+ flex: 0 0 auto; /* keep tiles full-size; don't shrink to fit - scroll instead */
+ width: 30px;
+ height: 30px;
+ border: 0;
+ border-radius: 8px;
+ cursor: pointer;
+ color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
+ font-weight: 700;
+ background: var(--speed-tile-bg, #f0f0f0);
+ border: 1px solid var(--speed-tile-border, #d8d8d8);
+}
+.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
+.dial-head { flex: 0 0 auto; padding: 8px; }
+.plugin-search {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 30px;
+ padding: 0 8px;
+ background: var(--panel, var(--orca-bg, #fff));
+ border: 1px solid var(--border, var(--orca-border, #ddd));
+ border-radius: 7px;
+}
+.plugin-search:focus-within {
+ border-color: var(--main-color, var(--orca-accent, #009688));
+ box-shadow: 0 0 0 2px rgba(0,150,136,.25);
+}
+.plugin-search-icon { display: inline-flex; color: var(--muted, var(--orca-muted, #6b7280)); }
+.plugin-search-input {
+ flex: 1;
+ min-width: 0;
+ background: transparent;
+ border: 0;
+ outline: 0;
+ color: var(--text, var(--orca-fg, #1b1c1e));
+ font: inherit;
+}
+.plugin-search-clear {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 12px;
+ height: 12px;
+ padding: 0;
+ border: 0;
+ border-radius: 50%;
+ color: var(--muted, var(--orca-muted, #6b7280));
+ cursor: pointer;
+ background: rgba(127,127,127,.20);
+}
+.plugin-search-clear[hidden] { display: none; }
+.dial-list {
+ flex: 0 1 auto;
+ min-height: 0;
+ /* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so
+ this list cap drives the whole window height: --rows full rows + a ~30% peek of the next row
+ as a "scroll for more" affordance. Bump --rows to show more rows; change 0.3 for a bigger/
+ smaller peek. --row-h MUST match .row min-height (44px). */
+ --row-h: 44px;
+ --rows: 5;
+ max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px);
+ overflow-y: auto;
+ padding: 4px 4px 6px;
+ scrollbar-width: thin;
+}
+.dial-list.empty { overflow-y: hidden; }
+.row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-height: 44px;
+ padding: 6px 8px;
+ border-radius: 7px;
+ cursor: pointer;
+}
+.row:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
+.row.sel { background: var(--row-selected, rgba(0,150,136,.18)); }
+.tile {
+ flex: 0 0 auto;
+ width: 26px;
+ height: 26px;
+ border-radius: 6px;
+ color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
+ font-weight: 700;
+ font-size: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--speed-tile-bg, #f0f0f0);
+ border: 1px solid var(--speed-tile-border, #d8d8d8);
+}
+.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
+.row-eyebrow {
+ font-size: 10px;
+ line-height: 1.3;
+ color: var(--muted, var(--orca-muted, #6b7280));
+ opacity: .85;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.row-line { display: flex; align-items: center; gap: 7px; min-width: 0; }
+.row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.row-name mark, .row-eyebrow mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; }
+.row-sc { flex: 0 0 auto; display: inline-flex; gap: 3px; }
+kbd {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
+ font-size: 11px;
+ color: var(--muted, var(--orca-muted, #6b7280));
+ background: rgba(127,127,127,.12);
+ border: 1px solid var(--border, var(--orca-border, #ddd));
+ border-radius: 4px;
+}
+.row.disabled { opacity: .5; cursor: not-allowed; }
+.star {
+ flex: 0 0 auto;
+ width: 24px;
+ height: 24px;
+ border: 0;
+ border-radius: 5px;
+ background: transparent;
+ color: var(--muted, var(--orca-muted, #6b7280));
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+}
+.star svg { display: block; }
+.row:hover .star:not(.on),
+.row.sel .star:not(.on) { opacity: .8; }
+.star.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); }
+.star:hover { background: rgba(127,127,127,.18); }
+.dial-empty {
+ min-height: 64px;
+ padding: 18px 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--muted, var(--orca-muted, #6b7280));
+ text-align: center;
+}
diff --git a/resources/web/dialog/css/theme.css b/resources/web/dialog/css/theme.css
index 665f0c941a..d2917edfae 100644
--- a/resources/web/dialog/css/theme.css
+++ b/resources/web/dialog/css/theme.css
@@ -51,6 +51,12 @@
--ctx-bg: #ffffff;
--ctx-border: #cccccc;
--ctx-hover: #efefef;
+
+ /* Speed dial icon tile treatment */
+ --speed-tile-bg: #efefef;
+ --speed-tile-border: #d8d8d8;
+ --speed-tile-text-s: 74%;
+ --speed-tile-text-l: 34%;
}
:root[data-orca-theme="dark"] {
@@ -84,6 +90,12 @@
--ctx-bg: #2a313a;
--ctx-border: #4b5664;
--ctx-hover: #3a4451;
+
+ /* Speed dial icon tile treatment */
+ --speed-tile-bg: #34343b;
+ --speed-tile-border: #50505a;
+ --speed-tile-text-s: 82%;
+ --speed-tile-text-l: 84%;
}
/* Re-theme the shared common.css chrome through variables (replaces dark.css's
diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt
index 647af33174..cb54d39d12 100644
--- a/src/slic3r/CMakeLists.txt
+++ b/src/slic3r/CMakeLists.txt
@@ -116,6 +116,10 @@ set(SLIC3R_GUI_SOURCES
GUI/PluginPickerDialog.hpp
GUI/PluginsDialog.cpp
GUI/PluginsDialog.hpp
+ GUI/PluginScriptRunner.cpp
+ GUI/PluginScriptRunner.hpp
+ GUI/SpeedDialDialog.cpp
+ GUI/SpeedDialDialog.hpp
GUI/ProcessRunner.cpp
GUI/ProcessRunner.hpp
GUI/TerminalDialog.cpp
diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp
index 09335d154c..64da34b10a 100644
--- a/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/src/slic3r/GUI/GLCanvas3D.cpp
@@ -1034,6 +1034,7 @@ wxDEFINE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
+wxDEFINE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_INSTANCE_ROTATED, SimpleEvent);
@@ -3502,6 +3503,11 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
break;
}
case '?': { post_event(SimpleEvent(EVT_GLCANVAS_QUESTION_MARK)); break; }
+ case ' ': {
+ if (m_canvas_type == ECanvasType::CanvasView3D)
+ post_event(SimpleEvent(EVT_GLCANVAS_OPEN_SPEED_DIAL));
+ break;
+ }
case 'A':
case 'a':
{
diff --git a/src/slic3r/GUI/GLCanvas3D.hpp b/src/slic3r/GUI/GLCanvas3D.hpp
index 72e7f1f8e2..d47193aaec 100644
--- a/src/slic3r/GUI/GLCanvas3D.hpp
+++ b/src/slic3r/GUI/GLCanvas3D.hpp
@@ -166,6 +166,7 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
+wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event); // data: +1 => increase, -1 => decrease
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index f6e15f8d08..2411d6ee69 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -141,6 +141,7 @@
#include "slic3r/Utils/bambu_networking.hpp"
#include "PluginsDialog.hpp"
+#include "SpeedDialDialog.hpp"
#include "TerminalDialog.hpp"
//#ifdef WIN32
@@ -8246,6 +8247,8 @@ void GUI_App::open_terminal_dialog()
m_terminal_dlg->Raise();
}
+void GUI_App::open_speed_dial() { open_speed_dial_popup(); }
+
void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::string& highlight_option)
{
bool app_layout_changed = false;
diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp
index 19aeef17b4..48789f3247 100644
--- a/src/slic3r/GUI/GUI_App.hpp
+++ b/src/slic3r/GUI/GUI_App.hpp
@@ -624,6 +624,7 @@ public:
void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_terminal_dialog();
+ void open_speed_dial();
void open_exportpresetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
virtual bool OnExceptionInMainLoop() override;
// Calls wxLaunchDefaultBrowser if user confirms in dialog.
diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp
index 3ee3a93e7d..956ef98933 100644
--- a/src/slic3r/GUI/KBShortcutsDialog.cpp
+++ b/src/slic3r/GUI/KBShortcutsDialog.cpp
@@ -267,6 +267,7 @@ void KBShortcutsDialog::fill_shortcuts()
{ "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") },
{ L("Tab"), L("Switch between Prepare/Preview") },
+ { L("Space"), L("Open actions speed dial") },
};
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 822ae7bd4c..12f0df30c8 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -5259,6 +5259,10 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
+ view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
+ if (this->q->is_view3D_shown())
+ wxGetApp().open_speed_dial();
+ });
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event& evt)
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
diff --git a/src/slic3r/GUI/PluginScriptRunner.cpp b/src/slic3r/GUI/PluginScriptRunner.cpp
new file mode 100644
index 0000000000..7ef27fd2ff
--- /dev/null
+++ b/src/slic3r/GUI/PluginScriptRunner.cpp
@@ -0,0 +1,148 @@
+#include "PluginScriptRunner.hpp"
+
+#include "GUI.hpp"
+#include "I18N.hpp"
+#include "slic3r/plugin/PluginManager.hpp"
+#include "slic3r/plugin/PythonInterpreter.hpp"
+#include "slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp"
+
+#include
+
+#include
+
+#include
+
+#include
+
+namespace Slic3r { namespace GUI {
+
+ScriptRunOutcome run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name)
+{
+ if (plugin_key.empty() || capability_name.empty()) {
+ BOOST_LOG_TRIVIAL(warning) << "Ignoring script run request with an empty plugin key or capability name";
+ return {};
+ }
+
+ PluginManager& manager = PluginManager::instance();
+ auto cap = manager.get_loader().get_plugin_capability_by_name(plugin_key, Slic3r::PluginCapabilityType::Script, capability_name);
+ if (!cap) {
+ BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for missing script capability. plugin_key=" << plugin_key
+ << " capability_name=" << capability_name;
+ return {};
+ }
+ if (!cap->enabled) {
+ BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for disabled script capability. plugin_key=" << plugin_key
+ << " capability_name=" << capability_name;
+ return {};
+ }
+
+ // A plugin's modal orca.host.ui dialog or the result message box pumps a nested event
+ // loop; a launcher surface could re-dispatch a run command mid-run. Refuse the
+ // overlapping run. UI-thread only, so a plain static is a sufficient latch.
+ static bool s_script_running = false;
+ if (s_script_running) {
+ BOOST_LOG_TRIVIAL(info) << "Ignoring script run request; a plugin is already running. plugin_key=" << plugin_key;
+ return {ScriptRunOutcome::Level::Busy, wxString()};
+ }
+ s_script_running = true;
+ ScopeGuard running_guard([]() { s_script_running = false; });
+
+ BOOST_LOG_TRIVIAL(info) << "Run script plugin requested. plugin_key=" << plugin_key << " capability_name=" << capability_name;
+
+ auto complete_with_error = [&manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
+ const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
+ if (!manager.get_catalog().set_plugin_error(plugin_key, normalized_error))
+ BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
+
+ if (!manager.get_loader().unload_plugin(plugin_key))
+ BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin after script error. plugin_key=" << plugin_key;
+
+ return ScriptRunOutcome{ScriptRunOutcome::Level::Error,
+ status_message.empty() ? from_u8(normalized_error) : status_message};
+ };
+
+ PluginDescriptor descriptor;
+ if (!manager.get_catalog().try_get_plugin_descriptor(plugin_key, descriptor)) {
+ BOOST_LOG_TRIVIAL(error) << "Cannot run script plugin because manifest was not found. plugin_key=" << plugin_key;
+ return complete_with_error("Plugin manifest was not found.", _L("Plugin manifest was not found."));
+ }
+
+ if (descriptor.has_error())
+ return complete_with_error(descriptor.normalized_error(), wxString());
+
+ // Should not reach here, handle for extra safety
+ if (!descriptor.is_metadata_valid()) {
+ std::string plugin_type_str = plugin_capability_type_to_string(descriptor.primary_capability_type());
+ std::string metadata_valid = descriptor.is_metadata_valid() ? "true" : "false";
+ const std::string plugin_error = "Cannot run plugin because its metadata is invalid:\n\tplugin type: " + plugin_type_str +
+ "\n\tmetadata_valid: " + metadata_valid;
+ BOOST_LOG_TRIVIAL(error) << "Cannot run plugin because its metadata is invalid. plugin_key=" << plugin_key
+ << " is_metadata_valid=" << descriptor.is_metadata_valid()
+ << " type=" << plugin_capability_type_to_string(descriptor.primary_capability_type());
+ return complete_with_error(plugin_error, _L("Only plugins with valid metadata can be run from this dialog."));
+ }
+
+ // Should not reach here as non-loaded plugins have disabled run buttons, handle for extra safety
+ if (!manager.get_loader().is_plugin_loaded(plugin_key)) {
+ BOOST_LOG_TRIVIAL(warning) << "Cannot run script plugin because it is not loaded. plugin_key=" << plugin_key;
+ return complete_with_error("Load the script plugin before running it: Cannot run script plugin because it is not loaded.",
+ _L("Load the script plugin before running it."));
+ }
+
+ auto plugin = std::dynamic_pointer_cast(cap->instance);
+ if (!plugin) {
+ BOOST_LOG_TRIVIAL(error) << "Loaded plugin does not implement ScriptPluginCapability. plugin_key=" << plugin_key;
+ return complete_with_error("The selected plugin is not a runnable script plugin: Loaded plugin does not implement ScriptPluginCapability.",
+ _L("The selected plugin is not a runnable script plugin."));
+ }
+
+ std::string error;
+ ExecutionResult result;
+
+ // Script plugins run on the main/UI thread (not a worker). They hold live, non-owning
+ // ModelObject*/ModelVolume*/ModelInstance* aliases into host data and can mint ObjectIDs,
+ // which libslic3r requires on the main thread (ObjectID.hpp's non-atomic s_last_id). Running
+ // here makes those reads/instantiations legal and means nothing mutates the model underneath
+ // a run. The trade-off is that a slow execute() freezes the UI: the contract (see
+ // plugin_development.md) is to keep execute() quick and offload heavy work to the plugin's own
+ // threading.Thread. orca.host.ui calls already no-op their main-thread marshaling here.
+ {
+ wxBusyCursor busy;
+ try {
+ PythonGILState gil;
+ result = plugin->execute();
+ } catch (const std::exception& ex) {
+ error = ex.what();
+ BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw exception. plugin_key=" << plugin_key << " error=" << error;
+ } catch (...) {
+ error = "Unknown error";
+ BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw unknown exception. plugin_key=" << plugin_key;
+ }
+ }
+
+ if (!error.empty()) {
+ plugin.reset();
+ cap.reset();
+ return complete_with_error(error, wxString());
+ }
+
+ BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
+ << " status=" << static_cast(result.status) << " message=" << result.message << " data=" << result.data;
+
+ const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError;
+ if (failed) {
+ plugin.reset();
+ cap.reset();
+ // complete_with_error normalizes an empty message to "Script plugin failed.".
+ return complete_with_error(result.message, wxString());
+ }
+
+ manager.clear_plugin_error(plugin_key);
+
+ const bool skipped = result.status == PluginResult::Skipped;
+ const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
+ return {skipped ? ScriptRunOutcome::Level::Info : ScriptRunOutcome::Level::Success,
+ result.message.empty() ? fallback : from_u8(result.message)};
+}
+
+}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/PluginScriptRunner.hpp b/src/slic3r/GUI/PluginScriptRunner.hpp
new file mode 100644
index 0000000000..69c723d8eb
--- /dev/null
+++ b/src/slic3r/GUI/PluginScriptRunner.hpp
@@ -0,0 +1,31 @@
+#ifndef slic3r_PluginScriptRunner_hpp_
+#define slic3r_PluginScriptRunner_hpp_
+
+#include
+
+#include
+
+namespace Slic3r { namespace GUI {
+
+// Outcome of running one SCRIPT capability, for the caller's own status surface
+// (PluginsDialog footer, speed-dial notification). An empty message means "nothing
+// worth showing" (stale/disabled requests that were silently ignored).
+struct ScriptRunOutcome
+{
+ enum class Level { Success, Info, Error, Busy };
+
+ Level level = Level::Info;
+ wxString message;
+};
+
+// Runs a SCRIPT plugin capability on the calling (UI) thread: validates the request,
+// executes under the Python GIL, and on failure records the plugin error in the catalog
+// and unloads the package. Shared by every surface that can launch a script so the
+// subtle parts (UI-thread requirement, GIL, re-entrancy latch, error bookkeeping) live
+// in one place. Returns Level::Busy when a script is already running (callers ignore).
+// Callers refresh their plugin views after any non-Busy outcome.
+ScriptRunOutcome run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name);
+
+}} // namespace Slic3r::GUI
+
+#endif
diff --git a/src/slic3r/GUI/PluginsDialog.cpp b/src/slic3r/GUI/PluginsDialog.cpp
index 070bc36a14..1422ffdc49 100644
--- a/src/slic3r/GUI/PluginsDialog.cpp
+++ b/src/slic3r/GUI/PluginsDialog.cpp
@@ -4,10 +4,9 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "OrcaCloudServiceAgent.hpp"
+#include "PluginScriptRunner.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/PluginManager.hpp"
-#include "slic3r/plugin/PythonInterpreter.hpp"
-#include "slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp"
#include
@@ -19,8 +18,6 @@
#include
#include
-#include
-
#include
#include
@@ -898,147 +895,22 @@ wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::string& capability_name)
{
- if (plugin_key.empty() || capability_name.empty()) {
- BOOST_LOG_TRIVIAL(warning) << "Ignoring run_script_plugin with an empty plugin key or capability name";
- send_plugins();
+ // Shared with the speed dial; validation, GIL/UI-thread execution, re-entrancy latch
+ // and error bookkeeping all live in run_script_plugin_capability.
+ const ScriptRunOutcome outcome = run_script_plugin_capability(plugin_key, capability_name);
+ if (outcome.level == ScriptRunOutcome::Level::Busy)
return;
- }
- PluginManager& manager = PluginManager::instance();
- auto cap = manager.get_loader().get_plugin_capability_by_name(
- plugin_key, Slic3r::PluginCapabilityType::Script, capability_name);
- if (!cap) {
- BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for missing script capability. plugin_key=" << plugin_key
- << " capability_name=" << capability_name;
- send_plugins();
- return;
- }
- if (!cap->enabled) {
- BOOST_LOG_TRIVIAL(warning) << "Ignoring stale run request for disabled script capability. plugin_key=" << plugin_key
- << " capability_name=" << capability_name;
- send_plugins();
- return;
- }
-
- // A plugin's modal orca.host.ui dialog or the result message box pumps a nested event
- // loop; the WebView could re-dispatch this command mid-run. Refuse the overlapping run.
- if (m_script_running) {
- BOOST_LOG_TRIVIAL(info) << "Ignoring run_script_plugin; a plugin is already running. plugin_key=" << plugin_key;
- return;
- }
- m_script_running = true;
- ScopeGuard running_guard([this]() { m_script_running = false; });
-
- BOOST_LOG_TRIVIAL(info) << "Run script plugin requested from Plugins dialog. plugin_key=" << plugin_key
- << " capability_name=" << capability_name;
-
- auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
- const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
- if (!manager.get_catalog().set_plugin_error(plugin_key, normalized_error))
- BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
-
- if (!manager.get_loader().unload_plugin(plugin_key))
- BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin after script error. plugin_key=" << plugin_key;
-
- send_plugins();
-
- // The row now shows an "Error" status and the Diagnostics tab holds the full text, so surface
- // the outcome in the footer status bar instead of a modal box (prefer the friendlier override).
- const wxString message = status_message.empty() ? from_u8(normalized_error) : status_message;
- show_status(message, "error");
- };
-
- PluginDescriptor descriptor;
- if (!get_descriptor(plugin_key, descriptor)) {
- BOOST_LOG_TRIVIAL(error) << "Cannot run script plugin because manifest was not found. plugin_key=" << plugin_key;
- complete_with_error("Plugin manifest was not found.", _L("Plugin manifest was not found."));
- return;
- }
-
- if (descriptor.has_error()) {
- complete_with_error(descriptor.normalized_error(), wxString());
- return;
- }
-
- // Should not reach here, handle for extra safety
- if (!descriptor.is_metadata_valid()) {
- std::string plugin_type_str = plugin_capability_type_to_string(descriptor.primary_capability_type());
- std::string metadata_valid = descriptor.is_metadata_valid() ? "true" : "false";
- const std::string plugin_error = "Cannot run plugin because its metadata is invalid:\n\tplugin type: " + plugin_type_str +
- "\n\tmetadata_valid: " + metadata_valid;
- BOOST_LOG_TRIVIAL(error) << "Cannot run plugin because its metadata is invalid. plugin_key=" << plugin_key
- << " is_metadata_valid=" << descriptor.is_metadata_valid()
- << " type=" << plugin_capability_type_to_string(descriptor.primary_capability_type());
- complete_with_error(plugin_error, _L("Only plugins with valid metadata can be run from this dialog."));
- return;
- }
-
- // Should not reach here as non-loaded plugins have disabled run buttons, handle for extra safety
- if (!manager.get_loader().is_plugin_loaded(plugin_key)) {
- BOOST_LOG_TRIVIAL(warning) << "Cannot run script plugin because it is not loaded. plugin_key=" << plugin_key;
- complete_with_error("Load the script plugin before running it: Cannot run script plugin because it is not loaded.",
- _L("Load the script plugin before running it."));
- return;
- }
-
- auto plugin = std::dynamic_pointer_cast(cap->instance);
- if (!plugin) {
- BOOST_LOG_TRIVIAL(error) << "Loaded plugin does not implement ScriptPluginCapability. plugin_key=" << plugin_key;
- complete_with_error("The selected plugin is not a runnable script plugin: Loaded plugin does not implement ScriptPluginCapability.",
- _L("The selected plugin is not a runnable script plugin."));
- return;
- }
-
- std::string error;
- ExecutionResult result;
-
- // Script plugins run on the main/UI thread (not a worker). They hold live, non-owning
- // ModelObject*/ModelVolume*/ModelInstance* aliases into host data and can mint ObjectIDs,
- // which libslic3r requires on the main thread (ObjectID.hpp's non-atomic s_last_id). Running
- // here makes those reads/instantiations legal and means nothing mutates the model underneath
- // a run. The trade-off is that a slow execute() freezes the UI: the contract (see
- // plugin_development.md) is to keep execute() quick and offload heavy work to the plugin's own
- // threading.Thread. orca.host.ui calls already no-op their main-thread marshaling here.
- {
- wxBusyCursor busy;
- try {
- PythonGILState gil;
- result = plugin->execute();
- } catch (const std::exception& ex) {
- error = ex.what();
- BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw exception. plugin_key=" << plugin_key << " error=" << error;
- } catch (...) {
- error = "Unknown error";
- BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw unknown exception. plugin_key=" << plugin_key;
- }
- }
-
- if (!error.empty()) {
- plugin.reset();
- cap.reset();
- complete_with_error(error, wxString());
- return;
- }
-
- BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
- << " status=" << static_cast(result.status) << " message=" << result.message << " data=" << result.data;
-
- const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError;
- if (failed) {
- plugin.reset();
- cap.reset();
- // complete_with_error normalizes an empty message to "Script plugin failed." and reports via the status bar.
- complete_with_error(result.message, wxString());
- return;
- }
-
- manager.clear_plugin_error(plugin_key);
send_plugins();
- const bool skipped = result.status == PluginResult::Skipped;
- const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
- const wxString message = result.message.empty() ? fallback : from_u8(result.message);
- show_status(message, skipped ? "info" : "success");
+ // The row shows any "Error" status and the Diagnostics tab holds the full text, so surface
+ // the outcome in the footer status bar instead of a modal box (prefer the friendlier override).
+ if (outcome.message.empty())
+ return;
+ const char* level = outcome.level == ScriptRunOutcome::Level::Error ? "error" :
+ outcome.level == ScriptRunOutcome::Level::Success ? "success" :
+ "info";
+ show_status(outcome.message, level);
}
void PluginsDialog::update_plugin(const std::string& plugin_key)
diff --git a/src/slic3r/GUI/PluginsDialog.hpp b/src/slic3r/GUI/PluginsDialog.hpp
index 55885262a5..fefb09946f 100644
--- a/src/slic3r/GUI/PluginsDialog.hpp
+++ b/src/slic3r/GUI/PluginsDialog.hpp
@@ -213,11 +213,6 @@ private:
PluginSortKey m_plugin_sort_key = PluginSortKey::None;
PluginSortOrder m_plugin_sort_order = PluginSortOrder::Asc;
- // Serializes run_script_plugin. With main-thread execution a plugin's orca.host.ui modal
- // (message/show_dialog) or the result message box pumps a nested event loop, which could
- // re-dispatch the web "run_script_plugin" command and start a second, overlapping run.
- bool m_script_running = false;
-
// Plugin whose asynchronous activation is in flight, awaited by resolve_pending_activation().
// Empty when no activation is pending.
std::string m_activating_plugin_key;
diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp
new file mode 100644
index 0000000000..a9189851ea
--- /dev/null
+++ b/src/slic3r/GUI/SpeedDialDialog.cpp
@@ -0,0 +1,462 @@
+#include "SpeedDialDialog.hpp"
+
+#include "GUI.hpp"
+#include "GUI_App.hpp"
+#include "MainFrame.hpp"
+#include "MsgDialog.hpp"
+#include "NotificationManager.hpp"
+#include "Plater.hpp"
+#include "PluginScriptRunner.hpp"
+#include "Widgets/PopupWindow.hpp"
+#include "Widgets/WebView.hpp"
+#include "Widgets/WebViewHostDialog.hpp"
+#include "Widgets/StateColor.hpp"
+#include "slic3r/plugin/PluginManager.hpp"
+
+#include
+
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include