diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index e05a927a03..5f54f6581d 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,12 +49,6 @@ jobs: cmakeVersion: "~4.3.0" # use most recent 4.3.x version useLocalCache: true useCloudCache: true - - name: Run web dialog JS tests - timeout-minutes: 5 - shell: bash - run: | - node resources/web/js/fuzzy-search.test.js - node resources/web/dialog/SpeedDial/speeddial.test.js - name: Unpackage and Run Unit Tests timeout-minutes: 20 shell: bash diff --git a/resources/web/data/text.js b/resources/web/data/text.js index 2f6682c051..0c803c0ada 100644 --- a/resources/web/data/text.js +++ b/resources/web/data/text.js @@ -150,6 +150,8 @@ var LangText = { sd_mode_advanced: "Advanced", sd_mode_expert: "Expert", sd_mode_develop: "Developer", + sd_wiki: "Wiki", + sd_no_wiki: "No wiki page for this action", }, ca_ES: { t1: "Benvingut a Orca Slicer", diff --git a/resources/web/dialog/SpeedDial/index.html b/resources/web/dialog/SpeedDial/index.html index a6f9e753a6..0137a2fcf2 100644 --- a/resources/web/dialog/SpeedDial/index.html +++ b/resources/web/dialog/SpeedDial/index.html @@ -28,6 +28,7 @@
+ diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 3dcbbd4fd1..d5615bd3f7 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -8,6 +8,7 @@ // - action.mode token -> ActionRegistry::mode_key / SpeedDialDialog::mode_label // - action.input "percent"/"tab" -> NativeCommands catalog (phases handled in activateEntry) // - action.icon SVG base name -> AppAction::icon / resources/images/.svg +// - action.desc/wiki -> AppAction::tooltip / help_url (footer detail strip) // - action list is frecency-sorted -> ActionRegistry::snapshot() // ---- state (populated by the C++ bridge via window.HandleStudio) ---- @@ -77,7 +78,7 @@ var tabOptions = []; // [{id,title}] - notebook pages, fetched on entering // ../../js/fuzzy-search.js, loaded before this script. Search is always case-insensitive. // element handles, assigned in OnInit (kept null so load-time touches no DOM) -var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null; +var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, countEl = null, detailEl = null; // ---- pure helpers (no DOM; unit-tested) ------------------------------------- // Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per @@ -400,6 +401,11 @@ function selectedActionId(sel, actions, favIds) { return a && a.id; } +function actionHasWiki(a) { return !!(a && a.wiki); } + +// Whether the action has anything for the footer strip to show (a description or a wiki link). +function actionHasDetail(a) { return !!(a && ((a.desc && a.desc.length) || a.wiki)); } + function foldLabel(s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); } // Title-case a source for display: "GCODE OPTIMIZER"/"iRoNiNg pRo" -> "Gcode Optimizer"/"Ironing Pro". @@ -996,9 +1002,43 @@ function renderList() { renderCommandsList(); } +// The action the footer describes: the current selection resolved through the active list/fav bar. +function currentDetailAction() { + if (phase !== "commands") return null; + var id = selectedActionId(sel, currentList(), currentVisibleFavs()); + return id ? byId(id) : null; +} + +// Footer detail strip: the selected action's description plus, when it has a wiki page, a link that +// opens it (same path as F1). Shown only when the highlighted action has something to say, so +// selecting a command with no description hides the strip. +function renderDetail() { + if (!detailEl) return; + var a = currentDetailAction(); + var show = phase === "commands" && actionHasDetail(a); + detailEl.hidden = !show; + detailEl.innerHTML = ""; + if (!show) return; + if (a && a.desc) { + var desc = document.createElement("div"); + desc.className = "detail-desc"; + desc.textContent = a.desc; + detailEl.appendChild(desc); + } + if (a && a.wiki) { + var link = document.createElement("button"); + link.type = "button"; + link.className = "detail-wiki"; + link.textContent = T("sd_wiki", "Wiki") + " (F1)"; + link.onclick = function (ev) { ev.stopPropagation(); SendMessage({ command: "open_wiki", id: a.id }); }; + detailEl.appendChild(link); + } +} + function render(opts) { renderFav(); renderList(); + renderDetail(); // Pin toggles don't move the selection, so they pass keepScroll to avoid snapping the list // back to a row that is currently off-screen. if (!(opts && opts.keepScroll)) @@ -1148,7 +1188,7 @@ function focusInput() { setTimeout(function () { if (qEl) qEl.focus(); }, 0); } // ---- init -------------------------------------------------------------------- function OnInit() { - qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); + qEl = $("q"); listEl = $("list"); favEl = $("favBar"); clearEl = $("clear"); eyeEl = $("favEyebrow"); countEl = $("count"); detailEl = $("detail"); // text.js's TranslatePage() targets jQuery `.trans` nodes; this page has none and defines its own // `$`, so don't call it. Runtime strings go through T() instead. qEl.placeholder = T("sd_search", "Search actions"); @@ -1187,6 +1227,15 @@ function OnInit() { document.addEventListener("keydown", function (e) { if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } + // F1 opens the selected setting's wiki page. Settings without one flash a hint instead. + if (e.key === "F1") { + e.preventDefault(); + if (phase !== "commands") return; + var help = currentDetailAction(); + if (actionHasWiki(help)) SendMessage({ command: "open_wiki", id: help.id }); + else flashHint(T("sd_no_wiki", "No wiki page for this action")); + return; + } // Pin/unpin the highlighted action: Ctrl/Cmd+B. Commands phase only (tabs/percent aren't pinnable). if (phase === "commands" && (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "b") { diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index 85a0bcd064..38f7c343ae 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -334,4 +334,20 @@ assert.equal(ctx.searchActions(modePool, "retraction")[0].id, "a3", 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"); +// actionHasWiki: the footer's wiki link/F1 path is offered only when the action carries a wiki flag. +assert.equal(ctx.actionHasWiki({ id: "x", wiki: true }), true, "a wiki-flagged setting offers the wiki action"); +assert.equal(ctx.actionHasWiki({ id: "x", wiki: false }), false, "a setting without a wiki path offers nothing"); +assert.equal(ctx.actionHasWiki({ id: "x" }), false, "a missing wiki field offers nothing"); +assert.equal(ctx.actionHasWiki(null), false, "no action selected offers nothing"); + +// actionHasDetail: the footer strip appears only when the highlighted action has a description or +// wiki link; selecting a plain command hides it. +assert.equal(ctx.actionHasDetail({ id: "a", desc: "Layer height" }), true, "a description shows the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", wiki: true }), true, "a wiki link shows the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", desc: "Layer height", wiki: true }), true, "both show the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", desc: "" }), false, "an empty description hides the footer"); +assert.equal(ctx.actionHasDetail({ id: "a", desc: "", wiki: false }), false, "empty description and false wiki hide the footer"); +assert.equal(ctx.actionHasDetail({ id: "a" }), false, "an action with neither hides the footer"); +assert.equal(ctx.actionHasDetail(null), false, "no selected action hides the footer"); + console.log("ok"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index c9532d8dab..8afc33d98c 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -436,3 +436,48 @@ body { color: var(--muted, var(--orca-muted, #6b7280)); text-align: center; } + +/* Footer detail strip: the selected action's description plus its wiki link. Auto-sizes to the + content; the description is clamped below. */ +.dial-detail { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-top: 1px solid var(--border, var(--orca-border, #ddd)); + overflow: hidden; +} + +.dial-detail[hidden] { + display: none; +} + +.detail-desc { + flex: 1 1 auto; + min-width: 0; + font-size: 11px; + line-height: 1.4; + color: var(--muted, var(--orca-muted, #6b7280)); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + overflow: hidden; + overflow-wrap: anywhere; +} + +.detail-wiki { + flex: 0 0 auto; + border: 0; + padding: 0; + background: transparent; + font: inherit; + font-size: 11px; + color: var(--main-color, var(--orca-accent, #009688)); + cursor: pointer; + white-space: nowrap; +} + +.detail-wiki:hover { + text-decoration: underline; +} diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index fb98b96a17..3770d6fb68 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -6,6 +6,7 @@ #include "MainFrame.hpp" #include "NativeCommands.hpp" #include "Notebook.hpp" +#include "OptionsGroup.hpp" #include "Plater.hpp" #include "Search.hpp" #include "Tab.hpp" @@ -571,6 +572,11 @@ void ActionRegistry::materialize_setting_actions() } } + // Footer description + wiki affordance; only settings whose row declared a wiki path have one. + action->tooltip = opt.tooltip; + if (!opt.wiki_path.empty()) + action->help_url = into_u8(OptionsGroup::get_url(opt.wiki_path)); + seed_from(stats, favs, id, *action); auto const action_id = action->id(); auto const app_action = std::shared_ptr(std::move(action)); @@ -723,7 +729,9 @@ nlohmann::json ActionRegistry::snapshot() {"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"}, {"input", a->input}, {"icon", a->icon}, - {"mode", mode_key(a->required_mode)}}); + {"mode", mode_key(a->required_mode)}, + {"desc", a->tooltip}, + {"wiki", !a->help_url.empty()}}); }; nlohmann::json actions = nlohmann::json::array(); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 3c71317eed..026e1bcee5 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -83,6 +83,10 @@ struct AppAction // Settings mode required to edit this action (SettingActions only). The palette prompts before // running an action whose mode is above the user's current mode. comSimple for everything else. ConfigOptionMode required_mode = comSimple; + // Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip). + std::string tooltip; + // Full wiki URL, when the action has one (SettingActions whose row declared a label_path). + std::string help_url; virtual ~AppAction() = default; // Re-resolves + runs (UI thread). `param` carries an optional per-run argument for diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 9930e6d872..2fc41ba90b 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -244,6 +244,12 @@ void OptionsGroup::append_line(const Line& line) { m_lines.emplace_back(line); + // Record each option's wiki path (Line::label_path) so the Speed Dial can offer an "open wiki" + // affordance for it. Settings tabs only; the searcher already exists by the time tabs are built. + if (m_use_custom_ctrl && !line.label_path.empty()) + for (const auto& opt : line.get_options()) + wxGetApp().sidebar().get_searcher().set_path(opt.opt_id, static_cast(config_type()), line.label_path); + if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty())) return; diff --git a/src/slic3r/GUI/OptionsGroup.hpp b/src/slic3r/GUI/OptionsGroup.hpp index b20d16aca9..921ac62f15 100644 --- a/src/slic3r/GUI/OptionsGroup.hpp +++ b/src/slic3r/GUI/OptionsGroup.hpp @@ -250,6 +250,10 @@ protected: virtual void back_to_initial_value(const std::string& opt_key) {} virtual void back_to_sys_value(const std::string& opt_key) {} + // Preset::Type of a settings group; -1 for groups not tied to a preset. Used by append_line to + // register each option's wiki path with the searcher. Overridden by ConfigOptionsGroup. + virtual int config_type() const { return -1; } + public: static wxString get_url(const std::string& path_end); static bool launch_browser(const std::string& path_end); @@ -273,7 +277,7 @@ public: OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {} const wxString& config_category() const throw() { return m_config_category; } - int config_type() const throw() { return m_config_type; } + int config_type() const throw() override { return m_config_type; } const t_opt_map& opt_map() const throw() { return m_opt_map; } void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; } diff --git a/src/slic3r/GUI/Search.cpp b/src/slic3r/GUI/Search.cpp index 16ec2051a9..916d93a40c 100644 --- a/src/slic3r/GUI/Search.cpp +++ b/src/slic3r/GUI/Search.cpp @@ -85,7 +85,7 @@ static std::string get_key(const std::string &opt_key, Preset::Type type) { retu void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode) { - auto emplace = [this, type](std::vector