From 16d285fea6fae0db16e9d0c7f9e12cd884694a13 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 10 Sep 2026 13:00:23 +0800 Subject: [PATCH] Cleanup comments and formatting --- resources/web/dialog/SpeedDial/speeddial.js | 25 +- .../web/dialog/SpeedDial/speeddial.test.js | 122 ++-- resources/web/dialog/SpeedDial/style.css | 596 +++++++++++------- src/slic3r/GUI/ActionRegistry.cpp | 4 +- src/slic3r/GUI/ActionRegistry.hpp | 47 +- src/slic3r/GUI/MainFrame.cpp | 8 +- src/slic3r/GUI/MainFrame.hpp | 9 +- src/slic3r/GUI/NativeCommands.cpp | 28 +- src/slic3r/GUI/NativeCommands.hpp | 4 +- 9 files changed, 488 insertions(+), 355 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index b92261b351..efe99db1b8 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -1,5 +1,5 @@ // 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) ---- 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 // "scroll past the last rendered row" reveals the next window. var K_ROWS = 50; -var ROW_H = 44; +var ROW_H = 44; var renderEnd = 0; -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 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 // search-cache: the normalized (folded+lowercased) needle for the current query pass. var searchNeedle = ""; @@ -31,8 +31,8 @@ var searchNeedle = ""; var phase = "commands"; 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 -// this script - it is shared with the Plugins dialog. Speed dial search is always case-insensitive. +// why: the fuzzy matcher (NormText/FuzzyRangesNorm/WholeWordRangesNorm) lives in shared +// ../../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) 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) { var m = wwRe.exec(norm || ""); 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); if (!r) return null; @@ -81,7 +81,7 @@ function fieldMatchScore(norm, wwRe) { gaps += r[i][0] - r[i - 1][1]; 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: @@ -177,9 +177,8 @@ function resultCountText(total, shown, query) { 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 -// " " (a leading space), so trim it; pages added with an empty title (Home, MainFrame adds -// TAB_ID_HOME with "") fall back to the title-cased id ("home" -> "Home"). +// Display label for a notebook tab. Trim any stray whitespace; pages added with an empty title +// (Home, MainFrame adds TAB_ID_HOME with "") fall back to the title-cased id ("home" -> "Home"). function tabTitle(t) { var title = (t && t.title) ? String(t.title).trim() : ""; 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 -// 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) { var favs = currentVisibleFavs(); var vi = favs.indexOf(id); @@ -990,7 +989,7 @@ function OnInit() { // commands phase, where the pinned bar is shown. if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) { var slotIdx = favIndexForDigit(e.key); - var favIds = currentVisibleFavs(); + var favIds = currentVisibleFavs(); if (slotIdx >= 0 && slotIdx < favIds.length) { e.preventDefault(); var fav = byId(favIds[slotIdx]); diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 6a0a42bc2e..5b51daffbe 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -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"); const duplicateActions = [ - { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, - { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } + { id: "0123456789abcdef", title: "Repair", source: "Mesh Tools" }, + { id: "fedcba9876543210", title: "Repair", source: "Mesh Tools" } ]; assert.equal( - ctx.actionLabel(duplicateActions[0], duplicateActions), - "Repair from Mesh Tools (0123456789abcdef)", - "duplicate labels should use the opaque id without interpreting its contents" + ctx.actionLabel(duplicateActions[0], duplicateActions), + "Repair from Mesh Tools (0123456789abcdef)", + "duplicate labels should use the opaque id without interpreting its contents" ); 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. assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [], - "empty query + no recents shows nothing"); + "empty query + no recents shows nothing"); assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""), - [duplicateActions[0]], - "empty query shows the recent list"); + [duplicateActions[0]], + "empty query shows the recent list"); 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. const tabOptions = [ - { id: "home", title: "Home" }, - { id: "prepare", title: "Prepare" }, - { id: "monitor", title: "Device" }, - { id: "project", title: "Project" } + { id: "home", title: "Home" }, + { id: "prepare", title: "Prepare" }, + { id: "monitor", title: "Device" }, + { id: "project", title: "Project" } ]; 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, - "a typed query filters tabs by title"); + "a typed query filters tabs by title"); 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"), [], - "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. 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", - "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", - "a populated title is kept as-is"); + "a populated title is kept as-is"); 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, - "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, - "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 // (no group headers, no actions-vs-settings discrimination). const pool = [ - { 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: "c2", title: "Top Surface Layers", 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: "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.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. 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, - "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", - "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 // 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. const perfectPool = [ - { 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: "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: "" } ]; 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 // contains the needle perfectly beats the action whose GROUP does, both being contiguous matches. const titleFirstPool = [ - { id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" }, - { id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" } + { id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" }, + { id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" } ]; 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 // whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost). 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"); 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 // the mixed recents (no discrimination) for an empty query. const mixed = [ - { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" }, - { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" } + { id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" }, + { id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" } ]; 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"], - "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). assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), - null, - "Enter with an empty query and no recents must not resolve to an action the list never showed" + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), [], ""), + null, + "Enter with an empty query and no recents must not resolve to an action the list never showed" ); assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), - "0123456789abcdef", - "a typed query resolves the list selection" + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), [], "rep"), + "0123456789abcdef", + "a typed query resolves the list selection" ); assert.equal( - ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""), - "0123456789abcdef", - "Enter with an empty query resolves the recent entry" + ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [duplicateActions[0]], ""), [], ""), + "0123456789abcdef", + "Enter with an empty query resolves the recent entry" ); assert.equal( - ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), - "fedcba9876543210", - "favourites stay runnable with an empty query - the fav bar is always visible" + ctx.selectedActionId({ zone: "fav", i: 0 }, duplicateActions, ["fedcba9876543210"], ""), + "fedcba9876543210", + "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. @@ -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 // 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 }, - "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 }, - "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 }, - "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 }, - "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 }, - "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 }, - "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 }, - "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 }, - "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, // clamped to the total. Drives the "render the next window on scroll / arrow-nav" append. diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index f7d045bc9a..07f0c992eb 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -1,285 +1,423 @@ -* { 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; +* { + 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 + 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, + 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; + 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. */ -.fav-eyebrow { flex: 0 0 auto; padding: 8px 10px 0; } +.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; - /* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare