mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Switched back to jump to setting instead
This commit is contained in:
@@ -37,18 +37,6 @@ var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering
|
||||
// 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;
|
||||
|
||||
// ---- inline setting editor state ---------------------------------------------
|
||||
// The "setting" phase (opened by activating a setting action) replaces the list with an editor card
|
||||
// for one option. phase transitions: commands -> setting -> (apply / open-in-sidebar) -> closed, or
|
||||
// Esc back to commands. settingDesc is the C++ descriptor; settingRows are the per-index control
|
||||
// descriptors (1 row for scalars, one per index for vectors); settingFieldEls hold the live controls.
|
||||
var settingId = ""; // the setting action id being edited
|
||||
var settingDesc = null; // {id,opt_key,type,title,breadcrumb,category,unit,tooltip,editable,control,cardinality,value|values,index_labels,enum_options,min,max,is_int}
|
||||
var settingRows = []; // [{index,kind,value,label,enum_options,min,max,unit,is_int}]
|
||||
var settingFieldEls = []; // [Element...] parallel to settingRows
|
||||
var settingPreviewIcon = null; // <img> beside the editor title, updated live on dropdown pick
|
||||
var openDropDownEl = null; // the custom dropdown toggle button whose option list is expanded
|
||||
|
||||
// ---- pure helpers (no DOM; unit-tested) -------------------------------------
|
||||
// Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per
|
||||
// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/source text correctly. The
|
||||
@@ -176,26 +164,6 @@ function shouldRenderActionList(query) {
|
||||
return !!((query || "").trim());
|
||||
}
|
||||
|
||||
// Label for a closed-enum entry, looked up from its enum_options by value; falls back to the value.
|
||||
// Pure so the node-vm test can exercise the dropdown label mapping. `value` is the current int value.
|
||||
function dropdownLabel(options, value) {
|
||||
var want = String(value == null ? "" : value);
|
||||
for (var i = 0; i < (options || []).length; i++)
|
||||
if (String(options[i].value) === want)
|
||||
return options[i].label != null && options[i].label !== "" ? String(options[i].label) : String(options[i].key != null ? options[i].key : options[i].value);
|
||||
return want;
|
||||
}
|
||||
|
||||
// Pure: the data:URI pictogram for a dropdown's selected value, or "" when none of the options has
|
||||
// one (most settings have no pattern icon). Mirrors dropdownLabel so tests can drive it without DOM.
|
||||
function dropdownIcon(options, value) {
|
||||
var want = String(value == null ? "" : value);
|
||||
for (var i = 0; i < (options || []).length; i++)
|
||||
if (String(options[i].value) === want && options[i].icon)
|
||||
return options[i].icon;
|
||||
return "";
|
||||
}
|
||||
|
||||
// Put an action's pattern pictogram into a tile (search row or favourites tile) when it has one,
|
||||
// otherwise fall back to the monogram. Toggles the has-icon class so CSS neutralises the hue.
|
||||
function fillTile(tile, a) {
|
||||
@@ -213,83 +181,6 @@ function fillTile(tile, a) {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-index control descriptors for the inline setting editor, derived from the C++ descriptor.
|
||||
// Pure so the node-vm test can exercise the scalar/vector + control mapping without a DOM.
|
||||
// Values are the current config value(s); vector options get one row per index, each labelled.
|
||||
function settingControlRows(desc) {
|
||||
if (!desc || !desc.editable) return [];
|
||||
var rows = [];
|
||||
var values = desc.cardinality === "vector" ? (desc.values || []) : [desc.value];
|
||||
var labels = desc.cardinality === "vector" ? (desc.index_labels || []) : [];
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
rows.push({
|
||||
index: i,
|
||||
kind: desc.control,
|
||||
value: values[i],
|
||||
label: labels[i] != null ? String(labels[i]) : (desc.cardinality === "vector" ? String(i + 1) : null),
|
||||
enum_options: desc.enum_options || [],
|
||||
min: typeof desc.min === "number" ? desc.min : null,
|
||||
max: typeof desc.max === "number" ? desc.max : null,
|
||||
unit: desc.unit || "",
|
||||
is_int: !!desc.is_int
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Pure: read the value a control would submit back for a setting. `el` is a DOM element (never
|
||||
// passed in tests). Returns undefined for an unusable value (empty/invalid number, out of range),
|
||||
// boolean for toggles, number for numeric, string otherwise.
|
||||
function settingControlValue(row, el) {
|
||||
if (!row || !el) return undefined;
|
||||
switch (row.kind) {
|
||||
case "toggle": return !!el.checked;
|
||||
case "number": {
|
||||
var raw = String(el.value || "").trim();
|
||||
if (raw === "") return undefined;
|
||||
var n = row.is_int ? parseInt(raw, 10) : parseFloat(raw);
|
||||
if (!isFinite(n)) return undefined;
|
||||
if (row.min != null && n < row.min) return undefined;
|
||||
if (row.max != null && n > row.max) return undefined;
|
||||
return n;
|
||||
}
|
||||
case "dropdown": {
|
||||
// value is stored on the toggle button's dataset (set when an option is picked).
|
||||
var v = parseInt(el.dataset ? el.dataset.value : "", 10);
|
||||
return isFinite(v) ? v : undefined;
|
||||
}
|
||||
case "combo": {
|
||||
// Open enum: free text field (never a select), so read it as the seeded integer value.
|
||||
var v = parseInt(el.value, 10);
|
||||
return isFinite(v) ? v : undefined;
|
||||
}
|
||||
case "color":
|
||||
case "text":
|
||||
case "percent": {
|
||||
// percent submission is a string ("10%", "0.5"); C++ parses + clamps it. Empty is invalid.
|
||||
var raw = String(el.value || "").trim();
|
||||
return raw === "" ? undefined : raw;
|
||||
}
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Pure: assemble the value payload for a setting from its edited control rows. Returns the scalar
|
||||
// for scalar settings, an array for vector settings, or undefined when any control is invalid.
|
||||
function settingCollectedValue(desc, rows, values) {
|
||||
if (!desc || !desc.editable) return undefined;
|
||||
if (desc.cardinality === "vector") {
|
||||
var out = [];
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var v = settingControlValue(rows[i], values[i]);
|
||||
if (v === undefined) return undefined;
|
||||
out.push(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return settingControlValue(rows[0], values[0]);
|
||||
}
|
||||
|
||||
// The active list for the main phase. A typed query ranks every action (commands/plugins/settings)
|
||||
// by relevance; an empty query shows the recent list (recents are a mixed bag - no discrimination).
|
||||
function commandList(actions, recents, query) {
|
||||
@@ -439,13 +330,9 @@ window.HandleStudio = function (payload) {
|
||||
lastResizeHeight = next.lastResizeHeight;
|
||||
phase = next.phase;
|
||||
tabOptions = next.tabOptions;
|
||||
// Reset any half-open setting editor (the dialog was closed/reopened), restoring the search.
|
||||
settingId = ""; settingDesc = null; settingRows = []; settingFieldEls = [];
|
||||
settingPreviewIcon = null;
|
||||
openDropDownEl = null;
|
||||
// why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav. It
|
||||
// survives an apply-then-reopen (which never goes through exitPhase), so without a reset the
|
||||
// leftover editor card would be mistaken for the empty-query commands list and never rebuilt.
|
||||
// why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav.
|
||||
// It survives a re-open (which never goes through exitPhase), so without a reset the cached
|
||||
// empty-query key would skip the rebuild and leave stale list content.
|
||||
builtKey = "";
|
||||
if (headEl) headEl.hidden = false;
|
||||
if (qEl) {
|
||||
@@ -455,16 +342,6 @@ window.HandleStudio = function (payload) {
|
||||
}
|
||||
render({ resize: true, resetScroll: true });
|
||||
focusInput();
|
||||
} else if (payload.command === "setting_descriptor") {
|
||||
// Inline editor loaded: render the card. Guard against a stale response for a different id.
|
||||
if (payload.descriptor && payload.descriptor.id === settingId)
|
||||
settingDesc = payload.descriptor;
|
||||
render({ resize: true });
|
||||
// why: keyboard focus must land on the field after the card is built, not stay on the hidden
|
||||
// search input. fire on a timeout so the element is attached and its content selectable.
|
||||
focusSettingEditor();
|
||||
} else if (payload.command === "apply_failed") {
|
||||
flashHint("Couldn't apply that value");
|
||||
} else if (payload.command === "tab_results") {
|
||||
tabOptions = payload.tabs || [];
|
||||
if (sel.zone === "list")
|
||||
@@ -649,7 +526,6 @@ function updateFavEyebrow(favs) {
|
||||
eyeEl.hidden = !a;
|
||||
}
|
||||
|
||||
// One settings row; has no star/tile because settings aren't pinnable.
|
||||
// A command/action row - used for search results, recents, and (because settings are actions now)
|
||||
// the setting options too. All rows are pinnable, so every row carries a star.
|
||||
function renderActionRow(a, i) {
|
||||
@@ -864,349 +740,11 @@ function renderPercentList() {
|
||||
listEl.appendChild(ph);
|
||||
}
|
||||
|
||||
// ---- inline setting editor (DOM stage) ---------------------------------------
|
||||
|
||||
// Collapse every open custom dropdown except `keep` (null collapses all). The menu list elements
|
||||
// are the .ed-dropdown-menu siblings of the toggle buttons we track via openDropDownEl.
|
||||
function closeOtherDropDowns(keep) {
|
||||
if (openDropDownEl && openDropDownEl !== keep && openDropDownEl.parentNode) {
|
||||
var m = openDropDownEl.parentNode.querySelector(".ed-dropdown-menu");
|
||||
if (m) m.hidden = true;
|
||||
openDropDownEl.classList.remove("open");
|
||||
}
|
||||
if (!keep)
|
||||
openDropDownEl = null;
|
||||
}
|
||||
|
||||
// Collapse the currently open dropdown, if any (kept for the editor's export/import-adjacent helpers).
|
||||
function closeEditorDropDown() { closeOtherDropDowns(null); }
|
||||
|
||||
// Place an open dropdown menu as a fixed overlay just under its toggle, so the menu floats over the
|
||||
// card (never resizing it) and is clamped to the popup's bottom edge with an internal scrollbar for
|
||||
// long option lists. position:fixed escapes the card/launcher overflow clipping that an absolute
|
||||
// menu would otherwise hit, keeping every option reachable within the window.
|
||||
function positionDropDownMenu(btn, menu) {
|
||||
var lrect = (document.querySelector(".launcher") || { getBoundingClientRect: function () { return { top: 0, bottom: window.innerHeight }; } }).getBoundingClientRect();
|
||||
var rect = btn.getBoundingClientRect();
|
||||
// Available room above and below the toggle, within the popup. Opening the menu must not push it
|
||||
// past the window edge (that's the unreachable-overflow bug) - pick whichever side has more room
|
||||
// and clamp the box to it. Overflow-y:auto scrolls any long list inside the menu itself.
|
||||
var spaceBelow = lrect.bottom - (rect.bottom + 8);
|
||||
var spaceAbove = (rect.top - 8) - lrect.top;
|
||||
var openUp = spaceBelow < spaceAbove;
|
||||
var maxH = Math.max(0, Math.min(openUp ? spaceAbove : spaceBelow, 200));
|
||||
menu.style.position = "fixed";
|
||||
menu.style.width = rect.width + "px";
|
||||
menu.style.left = rect.left + "px";
|
||||
menu.style.maxHeight = maxH + "px";
|
||||
if (openUp) {
|
||||
// bottom edge sits just above the toggle; the box grows upward to content height.
|
||||
menu.style.top = "auto";
|
||||
menu.style.bottom = (lrect.bottom - rect.top + 4) + "px";
|
||||
} else {
|
||||
menu.style.top = (rect.bottom + 4) + "px";
|
||||
menu.style.bottom = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
// Build the control element for one row (toggle/number/dropdown/combo/text/color) and seed it with
|
||||
// the current value. Returns {el, node, extra} - node is what is appended, extra carries a datalist.
|
||||
function settingInputFor(row) {
|
||||
var el;
|
||||
if (row.kind === "toggle") {
|
||||
el = document.createElement("input");
|
||||
el.type = "checkbox";
|
||||
el.checked = !!row.value;
|
||||
var sw = document.createElement("label");
|
||||
sw.className = "ed-switch";
|
||||
sw.appendChild(el);
|
||||
var slider = document.createElement("span");
|
||||
slider.className = "ed-slider";
|
||||
sw.appendChild(slider);
|
||||
return { el: el, node: sw };
|
||||
}
|
||||
if (row.kind === "number") {
|
||||
el = document.createElement("input");
|
||||
el.type = "number";
|
||||
el.step = row.is_int ? 1 : "any";
|
||||
if (row.min != null) el.min = row.min;
|
||||
if (row.max != null) el.max = row.max;
|
||||
if (row.value != null && row.value !== "") el.value = row.value;
|
||||
return { el: el, node: el };
|
||||
}
|
||||
if (row.kind === "dropdown") {
|
||||
// Native <select> popups are unreliable inside this wxWebView (a click synthesizes a
|
||||
// keydown that can reach the global Enter handler and apply+close). Build a custom
|
||||
// dropdown: a toggle button that expands an in-flow option list. Selection only updates
|
||||
// local state; nothing applies until Enter/Apply. The value lives on the toggle button's
|
||||
// dataset so settingControlValue can read it back without the DOM copy.
|
||||
var wrap = document.createElement("div");
|
||||
wrap.className = "ed-dropdown";
|
||||
var btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "ed-dropdown-toggle";
|
||||
btn.dataset.value = row.value != null ? String(row.value) : "";
|
||||
// The selected value's pattern pictogram (hidden when the value has none).
|
||||
var toggleIcon = document.createElement("img");
|
||||
toggleIcon.className = "ed-dropdown-icon";
|
||||
toggleIcon.setAttribute("aria-hidden", "true");
|
||||
toggleIcon.alt = "";
|
||||
var tIcon = dropdownIcon(row.enum_options || [], row.value);
|
||||
toggleIcon.src = tIcon || "";
|
||||
toggleIcon.hidden = !tIcon;
|
||||
btn.appendChild(toggleIcon);
|
||||
var label = document.createElement("span");
|
||||
label.className = "ed-dropdown-label";
|
||||
label.textContent = dropdownLabel(row.enum_options || [], row.value);
|
||||
btn.appendChild(label);
|
||||
var caret = document.createElement("span");
|
||||
caret.className = "ed-dropdown-caret";
|
||||
caret.textContent = "▾";
|
||||
btn.appendChild(caret);
|
||||
var listEl = document.createElement("div");
|
||||
listEl.className = "ed-dropdown-menu";
|
||||
listEl.hidden = true;
|
||||
(row.enum_options || []).forEach(function (o, oi) {
|
||||
var opt = document.createElement("button");
|
||||
opt.type = "button";
|
||||
opt.className = "ed-dropdown-option";
|
||||
if (o.icon) {
|
||||
var img = document.createElement("img");
|
||||
img.className = "ed-option-icon";
|
||||
img.src = o.icon;
|
||||
img.alt = "";
|
||||
img.setAttribute("aria-hidden", "true");
|
||||
opt.appendChild(img);
|
||||
}
|
||||
var optLabel = document.createElement("span");
|
||||
optLabel.className = "ed-option-label";
|
||||
optLabel.textContent = o.label;
|
||||
opt.appendChild(optLabel);
|
||||
if (String(o.value) === String(row.value))
|
||||
opt.classList.add("sel");
|
||||
opt.onclick = function (ev) {
|
||||
ev.stopPropagation();
|
||||
btn.dataset.value = String(o.value);
|
||||
label.textContent = o.label;
|
||||
toggleIcon.src = o.icon || "";
|
||||
toggleIcon.hidden = !o.icon;
|
||||
listEl.hidden = true;
|
||||
btn.classList.remove("open");
|
||||
openDropDownEl = null;
|
||||
onSettingValueChanged(settingDesc, o);
|
||||
};
|
||||
listEl.appendChild(opt);
|
||||
});
|
||||
btn.onclick = function (ev) {
|
||||
ev.stopPropagation();
|
||||
if (openDropDownEl === btn) {
|
||||
// clicking the open toggle closes it
|
||||
listEl.hidden = true;
|
||||
btn.classList.remove("open");
|
||||
openDropDownEl = null;
|
||||
return;
|
||||
}
|
||||
closeOtherDropDowns(null); // collapse any other open dropdown
|
||||
positionDropDownMenu(btn, listEl);
|
||||
listEl.hidden = false;
|
||||
btn.classList.add("open");
|
||||
openDropDownEl = btn;
|
||||
};
|
||||
wrap.appendChild(btn);
|
||||
wrap.appendChild(listEl);
|
||||
return { el: btn, node: wrap };
|
||||
}
|
||||
if (row.kind === "combo") {
|
||||
el = document.createElement("input");
|
||||
el.type = "text";
|
||||
var dl = document.createElement("datalist");
|
||||
el.setAttribute("list", dl.id = "ed-combo-" + row.index);
|
||||
(row.enum_options || []).forEach(function (o) {
|
||||
var op = document.createElement("option");
|
||||
op.value = o.value;
|
||||
op.textContent = o.label;
|
||||
dl.appendChild(op);
|
||||
});
|
||||
el.value = row.value != null ? String(row.value) : "";
|
||||
return { el: el, node: el, extra: dl };
|
||||
}
|
||||
if (row.kind === "color") {
|
||||
el = document.createElement("input");
|
||||
el.type = "color";
|
||||
el.value = row.value && /^#[0-9a-fA-F]{6}$/.test(row.value) ? row.value : "#000000";
|
||||
return { el: el, node: el };
|
||||
}
|
||||
if (row.kind === "percent") {
|
||||
// "mm or %" (coFloatOrPercent/coFloatsOrPercents): a free-text field showing the serialized
|
||||
// value (e.g. "10%" or "0.5"). The unit hints at the sidebar semantics (mm or %), so it's
|
||||
// not shown here - the value itself carries the % when applicable.
|
||||
el = document.createElement("input");
|
||||
el.type = "text";
|
||||
el.value = row.value != null ? String(row.value) : "";
|
||||
return { el: el, node: el };
|
||||
}
|
||||
// text
|
||||
el = document.createElement("input");
|
||||
el.type = "text";
|
||||
el.value = row.value != null ? String(row.value) : "";
|
||||
return { el: el, node: el };
|
||||
}
|
||||
|
||||
// One labeled control row in the editor card.
|
||||
function renderControlRow(row, i) {
|
||||
var wrap = document.createElement("div");
|
||||
wrap.className = "editor-row";
|
||||
if (row.label != null) {
|
||||
var lab = document.createElement("label");
|
||||
lab.className = "editor-label";
|
||||
lab.textContent = row.label;
|
||||
wrap.appendChild(lab);
|
||||
}
|
||||
var ctrl = settingInputFor(row);
|
||||
if (ctrl.extra)
|
||||
wrap.appendChild(ctrl.extra); // datalist for open-enum combos
|
||||
wrap.appendChild(ctrl.node);
|
||||
if (row.unit) {
|
||||
var unit = document.createElement("span");
|
||||
unit.className = "editor-unit";
|
||||
unit.textContent = row.unit;
|
||||
wrap.appendChild(unit);
|
||||
}
|
||||
settingFieldEls[i] = ctrl.el;
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// Render the editor card into listEl (phase === "setting"). Keeps the search head hidden so the
|
||||
// card owns the layout.
|
||||
function renderSettingStage() {
|
||||
listEl.innerHTML = "";
|
||||
listEl.className = "dial-list setting";
|
||||
if (countEl) countEl.hidden = true;
|
||||
if (!settingDesc || !settingDesc.opt_key) {
|
||||
var ph = document.createElement("div");
|
||||
ph.className = "dial-empty";
|
||||
ph.textContent = "Loading…";
|
||||
listEl.appendChild(ph);
|
||||
return;
|
||||
}
|
||||
var card = document.createElement("div");
|
||||
card.className = "dial-editor";
|
||||
if (settingDesc.breadcrumb) {
|
||||
var crumb = document.createElement("div");
|
||||
crumb.className = "row-eyebrow";
|
||||
crumb.textContent = settingDesc.breadcrumb;
|
||||
card.appendChild(crumb);
|
||||
}
|
||||
var titleRow = document.createElement("div");
|
||||
titleRow.className = "editor-title-row";
|
||||
var titleIcon = document.createElement("img");
|
||||
titleIcon.className = "editor-preview-icon";
|
||||
titleIcon.setAttribute("aria-hidden", "true");
|
||||
titleIcon.alt = "";
|
||||
var pIcon = dropdownIcon(settingDesc.enum_options || [], settingDesc.value);
|
||||
titleIcon.src = pIcon || "";
|
||||
titleIcon.hidden = !pIcon;
|
||||
titleRow.appendChild(titleIcon);
|
||||
settingPreviewIcon = titleIcon;
|
||||
var title = document.createElement("div");
|
||||
title.className = "editor-title";
|
||||
title.textContent = settingDesc.title || "";
|
||||
titleRow.appendChild(title);
|
||||
card.appendChild(titleRow);
|
||||
if (settingDesc.tooltip) {
|
||||
var tt = document.createElement("div");
|
||||
tt.className = "editor-tooltip";
|
||||
tt.textContent = settingDesc.tooltip;
|
||||
card.appendChild(tt);
|
||||
}
|
||||
|
||||
var actions = document.createElement("div");
|
||||
actions.className = "editor-actions";
|
||||
if (settingDesc.editable) {
|
||||
settingRows = settingControlRows(settingDesc);
|
||||
settingFieldEls = [];
|
||||
if (settingRows.length) {
|
||||
settingRows.forEach(function (row, i) { card.appendChild(renderControlRow(row, i)); });
|
||||
} else {
|
||||
var empty = document.createElement("div");
|
||||
empty.className = "dial-empty";
|
||||
empty.textContent = "Nothing editable here";
|
||||
card.appendChild(empty);
|
||||
}
|
||||
var apply = document.createElement("button");
|
||||
apply.className = "ed-btn ed-btn-primary";
|
||||
apply.textContent = "Apply";
|
||||
apply.onclick = applySetting;
|
||||
actions.appendChild(apply);
|
||||
} else {
|
||||
var ro = document.createElement("div");
|
||||
ro.className = "editor-readonly";
|
||||
ro.textContent = "This setting can't be edited here";
|
||||
card.appendChild(ro);
|
||||
var open = document.createElement("button");
|
||||
open.className = "ed-btn";
|
||||
open.textContent = "Open in sidebar";
|
||||
open.onclick = openSettingInSidebar;
|
||||
actions.appendChild(open);
|
||||
}
|
||||
var cancel = document.createElement("button");
|
||||
cancel.className = "ed-btn";
|
||||
cancel.textContent = "Cancel";
|
||||
cancel.onclick = exitPhase;
|
||||
actions.appendChild(cancel);
|
||||
card.appendChild(actions);
|
||||
var hint = document.createElement("div");
|
||||
hint.className = "editor-hint";
|
||||
hint.textContent = "Enter to apply · Esc to cancel";
|
||||
card.appendChild(hint);
|
||||
listEl.appendChild(card);
|
||||
}
|
||||
|
||||
function applySetting() {
|
||||
if (!settingDesc || !settingDesc.editable) return;
|
||||
var value = settingCollectedValue(settingDesc, settingRows, settingFieldEls);
|
||||
if (value === undefined) {
|
||||
flashHint("Enter a valid value");
|
||||
return;
|
||||
}
|
||||
SendMessage({ command: "set_setting", id: settingId, value: value });
|
||||
}
|
||||
|
||||
function openSettingInSidebar() {
|
||||
if (!settingDesc) return;
|
||||
SendMessage({ command: "open_setting_in_sidebar", opt_key: settingDesc.opt_key, type: settingDesc.type, category: settingDesc.category || "" });
|
||||
}
|
||||
|
||||
// When a dropdown option is picked, mirror its pattern pictogram onto the editor title's preview so
|
||||
// the current selection is visible without opening the menu. Non-enum / icon-less rows no-op.
|
||||
function onSettingValueChanged(desc, option) {
|
||||
if (!settingPreviewIcon) return;
|
||||
var icon = (option && option.icon) || "";
|
||||
settingPreviewIcon.src = icon;
|
||||
settingPreviewIcon.hidden = !icon;
|
||||
}
|
||||
|
||||
function enterSettingPhase(a) {
|
||||
if (!a) return;
|
||||
phase = "setting";
|
||||
query = ""; qEl.value = ""; syncClearButton();
|
||||
sel = { zone: "list", i: 0 };
|
||||
settingId = a.id;
|
||||
settingDesc = null;
|
||||
settingRows = [];
|
||||
settingFieldEls = [];
|
||||
if (headEl) headEl.hidden = true;
|
||||
render({ resetScroll: true });
|
||||
SendMessage({ command: "setting_descriptor", id: a.id });
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
if (phase === "tab")
|
||||
renderTabList();
|
||||
else if (phase === "percent")
|
||||
renderPercentList();
|
||||
else if (phase === "setting")
|
||||
renderSettingStage();
|
||||
else
|
||||
renderCommandsList();
|
||||
}
|
||||
@@ -1284,7 +822,6 @@ function activateEntry(a) {
|
||||
if (!a) return;
|
||||
if (a.input === "percent") { enterPercentPhase(); return; }
|
||||
if (a.input === "tab") { enterTabsPhase(); return; }
|
||||
if (a.input === "setting") { enterSettingPhase(a); return; }
|
||||
run(a);
|
||||
}
|
||||
|
||||
@@ -1337,14 +874,11 @@ function enterTabsPhase() {
|
||||
|
||||
function exitPhase() {
|
||||
phase = "commands"; tabOptions = []; query = ""; qEl.value = "";
|
||||
settingId = ""; settingDesc = null; settingRows = []; settingFieldEls = [];
|
||||
settingPreviewIcon = null;
|
||||
closeOtherDropDowns(null);
|
||||
if (headEl) headEl.hidden = false;
|
||||
sel = { zone: "list", i: 0 };
|
||||
// why: builtKey caches phase|query so renderCommandsList can skip a rebuild on arrow-nav/click.
|
||||
// Leftover from the setting phase it matches the (empty-query) commands key, which would skip
|
||||
// the rebuild and leave the editor card in the list. Reset it so the commands view is rebuilt.
|
||||
// It survives a second-phase exit (which never goes through exitPhase from the commands view),
|
||||
// so without a reset the cached empty-query key would skip the rebuild and leave stale content.
|
||||
builtKey = "";
|
||||
qEl.placeholder = "Search " + ACTIONS.length + " actions";
|
||||
syncClearButton();
|
||||
@@ -1354,19 +888,6 @@ function exitPhase() {
|
||||
|
||||
function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); }
|
||||
|
||||
// Move keyboard focus onto the first editable field of the setting editor card. Deferred so the
|
||||
// element is attached and its text is selectable by the time we focus it. For text-editable inputs
|
||||
// also select the existing value so the user can type straight over it.
|
||||
function focusSettingEditor() {
|
||||
setTimeout(function () {
|
||||
var el = settingFieldEls && settingFieldEls[0];
|
||||
if (!el) return;
|
||||
if (el.focus) el.focus();
|
||||
if ((el.tagName === "INPUT") && el.select)
|
||||
el.select();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ---- init --------------------------------------------------------------------
|
||||
function OnInit() {
|
||||
qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count");
|
||||
@@ -1397,21 +918,10 @@ function OnInit() {
|
||||
|
||||
// why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers).
|
||||
document.addEventListener("click", hideFavMenu);
|
||||
// Dismiss an open editor dropdown on any outside click. Toggle/option clicks stopPropagation
|
||||
// so they don't immediately close the menu they just opened/picked from.
|
||||
document.addEventListener("click", function () {
|
||||
if (openDropDownEl) closeEditorDropDown();
|
||||
});
|
||||
document.addEventListener("scroll", hideFavMenu, true);
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; }
|
||||
// While an editor dropdown menu is open it owns the keys: Escape closes the menu (a second
|
||||
// Esc exits the phase), Enter/arrows select options natively, and we must not apply/exit.
|
||||
if (phase === "setting" && openDropDownEl && openDropDownEl.parentNode) {
|
||||
if (e.key === "Escape") { e.preventDefault(); closeEditorDropDown(); }
|
||||
return;
|
||||
}
|
||||
// Quick-launch a numbered favourite: Alt/Option + digit (0 = the 10th). Only in the
|
||||
// commands phase, where the pinned bar is shown.
|
||||
if (phase === "commands" && e.altKey && !e.ctrlKey && !e.metaKey) {
|
||||
@@ -1432,8 +942,8 @@ function OnInit() {
|
||||
// let Left/Right fall through so they move the caret in the focused search field.
|
||||
var lr = e.key === "ArrowLeft" || e.key === "ArrowRight";
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp" || (lr && sel.zone === "fav")) {
|
||||
// In the percent/setting phases the input controls own the caret - arrows edit text, not rows.
|
||||
if (phase === "percent" || phase === "setting") return;
|
||||
// In the percent phase the input control owns the caret - arrows edit text, not rows.
|
||||
if (phase === "percent") return;
|
||||
e.preventDefault();
|
||||
sel = nextSel(sel, e.key, list.length, favs.length);
|
||||
// why: entering/leaving the fav zone toggles the eyebrow line, changing launcher height;
|
||||
@@ -1442,7 +952,6 @@ function OnInit() {
|
||||
} else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (phase === "percent") runJumpToLayer(query.trim());
|
||||
else if (phase === "setting") applySetting();
|
||||
else runSelected();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -159,73 +159,4 @@ assert.equal(ctx.revealTarget(100, -5, 50), 50, "negative start is clamped to th
|
||||
assert.equal(ctx.revealTarget(200, 50, 100), 150, "a scroll viewpoint reveals a window past the current rows");
|
||||
assert.equal(ctx.revealTarget(10, 0, 50), 10, "a list shorter than one window stays fully materialized");
|
||||
|
||||
// ---- inline setting editor helpers (settingControlRows / Value / CollectedValue) ----
|
||||
const scalarBoolDesc = {
|
||||
editable: true, control: "toggle", cardinality: "scalar", value: true,
|
||||
min: undefined, max: undefined, is_int: false, unit: ""
|
||||
};
|
||||
const scalarRows = ctx.settingControlRows(scalarBoolDesc);
|
||||
assert.equal(scalarRows.length, 1, "a scalar setting yields exactly one control row");
|
||||
assert.equal(scalarRows[0].kind, "toggle", "the control kind is carried through");
|
||||
assert.equal(scalarRows[0].index, 0, "the single row is indexed 0");
|
||||
assert.deepEqual(ctx.settingControlRows({ editable: false }), [], "a non-editable setting yields no control rows");
|
||||
|
||||
const vectorDesc = {
|
||||
editable: true, control: "number", cardinality: "vector", values: [0.2, 0.4],
|
||||
index_labels: ["1", "2"], min: 0, max: 1, is_int: false, unit: "mm"
|
||||
};
|
||||
const vectorRows = ctx.settingControlRows(vectorDesc);
|
||||
assert.equal(vectorRows.length, 2, "a vector setting yields one row per value");
|
||||
assert.deepEqual(vectorRows.map(function (r) { return r.value; }), [0.2, 0.4], "each row carries its current value");
|
||||
assert.deepEqual(vectorRows.map(function (r) { return r.label; }), ["1", "2"], "vector rows use the per-index labels");
|
||||
assert.equal(vectorRows[0].unit, "mm", "the unit is carried to each row");
|
||||
|
||||
// settingControlValue reads a control element (checked/value) as the shipped value.
|
||||
assert.equal(ctx.settingControlValue({ kind: "toggle" }, { checked: true }), true, "a toggle submits its checked state");
|
||||
assert.equal(ctx.settingControlValue({ kind: "toggle" }, { checked: false }), false, "an off toggle submits false");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "0.5" }), 0.5, "a float number parses");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: true, min: 0, max: 10 }, { value: "5" }), 5, "an int number parses");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "2" }), undefined, "an out-of-range number is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "number", is_int: false, min: 0, max: 1 }, { value: "" }), undefined, "an empty number is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "dropdown" }, { dataset: { value: "3" } }), 3, "an enum dropdown submits its stored int value");
|
||||
assert.equal(ctx.settingControlValue({ kind: "dropdown" }, { dataset: { value: "nope" } }), undefined, "a non-numeric dropdown value is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "dropdown" }, {}), undefined, "a dropdown with no value is rejected");
|
||||
// dropdownLabel maps the current int value to its human label, falling back to the value.
|
||||
const seamOptions = [
|
||||
{ value: 0, key: "nearest", label: "Nearest" },
|
||||
{ value: 1, key: "aligned", label: "Aligned" },
|
||||
{ value: 2, key: "random", label: "Random" }
|
||||
];
|
||||
assert.equal(ctx.dropdownLabel(seamOptions, 1), "Aligned", "the dropdown label for a known value is its label");
|
||||
assert.equal(ctx.dropdownLabel(seamOptions, 9), "9", "an unknown value falls back to the raw value");
|
||||
assert.equal(ctx.dropdownLabel([], 3), "3", "empty options fall back to the raw value");
|
||||
assert.equal(ctx.dropdownLabel(
|
||||
[{ value: 1, key: "aligned", label: "" }], 1), "aligned", "a blank label falls back to the key");
|
||||
// dropdownIcon maps the current int value to its pattern pictogram, empty when there is none.
|
||||
const patternOptions = [
|
||||
{ value: 0, key: "rectilinear", label: "Rectilinear" },
|
||||
{ value: 3, key: "gyroid", label: "Gyroid", icon: "data:image/svg+xml;base64,AAA" },
|
||||
{ value: 5, key: "grid", label: "Grid", icon: "data:image/svg+xml;base64,BBB" }
|
||||
];
|
||||
assert.equal(ctx.dropdownIcon(patternOptions, 3), "data:image/svg+xml;base64,AAA", "a known value returns its icon");
|
||||
assert.equal(ctx.dropdownIcon(patternOptions, 0), "", "a value with no icon returns empty");
|
||||
assert.equal(ctx.dropdownIcon(patternOptions, 9), "", "an unknown value returns empty");
|
||||
assert.equal(ctx.dropdownIcon([], 3), "", "empty options return empty");
|
||||
assert.equal(ctx.settingControlValue({ kind: "text" }, { value: "hello" }), "hello", "text submits as a string");
|
||||
// percent (coFloatOrPercent / coFloatsOrPercents): submits the raw typed string; empty is invalid.
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: "10%" }), "10%", "a percent value submits as its string");
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: "0.5" }), "0.5", "an mm value submits as a plain string");
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, { value: " " }), undefined, "a blank percent value is rejected");
|
||||
assert.equal(ctx.settingControlValue({ kind: "percent" }, {}), undefined, "a percent field with no value is rejected");
|
||||
// A percent scalar-to-scalar payload carries the raw string through unchanged.
|
||||
assert.equal(ctx.settingCollectedValue(
|
||||
{ editable: true, control: "percent", cardinality: "scalar", value: "10%" },
|
||||
[{ kind: "percent", value: "10%" }], [{ value: "10%" }]), "10%", "a percent scalar assembles its string");
|
||||
|
||||
// settingCollectedValue assembles the payload (scalar vs vector) for the set_setting message.
|
||||
assert.equal(ctx.settingCollectedValue({ editable: false }, [], []), undefined, "a non-editable setting yields no payload");
|
||||
assert.equal(ctx.settingCollectedValue(scalarBoolDesc, scalarRows, [{ checked: true }]), true, "a scalar assembles a single value");
|
||||
assert.deepEqual(ctx.settingCollectedValue(vectorDesc, vectorRows, [{ value: "0.3" }, { value: "0.7" }]), [0.3, 0.7], "a vector assembles an array");
|
||||
assert.equal(ctx.settingCollectedValue(vectorDesc, vectorRows, [{ value: "0.3" }, { value: "2" }]), undefined, "an invalid vector element aborts the whole payload");
|
||||
|
||||
console.log("ok");
|
||||
|
||||
@@ -294,145 +294,3 @@ kbd {
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- inline setting editor card (phase "setting") --------------------------- */
|
||||
.dial-list.setting { overflow-y: auto; }
|
||||
.dial-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
.editor-title { font-size: 14px; font-weight: 600; line-height: 1.3; }
|
||||
.editor-title-row { display: flex; align-items: center; gap: 8px; }
|
||||
.editor-preview-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.editor-preview-icon[hidden] { display: none; }
|
||||
.editor-tooltip { font-size: 11px; line-height: 1.4; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.editor-row { display: flex; align-items: center; gap: 8px; min-height: 32px; }
|
||||
.editor-label {
|
||||
flex: 0 0 auto;
|
||||
min-width: 64px;
|
||||
font-size: 12px;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.editor-unit { font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.editor-row input[type="number"],
|
||||
.editor-row input[type="text"],
|
||||
.editor-row select,
|
||||
.editor-row input[type="color"] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
}
|
||||
.editor-row input[type="color"] { padding: 2px 4px; cursor: pointer; }
|
||||
.editor-row input:focus { outline: none; border-color: var(--main-color, var(--orca-accent, #009688)); box-shadow: 0 0 0 2px rgba(0,150,136,.22); }
|
||||
/* custom dropdown (native <select> popups are flaky in the embedded webview) */
|
||||
.ed-dropdown { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
|
||||
.ed-dropdown-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ed-dropdown-toggle.open { border-color: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.ed-dropdown-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ed-dropdown-caret { flex: 0 0 auto; font-size: 10px; color: var(--muted, var(--orca-muted, #6b7280)); transition: transform .12s; }
|
||||
.ed-dropdown-toggle.open .ed-dropdown-caret { transform: rotate(180deg); }
|
||||
.ed-dropdown-menu {
|
||||
/* Overlay popover, not in-flow: opening it must NOT grow the editor card or the popup window.
|
||||
position/left/top/width/max-height are set from JS on open (position:fixed escapes the
|
||||
scrollable card and launcher overflow, so the menu clips to the popup and scrolls itself).
|
||||
These are only the visual defaults. */
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.16);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ed-dropdown-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ed-dropdown-option:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
|
||||
.ed-dropdown-option.sel { font-weight: 600; color: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.ed-option-label { flex: 1 1 auto; min-width: 0; }
|
||||
/* Pattern pictograms: filled with the option's icon, and (for values with none) absent. */
|
||||
.ed-option-icon,
|
||||
.ed-dropdown-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.ed-dropdown-icon[hidden] { display: none; }
|
||||
/* toggle switch */
|
||||
.ed-switch { position: relative; flex: 0 0 auto; width: 36px; height: 20px; }
|
||||
.ed-switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
|
||||
.ed-slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #c4c4c4;
|
||||
border-radius: 10px;
|
||||
transition: background .15s;
|
||||
}
|
||||
.ed-slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform .15s;
|
||||
}
|
||||
.ed-switch input:checked + .ed-slider { background: var(--main-color, var(--orca-accent, #009688)); }
|
||||
.ed-switch input:checked + .ed-slider::before { transform: translateX(16px); }
|
||||
.editor-readonly { padding: 4px 0; font-size: 12px; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
.editor-actions { display: flex; gap: 8px; padding-top: 4px; }
|
||||
.ed-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border, var(--orca-border, #ddd));
|
||||
border-radius: 6px;
|
||||
background: var(--panel, var(--orca-bg, #fff));
|
||||
color: var(--text, var(--orca-fg, #1b1c1e));
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ed-btn:hover { background: var(--row-hover, rgba(127,127,127,.16)); }
|
||||
.ed-btn-primary { background: var(--main-color, var(--orca-accent, #009688)); border-color: var(--main-color, var(--orca-accent, #009688)); color: #fff; }
|
||||
.ed-btn-primary:hover { filter: brightness(1.05); }
|
||||
.editor-hint { font-size: 10px; text-align: center; color: var(--muted, var(--orca-muted, #6b7280)); }
|
||||
|
||||
Reference in New Issue
Block a user