mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-22 00:12:34 +00:00
Add category headers. Alphabetical sorting when no search query. Recent count adjustments
This commit is contained in:
@@ -124,7 +124,9 @@ var LangText = {
|
||||
sd_showing: "Showing",
|
||||
sd_of: "of",
|
||||
sd_actions: "actions",
|
||||
sd_recent: "recent",
|
||||
sd_recent: "Recent",
|
||||
sd_plugins: "Plugins",
|
||||
sd_other: "Other",
|
||||
sd_matches: "matches",
|
||||
sd_tabs: "tabs",
|
||||
sd_no_match: "No actions match",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Speed Dial launcher page. Static-safe module: no DOM access at load time so a
|
||||
// node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel / commandList).
|
||||
// node vm can exercise the pure helpers (searchActions / filterTabs / actionLabel / nextSel /
|
||||
// commandList / commandSections / actionCategory / groupActions).
|
||||
|
||||
// ---- state (populated by the C++ bridge via window.HandleStudio) ----
|
||||
var ACTIONS = []; // [{id,title,source,group,input,icon,mode}], already frecency-sorted by C++
|
||||
var ACTIONS = []; // [{id,title,source,group,kind,input,icon,mode}], already frecency-sorted by C++
|
||||
var FAVS = []; // [id...]
|
||||
var RECENTS = []; // [{id,title,source,group,input,icon,mode}] - last-N launched
|
||||
var RECENTS = []; // [{id,title,source,group,kind,input,icon,mode}] - last-N launched
|
||||
var query = "";
|
||||
var sel = { zone: "list", i: 0 }; // zone: 'list' | 'fav'
|
||||
var lastResizeHeight = 0;
|
||||
@@ -47,6 +48,14 @@ var renderEnd = 0;
|
||||
var builtKey = ""; // phase|query|listLen - when it changes, rows are rebuilt from the first window
|
||||
var spacerEl = null; // the trailing height spacer, always the last child of listEl
|
||||
|
||||
// Section headers in the empty-query list ("Recent" then one per category). sectionStarts maps a flat
|
||||
// action index to the header label that sits above it. sectionTotal/Rendered count headers so the
|
||||
// bottom spacer reserves the same vertical space the not-yet-rendered headers will occupy.
|
||||
var SECTION_H = 30; // MUST match .dial-section height (30px)
|
||||
var sectionStarts = null;
|
||||
var sectionTotal = 0;
|
||||
var sectionRendered = 0;
|
||||
|
||||
// search-cache: the normalized (folded+lowercased) needle for the current query pass.
|
||||
var searchNeedle = "";
|
||||
|
||||
@@ -274,12 +283,75 @@ function fillTile(tile, a, mono) {
|
||||
tile.appendChild(img);
|
||||
}
|
||||
|
||||
// Category a row is grouped under in the empty-query list. Native commands and dynamic plate/recent
|
||||
// actions carry a group; settings derive their top-level preset type from the source breadcrumb
|
||||
// ("Process : Quality : Layers" -> "Process"); plugins all share one header.
|
||||
function actionCategory(a) {
|
||||
if (!a) return T("sd_other", "Other");
|
||||
if (a.kind === "plugin") return T("sd_plugins", "Plugins");
|
||||
if (a.group) return a.group;
|
||||
var src = a.source || "";
|
||||
var sep = src.indexOf(" : ");
|
||||
var cat = sep === -1 ? src : src.slice(0, sep);
|
||||
return cat || T("sd_other", "Other");
|
||||
}
|
||||
|
||||
// Stable-bucket actions by category, then order the groups alphabetically. Within a group the incoming
|
||||
// (frecency) order is kept. Pure so the node-vm test can exercise grouping.
|
||||
function groupActions(list) {
|
||||
var buckets = Object.create(null);
|
||||
var order = [];
|
||||
(list || []).forEach(function (a) {
|
||||
var c = actionCategory(a);
|
||||
if (!buckets[c]) { buckets[c] = []; order.push(c); }
|
||||
buckets[c].push(a);
|
||||
});
|
||||
order.sort(function (x, y) {
|
||||
var a = x.toLowerCase(), b = y.toLowerCase();
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
});
|
||||
var out = [];
|
||||
order.forEach(function (c) { out = out.concat(buckets[c]); });
|
||||
return out;
|
||||
}
|
||||
|
||||
// The active list for the main phase. A typed query ranks every action (commands/plugins/settings)
|
||||
// by relevance; an empty query shows the recent list (recents are a mixed bag - no discrimination).
|
||||
// by relevance; an empty query shows the recents first, then the rest grouped under category headers.
|
||||
// The empty-query result is cached on the action/recents array identities so scrolling doesn't regroup.
|
||||
var commandListCache = null;
|
||||
function commandList(actions, recents, query) {
|
||||
var all = actions || [];
|
||||
if (shouldRenderActionList(query))
|
||||
return searchActions(actions || [], query);
|
||||
return (recents || []).slice(0);
|
||||
return searchActions(all, query);
|
||||
var rec = recents || [];
|
||||
if (commandListCache && commandListCache.actions === all && commandListCache.recents === rec)
|
||||
return commandListCache.list;
|
||||
var recentIds = {};
|
||||
for (var i = 0; i < rec.length; i++)
|
||||
recentIds[rec[i].id] = true;
|
||||
var rest = all.filter(function (a) { return !recentIds[a.id]; });
|
||||
var list = rec.concat(groupActions(rest));
|
||||
commandListCache = { actions: all, recents: rec, list: list };
|
||||
return list;
|
||||
}
|
||||
|
||||
// Section headers for the main list: "Recent" (when recents exist) then one per category. The list is
|
||||
// already grouped, so a header is emitted whenever the category changes. A typed query has no headers.
|
||||
// Returns {startIndex: label}, where startIndex is the flat list index the header sits above.
|
||||
function commandSections(list, recentsLen, query) {
|
||||
if (shouldRenderActionList(query) || !list || !list.length)
|
||||
return null;
|
||||
var sections = {};
|
||||
if (recentsLen > 0)
|
||||
sections[0] = T("sd_recent", "Recent");
|
||||
var prev = null;
|
||||
for (var i = recentsLen; i < list.length; i++) {
|
||||
var c = actionCategory(list[i]);
|
||||
if (i === recentsLen || c !== prev)
|
||||
sections[i] = c;
|
||||
prev = c;
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
// Resolve the selection cursor {zone,i} to the action id it points at: fav zone indexes the
|
||||
@@ -676,16 +748,31 @@ function renderActionRow(a, i) {
|
||||
return shell.row;
|
||||
}
|
||||
|
||||
// Append rows [from, to) into listEl, always inserting before the bottom spacer so row order is preserved.
|
||||
// Append rows [from, to) into listEl, always inserting before the bottom spacer so row order is
|
||||
// preserved. A section header is inserted just before the first row of its section.
|
||||
function appendActionRows(list, from, to) {
|
||||
var spacer = spacerEl || ensureSpacer();
|
||||
for (var i = from; i < to; i++) {
|
||||
if (sectionStarts && sectionStarts[i] !== undefined) {
|
||||
var header = document.createElement("div");
|
||||
header.className = "dial-section";
|
||||
header.textContent = sectionStarts[i];
|
||||
listEl.insertBefore(header, spacer);
|
||||
sectionRendered++;
|
||||
}
|
||||
var row = renderActionRow(list[i], i);
|
||||
row.setAttribute("data-idx", i);
|
||||
listEl.insertBefore(row, spacer);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the section map for the current list and reset the rendered-header counters (a fresh build).
|
||||
function setSections(sections) {
|
||||
sectionStarts = sections || null;
|
||||
sectionTotal = sectionStarts ? Object.keys(sectionStarts).length : 0;
|
||||
sectionRendered = 0;
|
||||
}
|
||||
|
||||
// Ensure the bottom spacer exists as the last child of listEl. It is (re)created on rebuild because
|
||||
// listEl.innerHTML="" destroys the old node.
|
||||
function ensureSpacer() {
|
||||
@@ -697,10 +784,13 @@ function ensureSpacer() {
|
||||
return spacerEl;
|
||||
}
|
||||
|
||||
// Size the spacer to the un-rendered tail so the scrollbar reflects the full match count.
|
||||
// Size the spacer to the un-rendered tail so the scrollbar reflects the full match count. Pending
|
||||
// section headers reserve their own height too, so the last rows stay reachable.
|
||||
function setBottomSpacer(total) {
|
||||
ensureSpacer();
|
||||
spacerEl.style.height = Math.max(0, total - renderEnd) * ROW_H + "px";
|
||||
var remainingRows = Math.max(0, total - renderEnd);
|
||||
var remainingHeaders = Math.max(0, sectionTotal - sectionRendered);
|
||||
spacerEl.style.height = (remainingRows * ROW_H + remainingHeaders * SECTION_H) + "px";
|
||||
}
|
||||
|
||||
// Reveal rows up to `upto` (an exclusive index), appending without rebuilding the whole list. Used by
|
||||
@@ -720,6 +810,7 @@ function rebuildCommandsList(list) {
|
||||
listEl.className = "dial-list";
|
||||
ensureSpacer();
|
||||
renderEnd = 0;
|
||||
sectionRendered = 0;
|
||||
appendActionRows(list, 0, Math.min(list.length, K_ROWS));
|
||||
renderEnd = Math.min(list.length, K_ROWS);
|
||||
setBottomSpacer(list.length);
|
||||
@@ -754,6 +845,7 @@ function renderEmpty(text) {
|
||||
listEl.innerHTML = "";
|
||||
spacerEl = null;
|
||||
listEl.className = "dial-list empty";
|
||||
setSections(null);
|
||||
if (countEl) countEl.hidden = true;
|
||||
var empty = document.createElement("div");
|
||||
empty.className = "dial-empty";
|
||||
@@ -782,6 +874,8 @@ function renderCommandsList() {
|
||||
var key = buildKey() + "|" + total;
|
||||
if (key !== builtKey) {
|
||||
builtKey = key;
|
||||
// Headers split the empty-query list into recents + category groups; typed queries have none.
|
||||
setSections(showList ? null : commandSections(list, (RECENTS || []).length, query));
|
||||
rebuildCommandsList(list);
|
||||
} else if (sel.i >= renderEnd) {
|
||||
// Arrow-nav walked past the rendered window - reveal enough to keep the selection visible.
|
||||
@@ -789,9 +883,11 @@ function renderCommandsList() {
|
||||
}
|
||||
|
||||
listEl.className = "dial-list";
|
||||
// The empty-query list is labelled by its section headers instead.
|
||||
if (countEl) {
|
||||
countEl.hidden = false;
|
||||
countEl.textContent = showList ? resultCountText(ACTIONS.length, total, query) : total + " " + T("sd_recent", "recent");
|
||||
countEl.hidden = !showList;
|
||||
if (showList)
|
||||
countEl.textContent = resultCountText(ACTIONS.length, total, query);
|
||||
}
|
||||
updateSelection();
|
||||
updatePins(list);
|
||||
|
||||
@@ -22,16 +22,16 @@ assert.equal(
|
||||
"duplicate labels should use the opaque id without interpreting its contents"
|
||||
);
|
||||
|
||||
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search keeps recent/empty list");
|
||||
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search keeps recent/empty list");
|
||||
assert.equal(ctx.shouldRenderActionList(""), false, "an empty search shows the recents+pool list");
|
||||
assert.equal(ctx.shouldRenderActionList(" "), false, "whitespace-only search shows the recents+pool list");
|
||||
assert.equal(ctx.shouldRenderActionList("r"), true, "typing starts rendering matching actions");
|
||||
|
||||
// commandList: an empty query shows recents; a typed query filters all actions.
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [], ""), [],
|
||||
"empty query + no recents shows nothing");
|
||||
// commandList: an empty query shows recents first, then every other action; a typed query filters all.
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [], ""), duplicateActions,
|
||||
"empty query + no recents shows the whole action pool");
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [duplicateActions[0]], ""),
|
||||
[duplicateActions[0]],
|
||||
"empty query shows the recent list");
|
||||
[duplicateActions[0], duplicateActions[1]],
|
||||
"empty query shows the recent first, then the remaining actions");
|
||||
assert.deepEqual(ctx.commandList(duplicateActions, [], "rep"), duplicateActions,
|
||||
"a typed query filters actions (both identical titles match) instead of showing recents");
|
||||
|
||||
@@ -117,22 +117,73 @@ const negativePool = [
|
||||
assert.equal(ctx.searchActions(negativePool, "ornt").length, 1,
|
||||
"a low-score fuzzy match is not mistaken for no match");
|
||||
|
||||
// actionCategory: a command/dynamic action's group is its category; a setting uses the top-level
|
||||
// source segment; every plugin shares one header; a category-less action falls back to "Other".
|
||||
assert.equal(ctx.actionCategory({ id: "c", group: "Help", source: "OrcaSlicer", kind: "command" }), "Help",
|
||||
"a command's group is its category");
|
||||
assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Process : Quality : Layers", kind: "command" }), "Process",
|
||||
"a setting's category is the top-level source segment");
|
||||
assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Filament : Cooling", kind: "command" }), "Filament",
|
||||
"a Filament setting groups under Filament");
|
||||
assert.equal(ctx.actionCategory({ id: "plugin_script_action:Foo:bar.py", group: "", source: "Gcode Optimizer", kind: "plugin" }), "Plugins",
|
||||
"every plugin shares one Plugins header");
|
||||
assert.equal(ctx.actionCategory({ id: "x", group: "", source: "", kind: "command" }), "Other",
|
||||
"a category-less action falls back to Other");
|
||||
|
||||
// groupActions: bucket by category, order the groups alphabetically, keep the incoming order within
|
||||
// each group (the pool arrives frecency-sorted).
|
||||
const groupPool = [
|
||||
{ id: "q1", title: "Q1", source: "Quality", group: "Quality", kind: "command" },
|
||||
{ id: "h1", title: "H1", source: "OrcaSlicer", group: "Help", kind: "command" },
|
||||
{ id: "p1", title: "P1", source: "Process : A", group: "", kind: "command" },
|
||||
{ id: "h2", title: "H2", source: "OrcaSlicer", group: "Help", kind: "command" }
|
||||
];
|
||||
assert.deepEqual(ctx.groupActions(groupPool).map(function (a) { return a.id; }), ["h1", "h2", "p1", "q1"],
|
||||
"groups are alphabetical and each group keeps its incoming order");
|
||||
|
||||
// commandList (the main-phase list) delegates to the ranked search for a typed query and returns
|
||||
// the mixed recents (no discrimination) for an empty query.
|
||||
// recents + the category-grouped pool for an empty query.
|
||||
const mixed = [
|
||||
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", input: "" },
|
||||
{ id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", input: "" }
|
||||
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", kind: "command", input: "" },
|
||||
{ id: "set", title: "Sparse Infill Density", source: "Quality", group: "Quality", kind: "command", input: "" }
|
||||
];
|
||||
assert.equal(ctx.commandList(mixed, [], "sli")[0].id, "cmd",
|
||||
"a typed query keeps the relevance-ranked action list (best match first)");
|
||||
assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd"],
|
||||
"an empty query shows the mixed recents list verbatim");
|
||||
assert.deepEqual(ctx.commandList(mixed, mixed.slice(0, 1), "").map(function (a) { return a.id; }), ["cmd", "set"],
|
||||
"an empty query shows the recents first and de-dupes them out of the tail");
|
||||
assert.deepEqual(ctx.commandList(mixed, [], "").map(function (a) { return a.id; }), ["cmd", "set"],
|
||||
"empty query + no recents shows the whole action pool");
|
||||
assert.deepEqual(ctx.commandList(mixed, [mixed[1]], "").map(function (a) { return a.id; }), ["set", "cmd"],
|
||||
"the recent is hoisted above the alphabetically-ordered groups");
|
||||
|
||||
// selectedActionId: resolves the active list (recents for an empty query, filtered list otherwise).
|
||||
// commandSections: "Recent" (when recents exist) plus one header per category in the grouped list;
|
||||
// a typed query or an empty list yields no headers. Uses a computed grouped list so the recents
|
||||
// hoist and the category ordering are exercised together.
|
||||
const sectionPool = [
|
||||
{ id: "cmd", title: "Slice", source: "OrcaSlicer", group: "Commands", kind: "command" },
|
||||
{ id: "help", title: "Shortcuts", source: "OrcaSlicer", group: "Help", kind: "command" },
|
||||
{ id: "set", title: "Infill", source: "Quality", group: "Quality", kind: "command" }
|
||||
];
|
||||
const sectionList = ctx.commandList(sectionPool, [sectionPool[0]], "");
|
||||
assert.deepEqual(sectionList.map(function (a) { return a.id; }), ["cmd", "help", "set"],
|
||||
"recents are hoisted, then the rest is grouped alphabetically (Commands, Help, Quality)");
|
||||
assert.deepEqual(ctx.commandSections(sectionList, 1, ""), { 0: "Recent", 1: "Help", 2: "Quality" },
|
||||
"recents + grouped actions get one header per category");
|
||||
assert.deepEqual(ctx.commandSections([sectionPool[0]], 1, ""), { 0: "Recent" },
|
||||
"a list that is all recents gets only the Recent header");
|
||||
assert.deepEqual(ctx.commandSections(ctx.commandList(sectionPool, [], ""), 0, ""),
|
||||
{ 0: "Commands", 1: "Help", 2: "Quality" },
|
||||
"with no recents the grouped list still gets category headers");
|
||||
assert.equal(ctx.commandSections(sectionList, 1, "sli"), null,
|
||||
"a typed query has no section headers");
|
||||
assert.equal(ctx.commandSections([], 0, ""), null,
|
||||
"an empty list has no section headers");
|
||||
|
||||
// selectedActionId: resolves the active list (recents+pool for an empty query, filtered list otherwise).
|
||||
assert.equal(
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], ""), []),
|
||||
null,
|
||||
"Enter with an empty query and no recents must not resolve to an action the list never showed"
|
||||
"0123456789abcdef",
|
||||
"Enter with an empty query resolves the first action in the recents+pool list"
|
||||
);
|
||||
assert.equal(
|
||||
ctx.selectedActionId({ zone: "list", i: 0 }, ctx.commandList(duplicateActions, [], "rep"), []),
|
||||
|
||||
@@ -279,6 +279,21 @@ body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Section header in the empty-query list ("Recent" then one per category). Its 30px height MUST match
|
||||
SECTION_H in speeddial.js, which reserves header space in the windowed-list bottom spacer. */
|
||||
.dial-section {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted, var(--orca-muted, #6b7280));
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
/* Bottom spacer for the windowed command list: fills the un-rendered tail so the scrollbar reflects
|
||||
the full match count, while only a near-viewport window of rows exists in the DOM. */
|
||||
.dial-spacer-bottom {
|
||||
|
||||
@@ -286,6 +286,8 @@ void AppConfig::set_defaults()
|
||||
// The getter already defaults, parses and clamps; write back what it resolves to.
|
||||
set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
|
||||
|
||||
set(SETTING_SPEED_DIAL_RECENT_COUNT, std::to_string(get_speed_dial_recent_count()));
|
||||
|
||||
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
|
||||
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
|
||||
|
||||
@@ -1664,6 +1666,22 @@ int AppConfig::get_plugin_pages_visible_count() const
|
||||
return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
|
||||
}
|
||||
|
||||
int AppConfig::get_speed_dial_recent_count() const
|
||||
{
|
||||
std::string value = get(SETTING_SPEED_DIAL_RECENT_COUNT);
|
||||
if (value.empty())
|
||||
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
|
||||
int recent_count = SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
try {
|
||||
recent_count = std::stoi(value);
|
||||
}
|
||||
catch (...) {
|
||||
return SPEED_DIAL_RECENT_COUNT_DEFAULT;
|
||||
}
|
||||
return std::clamp(recent_count, SPEED_DIAL_RECENT_COUNT_MIN, SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
}
|
||||
|
||||
std::vector<std::string> AppConfig::get_skipped_network_versions() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
|
||||
@@ -46,6 +46,11 @@ using namespace nlohmann;
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
|
||||
|
||||
#define SETTING_SPEED_DIAL_RECENT_COUNT "speed_dial_recent_count"
|
||||
#define SPEED_DIAL_RECENT_COUNT_MIN 0
|
||||
#define SPEED_DIAL_RECENT_COUNT_DEFAULT 5
|
||||
#define SPEED_DIAL_RECENT_COUNT_MAX 10
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
|
||||
#else
|
||||
@@ -394,6 +399,9 @@ public:
|
||||
// dropdown on the last tab.
|
||||
int get_plugin_pages_visible_count() const;
|
||||
|
||||
// Number of recently launched actions shown at the top of the Speed Dial; 0 hides them.
|
||||
int get_speed_dial_recent_count() const;
|
||||
|
||||
std::vector<std::string> get_skipped_network_versions() const;
|
||||
void add_skipped_network_version(const std::string& version);
|
||||
bool is_network_version_skipped(const std::string& version) const;
|
||||
|
||||
@@ -720,6 +720,7 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
{"title", a->title()},
|
||||
{"source", a->source_name()},
|
||||
{"group", a->group},
|
||||
{"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"},
|
||||
{"input", a->input},
|
||||
{"icon", a->icon},
|
||||
{"mode", mode_key(a->required_mode)}});
|
||||
@@ -744,8 +745,9 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
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;
|
||||
// Recent = the last-N launched actions by recency (only actions with a run history). N is a
|
||||
// user preference; 0 hides recents without affecting the frecency order below.
|
||||
const size_t recent_limit = size_t(wxGetApp().app_config->get_speed_dial_recent_count());
|
||||
std::vector<const AppAction*> recent;
|
||||
for (const auto& entry : m_actions)
|
||||
if (entry.second->last > 0)
|
||||
@@ -755,8 +757,8 @@ nlohmann::json ActionRegistry::snapshot()
|
||||
return a->last > b->last;
|
||||
return a->id() < b->id();
|
||||
});
|
||||
if (recent.size() > kRecentLimit)
|
||||
recent.resize(kRecentLimit);
|
||||
if (recent.size() > recent_limit)
|
||||
recent.resize(recent_limit);
|
||||
nlohmann::json recent_json = nlohmann::json::array();
|
||||
for (const AppAction* a : recent)
|
||||
recent_json.push_back(action_to_json(a));
|
||||
|
||||
@@ -1728,6 +1728,16 @@ void PreferencesDialog::create_items()
|
||||
"enable_speed_dial");
|
||||
g_sizer->Add(item_speed_dial);
|
||||
|
||||
auto item_speed_dial_recents = create_item_spinctrl(
|
||||
_L("Recent actions"),
|
||||
"",
|
||||
_L("actions"),
|
||||
_L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."),
|
||||
SETTING_SPEED_DIAL_RECENT_COUNT,
|
||||
SPEED_DIAL_RECENT_COUNT_MIN,
|
||||
SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
g_sizer->Add(item_speed_dial_recents);
|
||||
|
||||
#if 0
|
||||
g_sizer->Add(create_item_title(_L("Filament Grouping")), 1, wxEXPAND);
|
||||
//temporarily disable it
|
||||
|
||||
@@ -43,3 +43,41 @@ TEST_CASE("AppConfig network version helpers", "[AppConfig]") {
|
||||
REQUIRE(config.is_network_version_skipped("02.01.01.52"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("AppConfig Speed Dial recent count defaults, clamps and parses", "[AppConfig]") {
|
||||
AppConfig config;
|
||||
|
||||
SECTION("unset falls back to the default") {
|
||||
REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_DEFAULT);
|
||||
}
|
||||
|
||||
SECTION("zero disables recents") {
|
||||
config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "0");
|
||||
REQUIRE(config.get_speed_dial_recent_count() == 0);
|
||||
}
|
||||
|
||||
SECTION("a value in range is returned as-is") {
|
||||
config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "7");
|
||||
REQUIRE(config.get_speed_dial_recent_count() == 7);
|
||||
}
|
||||
|
||||
SECTION("the maximum is kept") {
|
||||
config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "10");
|
||||
REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
}
|
||||
|
||||
SECTION("values above the maximum clamp down") {
|
||||
config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "42");
|
||||
REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
}
|
||||
|
||||
SECTION("negative values clamp up to 0") {
|
||||
config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "-3");
|
||||
REQUIRE(config.get_speed_dial_recent_count() == 0);
|
||||
}
|
||||
|
||||
SECTION("garbage falls back to the default") {
|
||||
config.set(SETTING_SPEED_DIAL_RECENT_COUNT, "abc");
|
||||
REQUIRE(config.get_speed_dial_recent_count() == SPEED_DIAL_RECENT_COUNT_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user