Add right-click menu to reorder or unpin favs

Right-clicking a fav tile now opens a small
context menu with Move left, Move right, and
Unpin, instead of relying on drag-only reorder.

- speeddial.js: oncontextmenu handler, the
  menu build/position/dismiss logic, and an
  Escape-key close.
- style.css: .ctx-menu/.ctx-item styling.
- ActionRegistry: reorder_favourites persists
  the new bar order to AppConfig.
- SpeedDialDialog: routes the new
  reorder_favourites command from the page.
This commit is contained in:
Andrew
2026-07-10 17:18:00 +08:00
parent 7c9fa801fb
commit 35d189f1c7
5 changed files with 110 additions and 0 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -254,6 +254,23 @@ void ActionRegistry::set_favourite(const std::string& id, bool on)
live->favourite = on;
}
void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
{
assert(wxThread::IsMain());
auto cur = read_string_array("favourite_actions");
std::vector<std::string> 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());

View File

@@ -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<std::string>& 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);

View File

@@ -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<std::string> ids;
if (p.contains("ids") && p["ids"].is_array())
for (auto& e : p["ids"]) if (e.is_string()) ids.push_back(e.get<std::string>());
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();