diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js
index 04844aae43..b92261b351 100644
--- a/resources/web/dialog/SpeedDial/speeddial.js
+++ b/resources/web/dialog/SpeedDial/speeddial.js
@@ -375,7 +375,7 @@ window.HandleStudio = function (payload) {
// Favourites are at the quick-launch cap - undo the optimistic pin and flash a hint.
var fid = payload.id;
if (fid && FAVS.indexOf(fid) !== -1) FAVS.splice(FAVS.indexOf(fid), 1);
- render({ resize: true });
+ render({ resize: true, keepScroll: true });
flashHint("Favourites are full (" + (payload.limit || K_FAV_LIMIT) + " max)");
}
};
@@ -434,10 +434,19 @@ function markedText(className, text, match) {
return node;
}
-function starSvg(on) {
+// A bookmark glyph: outlined when unpinned, filled when saved to favourites.
+function pinSvg(on) {
return '';
+ '';
+}
+
+// Sync one pin button to its favourite state. Shared by row construction and the in-place
+// updatePins pass so the two can't drift.
+function setPinState(pin, on) {
+ pin.classList.toggle("on", on);
+ pin.innerHTML = pinSvg(on);
+ pin.title = on ? "Unpin from favourites (Ctrl+B)" : "Pin to favourites (Ctrl+B)";
}
// ---- render ------------------------------------------------------------------
@@ -551,7 +560,7 @@ function updateFavEyebrow(favs) {
}
// 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.
+// the setting options too. All rows are pinnable, so every row carries a bookmark.
function renderActionRow(a, i) {
var on = FAVS.indexOf(a.id) !== -1;
var row = document.createElement("div");
@@ -591,14 +600,13 @@ function renderActionRow(a, i) {
row.appendChild(tile);
row.appendChild(left);
- var star = document.createElement("button");
- star.className = "star" + (on ? " on" : "");
- star.innerHTML = starSvg(on);
- star.title = on ? "Unpin from favourites" : "Pin to favourites";
- star.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); };
+ var pin = document.createElement("button");
+ pin.className = "pin";
+ setPinState(pin, on);
+ pin.onclick = function (ev) { ev.stopPropagation(); toggleFav(a.id); };
// why: two quick fav/unfav clicks must not dblclick-run the row
- star.ondblclick = function (ev) { ev.stopPropagation(); };
- row.appendChild(star);
+ pin.ondblclick = function (ev) { ev.stopPropagation(); };
+ row.appendChild(pin);
row.onclick = function () { sel = { zone: "list", i: i }; render({ resize: true }); };
row.ondblclick = function () { sel = { zone: "list", i: i }; activateEntry(a); };
@@ -663,6 +671,21 @@ function updateSelection() {
}
}
+// Sync the pin buttons in place when FAVS changes but the row set doesn't (fav toggle), so a
+// bookmark fills/empties without a rebuild that would reset scroll. Rows carry data-idx into the
+// active list.
+function updatePins(list) {
+ var rows = listEl ? listEl.querySelectorAll(".row") : [];
+ for (var i = 0; i < rows.length; i++) {
+ var a = list[parseInt(rows[i].getAttribute("data-idx"), 10)];
+ var pin = rows[i].querySelector(".pin");
+ if (!a || !pin) continue;
+ var on = FAVS.indexOf(a.id) !== -1;
+ if (pin.classList.contains("on") !== on)
+ setPinState(pin, on);
+ }
+}
+
function renderCommandsList() {
var list = currentList();
var total = list.length;
@@ -702,9 +725,10 @@ function renderCommandsList() {
countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " recent";
}
updateSelection();
+ updatePins(list);
}
-// A tab row: no star/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses
+// A tab row: no pin/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses
// tabTitle so pages added with an empty text (e.g. Home) still show a label.
function renderTabRow(t, i) {
var label = tabTitle(t);
@@ -781,7 +805,10 @@ function renderList() {
function render(opts) {
renderFav();
renderList();
- scrollSelectedIntoView();
+ // 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))
+ scrollSelectedIntoView();
if (opts && opts.resetScroll)
resetScrollPositions(listEl, document);
if (opts && opts.resize)
@@ -835,7 +862,7 @@ function toggleFav(id) {
var newState = k === -1;
if (newState) FAVS.push(id); else FAVS.splice(k, 1);
SendMessage({ command: "toggle_favourite", id: id, fav: newState });
- render({ resize: true });
+ render({ resize: true, keepScroll: true });
}
// Fire a command/plugin action; C++ owns the run-confirm (native dialog) + suppression, then
@@ -951,6 +978,14 @@ function OnInit() {
document.addEventListener("keydown", function (e) {
if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); 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") {
+ e.preventDefault();
+ var id = selectedActionId(sel, currentList(), currentVisibleFavs(), query);
+ if (id) toggleFav(id);
+ 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) {
diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css
index aa8bdc7c4b..f7d045bc9a 100644
--- a/resources/web/dialog/SpeedDial/style.css
+++ b/resources/web/dialog/SpeedDial/style.css
@@ -255,7 +255,7 @@ kbd {
border: 1px solid var(--border, var(--orca-border, #ddd));
border-radius: 4px;
}
-.star {
+.pin {
flex: 0 0 auto;
width: 24px;
height: 24px;
@@ -269,11 +269,11 @@ kbd {
justify-content: center;
opacity: 0;
}
-.star svg { display: block; }
-.row:hover .star:not(.on),
-.row.sel .star:not(.on) { opacity: .5; }
-.star.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); }
-.star:hover { background: rgba(127,127,127,.18); }
+.pin svg { display: block; }
+.row:hover .pin:not(.on),
+.row.sel .pin:not(.on) { opacity: .5; }
+.pin.on { opacity: 1; color: var(--main-color, var(--orca-accent, #009688)); }
+.pin:hover { background: rgba(127,127,127,.18); }
.dial-empty {
min-height: 64px;
padding: 18px 10px;
diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp
index 92916eb538..f6e124c118 100644
--- a/src/slic3r/GUI/ActionRegistry.cpp
+++ b/src/slic3r/GUI/ActionRegistry.cpp
@@ -728,9 +728,18 @@ nlohmann::json ActionRegistry::snapshot()
// why: favourites is the ORDERED pin list - it must come from favourite_actions
// as stored, not be re-derived from the frecency-sorted actions (that would
- // reorder the favourites bar). The page (js) filters out ids with no live action itself.
- // Cap on read so the bar cannot exceed the quick-launch slots (kFavLimit).
- nlohmann::json favourites(favourite_ids());
+ // reorder the favourites bar). Drop pins with no live action (an option hidden by the
+ // current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so
+ // invisible pins can't silently fill the quick-launch cap. Order is preserved.
+ std::vector favs = favourite_ids();
+ std::vector live_favs;
+ live_favs.reserve(favs.size());
+ for (const auto& id : favs)
+ if (m_actions.count(id))
+ live_favs.push_back(id);
+ if (live_favs.size() != favs.size())
+ write_section("favourite_actions", nlohmann::json(live_favs));
+ nlohmann::json favourites(live_favs);
// Recent = the last-N launched actions by recency (only actions with a run history).
constexpr size_t kRecentLimit = 5;
diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp
index b199d0b5ae..fbc446c5a0 100644
--- a/src/slic3r/GUI/SpeedDialDialog.cpp
+++ b/src/slic3r/GUI/SpeedDialDialog.cpp
@@ -127,7 +127,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
send_actions();
} else if (command == "toggle_favourite") {
// set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the
- // star and show a "favourites are full" hint instead of silently losing the pin.
+ // pin and show a "favourites are full" hint instead of silently losing the favourite.
const std::string fav_id = payload.value("id", "");
const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false));
if (!ok)