Cleanup comments and formatting

This commit is contained in:
Lam Wei Lun
2026-09-10 13:00:23 +08:00
parent 9fc6c45770
commit 16d285fea6
9 changed files with 488 additions and 355 deletions
+12 -13
View File
@@ -1,5 +1,5 @@
// Speed Dial launcher page. Static-safe module: no DOM access at load time so a // Speed Dial launcher page. Static-safe module: no DOM access at load time so a
// node vm can exercise the pure helpers (filterActions / actionLabel / nextSel / commandList). // node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / commandList).
// ---- state (populated by the C++ bridge via window.HandleStudio) ---- // ---- state (populated by the C++ bridge via window.HandleStudio) ----
var ACTIONS = []; // [{id,title,source,group,input,shortcut}], already frecency-sorted by C++ var ACTIONS = []; // [{id,title,source,group,input,shortcut}], already frecency-sorted by C++
@@ -17,10 +17,10 @@ var matchIndex = {};
// a bottom spacer fills the rest of the list so the scrollbar reflects the full match count and // a bottom spacer fills the rest of the list so the scrollbar reflects the full match count and
// "scroll past the last rendered row" reveals the next window. // "scroll past the last rendered row" reveals the next window.
var K_ROWS = 50; var K_ROWS = 50;
var ROW_H = 44; var ROW_H = 44;
var renderEnd = 0; var renderEnd = 0;
var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window
var spacerEl = null; // the trailing height spacer, always the last child of listEl var spacerEl = null; // the trailing height spacer, always the last child of listEl
// search-cache: the normalized (folded+lowercased) needle for the current query pass. // search-cache: the normalized (folded+lowercased) needle for the current query pass.
var searchNeedle = ""; var searchNeedle = "";
@@ -31,8 +31,8 @@ var searchNeedle = "";
var phase = "commands"; var phase = "commands";
var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering the tab phase var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering the tab phase
// why: fuzzy matcher (FoldChar/Norm/FuzzyRanges) lives in shared ../../js/fuzzy-search.js, loaded before // why: the fuzzy matcher (NormText/FuzzyRangesNorm/WholeWordRangesNorm) lives in shared
// this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive. // ../../js/fuzzy-search.js, loaded before this script. Search is always case-insensitive.
// element handles, assigned in OnInit (kept null so load-time touches no DOM) // element handles, assigned in OnInit (kept null so load-time touches no DOM)
var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, headEl = null; var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, headEl = null;
@@ -71,7 +71,7 @@ function fieldMatchScore(norm, wwRe) {
if (wwRe) { if (wwRe) {
var m = wwRe.exec(norm || ""); var m = wwRe.exec(norm || "");
if (m) if (m)
return {score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true}; return { score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true };
} }
var r = FuzzyRangesNorm(norm || "", searchNeedle); var r = FuzzyRangesNorm(norm || "", searchNeedle);
if (!r) return null; if (!r) return null;
@@ -81,7 +81,7 @@ function fieldMatchScore(norm, wwRe) {
gaps += r[i][0] - r[i - 1][1]; gaps += r[i][0] - r[i - 1][1];
len += r[i][1] - r[i][0]; len += r[i][1] - r[i][0];
} }
return {score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length}; return { score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length };
} }
// Combine the per-field match scores into one comparable value. Ranking tiers, strongest first: // Combine the per-field match scores into one comparable value. Ranking tiers, strongest first:
@@ -177,9 +177,8 @@ function resultCountText(total, shown, query) {
return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions"; return (query || "").trim() ? "Showing " + shown + " of " + total + " actions" : total + " actions";
} }
// Display label for a notebook tab. Notebook's ButtonsListCtrl labels every non-empty page as // Display label for a notebook tab. Trim any stray whitespace; pages added with an empty title
// " <text>" (a leading space), so trim it; pages added with an empty title (Home, MainFrame adds // (Home, MainFrame adds TAB_ID_HOME with "") fall back to the title-cased id ("home" -> "Home").
// TAB_ID_HOME with "") fall back to the title-cased id ("home" -> "Home").
function tabTitle(t) { function tabTitle(t) {
var title = (t && t.title) ? String(t.title).trim() : ""; var title = (t && t.title) ? String(t.title).trim() : "";
return title || prettySource((t && t.id) || ""); return title || prettySource((t && t.id) || "");
@@ -537,7 +536,7 @@ function showFavMenu(x, y, id) {
} }
// Swap a favourite with its visible neighbour (dir -1/+1) and persist the new order. Swapping // Swap a favourite with its visible neighbour (dir -1/+1) and persist the new order. Swapping
// by id inside FAVS (not the visible slice) keeps any hidden pins (no live action) in place. // by id inside FAVS (not the visible slice) keeps the persisted order stable.
function moveFav(id, dir) { function moveFav(id, dir) {
var favs = currentVisibleFavs(); var favs = currentVisibleFavs();
var vi = favs.indexOf(id); var vi = favs.indexOf(id);
@@ -990,7 +989,7 @@ function OnInit() {
// commands phase, where the pinned bar is shown. // commands phase, where the pinned bar is shown.
if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) { if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) {
var slotIdx = favIndexForDigit(e.key); var slotIdx = favIndexForDigit(e.key);
var favIds = currentVisibleFavs(); var favIds = currentVisibleFavs();
if (slotIdx >= 0 && slotIdx < favIds.length) { if (slotIdx >= 0 && slotIdx < favIds.length) {
e.preventDefault(); e.preventDefault();
var fav = byId(favIds[slotIdx]); var fav = byId(favIds[slotIdx]);
@@ -13,13 +13,13 @@ vm.runInContext(fs.readFileSync(__dirname + "/speeddial.js", "utf8"), ctx);
assert.equal(typeof ctx.parseId, "undefined", "opaque action ids must never be parsed"); assert.equal(typeof ctx.parseId, "undefined", "opaque action ids must never be parsed");
const duplicateActions = [ const duplicateActions = [
{ id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" },
{ id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" }
]; ];
assert.equal( assert.equal(
ctx.actionLabel(duplicateActions[0], duplicateActions), ctx.actionLabel(duplicateActions[0], duplicateActions),
"Repair from Mesh Tools (0123456789abcdef)", "Repair from Mesh Tools (0123456789abcdef)",
"duplicate labels should use the opaque id without interpreting its contents" "duplicate labels should use the opaque id without interpreting its contents"
); );
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list"); assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list");
@@ -28,118 +28,118 @@ assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering mat
// commandList: an empty query shows recents; a typed query filters all actions. // commandList: an empty query shows recents; a typed query filters all actions.
assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [], assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [],
"empty query + no recents shows nothing"); "empty query + no recents shows nothing");
assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""), assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""),
[duplicateActions[0]], [duplicateActions[0]],
"empty query shows the recent list"); "empty query shows the recent list");
assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions, assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions,
"a typed query filters actions (both identical titles match) instead of showing recents"); "a typed query filters actions (both identical titles match) instead of showing recents");
// filterTabs (tab phase): an empty query keeps the whole list; a typed query filters by title/id. // filterTabs (tab phase): an empty query keeps the whole list; a typed query filters by title/id.
const tabOptions = [ const tabOptions = [
{ id: "home", title: "Home" }, { id: "home", title: "Home" },
{ id: "prepare", title: "Prepare" }, { id: "prepare", title: "Prepare" },
{ id: "monitor", title: "Device" }, { id: "monitor", title: "Device" },
{ id: "project", title: "Project" } { id: "project", title: "Project" }
]; ];
assert.deepEqual(ctx.filterTabs(tabOptions, ""), tabOptions, assert.deepEqual(ctx.filterTabs(tabOptions, ""), tabOptions,
"empty query keeps the whole tab list"); "empty query keeps the whole tab list");
assert.equal(ctx.filterTabs(tabOptions, "prep").length, 1, assert.equal(ctx.filterTabs(tabOptions, "prep").length, 1,
"a typed query filters tabs by title"); "a typed query filters tabs by title");
assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1, assert.equal(ctx.filterTabs(tabOptions, "Device").length, 1,
"a typed query matches a tab title"); "a typed query matches a tab title");
assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [], assert.deepEqual(ctx.filterTabs(tabOptions, "zzz"), [],
"a typed query with no match returns an empty list"); "a typed query with no match returns an empty list");
// tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id. // tabTitle: pages added with an empty title (e.g. MainFrame's Home tab) fall back to the id.
assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home", assert.equal(ctx.tabTitle({ id: "home", title: "" }), "Home",
"an empty title falls back to the title-cased id"); "an empty title falls back to the title-cased id");
assert.equal(ctx.tabTitle({ id: "home" }), "Home", assert.equal(ctx.tabTitle({ id: "home" }), "Home",
"a missing title falls back to the title-cased id"); "a missing title falls back to the title-cased id");
assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare", assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare",
"a populated title is kept as-is"); "a populated title is kept as-is");
assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare", assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare",
"a leading space from the Notebook button label is trimmed so the label shows cleanly"); "a stray leading space in a tab title is trimmed so the label shows cleanly");
assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1, assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
"an untitled tab still matches a typed query via the id/title fallback"); "an untitled tab still matches a typed query via the id/title fallback");
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1, assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
"a leading-space tab title still matches a typed query"); "a leading-space tab title still matches a typed query");
// The main phase is ONE pool: commands/plugins/settings are all actions, ranked by relevance // The main phase is ONE pool: commands/plugins/settings are all actions, ranked by relevance
// (no group headers, no actions-vs-settings discrimination). // (no group headers, no actions-vs-settings discrimination).
const pool = [ const pool = [
{ id: "c1", title: "Layer Height", source: "Quality", group: "Quality : Layers", input: "" }, { id: "c1", title: "Layer Height", source: "Quality", group: "Quality : Layers", input: "" },
{ id: "s1", title: "Go to layer (percent)", source: "OrcaSlicer", group: "Commands", input: "percent" }, { id: "s1", title: "Go to layer (percent)", source: "OrcaSlicer", group: "Commands", input: "percent" },
{ id: "c2", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", input: "" } { id: "c2", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", input: "" }
]; ];
assert.deepEqual(ctx.searchActions(pool, ""), pool, "an empty query returns the pool unchanged"); assert.deepEqual(ctx.searchActions(pool, ""), pool, "an empty query returns the pool unchanged");
assert.equal(ctx.searchActions(pool, "zzz").length, 0, "a query with no match returns nothing"); assert.equal(ctx.searchActions(pool, "zzz").length, 0, "a query with no match returns nothing");
// "layer" matches multiple; the exact-titled action ranks above the loosely-matching command. // "layer" matches multiple; the exact-titled action ranks above the loosely-matching command.
assert.equal(ctx.searchActions(pool, "layer")[0].id, "c1", assert.equal(ctx.searchActions(pool, "layer")[0].id, "c1",
"a title-exact match ranks above a partial match"); "a title-exact match ranks above a partial match");
assert.equal(ctx.searchActions(pool, "layer").length >= 2, true, assert.equal(ctx.searchActions(pool, "layer").length >= 2, true,
"both a setting and a command match the same query in the same list"); "both a setting and a command match the same query in the same list");
assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2", assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2",
"a later-but-precise match still ranks by relevance, not by pool type"); "a later-but-precise match still ranks by relevance, not by pool type");
// A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a // A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a
// contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"), // contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"),
// which is what the old flat title-bonus ranking got backwards. // which is what the old flat title-bonus ranking got backwards.
const perfectPool = [ const perfectPool = [
{ id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" }, { id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" },
{ id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" } { id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" }
]; ];
assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent", assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent",
"a contiguous header/group match ranks above a scattered fuzzy title match"); "a contiguous header/group match ranks above a scattered fuzzy title match");
// Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE // Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE
// contains the needle perfectly beats the action whose GROUP does, both being contiguous matches. // contains the needle perfectly beats the action whose GROUP does, both being contiguous matches.
const titleFirstPool = [ const titleFirstPool = [
{ id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" }, { id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" },
{ id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" } { id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" }
]; ];
assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t", assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t",
"a perfect title match ranks above an equally-perfect group match"); "a perfect title match ranks above an equally-perfect group match");
// Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the // Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the
// whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost). // whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost).
const orientPool = [ const orientPool = [
{ id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" } { id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" }
]; ];
ctx.searchActions(orientPool, "orient"); ctx.searchActions(orientPool, "orient");
assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]], assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]],
"a whole-word match highlights the full word, not a scattered fuzzy pick"); "a whole-word match highlights the full word, not a scattered fuzzy pick");
// commandList (the main-phase list) delegates to the ranked search for a typed query and returns // commandList (the main-phase list) delegates to the ranked search for a typed query and returns
// the mixed recents (no discrimination) for an empty query. // the mixed recents (no discrimination) for an empty query.
const mixed = [ const mixed = [
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" }, { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" },
{ id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" } { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" }
]; ];
assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd", assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd",
"a typed query keeps the relevance-ranked action list (best match first)"); "a typed query keeps the relevance-ranked action list (best match first)");
assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd"], assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd"],
"an empty query shows the mixed recents list verbatim"); "an empty query shows the mixed recents list verbatim");
// selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise). // selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise).
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""),
null, null,
"Enter with an empty query and no recents must not resolve to an action the list never showed" "Enter with an empty query and no recents must not resolve to an action the list never showed"
); );
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"),
"0123456789abcdef", "0123456789abcdef",
"a typed query resolves the list selection" "a typed query resolves the list selection"
); );
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""), ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""),
"0123456789abcdef", "0123456789abcdef",
"Enter with an empty query resolves the recent entry" "Enter with an empty query resolves the recent entry"
); );
assert.equal( assert.equal(
ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""),
"fedcba9876543210", "fedcba9876543210",
"favourites stay runnable with an empty query - the fav bar is always visible" "favourites stay runnable with an empty query - the fav bar is always visible"
); );
// Fav quick-launch slots: digit 1..9 -> index 0..8, digit 0 -> index 9 (the 10th), else -1. // Fav quick-launch slots: digit 1..9 -> index 0..8, digit 0 -> index 9 (the 10th), else -1.
@@ -160,21 +160,21 @@ assert.equal(ctx.K_FAV_LIMIT, 10, "the slot count matches the quick-launch cap")
// nextSel: arrow-nav wrapping. Down wraps at the list bottom to the first row; Up wraps at the // nextSel: arrow-nav wrapping. Down wraps at the list bottom to the first row; Up wraps at the
// list top to the last row ONLY when there's no fav bar above (else it goes to the fav bar). // list top to the last row ONLY when there's no fav bar above (else it goes to the fav bar).
assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowDown", 3, 0), { zone: "list", i: 0 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowDown", 3, 0), { zone: "list", i: 0 },
"ArrowDown at the last row wraps to the first row"); "ArrowDown at the last row wraps to the first row");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 1 }, "ArrowDown", 3, 0), { zone: "list", i: 2 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 1 }, "ArrowDown", 3, 0), { zone: "list", i: 2 },
"ArrowDown in the middle advances by one"); "ArrowDown in the middle advances by one");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 0), { zone: "list", i: 2 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 0), { zone: "list", i: 2 },
"ArrowUp at the first row with no fav bar wraps to the last row"); "ArrowUp at the first row with no fav bar wraps to the last row");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 2), { zone: "fav", i: 0 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 3, 2), { zone: "fav", i: 0 },
"ArrowUp at the first row with a fav bar goes to the fav bar (unchanged)"); "ArrowUp at the first row with a fav bar goes to the fav bar (unchanged)");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowUp", 3, 0), { zone: "list", i: 1 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 2 }, "ArrowUp", 3, 0), { zone: "list", i: 1 },
"ArrowUp in the middle moves up by one"); "ArrowUp in the middle moves up by one");
assert.deepEqual(ctx.nextSel({ zone: "fav", i: 1 }, "ArrowDown", 3, 2), { zone: "list", i: 0 }, assert.deepEqual(ctx.nextSel({ zone: "fav", i: 1 }, "ArrowDown", 3, 2), { zone: "list", i: 0 },
"ArrowDown from the fav bar lands on the first list row"); "ArrowDown from the fav bar lands on the first list row");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowDown", 1, 0), { zone: "list", i: 0 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowDown", 1, 0), { zone: "list", i: 0 },
"a single-row list never wraps off the end"); "a single-row list never wraps off the end");
assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 1, 0), { zone: "list", i: 0 }, assert.deepEqual(ctx.nextSel({ zone: "list", i: 0 }, "ArrowUp", 1, 0), { zone: "list", i: 0 },
"ArrowUp on the only row stays put"); "ArrowUp on the only row stays put");
// Windowed list reveal: how many rows must be materialized to cover `fromIndex` plus `size` more, // Windowed list reveal: how many rows must be materialized to cover `fromIndex` plus `size` more,
// clamped to the total. Drives the "render the next window on scroll / arrow-nav" append. // clamped to the total. Drives the "render the next window on scroll / arrow-nav" append.
+367 -229
View File
@@ -1,285 +1,423 @@
* { box-sizing: border-box; } * {
html, body { margin: 0; } box-sizing: border-box;
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;
} }
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 { .launcher {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--panel, var(--orca-bg, #fff)); background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
overflow: hidden; overflow: hidden;
/* why: no height cap here - the launcher reports its true natural height to C++, which /* 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 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. */ bounds growth; capping the launcher would make the measurement circular. */
} }
.fav-bar { .fav-bar {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 9px 10px; padding: 9px 10px;
border-bottom: 1px solid var(--border, var(--orca-border, #ddd)); border-bottom: 1px solid var(--border, var(--orca-border, #ddd));
overflow-x: auto; /* scroll horizontally once favourites overflow the row */ overflow-x: auto;
scrollbar-width: thin; /* scroll horizontally once favourites overflow the row */
/* why: keep scrollIntoView (arrow-nav) from scrolling the first/last tile flush to the edge, 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. */ which would clip its selected outline. Matches the 10px horizontal padding. */
scroll-padding-inline: 10px; scroll-padding-inline: 10px;
} }
.fav-bar[hidden] { display: none; }
.fav-bar[hidden] {
display: none;
}
/* Name of the selected favourite, above the bar; left-aligned to the tiles' 10px inset. */ /* 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-eyebrow {
flex: 0 0 auto;
padding: 8px 10px 0;
}
.fav-tile { .fav-tile {
flex: 0 0 auto; /* keep tiles full-size; don't shrink to fit - scroll instead */ flex: 0 0 auto;
width: 30px; /* keep tiles full-size; don't shrink to fit - scroll instead */
height: 30px; width: 30px;
border: 0; height: 30px;
border-radius: 8px; border: 0;
cursor: pointer; border-radius: 8px;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); cursor: pointer;
font-weight: 700; color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
/* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits font-weight: 700;
/* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits
13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */ 13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */
font-size: 12px; font-size: 12px;
padding: 0; padding: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
background: var(--speed-tile-bg, #f0f0f0); background: var(--speed-tile-bg, #f0f0f0);
border: 1px solid var(--speed-tile-border, #d8d8d8); border: 1px solid var(--speed-tile-border, #d8d8d8);
} }
.fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; }
.fav-tile.sel {
outline: 2px solid var(--main-color, var(--orca-accent, #009688));
outline-offset: 2px;
}
/* Hover-revealed remove button in the tile's corner; sits inside the tile bounds so overflow:hidden clips it. */ /* Hover-revealed remove button in the tile's corner; sits inside the tile bounds so overflow:hidden clips it. */
.fav-tile { position: relative; } .fav-tile {
.fav-unpin { position: relative;
position: absolute;
top: 1px;
right: 1px;
width: 13px;
height: 13px;
padding: 0;
border: 0;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127,127,127,.35);
cursor: pointer;
opacity: 0;
} }
.fav-unpin {
position: absolute;
top: 1px;
right: 1px;
width: 13px;
height: 13px;
padding: 0;
border: 0;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127, 127, 127, .35);
cursor: pointer;
opacity: 0;
}
.fav-tile:hover .fav-unpin, .fav-tile:hover .fav-unpin,
.fav-tile.sel .fav-unpin { opacity: 1; } .fav-tile.sel .fav-unpin {
.fav-unpin:hover { background: rgba(127,127,127,.6); } opacity: 1;
}
.fav-unpin:hover {
background: rgba(127, 127, 127, .6);
}
/* Numbered quick-launch slot (Alt/Option+digit), top-left corner of the fav tile. */ /* Numbered quick-launch slot (Alt/Option+digit), top-left corner of the fav tile. */
.fav-slot { .fav-slot {
position: absolute; position: absolute;
top: 1px; top: 1px;
left: 1px; left: 1px;
min-width: 11px; min-width: 11px;
height: 11px; height: 11px;
padding: 0 2px; padding: 0 2px;
border-radius: 3px; border-radius: 3px;
font-size: 8px; font-size: 8px;
font-weight: 600; font-weight: 600;
line-height: 11px; line-height: 11px;
text-align: center; text-align: center;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127,127,127,.22); background: rgba(127, 127, 127, .22);
} }
/* Transient flash banner pinned to the top of the launcher (e.g. "favourites are full"). */ /* Transient flash banner pinned to the top of the launcher (e.g. "favourites are full"). */
.dial-flash { .dial-flash {
flex: 0 0 auto; flex: 0 0 auto;
padding: 4px 10px; padding: 4px 10px;
font-size: 11px; font-size: 11px;
color: var(--plugin-status-warn, #b45309); color: var(--plugin-status-warn, #b45309);
background: var(--plugin-status-warn-bg, rgba(255,193,7,.22)); background: var(--plugin-status-warn-bg, rgba(255, 193, 7, .22));
animation: dial-flash-fade 2.5s ease forwards; animation: dial-flash-fade 2.5s ease forwards;
} }
@keyframes dial-flash-fade { 0% { opacity: 0; } 10% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } }
@keyframes dial-flash-fade {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.ctx-menu { .ctx-menu {
position: fixed; position: fixed;
z-index: 10; z-index: 10;
min-width: 120px; min-width: 120px;
padding: 4px; padding: 4px;
background: var(--panel, var(--orca-bg, #fff)); background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px; border-radius: 7px;
box-shadow: 0 4px 16px rgba(0,0,0,.18); box-shadow: 0 4px 16px rgba(0, 0, 0, .18);
} }
.ctx-menu[hidden] { display: none; }
.ctx-menu[hidden] {
display: none;
}
.ctx-item { .ctx-item {
display: block; display: block;
width: 100%; width: 100%;
padding: 6px 10px; padding: 6px 10px;
border: 0; border: 0;
border-radius: 5px; border-radius: 5px;
background: transparent; background: transparent;
color: var(--text, var(--orca-fg, #1b1c1e)); color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit; font: inherit;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
} }
.ctx-item:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
.ctx-item:disabled { opacity: .4; cursor: default; } .ctx-item:hover {
.ctx-item:disabled:hover { background: transparent; } background: var(--row-hover, rgba(127, 127, 127, .16));
.dial-head { flex: 0 0 auto; padding: 8px; } }
.ctx-item:disabled {
opacity: .4;
cursor: default;
}
.ctx-item:disabled:hover {
background: transparent;
}
.dial-head {
flex: 0 0 auto;
padding: 8px;
}
.plugin-search { .plugin-search {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
height: 30px; height: 30px;
padding: 0 8px; padding: 0 8px;
background: var(--panel, var(--orca-bg, #fff)); background: var(--panel, var(--orca-bg, #fff));
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 7px; border-radius: 7px;
} }
.plugin-search:focus-within { .plugin-search:focus-within {
border-color: var(--main-color, var(--orca-accent, #009688)); border-color: var(--main-color, var(--orca-accent, #009688));
box-shadow: 0 0 0 2px rgba(0,150,136,.25); 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-icon {
display: inline-flex;
color: var(--muted, var(--orca-muted, #6b7280));
}
.plugin-search-input { .plugin-search-input {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
background: transparent; background: transparent;
border: 0; border: 0;
outline: 0; outline: 0;
color: var(--text, var(--orca-fg, #1b1c1e)); color: var(--text, var(--orca-fg, #1b1c1e));
font: inherit; font: inherit;
} }
.plugin-search-clear { .plugin-search-clear {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 12px; width: 12px;
height: 12px; height: 12px;
padding: 0; padding: 0;
border: 0; border: 0;
border-radius: 50%; border-radius: 50%;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer; cursor: pointer;
background: rgba(127,127,127,.20); background: rgba(127, 127, 127, .20);
} }
.plugin-search-clear[hidden] { display: none; }
.plugin-search-clear[hidden] {
display: none;
}
.dial-count { .dial-count {
padding: 4px 4px 0; padding: 4px 4px 0;
text-align: left; text-align: left;
font-size: 11px; font-size: 11px;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
} }
.dial-count[hidden] { display: none; }
/* Section header inside the list (e.g. the "Recent" row above the recent entries). */ .dial-count[hidden] {
.dial-group { display: none;
padding: 8px 8px 2px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted, var(--orca-muted, #6b7280));
} }
.dial-list { .dial-list {
flex: 0 1 auto; flex: 0 1 auto;
min-height: 0; min-height: 0;
/* ADJUST HEIGHT HERE. The popup auto-resizes to the content (HTML is the source of truth), so /* 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 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/ 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). */ smaller peek. --row-h MUST match .row min-height (44px). */
--row-h: 44px; --row-h: 44px;
--rows: 5; --rows: 5;
max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px); max-height: calc(var(--rows) * var(--row-h) + 0.3 * var(--row-h) + 4px);
overflow-y: auto; overflow-y: auto;
padding: 4px 4px 6px; padding: 4px 4px 6px;
scrollbar-width: thin; scrollbar-width: thin;
} }
.dial-list.empty { overflow-y: hidden; }
.dial-list.empty {
overflow-y: hidden;
}
/* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects /* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects
the full match count, while only a near-viewport window of rows exists in the DOM. */ the full match count, while only a near-viewport window of rows exists in the DOM. */
.dial-spacer-bottom { flex: 0 0 auto; } .dial-spacer-bottom {
flex: 0 0 auto;
}
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
min-height: 44px; min-height: 44px;
padding: 6px 8px; padding: 6px 8px;
border-radius: 7px; border-radius: 7px;
cursor: pointer; cursor: pointer;
} }
.row:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
.row.sel { background: var(--row-selected, rgba(0,150,136,.18)); } .row:hover {
background: var(--row-hover, rgba(127, 127, 127, .16));
}
.row.sel {
background: var(--row-selected, rgba(0, 150, 136, .18));
}
.tile { .tile {
flex: 0 0 auto; flex: 0 0 auto;
width: 26px; width: 26px;
height: 26px; height: 26px;
border-radius: 6px; border-radius: 6px;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
font-weight: 700; font-weight: 700;
font-size: 12px; font-size: 12px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--speed-tile-bg, #f0f0f0); background: var(--speed-tile-bg, #f0f0f0);
border: 1px solid var(--speed-tile-border, #d8d8d8); border: 1px solid var(--speed-tile-border, #d8d8d8);
} }
.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.row-left {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
}
.row-eyebrow { .row-eyebrow {
font-size: 10px; font-size: 10px;
line-height: 1.3; line-height: 1.3;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
opacity: .85; opacity: .85;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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-line {
.row-name mark, .row-eyebrow mark { background: var(--plugin-status-warn-bg); color: var(--plugin-status-warn); border-radius: 2px; } display: flex;
.row-sc { flex: 0 0 auto; display: inline-flex; gap: 3px; } 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 { kbd {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 18px; min-width: 18px;
height: 18px; height: 18px;
padding: 0 5px; padding: 0 5px;
font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
font-size: 11px; font-size: 11px;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
background: rgba(127,127,127,.12); background: rgba(127, 127, 127, .12);
border: 1px solid var(--border, var(--orca-border, #ddd)); border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 4px; border-radius: 4px;
} }
.pin { .pin {
flex: 0 0 auto; flex: 0 0 auto;
width: 24px; width: 24px;
height: 24px; height: 24px;
border: 0; border: 0;
border-radius: 5px; border-radius: 5px;
background: transparent; background: transparent;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
opacity: 0; opacity: 0;
} }
.pin svg { display: block; }
.pin svg {
display: block;
}
.row:hover .pin:not(.on), .row:hover .pin:not(.on),
.row.sel .pin:not(.on) { opacity: .5; } .row.sel .pin:not(.on) {
.pin.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); } opacity: .5;
.pin:hover { background: rgba(127,127,127,.18); } }
.pin.on {
opacity: 1;
color: var(--main-color, var(--orca-accent, #009688));
}
.pin:hover {
background: rgba(127, 127, 127, .18);
}
.dial-empty { .dial-empty {
min-height: 64px; min-height: 64px;
padding: 18px 10px; padding: 18px 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--muted, var(--orca-muted, #6b7280)); color: var(--muted, var(--orca-muted, #6b7280));
text-align: center; text-align: center;
} }
+2 -2
View File
@@ -200,8 +200,8 @@ struct CommandAction : AppAction
private: private:
explicit CommandAction(const NativeCommand& c) explicit CommandAction(const NativeCommand& c)
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName), : AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName)
command_key(c.key) , command_key(c.key)
{ {
this->kind = AppActionKind::Command; this->kind = AppActionKind::Command;
this->group = c.group; this->group = c.group;
+23 -24
View File
@@ -19,7 +19,7 @@ namespace Slic3r { namespace GUI {
// How a source's action set changed. Drives the registry's refresh handlers. // How a source's action set changed. Drives the registry's refresh handlers.
enum class ActionChange { Added, Removed }; enum class ActionChange { Added, Removed };
// What kind of runnable thing an action is. Drives the palette section + dispatch. // What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't).
enum class AppActionKind { Plugin, Command }; enum class AppActionKind { Plugin, Command };
// Result of running an AppAction, in the action layer's own vocabulary. Concrete // Result of running an AppAction, in the action layer's own vocabulary. Concrete
@@ -28,8 +28,8 @@ struct AppActionRunResult
{ {
enum class Level { Success, Info, Error, Busy }; enum class Level { Success, Info, Error, Busy };
Level level = Level::Info; Level level = Level::Info;
wxString message; // empty = "nothing worth showing" wxString message; // empty = "nothing worth showing"
}; };
// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the // Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the
@@ -64,16 +64,16 @@ struct AppAction
} }
// seeded from AppConfig for the snapshot / sort: // seeded from AppConfig for the snapshot / sort:
bool favourite = false; bool favourite = false;
int count = 0; int count = 0;
long long last = 0; // epoch seconds long long last = 0; // epoch seconds
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the // Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
// source name); Command sets a section label (e.g. "Commands", "Mode"). // source name); Command sets a section label (e.g. "Commands", "Mode").
AppActionKind kind = AppActionKind::Plugin; AppActionKind kind = AppActionKind::Plugin;
std::string group; std::string group;
// Second-phase input descriptor for the palette: "settings" (jump to a config option) // Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
// or "percent" (jump to layer by a 0-100 value). Empty = run immediately on activation. // value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
std::string input; std::string input;
virtual ~AppAction() = default; virtual ~AppAction() = default;
@@ -87,18 +87,17 @@ protected:
// why: source_key (not the display name) carries identity, so renaming the source's // why: source_key (not the display name) carries identity, so renaming the source's
// display name leaves the id - and its persisted stats/favourite - intact. // display name leaves the id - and its persisted stats/favourite - intact.
AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name) AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name)
: m_id(compose_id(prefix, title, source_key)), : m_id(compose_id(prefix, title, source_key))
m_title(std::move(title)), , m_title(std::move(title))
m_source_key(std::move(source_key)), , m_source_key(std::move(source_key))
m_source_name(std::move(source_name)) {} , m_source_name(std::move(source_name))
{}
// Explicit-id ctor: for actions whose id must NOT be derived from the display title // Explicit-id ctor: for actions whose id must NOT be derived from the display title
// (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it). // (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it).
AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name) AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name)
: m_id(std::move(id.id)), : m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name))
m_title(std::move(title)), {}
m_source_key(std::move(source_key)),
m_source_name(std::move(source_name)) {}
private: private:
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
@@ -137,7 +136,7 @@ public:
void remove(const std::string& id); void remove(const std::string& id);
// Always-clean read surface. UI thread only. // Always-clean read surface. UI thread only.
const AppAction* by_id(const std::string& id) const; const AppAction* by_id(const std::string& id) const;
// Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0). // Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0).
static constexpr size_t kFavLimit = 10; static constexpr size_t kFavLimit = 10;
@@ -146,8 +145,8 @@ public:
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
// Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the // Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
// caller can surface a "favourites are full" hint instead of silently dropping the pin. // caller can surface a "favourites are full" hint instead of silently dropping the pin.
bool set_favourite(const std::string& id, bool on); bool set_favourite(const std::string& id, bool on);
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
// Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the // Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the
// visible bar the palette renders. // visible bar the palette renders.
@@ -169,8 +168,8 @@ public:
nlohmann::json tab_options() const; nlohmann::json tab_options() const;
private: private:
void seed_state(AppAction& a) const; // favourite/stats from config void seed_state(AppAction& a) const; // favourite/stats from config
AppAction* find(const std::string& id); AppAction* find(const std::string& id);
// (Re)materialise the current visible config settings as SettingActions from the live // (Re)materialise the current visible config settings as SettingActions from the live
// searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones. // searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones.
@@ -192,8 +191,8 @@ private:
void refresh_source(const std::string& plugin_key, ActionChange change); void refresh_source(const std::string& plugin_key, ActionChange change);
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change); void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
bool m_started = false; // init() runs exactly once; guards double-subscription bool m_started = false; // init() runs exactly once; guards double-subscription
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
}; };
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+4 -4
View File
@@ -4375,10 +4375,10 @@ void MainFrame::technology_changed()
m_menubar->SetMenuLabel(id, pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); m_menubar->SetMenuLabel(id, pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings"));
} }
// Opens the calibration wizard for `calib_kind`, reusing the cached member dialogs the Calibration // Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle:
// menu builds. This is the single source of truth for the wizard lifecycle: the Calibration menu // the Calibration menu handlers and the Speed Dial native commands both call it. Most wizards are
// handlers and the Speed Dial native commands both call it, so they share the same per-wizard // cached members reused across launches; cornering/input-shaping build a fresh transient dialog.
// member (fresh on first launch, reused thereafter). Call while the Prepare (3D) panel is shown. // Call while the Prepare (3D) panel is shown.
void MainFrame::run_calibration(CalibKind calib_kind) void MainFrame::run_calibration(CalibKind calib_kind)
{ {
switch (calib_kind) { switch (calib_kind) {
+4 -5
View File
@@ -107,7 +107,6 @@ protected:
}; };
// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners. // Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners.
// Kept in order with the wizard list below.
enum class CalibKind : int enum class CalibKind : int
{ {
Temperature, Temperature,
@@ -364,10 +363,10 @@ public:
void technology_changed(); void technology_changed();
// Opens the calibration wizard for `kind`, reusing the cached member dialogs the Calibration // Opens the calibration wizard for `kind`. Single source of truth for the wizard lifecycle:
// menu builds (m_*_calib_dlg). Single source of truth for the wizard lifecycle: the Calibration // the Calibration menu handlers and the Speed Dial native commands both call this. Most wizards
// menu handlers and the Speed Dial native commands both call this. Call while the Prepare (3D) // are cached members; cornering/input-shaping are transient. Call while the Prepare (3D) panel
// panel is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first). // is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first).
void run_calibration(CalibKind calib_kind); void run_calibration(CalibKind calib_kind);
//BBS //BBS
+13 -15
View File
@@ -58,9 +58,9 @@ AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Pla
return {AppActionRunResult::Level::Success}; return {AppActionRunResult::Level::Success};
} }
// Jump the preview to a layer selected by a 0-100 percent of the layer range. Best-effort: switches // Jump the preview to a layer selected by a 0-100 percent of the layer range. The caller has already
// to the preview tab and requests a slice; if the slicer result is already present the slider is // switched to Preview (which may request a slice); if a slicer result is present the slider is
// repositioned immediately, otherwise the user can re-run after slicing. // repositioned immediately, otherwise the jump is a no-op until the user re-slices.
void go_to_layer(Plater* plater, const std::string& param) void go_to_layer(Plater* plater, const std::string& param)
{ {
if (!plater) if (!plater)
@@ -100,8 +100,8 @@ AppActionRunResult view_command(Plater* plater, const std::string& dir)
return {AppActionRunResult::Level::Success}; return {AppActionRunResult::Level::Success};
} }
// Calibration wizards. Reuses MainFrame::run_calibration so the speed dial shows the same cached // Calibration wizards. Routes through MainFrame::run_calibration, the same entry point as the
// member dialogs as the Calibration menu (the menu handlers call run_calibration too). // Calibration menu (which caches most of the wizard dialogs).
AppActionRunResult calib_command(CalibKind kind) AppActionRunResult calib_command(CalibKind kind)
{ {
MainFrame* mf = wxGetApp().mainframe; MainFrame* mf = wxGetApp().mainframe;
@@ -115,8 +115,8 @@ AppActionRunResult calib_command(CalibKind kind)
std::vector<NativeCommand> build_command_catalog() std::vector<NativeCommand> build_command_catalog()
{ {
std::vector<NativeCommand> out; std::vector<NativeCommand> out;
auto add = [&](std::string key, std::string title, std::string group, auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) { std::string input = {}) {
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)}); out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)});
}; };
@@ -207,12 +207,11 @@ std::vector<NativeCommand> build_command_catalog()
plater->export_gcode_3mf(false); plater->export_gcode_3mf(false);
return AppActionRunResult{AppActionRunResult::Level::Success}; return AppActionRunResult{AppActionRunResult::Level::Success};
}); });
add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), [](const std::string&) {
[](const std::string&) { if (Plater* plater = wxGetApp().plater())
if (Plater* plater = wxGetApp().plater()) plater->export_gcode_3mf(true);
plater->export_gcode_3mf(true); return AppActionRunResult{AppActionRunResult::Level::Success};
return AppActionRunResult{AppActionRunResult::Level::Success}; });
});
// ---- Calibration ---- // ---- Calibration ----
add("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), add("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"),
@@ -231,8 +230,7 @@ std::vector<NativeCommand> build_command_catalog()
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); }); [](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
add("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), add("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); }); [](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), [](const std::string&) { return calib_command(CalibKind::VFA); });
[](const std::string&) { return calib_command(CalibKind::VFA); });
// ---- View ---- // ---- View ----
for (auto [key, dir, title] : for (auto [key, dir, title] :
+2 -2
View File
@@ -16,12 +16,12 @@ struct NativeCommand
std::string key; std::string key;
std::string title; std::string title;
std::string group; std::string group;
std::string input; // "settings"/"percent"/"tab" or "" for immediate run std::string input; // "percent"/"tab" or "" for immediate run
std::function<AppActionRunResult(const std::string& param)> runner; std::function<AppActionRunResult(const std::string& param)> runner;
}; };
namespace NativeCommands { namespace NativeCommands {
// The full built-in command catalog, built once (init()). UI thread only. // The full built-in command catalog, built once on first use. UI thread only.
const std::vector<NativeCommand>& catalog(); const std::vector<NativeCommand>& catalog();
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only. // Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.