From c3a21fa0d416e1d8c90b16059d1ab7a09212bf47 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 9 Sep 2026 12:26:09 +0800 Subject: [PATCH] Switched back to jump to setting instead --- resources/web/dialog/SpeedDial/speeddial.js | 505 +----------------- .../web/dialog/SpeedDial/speeddial.test.js | 69 --- resources/web/dialog/SpeedDial/style.css | 142 ----- src/slic3r/GUI/ActionRegistry.cpp | 369 +------------ src/slic3r/GUI/ActionRegistry.hpp | 10 - src/slic3r/GUI/SpeedDialDialog.cpp | 25 - 6 files changed, 11 insertions(+), 1109 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 65d2e24430..78ba8a138d 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -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; // 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 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)); } diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index ef31401909..b55455f45d 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -180,17 +180,15 @@ struct SettingAction : AppAction , type(type_in) , category(std::move(category_in)) { - // A setting is a two-phase command: activating it opens the inline editor (the "setting" - // phase) instead of running. Native run() is a no-op fallback; the editor applies through - // apply_setting(). + // A setting is a single-phase command: activating it jumps the sidebar to the option + // (like the sidebar's own settings search), then the dial closes. run() performs the jump. this->kind = AppActionKind::Command; this->group = std::move(group); - this->input = "setting"; } AppActionRunResult run(const std::string& /*param*/) const override { - // Two-phase: the palette collects the edit; native run() is a no-op fallback. + wxGetApp().sidebar().jump_to_option(opt_key, type, category); return {AppActionRunResult::Level::Success}; } @@ -693,45 +691,7 @@ std::vector> native_commands() return out; } -// ---- inline setting editor helpers ------------------------------------------ - -// The inline editor "control" kind for an option, or "" when it can't be edited inline -// (plugin-backed values, points, serialized strings, etc.). readonly/legend options are -// filtered out of the search entirely in materialize_setting_actions(), so they never -// reach here. -std::string setting_control(const ConfigOptionDef& def) -{ - if (def.readonly || def.gui_type == ConfigOptionDef::GUIType::legend || - def.gui_type == ConfigOptionDef::GUIType::one_string || def.is_plugin_backed()) - return ""; - // Serialized vectors are entered as ONE semicolon-separated field (e.g. post_process), which the - // per-index editor doesn't model - keep them in the open-in-sidebar bucket. - if (def.gui_flags.find("serialized") != std::string::npos) - return ""; - switch (def.gui_type) { - case ConfigOptionDef::GUIType::color: return "color"; - case ConfigOptionDef::GUIType::i_enum_open: - case ConfigOptionDef::GUIType::f_enum_open: return "combo"; - default: break; - } - switch (def.type) { - case coBool: - case coBools: return "toggle"; - case coEnum: - case coEnums: return def.enum_values.empty() ? "combo" : "dropdown"; - case coInt: - case coInts: - case coFloat: - case coFloats: - case coPercent: - case coPercents: return "number"; - case coFloatOrPercent: - case coFloatsOrPercents: return "percent"; - case coString: - case coStrings: return "text"; - default: return ""; - } -} +// ---- setting action helpers -------------------------------------------------- // Self-contained base64 encoder (for the tiny pictogram SVGs), avoiding a dependency on the exact // wxBase64Encode overload/return type across wx versions. @@ -790,41 +750,6 @@ std::string setting_icon_for_key(const std::string& key) return "data:image/svg+xml;base64," + base64_encode(data); } -// [{value,key,label,icon}...] for an enum/combo. Ordered by enum_values when present, else by the -// keys_map iteration. label falls back to the key when enum_labels doesn't provide one. `icon` is -// the value's pattern pictogram when one exists, empty otherwise. -nlohmann::json setting_enum_options(const ConfigOptionDef& def) -{ - nlohmann::json out = nlohmann::json::array(); - auto label_at = [&](size_t i, const std::string& key) -> std::string { - return (i < def.enum_labels.size() && !def.enum_labels[i].empty()) ? def.enum_labels[i] : key; - }; - if (def.enum_keys_map != nullptr) { - std::vector ordered; - if (!def.enum_values.empty()) - ordered = def.enum_values; - else - for (const auto& kv : *def.enum_keys_map) - ordered.push_back(kv.first); - for (size_t i = 0; i < ordered.size(); ++i) { - auto it = def.enum_keys_map->find(ordered[i]); - if (it == def.enum_keys_map->end()) - continue; - out.push_back({{"value", it->second}, - {"key", ordered[i]}, - {"label", label_at(i, ordered[i])}, - {"icon", setting_icon_for_key(ordered[i])}}); - } - } else { - for (size_t i = 0; i < def.enum_values.size(); ++i) - out.push_back({{"value", (long long) i}, - {"key", def.enum_values[i]}, - {"label", label_at(i, def.enum_values[i])}, - {"icon", setting_icon_for_key(def.enum_values[i])}}); - } - return out; -} - // The pattern pictogram for a setting's CURRENT value (its enum int), empty when it isn't a // pattern-style enum or the value has no icon. Used for the search-result tile. std::string setting_action_icon(const SettingAction& a) @@ -854,146 +779,6 @@ std::string setting_action_icon(const SettingAction& a) std::string SettingAction::icon() const { return setting_action_icon(*this); } -// Current value of the option at vector index `idx` as JSON (bool/number/string), or null for a -// type the inline editor doesn't render. `config` is the tab's live config; when an option is -// absent the def's default is shown. -nlohmann::json setting_value_json(const DynamicPrintConfig& config, const ConfigOptionDef& def, size_t idx) -{ - const ConfigOption* opt = config.option(def.opt_key); - const ConfigOption* root = opt ? opt : def.default_value.get(); - if (!root) - return nullptr; - switch (def.type) { - case coBool: return root->getBool(); - case coInt: return root->getInt(); - case coFloat: return root->getFloat(); - case coPercent: return root->getFloat(); - case coString: return static_cast(root)->value; - case coEnum: return root->getInt(); - case coBools: { - if (auto v = dynamic_cast(root)) - return bool(v->get_at(idx)); - if (auto v = dynamic_cast(root)) - return bool(v->get_at(idx) != 0); - return nullptr; - } - case coInts: { - if (auto v = dynamic_cast(root)) - return v->get_at(idx); - if (auto v = dynamic_cast(root)) { - const int nil = ConfigOptionIntsNullable::nil_value(); - int val = v->get_at(idx); - return val == nil ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coFloats: { - if (auto v = dynamic_cast(root)) - return v->get_at(idx); - if (auto v = dynamic_cast(root)) { - double val = v->get_at(idx); - return std::isnan(val) ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coPercents: { - if (auto v = dynamic_cast(root)) - return v->get_at(idx); - if (auto v = dynamic_cast(root)) { - double val = v->get_at(idx); - return std::isnan(val) ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coStrings: return static_cast(root)->get_at(idx); - case coEnums: { - if (auto v = dynamic_cast(root)) - return v->get_at(idx); - if (auto v = dynamic_cast(root)) { - const int nil = ConfigOptionEnumsGenericNullable::nil_value(); - int val = v->get_at(idx); - return val == nil ? nlohmann::json(nullptr) : nlohmann::json(val); - } - return nullptr; - } - case coFloatOrPercent: return root->serialize(); - case coFloatsOrPercents: { - if (auto v = dynamic_cast(root)) { - auto ss = v->vserialize(); - return idx < ss.size() ? ss[idx] : nullptr; - } - if (auto v = dynamic_cast(root)) { - auto ss = v->vserialize(); - return idx < ss.size() ? ss[idx] : nullptr; - } - return nullptr; - } - default: return nullptr; - } -} - -// How many scalar values the option currently has (1 for scalars, the array length for vectors). -size_t setting_value_count(const DynamicPrintConfig& config, const ConfigOptionDef& def) -{ - if ((int(def.type) & int(coVectorType)) == 0) - return 1; - // size() lives on ConfigOptionVectorBase, not ConfigOption - dynamic_cast to it covers every - // vector type (and their nullable variants) polymorphically. - const ConfigOption* opt = config.option(def.opt_key); - if (opt) - if (auto v = dynamic_cast(opt)) - return v->size(); - if (def.default_value) - if (auto v = dynamic_cast(def.default_value.get())) - return v->size(); - return 1; -} - -// boost::any for a single element, matching what Slic3r::GUI::change_opt_value expects. -boost::any setting_any_from_json(const ConfigOptionDef& def, const nlohmann::json& v) -{ - if (!v.is_null()) { - switch (def.type) { - case coBool: return boost::any(v.is_boolean() ? v.get() : v.get() != 0); - case coInt: return boost::any(v.get()); - case coFloat: - case coPercent: return boost::any(v.get()); - case coString: return boost::any(v.get()); - case coEnum: return boost::any(v.get()); - case coBools: return boost::any(static_cast(v.get() ? 1 : 0)); - case coInts: return boost::any(v.get()); - case coFloats: - case coPercents: return boost::any(v.get()); - case coStrings: return boost::any(v.get()); - case coFloatOrPercent: - case coFloatsOrPercents: { - // change_opt_value detects "percent" via a trailing '%', so trim whitespace first or a - // stray space (e.g. "10% ") would be misread as mm. - std::string s = v.is_string() ? v.get() : std::to_string(v.get()); - boost::trim(s); - // An empty string would make change_opt_value's str.back() UB - bail out to a rejected apply. - return s.empty() ? boost::any() : boost::any(s); - } - case coEnums: return boost::any(v.get()); - default: break; - } - } - // Coerce numeric types that may arrive as a different JSON numeric type. - if (v.is_number()) { - switch (def.type) { - case coInt: - case coEnums: return boost::any(v.get()); - case coFloat: - case coPercent: - case coInts: - case coFloats: - case coPercents: return boost::any(v.get()); - default: break; - } - } - return boost::any(); -} - } // namespace ActionRegistry::~ActionRegistry() = default; @@ -1257,16 +1042,6 @@ void ActionRegistry::materialize_setting_actions() std::unordered_set seen; for (const Search::Option& opt : options) { - // Omit rows the inline editor can't represent and that aren't useful as a jump target: - // readonly (e.g. the detected thread count) and legend (static text) GUI rows. They remain - // in the sidebar's own search; only the Speed Dial pool drops them. - Tab* tab = wxGetApp().get_tab(opt.type); - if (tab && tab->get_config()) { - const ConfigOptionDef* def = tab->get_config()->def()->get(opt.opt_key()); - if (!def || def->readonly || def->gui_type == ConfigOptionDef::GUIType::legend) - continue; - } - const std::string id = SettingAction::id_for(opt.opt_key(), opt.type); seen.insert(id); @@ -1468,140 +1243,4 @@ nlohmann::json ActionRegistry::tab_options() const return out; } -// ---- inline setting editor (read the current value) -------------------------- - -nlohmann::json ActionRegistry::setting_descriptor(const std::string& id) const -{ - assert(wxThread::IsMain()); - const AppAction* a = by_id(id); - const SettingAction* sa = dynamic_cast(a); - if (!sa) - return nlohmann::json::object(); - Tab* tab = wxGetApp().get_tab(sa->type); - if (!tab) - return nlohmann::json::object(); - DynamicPrintConfig* config = tab->get_config(); - if (!config) - return nlohmann::json::object(); - const ConfigOptionDef* def = config->def()->get(sa->opt_key); - if (!def) - return nlohmann::json::object(); - - const std::string control = setting_control(*def); - const bool vector = (int(def->type) & int(coVectorType)) != 0; - - nlohmann::json d = {{"id", sa->id()}, - {"opt_key", sa->opt_key}, - {"type", int(sa->type)}, - {"title", a->title()}, - {"breadcrumb", a->source_name()}, - {"category", boost::nowide::narrow(sa->category)}, - {"unit", def->sidetext}, - {"tooltip", def->tooltip}, - {"editable", !control.empty()}, - {"control", control}, - {"cardinality", vector ? "vector" : "scalar"}}; - - if (!control.empty()) { - // Hide unbounded min/max so the page doesn't clamp a sane value to ±FLT_MAX. - if (def->min > -FLT_MAX) - d["min"] = def->min; - if (def->max < FLT_MAX) - d["max"] = def->max; - if (control == "number") - d["is_int"] = (def->type == coInt || def->type == coInts); - if (control == "dropdown" || control == "combo") - d["enum_options"] = setting_enum_options(*def); - if (vector) { - nlohmann::json values = nlohmann::json::array(); - nlohmann::json labels = nlohmann::json::array(); - const size_t n = setting_value_count(*config, *def); - for (size_t i = 0; i < n; ++i) { - values.push_back(setting_value_json(*config, *def, i)); - labels.push_back(std::to_string(i + 1)); - } - d["values"] = std::move(values); - d["index_labels"] = std::move(labels); - } else { - d["value"] = setting_value_json(*config, *def, 0); - } - } - return d; -} - -// ---- inline setting editor (write the edited value back) --------------------- - -bool ActionRegistry::apply_setting(const std::string& id, const nlohmann::json& value) -{ - assert(wxThread::IsMain()); - const AppAction* a = by_id(id); - const SettingAction* sa = dynamic_cast(a); - if (!sa) - return false; - Tab* tab = wxGetApp().get_tab(sa->type); - if (!tab) - return false; - DynamicPrintConfig* config = tab->get_config(); - if (!config) - return false; - const ConfigOptionDef* def = config->def()->get(sa->opt_key); - if (!def) - return false; - const std::string control = setting_control(*def); - if (control.empty()) - return false; - - const bool vector = (int(def->type) & int(coVectorType)) != 0; - const size_t n = vector ? (value.is_array() ? value.size() : 0) : 1; - if (vector && n == 0) - return false; - - for (size_t i = 0; i < n; ++i) { - const nlohmann::json& elem = vector ? value[i] : value; - boost::any any = setting_any_from_json(*def, elem); - if (any.empty()) - return false; - if (control == "number" && elem.is_number()) { - const double d = elem.get(); - if (d < def->min || d > def->max) - return false; - } - if (control == "percent" && elem.is_string()) { - // "mm or %" value: strip a trailing %/whitespace, clamp the numeric part to [min,max]. - // Reject anything that isn't a well-formed number (which change_opt_value would throw on). - std::string s = elem.get(); - boost::trim(s); - if (!s.empty() && s.back() == '%') - s.pop_back(); - boost::trim(s); - if (s.empty()) - return false; - char* end = nullptr; - const double d = std::strtod(s.c_str(), &end); - if (end == s.c_str() || *end != '\0') - return false; - if (d < def->min || d > def->max) - return false; - } - Slic3r::GUI::change_opt_value(*config, sa->opt_key, any, int(i)); - } - - // Mark the preset modified like a sidebar edit. Scalar options also get the standard - // post-change hook so dependent settings refresh; vector options have no unambiguous scalar - // value to pass, so on_value_change is skipped (the config write + dirty flag is still correct). - tab->update_dirty(); - if (!vector) { - boost::any any = setting_any_from_json(*def, value); - if (!any.empty()) - tab->on_value_change(sa->opt_key, any); - } - - // The config write is separate from the on-screen Field, so repaint the field(s) that display - // this option (on whatever page they live, not just the active page) - otherwise the sidebar - // shows the "modified" arrow but keeps the stale value pushed to the last edit/reload. - if (Page* page = nullptr; tab->get_field(sa->opt_key, &page) && page) - page->reload_config(); - return true; -} - }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index d7bd1fa583..6daafdde56 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -173,16 +173,6 @@ public: // plugins) aren't separate pages and are not listed. Call on the UI thread; null-safe. nlohmann::json tab_options() const; - // Inline setting editor descriptor for a SettingAction id. Returns the JSON the palette - // renders: {id, opt_key, type, title, breadcrumb, category, group, unit, tooltip, editable, - // control ("toggle|number|dropdown|combo|text|color"), cardinality ("scalar"|"vector"), - // value|values, index_labels[], enum_options[], min|max}. Empty object for a non-setting id. - nlohmann::json setting_descriptor(const std::string& id) const; - // Apply an edit submitted by the palette. `value` is the control's JSON payload (scalar, or an - // array for vector settings). Writes the value(s) into the global preset config and marks the - // preset dirty, exactly like a sidebar edit. Returns false on a bad id/type/value. - bool apply_setting(const std::string& id, const nlohmann::json& value); - private: void seed_state(AppAction& a) const; // favourite/stats from config AppAction* find(const std::string& id); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index 593d8a7344..b199d0b5ae 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -9,10 +9,6 @@ #include "Plater.hpp" #include "Widgets/WebViewHostDialog.hpp" -#include - -#include - #include #include @@ -155,27 +151,6 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload) if (wxGetApp().mainframe) wxGetApp().mainframe->select_tab(from_u8(tab_id)); } - } else if (command == "setting_descriptor") { - // Inline editor: hand the page the descriptor for the setting it's editing. - const std::string id = payload.value("id", ""); - call_web_handler( - {{"command", "setting_descriptor"}, {"descriptor", wxGetApp().action_registry().setting_descriptor(id)}}); - } else if (command == "set_setting") { - // Inline editor submit. Apply the value; on success close the dialog. - const std::string id = payload.value("id", ""); - const nlohmann::json value = payload.contains("value") ? payload["value"] : nlohmann::json(nullptr); - if (id.empty() || !wxGetApp().action_registry().apply_setting(id, value)) { - call_web_handler({{"command", "apply_failed"}, {"id", id}}); - return; - } - Hide(); - } else if (command == "open_setting_in_sidebar") { - // Non-inline-editable setting (points, plugin-backed, float-or-percent): jump the sidebar. - Hide(); - const std::string opt_key = payload.value("opt_key", ""); - const std::string category = payload.value("category", ""); - if (!opt_key.empty()) - wxGetApp().sidebar().jump_to_option(opt_key, Preset::Type(payload.value("type", int(Preset::TYPE_INVALID))), boost::nowide::widen(category)); } else if (command == "resize") resize_to_content(json_int_or(payload, "height", 0)); }