diff --git a/resources/web/dialog/SpeedDial/speeddial.js b/resources/web/dialog/SpeedDial/speeddial.js index b39867a0b7..e0781a3c98 100644 --- a/resources/web/dialog/SpeedDial/speeddial.js +++ b/resources/web/dialog/SpeedDial/speeddial.js @@ -273,10 +273,65 @@ function renderFav() { tile.title = a.title; tile.setAttribute("aria-label", actionLabel(a, ACTIONS)); tile.onclick = function () { sel = { zone: "fav", i: i }; run(a); }; + tile.oncontextmenu = function (ev) { + ev.preventDefault(); + // why: selecting shows the eyebrow, which grows the launcher - resize so the popup + // isn't clipped (mirrors arrow-nav). requestResize no-ops when height is unchanged. + sel = { zone: "fav", i: i }; render({ resize: true }); + showFavMenu(ev.clientX, ev.clientY, id); + }; favEl.appendChild(tile); }); } +// ---- favourite context menu (right-click a tile) ----------------------------- +var favMenuEl = null; + +function hideFavMenu() { if (favMenuEl) favMenuEl.hidden = true; } + +function addFavMenuItem(label, enabled, fn) { + var item = document.createElement("button"); + item.className = "ctx-item"; + item.textContent = label; + item.disabled = !enabled; + item.onclick = function () { hideFavMenu(); fn(); }; + favMenuEl.appendChild(item); +} + +// One reused menu node (Move left/right + Unpin), positioned at the cursor and clamped +// to the viewport. Native browser context menus can't add items, so we roll our own tiny one. +function showFavMenu(x, y, id) { + if (!favMenuEl) { + favMenuEl = document.createElement("div"); + favMenuEl.className = "ctx-menu"; + document.body.appendChild(favMenuEl); + } + favMenuEl.innerHTML = ""; + var favs = currentVisibleFavs(); + var vi = favs.indexOf(id); + addFavMenuItem("Move left", vi > 0, function () { moveFav(id, -1); }); + addFavMenuItem("Move right", vi >= 0 && vi < favs.length - 1, function () { moveFav(id, 1); }); + addFavMenuItem("Unpin", true, function () { toggleFav(id); }); + favMenuEl.hidden = false; + favMenuEl.style.left = Math.max(0, Math.min(x, window.innerWidth - favMenuEl.offsetWidth - 4)) + "px"; + favMenuEl.style.top = Math.max(0, Math.min(y, window.innerHeight - favMenuEl.offsetHeight - 4)) + "px"; +} + +// Swap a favourite with its visible neighbour (dir -1/+1) and persist the new order. Swapping +// by id inside FAVS (not the visible slice) keeps any hidden pins (no live action) in place. +function moveFav(id, dir) { + var favs = currentVisibleFavs(); + var vi = favs.indexOf(id); + var ni = vi + dir; + if (vi === -1 || ni < 0 || ni >= favs.length) return; + var a = FAVS.indexOf(id), b = FAVS.indexOf(favs[ni]); + if (a === -1 || b === -1) return; + FAVS[a] = favs[ni]; FAVS[b] = id; + SendMessage({ command: "reorder_favourites", ids: FAVS.slice() }); + sel = { zone: "fav", i: ni }; + render({ resize: true }); +} + // Name of the selected favourite, shown above the bar; hidden unless a fav is selected. function updateFavEyebrow(favs) { if (!eyeEl) return; @@ -432,7 +487,12 @@ function OnInit() { query = qEl.value; sel = { zone: "list", i: 0 }; syncClearButton(); render({ resize: true, resetScroll: true }); }); + // why: dismiss the fav context menu on any click/scroll away from it (capture scroll to catch nested scrollers). + document.addEventListener("click", hideFavMenu); + document.addEventListener("scroll", hideFavMenu, true); + document.addEventListener("keydown", function (e) { + if (favMenuEl && !favMenuEl.hidden && e.key === "Escape") { e.preventDefault(); hideFavMenu(); return; } var arr = filterActions(ACTIONS, query); var favs = currentVisibleFavs(); // why: Up/Down always navigate; Left/Right only navigate the fav bar. In the list zone, diff --git a/resources/web/dialog/SpeedDial/style.css b/resources/web/dialog/SpeedDial/style.css index 61e46f5a73..6f2a19ec30 100644 --- a/resources/web/dialog/SpeedDial/style.css +++ b/resources/web/dialog/SpeedDial/style.css @@ -55,6 +55,32 @@ body { border: 1px solid var(--speed-tile-border, #d8d8d8); } .fav-tile.sel { outline: 2px solid var(--main-color, var(--orca-accent, #009688)); outline-offset: 2px; } +.ctx-menu { + position: fixed; + z-index: 10; + min-width: 120px; + padding: 4px; + background: var(--panel, var(--orca-bg, #fff)); + border: 1px solid var(--border, var(--orca-border, #ddd)); + border-radius: 7px; + box-shadow: 0 4px 16px rgba(0,0,0,.18); +} +.ctx-menu[hidden] { display: none; } +.ctx-item { + display: block; + width: 100%; + padding: 6px 10px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--text, var(--orca-fg, #1b1c1e)); + font: inherit; + text-align: left; + cursor: pointer; +} +.ctx-item:hover { background: var(--row-hover, rgba(127,127,127,.16)); } +.ctx-item:disabled { opacity: .4; cursor: default; } +.ctx-item:disabled:hover { background: transparent; } .dial-head { flex: 0 0 auto; padding: 8px; } .plugin-search { display: flex; diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp index eeeda3b9ad..a2b30ae677 100644 --- a/src/slic3r/GUI/ActionRegistry.cpp +++ b/src/slic3r/GUI/ActionRegistry.cpp @@ -254,6 +254,23 @@ void ActionRegistry::set_favourite(const std::string& id, bool on) live->favourite = on; } +void ActionRegistry::reorder_favourites(const std::vector& ids) +{ + assert(wxThread::IsMain()); + auto cur = read_string_array("favourite_actions"); + std::vector next; + // keep the requested order, but only ids that are actually favourites (guard a bad payload) + for (const auto& id : ids) + if (std::find(cur.begin(), cur.end(), id) != cur.end() && + std::find(next.begin(), next.end(), id) == next.end()) + next.push_back(id); + // why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar) + for (const auto& id : cur) + if (std::find(next.begin(), next.end(), id) == next.end()) + next.push_back(id); + write_section("favourite_actions", nlohmann::json(next)); +} + bool ActionRegistry::should_ask(const std::string& plugin_key) const { assert(wxThread::IsMain()); diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp index 456a8d3ba4..022194e139 100644 --- a/src/slic3r/GUI/ActionRegistry.hpp +++ b/src/slic3r/GUI/ActionRegistry.hpp @@ -58,6 +58,7 @@ public: // Dispatch + write-through (registry is the only thing that touches AppConfig). ScriptRunOutcome run(const std::string& id); // runs + bumps stats void set_favourite(const std::string& id, bool on); + void reorder_favourites(const std::vector& ids); // persist a new bar order bool should_ask(const std::string& plugin_key) const; // run-confirm gate void suppress_ask(const std::string& plugin_key); diff --git a/src/slic3r/GUI/SpeedDialDialog.cpp b/src/slic3r/GUI/SpeedDialDialog.cpp index d7869ad757..c0f392fed8 100644 --- a/src/slic3r/GUI/SpeedDialDialog.cpp +++ b/src/slic3r/GUI/SpeedDialDialog.cpp @@ -189,6 +189,12 @@ private: send_actions(); } else if (cmd == "toggle_favourite") wxGetApp().action_registry().set_favourite(p.value("id", ""), p.value("fav", false)); + else if (cmd == "reorder_favourites") { + std::vector ids; + if (p.contains("ids") && p["ids"].is_array()) + for (auto& e : p["ids"]) if (e.is_string()) ids.push_back(e.get()); + wxGetApp().action_registry().reorder_favourites(ids); + } else if (cmd == "run_action") run_action(p.value("id", ""), p.value("title", "")); else if (cmd == "resize") resize_to_content(json_int_or(p, "height", 0)); else if (cmd == "close_page") Dismiss();