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)