mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 10:51:22 +00:00
Add Help actions. Add Toggle Developer action. Add warning popup when switching between process mode. Add primitives/handy models actions
This commit is contained in:
@@ -10,6 +10,11 @@ var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
|
||||
var lastResizeHeight = 0;
|
||||
var matchIndex = {};
|
||||
|
||||
// The user's current settings mode (from the C++ payload) plus the rank order of the modes. Each
|
||||
// action carries the mode it requires, so "would this need a switch?" is a rank comparison.
|
||||
var USER_MODE = "simple";
|
||||
var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 };
|
||||
|
||||
// ---- windowed list render ----------------------------------------------------
|
||||
// The command list is rendered in windows (append-on-scroll) so a huge settings pool doesn't build
|
||||
// the whole DOM per keystroke. Rows are exactly ROW_H tall (matches .row min-height 44px; see --row-h,
|
||||
@@ -105,11 +110,15 @@ function searchActions(actions, query) {
|
||||
matchIndex = {};
|
||||
if (!q) { searchNeedle = ""; return list.slice(0); }
|
||||
searchNeedle = NormText(q, false);
|
||||
// Mode keywords ("advanced"/"expert"/"developer") are a union, not a filter: the normal text
|
||||
// search still runs on the full query, and every setting requiring a named mode is appended.
|
||||
var modes = modeFilterFromQuery(q);
|
||||
// Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks
|
||||
// between fields, and EscapeRegExp keeps regex metachars in the query literal.
|
||||
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
|
||||
|
||||
var scored = [];
|
||||
var seen = {};
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var a = list[i];
|
||||
var t = fieldMatchScore(titleNorm(a), wwRe);
|
||||
@@ -126,13 +135,28 @@ function searchActions(actions, query) {
|
||||
useEyebrowGroup: !!(a.group)
|
||||
};
|
||||
scored.push({ a: a, s: score });
|
||||
seen[a.id] = true;
|
||||
}
|
||||
scored.sort(function (x, y) {
|
||||
if (x.s !== y.s) return y.s - x.s;
|
||||
if (x.a.title !== y.a.title) return x.a.title < y.a.title ? -1 : 1;
|
||||
return x.a.id < y.a.id ? -1 : x.a.id > y.a.id ? 1 : 0;
|
||||
});
|
||||
return scored.map(function (e) { return e.a; });
|
||||
var result = scored.map(function (e) { return e.a; });
|
||||
if (modes.length) {
|
||||
// Settings requiring a named mode, after the ranked text matches and without duplicates.
|
||||
var extras = [];
|
||||
for (var j = 0; j < list.length; j++) {
|
||||
if (!seen[list[j].id] && modes.indexOf(list[j].mode) !== -1)
|
||||
extras.push(list[j]);
|
||||
}
|
||||
extras.sort(function (x, y) {
|
||||
if (x.title !== y.title) return x.title < y.title ? -1 : 1;
|
||||
return x.id < y.id ? -1 : x.id > y.id ? 1 : 0;
|
||||
});
|
||||
result = result.concat(extras);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Pure: how many rows must be materialized to cover the given starting index plus `size` more.
|
||||
@@ -264,6 +288,40 @@ function prettySource(source) {
|
||||
return String(source || "").toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); });
|
||||
}
|
||||
|
||||
// True when the action's required mode is above the user's current mode, i.e. selecting it will
|
||||
// prompt a mode switch. Unknown/empty modes are treated as "simple" so commands never flag.
|
||||
function needsModeSwitch(item, userMode) {
|
||||
var need = MODE_RANK[(item && item.mode) || "simple"] || 0;
|
||||
var have = MODE_RANK[userMode || "simple"] || 0;
|
||||
return need > have;
|
||||
}
|
||||
|
||||
// Short mode tag for an action that needs a switch, or "" when it is already available.
|
||||
function modeBadge(item, userMode) {
|
||||
if (!needsModeSwitch(item, userMode)) return "";
|
||||
if (item.mode === "develop") return "Developer";
|
||||
if (item.mode === "expert") return "Expert";
|
||||
if (item.mode === "advanced") return "Advanced";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Search keywords that name a settings mode. "developer" (and the internal "develop") both select
|
||||
// Developer; Simple is deliberately absent so it never floods the list with every command.
|
||||
var MODE_WORDS = { advanced: "advanced", expert: "expert", developer: "develop", develop: "develop" };
|
||||
|
||||
// The mode values named as whole words in `query`, deduped. Case/diacritic-insensitive via Norm. A
|
||||
// query with no mode keyword returns [] so the normal text search is completely unaffected.
|
||||
function modeFilterFromQuery(query) {
|
||||
var norm = NormText(String(query || "").trim(), false);
|
||||
var found = [];
|
||||
norm.split(/[^a-z0-9]+/).forEach(function (word) {
|
||||
var mode = MODE_WORDS[word];
|
||||
if (mode && found.indexOf(mode) === -1)
|
||||
found.push(mode);
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
// Accessible label "Title from Pretty Source", disambiguated with the opaque action id when another
|
||||
// action shares the same title+source (case/separator-insensitive) - so two rows never read out identically.
|
||||
function actionLabel(action, actions) {
|
||||
@@ -289,6 +347,7 @@ function stateFromPayload(payload) {
|
||||
actions: payload.actions || [],
|
||||
favourites: payload.favourites || [],
|
||||
recent: payload.recent || [],
|
||||
userMode: payload.user_mode || "simple",
|
||||
query: "",
|
||||
sel: { zone: "list", i: 0 },
|
||||
lastResizeHeight: 0,
|
||||
@@ -348,6 +407,7 @@ window.HandleStudio = function (payload) {
|
||||
ACTIONS = next.actions;
|
||||
FAVS = next.favourites;
|
||||
RECENTS = next.recent;
|
||||
USER_MODE = next.userMode;
|
||||
query = next.query;
|
||||
sel = next.sel;
|
||||
lastResizeHeight = next.lastResizeHeight;
|
||||
@@ -470,7 +530,8 @@ function renderFav() {
|
||||
tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : "");
|
||||
tile.style.setProperty("--h", hue(id));
|
||||
fillTile(tile, a);
|
||||
tile.title = a.title;
|
||||
var tileBadge = modeBadge(a, USER_MODE);
|
||||
tile.title = a.title + (tileBadge ? " (" + tileBadge + ")" : "");
|
||||
tile.setAttribute("aria-label", actionLabel(a, ACTIONS));
|
||||
tile.onclick = function () { sel = { zone: "fav", i: i }; activateEntry(a); };
|
||||
// Numbered quick-launch badge (Alt/Option+digit), drawn on the corner.
|
||||
@@ -584,6 +645,13 @@ function renderActionRow(a, i) {
|
||||
line.className = "row-line";
|
||||
var name = markedText("row-name", a.title, mi ? mi.title : null);
|
||||
line.appendChild(name);
|
||||
var badge = modeBadge(a, USER_MODE);
|
||||
if (badge) {
|
||||
var tag = document.createElement("span");
|
||||
tag.className = "row-mode";
|
||||
tag.textContent = badge;
|
||||
line.appendChild(tag);
|
||||
}
|
||||
if (a.shortcut) {
|
||||
var sc = document.createElement("div");
|
||||
sc.className = "row-sc";
|
||||
|
||||
@@ -211,4 +211,53 @@ assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR",
|
||||
assert.equal(ctx.tileCode({ id: "x", title: "Slice", source: "OrcaSlicer" }, [{ id: "x", title: "Slice", source: "OrcaSlicer" }]),
|
||||
"S", "a unique title resolves to the bare title initial");
|
||||
|
||||
// needsModeSwitch: a setting is gated only when its required mode outranks the user's current mode.
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "simple"), true, "Advanced is gated in Simple mode");
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "simple"), true, "Expert is gated in Simple mode");
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "expert" }, "advanced"), true, "Expert is gated in Advanced mode");
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "expert"), true, "Developer is gated in Expert mode");
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "simple" }, "simple"), false, "a Simple setting is not gated");
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "advanced" }, "advanced"), false, "an Advanced setting is not gated in Advanced mode");
|
||||
assert.equal(ctx.needsModeSwitch({ mode: "develop" }, "develop"), false, "a Developer setting is not gated in Developer mode");
|
||||
assert.equal(ctx.needsModeSwitch({}, "simple"), false, "a command with no mode is never gated");
|
||||
|
||||
// modeBadge: the tag text for gated settings, empty once the setting is available.
|
||||
assert.equal(ctx.modeBadge({ mode: "advanced" }, "simple"), "Advanced", "Advanced badge text");
|
||||
assert.equal(ctx.modeBadge({ mode: "expert" }, "simple"), "Expert", "Expert badge text");
|
||||
assert.equal(ctx.modeBadge({ mode: "develop" }, "simple"), "Developer", "Developer badge text");
|
||||
assert.equal(ctx.modeBadge({ mode: "advanced" }, "advanced"), "", "no badge when the mode already matches");
|
||||
|
||||
// modeFilterFromQuery: whole-word, case-insensitive mode keywords -> internal mode values. "developer"
|
||||
// maps to the internal "develop"; "simple" is deliberately not a keyword; a prefix is not a match.
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("advanced"), ["advanced"], "Advanced is a mode keyword");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("Expert"), ["expert"], "the keyword is case-insensitive");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("developer"), ["develop"], "Developer maps to the develop value");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("develop"), ["develop"], "the internal develop spelling also works");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("expert retraction"), ["expert"], "a keyword is found among other text");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("advanced expert"), ["advanced", "expert"], "multiple keywords are deduped in order");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("advanced advanced"), ["advanced"], "a repeated keyword is deduped");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("simple"), [], "Simple is not a mode keyword");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery("advance"), [], "a mode-word prefix is not a whole-word match");
|
||||
assert.deepEqual(ctx.modeFilterFromQuery(""), [], "an empty query names no mode");
|
||||
|
||||
// searchActions mode union: a mode keyword keeps the normal text matches AND appends every setting
|
||||
// requiring that mode. Not a filter - a Simple setting literally named "Advanced..." still shows, and
|
||||
// commands (mode "simple") are never pulled in by a keyword.
|
||||
const modePool = [
|
||||
{ id: "a1", title: "Top Surface Layers", source: "Quality", group: "Quality : Layers", mode: "advanced" },
|
||||
{ id: "a2", title: "Advanced Detection", source: "Quality", group: "Quality", mode: "simple" },
|
||||
{ id: "a3", title: "Retraction Length", source: "Process", group: "Process : Quality", mode: "expert" },
|
||||
{ id: "a4", title: "Slice", source: "OrcaSlicer", group: "Commands", mode: "simple" }
|
||||
];
|
||||
var adv = ctx.searchActions(modePool, "advanced");
|
||||
assert.deepEqual(adv.map(function (a) { return a.id; }), ["a2", "a1"],
|
||||
"a text match (a2) ranks above the mode-only setting (a1), and no expert/command leaks in");
|
||||
var expert = ctx.searchActions(modePool, "expert");
|
||||
assert.deepEqual(expert.map(function (a) { return a.id; }), ["a3"], "the expert keyword pulls in the expert setting");
|
||||
assert.deepEqual(ctx.searchActions(modePool, "developer"), [], "no developer settings means no mode extras");
|
||||
assert.equal(ctx.searchActions(modePool, "retraction")[0].id, "a3",
|
||||
"a query with no mode keyword is unaffected by the mode union");
|
||||
assert.equal(ctx.searchActions([{ id: "both", title: "Advanced", source: "Quality", group: "", mode: "advanced" }], "advanced").length,
|
||||
1, "a setting that both matches text and requires the mode appears exactly once");
|
||||
|
||||
console.log("ok");
|
||||
|
||||
@@ -364,6 +364,18 @@ body {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
/* Mode tag on settings above the user's current mode (Advanced/Expert/Developer). */
|
||||
.row-mode {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
border-radius: 8px;
|
||||
color: var(--plugin-status-warn);
|
||||
background: var(--plugin-status-warn-bg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user