From 42bee12481254ebaccb425533526d6e2548e8200 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 11 Sep 2026 11:36:24 +0800 Subject: [PATCH] Speed Dial: replace tile monograms with native SVG icons Tiles previously rendered a colored monogram (title/source initials plus an ordinal) with a per-id hue. Show the matching native SVG icon instead: - AppAction/NativeCommand gain an `icon` field; commands get a curated key->icon table and settings inherit their group header's icon. - Notebook tracks each page's resource icon name and reports it in tab_options(), so the tab picker can show it too. - Searcher records the group icon so settings keep it through search. - Web tile rendering swaps monogramFor/hue for an ; drop the now-unused hue/text CSS vars. Add a test asserting every non-empty icon resolves to a shipped SVG. --- resources/web/dialog/SpeedDial/speeddial.js | 67 +++++-------- .../web/dialog/SpeedDial/speeddial.test.js | 24 ++--- resources/web/dialog/SpeedDial/style.css | 21 +++-- resources/web/dialog/css/theme.css | 4 - src/slic3r/GUI/ActionRegistry.cpp | 19 +++- src/slic3r/GUI/ActionRegistry.hpp | 3 + src/slic3r/GUI/NativeCommands.cpp | 94 ++++++++++++++++++- src/slic3r/GUI/NativeCommands.hpp | 4 +- src/slic3r/GUI/Notebook.cpp | 2 + src/slic3r/GUI/Notebook.hpp | 10 ++ src/slic3r/GUI/OptionsGroup.cpp | 2 +- src/slic3r/GUI/Search.cpp | 7 +- src/slic3r/GUI/Search.hpp | 5 +- src/slic3r/GUI/Tab.cpp | 8 +- tests/slic3rutils/test_action_source.cpp | 43 +++++++++ 15 files changed, 231 insertions(+), 82 deletions(-) diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index 6f251d35eb..9045262ea3 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -222,42 +222,29 @@ function shouldRenderActionList(query) { return !!((query || "").trim()); } -// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source -// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct. -// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and -// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs. -function monogramFor(item, list, titleOf, sourceOf, idOf) { - var items = list || []; - var title = titleOf(item) || " "; - var ti = title.charAt(0).toUpperCase(); - var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; }); - if (sameTitle.length <= 1) - return ti; - var source = sourceOf(item) || " "; - var pi = source.charAt(0).toUpperCase(); - var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; }); - if (sameSource.length <= 1) - return pi + ti; - sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; }); - for (var i = 0; i < sameSource.length; i++) - if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item)) - return pi + ti + (i + 1); - return pi + ti; +// Tile pictogram base path. The page lives at resources/web/dialog/SpeedDial/, so this climbs to +// resources/images/ where the same SVG icons the native GUI controls use are shipped. +var ICON_BASE = "../../../images/"; + +// SVG base name for an action's tile pictogram, or "" when it has none (commands without a GUI +// icon, plugins). Pure so the node-vm test can exercise it. +function actionIcon(a) { + return (a && a.icon) ? a.icon : ""; } -// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so -// they share this ladder (title initial, then source, then a stable ordinal). -function tileCode(action, actions) { - return monogramFor(action, actions, - function (o) { return o.title; }, - function (o) { return o.source; }, - function (o) { return o.id; }); -} - -// Put an action's monogram into a tile (search row or favourites tile). A null action (a tab row -// with no backing action) renders an empty tile. -function fillTile(tile, a) { - tile.textContent = a ? tileCode(a, ACTIONS) : ""; +// Put a pictogram into a tile (search row, favourites tile, or tab row). No icon leaves the tile +// blank. `mono` marks the white tab-strip glyphs, which the CSS recolors to the shared gray. +function fillTile(tile, a, mono) { + tile.textContent = ""; + var icon = actionIcon(a); + if (!icon) + return; + var img = document.createElement("img"); + img.className = mono ? "tile-icon tab-mono" : "tile-icon"; + img.src = ICON_BASE + icon + ".svg"; + img.alt = ""; + img.setAttribute("aria-hidden", "true"); + tile.appendChild(img); } // The active list for the main phase. A typed query ranks every action (commands/plugins/settings) @@ -461,13 +448,6 @@ function currentList() { return []; // percent - the input itself is the only field } -function hue(id) { - var h = 0; - for (var i = 0; i < id.length; i++) - h = (h * 31 + id.charCodeAt(i)) >>> 0; - return h % 360; -} - // Build a
with the search-match ranges wrapped in . Used for both the // title and the source eyebrow. Pure (only touches the document factory), so the node-vm test never // calls it and load-time stays DOM-free. @@ -528,7 +508,6 @@ function renderFav() { var a = byId(id); var tile = document.createElement("button"); tile.className = "fav-tile" + (sel.zone === "fav" && sel.i === i ? " sel" : ""); - tile.style.setProperty("--h", hue(id)); fillTile(tile, a); var tileBadge = modeBadge(a, USER_MODE); tile.title = a.title + (tileBadge ? " (" + tileBadge + ")" : ""); @@ -629,7 +608,6 @@ function renderActionRow(a, i) { var tile = document.createElement("div"); tile.className = "tile"; - tile.style.setProperty("--h", hue(a.id)); fillTile(tile, a); var left = document.createElement("div"); @@ -805,8 +783,7 @@ function renderTabRow(t, i) { var tile = document.createElement("div"); tile.className = "tile"; - tile.style.setProperty("--h", hue(t.id)); - fillTile(tile, null); + fillTile(tile, t, true); var left = document.createElement("div"); left.className = "row-left"; diff --git a/resources/web/dialog/SpeedDial/speeddial.test.js b/resources/web/dialog/SpeedDial/speeddial.test.js index c8fd43add6..4fe72aba11 100644 --- a/resources/web/dialog/SpeedDial/speeddial.test.js +++ b/resources/web/dialog/SpeedDial/speeddial.test.js @@ -195,21 +195,15 @@ assert.deepEqual(ctx.visibleFavourites(["b", "a", "b"], [{ id: "a" }, { id: "b" assert.deepEqual(ctx.visibleFavourites([], [{ id: "a" }]), [], "no pins renders an empty quick-bar"); assert.deepEqual(ctx.visibleFavourites(["a"], []), [], "a stale config with no actions renders nothing"); -// tileCode: monogram ladder - title initial, then title+source initials, then a stable ordinal -// by id. The ordinal is keyed by id, not by list order, so frecency reshuffles never renumber tiles. -const monoPool = [ - { id: "z", title: "Repair", source: "Mesh Tools" }, - { id: "a", title: "Repair", source: "Mesh Tools" }, - { id: "b", title: "Repair", source: "Filament" } -]; -assert.equal(ctx.tileCode(monoPool[0], monoPool), "MR2", - "same title+source resolves to source+title initials with an id-keyed ordinal (id z sorts after id a)"); -assert.equal(ctx.tileCode(monoPool[1], monoPool), "MR1", - "the earlier id is numbered first among same-title+source tiles"); -assert.equal(ctx.tileCode(monoPool[2], monoPool), "FR", - "same title but different source resolves to source+title initials"); -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"); +// actionIcon: the SVG base name for a tile's pictogram, or "" when the action has none (blank tile). +assert.equal(ctx.actionIcon({ id: "x", title: "Slice", icon: "media_play" }), "media_play", + "an action's icon base name is returned verbatim"); +assert.equal(ctx.actionIcon({ id: "x", title: "Go to tab...", icon: "" }), "", + "an empty icon renders a blank tile"); +assert.equal(ctx.actionIcon({ id: "x", title: "Plugin action" }), "", + "a missing icon field renders a blank tile"); +assert.equal(ctx.actionIcon(null), "", + "a null action (tab row) renders a blank tile"); // 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"); diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index d6bc8ff8bf..3aea0cf15a 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -60,9 +60,7 @@ body { border: 0; border-radius: 8px; cursor: pointer; - color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%)); - font-weight: 700; - /* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare