mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 11:21:30 +00:00
Cleanup comments and formatting
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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] :
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user